From 680829e5274344f10cdfdc986285c019037cb18f Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Sun, 1 Nov 2026 13:47:06 +0000 Subject: [PATCH 01/15] Add a Trends screen for glucose, insulin and Loop status over time Trends reviews one period at a time (a day, 7, 14, 30 or 90 days) on one screen: summary tiles (average with SD and CV, GMI, insulin, carbs and boluses per day, Loop active), time in range, the daily pattern (AGP), basal and bolus, insulin per day and Loop status with its gaps. A one-day period leads with that day's timeline. Tapping a day opens a day page with the insulin timeline and active insulin and carbs, stepping a day at a time. The period exports as a PDF report or a CSV with one row per day. Inside Loop's 7 days the figures come from Loop's own stores, so manual and automatic boluses, temp basals, historical targets and presets are known. Older days come from Apple Health. Closed and open loop are recorded from launch, because Loop's automation history only covers 36 hours. Trends opens from Settings, beside Statistics, as a sheet with its own navigation and a Done button. Statistics and Current Setup are unchanged. This is part 1 of 6 splitting #2621. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QHnVP7ynB62LNuuYMy3yvH --- Loop.xcodeproj/project.pbxproj | 44 + .../accent-ink.colorset/Contents.json | 38 + .../agp-inner.colorset/Contents.json | 38 + .../agp-outer.colorset/Contents.json | 38 + .../basal-edge.colorset/Contents.json | 38 + .../basal.colorset/Contents.json | 38 + .../bolus.colorset/Contents.json | 38 + .../carbs-ink.colorset/Contents.json | 38 + .../control-thumb.colorset/Contents.json | 38 + .../glucose-ink.colorset/Contents.json | 38 + .../ink-muted.colorset/Contents.json | 38 + .../insulin-ink.colorset/Contents.json | 38 + .../range-high.colorset/Contents.json | 38 + .../range-in.colorset/Contents.json | 38 + .../range-low.colorset/Contents.json | 38 + .../range-very-high.colorset/Contents.json | 38 + .../range-very-low.colorset/Contents.json | 38 + .../target-band.colorset/Contents.json | 38 + Loop/Localizable.xcstrings | 19686 +++++++++++++++- Loop/Managers/LoopAppManager.swift | 10 + Loop/Redesign/DayCharts.swift | 548 + Loop/Redesign/DayDetailView.swift | 198 + Loop/Redesign/LoopDesign.swift | 341 + Loop/Redesign/LoopHistoryMath.swift | 352 + Loop/Redesign/LoopHistoryModels.swift | 342 + Loop/Redesign/LoopHistoryProvider.swift | 450 + Loop/Redesign/TrendsComponents.swift | 766 + Loop/Redesign/TrendsScreen.swift | 555 + Loop/View Models/SettingsViewModel.swift | 2 + Loop/Views/SettingsView.swift | 18 + LoopTests/Managers/LoopDataManagerTests.swift | 56 + LoopTests/Models/LoopHistoryMathTests.swift | 340 + 32 files changed, 24350 insertions(+), 4 deletions(-) create mode 100644 Loop/DerivedAssetsBase.xcassets/accent-ink.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/agp-inner.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/agp-outer.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/basal-edge.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/basal.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/carbs-ink.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/control-thumb.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/glucose-ink.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/ink-muted.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/insulin-ink.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/range-high.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/range-in.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/range-low.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/range-very-high.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/range-very-low.colorset/Contents.json create mode 100644 Loop/DerivedAssetsBase.xcassets/target-band.colorset/Contents.json create mode 100644 Loop/Redesign/DayCharts.swift create mode 100644 Loop/Redesign/DayDetailView.swift create mode 100644 Loop/Redesign/LoopDesign.swift create mode 100644 Loop/Redesign/LoopHistoryMath.swift create mode 100644 Loop/Redesign/LoopHistoryModels.swift create mode 100644 Loop/Redesign/LoopHistoryProvider.swift create mode 100644 Loop/Redesign/TrendsComponents.swift create mode 100644 Loop/Redesign/TrendsScreen.swift create mode 100644 LoopTests/Models/LoopHistoryMathTests.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 5230c2ce5e..1a320a9edc 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -750,6 +750,15 @@ 8DC1CE32F39301122B550325 /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; ABD9218EB98C9216FBB04382 /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; AF79DFAB0F55143D66022668 /* LoopKitTypeAliases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */; }; + 89C0C34F420190E8CC7D03B1 /* DayCharts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40DE2F3FC02CBA3BE7AF2F /* DayCharts.swift */; }; + 332CC9FC011E78CE5BEA583C /* LoopDesign.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BD3DDF41B598A8C4B5CEB92 /* LoopDesign.swift */; }; + 0C70F4D862F6D3FDB3D91D87 /* LoopHistoryMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21622B2355AED8F6A12FE17C /* LoopHistoryMath.swift */; }; + 2ADBB92A8ABB74360D18EEFA /* LoopHistoryModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471FFD525A4EB897745608BE /* LoopHistoryModels.swift */; }; + A743DC26D066EE5AC88C2F45 /* LoopHistoryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09448CF97996C40BEDDE6588 /* LoopHistoryProvider.swift */; }; + 6A6C1E1C7A26EC88AFB4FAB1 /* DayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3A83D311A9ECAE563F74A51 /* DayDetailView.swift */; }; + 591F13AF435B37B4818E2F2B /* TrendsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFDE56977CC73CBDA1170C5 /* TrendsComponents.swift */; }; + 5CD0888FF33D6F844A81E638 /* TrendsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0FD64B2B3E9C9AA8764684 /* TrendsScreen.swift */; }; + 97CF13197936D2794766C84A /* LoopHistoryMathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B08644C3031D9E815F9892 /* LoopHistoryMathTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1624,6 +1633,15 @@ 35FAEFD820F50BFF211AC6D9 /* LoopKitTypeAliases.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LoopKitTypeAliases.swift; sourceTree = ""; }; 36421A4B9676EF68CDDC4ECD /* AlgorithmBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AlgorithmBridge.swift; sourceTree = ""; }; 6C372374D3D3D9C5F8E3AD93 /* TemporaryScheduleOverrideAlgorithmTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryScheduleOverrideAlgorithmTests.swift; sourceTree = ""; }; + 6F40DE2F3FC02CBA3BE7AF2F /* DayCharts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayCharts.swift; sourceTree = ""; }; + 0BD3DDF41B598A8C4B5CEB92 /* LoopDesign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopDesign.swift; sourceTree = ""; }; + 21622B2355AED8F6A12FE17C /* LoopHistoryMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopHistoryMath.swift; sourceTree = ""; }; + 471FFD525A4EB897745608BE /* LoopHistoryModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopHistoryModels.swift; sourceTree = ""; }; + 09448CF97996C40BEDDE6588 /* LoopHistoryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopHistoryProvider.swift; sourceTree = ""; }; + E3A83D311A9ECAE563F74A51 /* DayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayDetailView.swift; sourceTree = ""; }; + 4BFDE56977CC73CBDA1170C5 /* TrendsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsComponents.swift; sourceTree = ""; }; + AF0FD64B2B3E9C9AA8764684 /* TrendsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsScreen.swift; sourceTree = ""; }; + 42B08644C3031D9E815F9892 /* LoopHistoryMathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopHistoryMathTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1973,6 +1991,7 @@ 43776F8E1B8022E90074EA36 /* Loop */ = { isa = PBXGroup; children = ( + 8574687EF267DC59043C0242 /* Redesign */, C16DA84022E8E104008624C2 /* Plugins */, B66D1F322E6A5D6600471149 /* Localizable.xcstrings */, B66D1F382E6A5D6600471149 /* InfoPlist.xcstrings */, @@ -2872,6 +2891,7 @@ 6C1F8E3BA2D45079B3E2F512 /* OutcomesPackageTests.swift */, BE36167100E59D7413C50F63 /* SetupComparisonTests.swift */, FEEDF0000000000000000005 /* GlucoseStatisticsTests.swift */, + 42B08644C3031D9E815F9892 /* LoopHistoryMathTests.swift */, A9DFAFB224F0415E00950D1E /* CarbBackfillRequestUserInfoTests.swift */, A963B279252CEBAE0062AA12 /* SetBolusUserInfoTests.swift */, C19008FF252271BB00721625 /* SimpleBolusCalculatorTests.swift */, @@ -3052,6 +3072,21 @@ path = Fixtures; sourceTree = ""; }; + 8574687EF267DC59043C0242 /* Redesign */ = { + isa = PBXGroup; + children = ( + AF0FD64B2B3E9C9AA8764684 /* TrendsScreen.swift */, + 4BFDE56977CC73CBDA1170C5 /* TrendsComponents.swift */, + E3A83D311A9ECAE563F74A51 /* DayDetailView.swift */, + 09448CF97996C40BEDDE6588 /* LoopHistoryProvider.swift */, + 471FFD525A4EB897745608BE /* LoopHistoryModels.swift */, + 21622B2355AED8F6A12FE17C /* LoopHistoryMath.swift */, + 0BD3DDF41B598A8C4B5CEB92 /* LoopDesign.swift */, + 6F40DE2F3FC02CBA3BE7AF2F /* DayCharts.swift */, + ); + path = Redesign; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -3713,6 +3748,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5CD0888FF33D6F844A81E638 /* TrendsScreen.swift in Sources */, + 591F13AF435B37B4818E2F2B /* TrendsComponents.swift in Sources */, + 6A6C1E1C7A26EC88AFB4FAB1 /* DayDetailView.swift in Sources */, + A743DC26D066EE5AC88C2F45 /* LoopHistoryProvider.swift in Sources */, + 2ADBB92A8ABB74360D18EEFA /* LoopHistoryModels.swift in Sources */, + 0C70F4D862F6D3FDB3D91D87 /* LoopHistoryMath.swift in Sources */, + 332CC9FC011E78CE5BEA583C /* LoopDesign.swift in Sources */, + 89C0C34F420190E8CC7D03B1 /* DayCharts.swift in Sources */, C17824A51E1AD4D100D9D25C /* ManualBolusRecommendation.swift in Sources */, 897A5A9624C2175B00C4E71D /* BolusEntryView.swift in Sources */, 4F70C2131DE90339006380B7 /* StatusExtensionContext.swift in Sources */, @@ -4186,6 +4229,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 97CF13197936D2794766C84A /* LoopHistoryMathTests.swift in Sources */, A9DF02CB24F72B9E00B7C988 /* CriticalEventLogTests.swift in Sources */, 1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */, C1777A6625A125F100595963 /* ManualEntryDoseViewModelTests.swift in Sources */, diff --git a/Loop/DerivedAssetsBase.xcassets/accent-ink.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/accent-ink.colorset/Contents.json new file mode 100644 index 0000000000..af109ce19a --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/accent-ink.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.000", + "green": "0.384", + "blue": "0.800", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.039", + "green": "0.518", + "blue": "1.000", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/agp-inner.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/agp-inner.colorset/Contents.json new file mode 100644 index 0000000000..2ddc0f5f90 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/agp-inner.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.000", + "green": "0.690", + "blue": "1.000", + "alpha": "0.340" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.388", + "green": "0.729", + "blue": "1.000", + "alpha": "0.380" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/agp-outer.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/agp-outer.colorset/Contents.json new file mode 100644 index 0000000000..270771d475 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/agp-outer.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.000", + "green": "0.690", + "blue": "1.000", + "alpha": "0.160" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.388", + "green": "0.729", + "blue": "1.000", + "alpha": "0.180" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/basal-edge.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/basal-edge.colorset/Contents.json new file mode 100644 index 0000000000..fa72f9b84c --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/basal-edge.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.890", + "green": "0.725", + "blue": "0.514", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.431", + "green": "0.322", + "blue": "0.196", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/basal.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/basal.colorset/Contents.json new file mode 100644 index 0000000000..76f27a3287 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/basal.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.988", + "green": "0.890", + "blue": "0.761", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.263", + "green": "0.188", + "blue": "0.106", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json new file mode 100644 index 0000000000..30b11e52a8 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.663", + "green": "0.282", + "blue": "0.039", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "1.000", + "green": "0.639", + "blue": "0.278", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/carbs-ink.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/carbs-ink.colorset/Contents.json new file mode 100644 index 0000000000..df21da3860 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/carbs-ink.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.184", + "green": "0.490", + "blue": "0.086", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.349", + "green": "0.894", + "blue": "0.200", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/control-thumb.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/control-thumb.colorset/Contents.json new file mode 100644 index 0000000000..34b15edebe --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/control-thumb.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "1.000", + "green": "1.000", + "blue": "1.000", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.388", + "green": "0.388", + "blue": "0.400", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/glucose-ink.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/glucose-ink.colorset/Contents.json new file mode 100644 index 0000000000..b704d8af81 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/glucose-ink.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.000", + "green": "0.439", + "blue": "0.722", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.388", + "green": "0.729", + "blue": "1.000", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/ink-muted.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/ink-muted.colorset/Contents.json new file mode 100644 index 0000000000..6eda3190f3 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/ink-muted.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.424", + "green": "0.424", + "blue": "0.439", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.682", + "green": "0.682", + "blue": "0.698", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/insulin-ink.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/insulin-ink.colorset/Contents.json new file mode 100644 index 0000000000..1acea580ea --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/insulin-ink.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.541", + "green": "0.227", + "blue": "0.024", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "1.000", + "green": "0.639", + "blue": "0.278", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/range-high.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/range-high.colorset/Contents.json new file mode 100644 index 0000000000..28f578bf71 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/range-high.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.949", + "green": "0.718", + "blue": "0.020", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "1.000", + "green": "0.839", + "blue": "0.039", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/range-in.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/range-in.colorset/Contents.json new file mode 100644 index 0000000000..3d0f452a75 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/range-in.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.184", + "green": "0.659", + "blue": "0.310", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.196", + "green": "0.843", + "blue": "0.294", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/range-low.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/range-low.colorset/Contents.json new file mode 100644 index 0000000000..1349d6a59e --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/range-low.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.898", + "green": "0.282", + "blue": "0.302", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "1.000", + "green": "0.624", + "blue": "0.604", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/range-very-high.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/range-very-high.colorset/Contents.json new file mode 100644 index 0000000000..b2851438d1 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/range-very-high.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.541", + "green": "0.380", + "blue": "0.000", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.788", + "green": "0.541", + "blue": "0.000", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/range-very-low.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/range-very-low.colorset/Contents.json new file mode 100644 index 0000000000..46e9e70555 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/range-very-low.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.608", + "green": "0.110", + "blue": "0.122", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "1.000", + "green": "0.271", + "blue": "0.227", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/target-band.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/target-band.colorset/Contents.json new file mode 100644 index 0000000000..6ff45863c5 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/target-band.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.000", + "green": "0.690", + "blue": "1.000", + "alpha": "0.140" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "0.388", + "green": "0.729", + "blue": "1.000", + "alpha": "0.180" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 92601ae0e8..6cb9a9d92e 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -12,6 +12,18908 @@ }, " -" : { + }, + "%1$@ d" : { + "comment" : "Trends period picker, short label for a period of several days, e.g. 7 d. (1: number of days)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ يوم" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ d" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ d" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ T" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ d" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ pv" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ j" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ימ׳" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ nap" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@日" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ d" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ d" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ d" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ d" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ z" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ д" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ d" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ d" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ д" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@天" + } + } + } + }, + "%1$@ entries" : { + "comment" : "Trends Carbs per day tile detail: average number of carb entries per day, a fractional value such as 2.5, so not pluralised. (1: number with one decimal)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ إدخالات" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "záznamů: %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ registreringer" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ Einträge" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ entradas" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ kirjausta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ entrées" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ רשומות" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ bejegyzés" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ inserimenti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@件" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ registreringer" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ registraties" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "wpisów: %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ registros" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ intrări" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "записей: %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "záznamov: %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ registreringar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ giriş" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "записів: %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ lần nhập" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@条" + } + } + } + }, + "%1$@ g" : { + "comment" : "Format string for an amount of carbohydrate in grams. (1: grams)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ г" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ gr" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ г" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ g" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ 克" + } + } + } + }, + "%1$@ h" : { + "comment" : "Time in range row: average hours a day spent in the band, abbreviated. (1: hours with one decimal)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ س" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ h" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ t" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ Std" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ h" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ h" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ h" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ שע׳" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ óra" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ h" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@時間" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ t" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ u" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ godz." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ h" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ h" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ч" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ h" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ h" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ sa" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ год" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ giờ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ 小时" + } + } + } + }, + "%1$@ manual, %2$@ automatic" : { + "comment" : "VoiceOver: how the bolus divides. (1: manual units, 2: automatic units)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ يدوي، %2$@ تلقائي" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ručně, %2$@ automaticky" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manuel, %2$@ automatisk" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manuell, %2$@ automatisch" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manual, %2$@ automático" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manuaalinen, %2$@ automaattinen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manuel, %2$@ automatique" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ידני, %2$@ אוטומטי" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ kézi, %2$@ automatikus" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manuale, %2$@ automatico" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "手動 %1$@、自動 %2$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manuell, %2$@ automatisk" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ handmatig, %2$@ automatisch" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ręcznie, %2$@ automatycznie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manual, %2$@ automático" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manual, %2$@ automat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ вручную, %2$@ автоматически" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ručne, %2$@ automaticky" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manuell, %2$@ automatisk" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ manuel, %2$@ otomatik" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ вручну, %2$@ автоматично" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ thủ công, %2$@ tự động" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "手动 %1$@,自动 %2$@" + } + } + } + }, + "%1$@ mmol/mol" : { + "comment" : "Trends GMI tile detail: the GMI in IFCC units. (1: value)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ mmol/mol" + } + } + } + }, + "%1$@ of %2$@" : { + "comment" : "Trends Loop active tile detail: time in closed loop out of the time Loop status is known. (1: duration in closed loop, e.g. 6d 4h, 2: duration covered, e.g. 7d) Trends: how many days of the period are averaged. (1: days averaged, a number; 2: days in the period, e.g. 14 days)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ من %2$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ z %2$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ af %2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ von %2$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ de %2$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ / %2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ sur %2$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ מתוך %2$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ / %2$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ su %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ / %2$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ av %2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ van %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ z %2$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ de %2$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ din %2$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ из %2$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ z %2$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ av %2$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ / %2$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ з %2$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ / %2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ / %2$@" + } + } + } + }, + "%1$@ U/h" : { + "comment" : "Format string for an insulin rate in units per hour. (1: rate)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ وحدة لكل ساعة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/hod" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ E/t" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ IE/Std" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/h" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/h" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/h" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "U/hr %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/óra" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/ora" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ 単位/時" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ E/t" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ E/uur" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ J/h" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/h" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/oră" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ Ед/ч" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ J/hod" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ IE/h" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ Ü/Sa" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/год" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/giờ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U/小时" + } + } + } + }, + "%1$@%%" : { + "comment" : "A percentage. (1: number)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %%" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %%" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %%" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %%" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %%" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %%" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%%%1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%%" + } + } + } + }, + "%1$@%% basal" : { + "comment" : "Trends Insulin per day tile detail: share of insulin delivered as basal. (1: percentage number)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% القاعدية" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "bazál %1$@%%" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% basal" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% Basal" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% basal" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% basaalia" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% basal" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% Basal" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% basal" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% basale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベーサル %1$@%%" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% basal" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% basaal" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "baza %1$@%%" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% basal" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% bazală" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "базал %1$@%%" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "bazál %1$@%%" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% basal" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%%%1$@ bazal" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "базал %1$@%%" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% Basal" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "基础 %1$@%%" + } + } + } + }, + "%1$@%% basal · %2$@" : { + "comment" : "Trends Insulin per day tile detail when only some days are averaged. (1: basal share as a percentage number, 2: days averaged, e.g. 5 of 14 days)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% القاعدية · %2$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "bazál %1$@%% · %2$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% basal · %2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% Basal · %2$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% basal · %2$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% basaalia · %2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% basal · %2$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% Basal · %2$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% basal · %2$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% basale · %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベーサル %1$@%% · %2$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% basal · %2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% basaal · %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "baza %1$@%% · %2$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% basal · %2$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% bazală · %2$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "базал %1$@%% · %2$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "bazál %1$@%% · %2$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% basal · %2$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%%%1$@ bazal · %2$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "базал %1$@%% · %2$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% Basal · %2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "基础 %1$@%% · %2$@" + } + } + } + }, + "%1$@%% CGM active" : { + "comment" : "Trends Time in range card: share of the period with CGM readings. (1: percentage number)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% CGM نشط" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM aktivní %1$@%%" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% CGM aktiv" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% CGM aktiv" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM activo %1$@%%" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% CGM aktiivinen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM actif %1$@ %%" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% CGM פעיל" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% CGM aktív" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM attivo %1$@%%" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM稼働 %1$@%%" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% CGM aktiv" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% CGM actief" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM aktywny %1$@%%" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM ativo %1$@%%" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@%% CGM activ" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM активен %1$@%%" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM aktívny %1$@%%" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ %% CGM aktiv" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%%%1$@ CGM aktif" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM активний %1$@%%" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM hoạt động %1$@%%" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM 在线 %1$@%%" + } + } + } + }, + "%1$@, %2$@ %3$@: %4$@ percent, %5$@ hours a day" : { + "comment" : "VoiceOver label for a Time in range row. (1: band name, e.g. In range, 2: glucose thresholds, e.g. 3.9–10.0, 3: glucose unit, 4: percentage number, 5: average hours a day with one decimal)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@، %2$@ %3$@: %4$@ بالمئة، %5$@ ساعة يوميًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: procenta — %4$@, hodin denně — %5$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ procent, %5$@ timer om dagen" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ Prozent, %5$@ Stunden pro Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ por ciento, %5$@ horas al día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ prosenttia, %5$@ tuntia päivässä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@ : %4$@ pour cent, %5$@ heures par jour" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ אחוז, %5$@ שעות ביום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ százalék, napi %5$@ óra" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ percento, %5$@ ore al giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@、%2$@ %3$@:%4$@パーセント、1日%5$@時間" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ prosent, %5$@ timer i døgnet" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ procent, %5$@ uur per dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: procent — %4$@, godzin dziennie — %5$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ por cento, %5$@ horas por dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ procente, %5$@ ore pe zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: процент — %4$@, часов в день — %5$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: percentá — %4$@, hodín denne — %5$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ procent, %5$@ timmar per dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: yüzde %4$@, günde %5$@ saat" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: відсоток — %4$@, годин на день — %5$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@, %2$@ %3$@: %4$@ phần trăm, %5$@ giờ mỗi ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@,%2$@ %3$@:百分之%4$@,每天%5$@小时" + } + } + } + }, + "%lld days" : { + "comment" : "A number of days, e.g. a Trends period or how many days are averaged. (1: number of days) Trends period picker, VoiceOver name for a period of several days. (1: number of days)", + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld day" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld days" + } + } + } + } + }, + "ar" : { + "variations" : { + "plural" : { + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld يوم" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld يوم" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld يوم" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld أيام" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld يومًا" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld يوم" + } + } + } + } + }, + "cs" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld den" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dny" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dne" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dní" + } + } + } + } + }, + "da" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dag" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dage" + } + } + } + } + }, + "de" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Tag" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Tage" + } + } + } + } + }, + "es" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld día" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld días" + } + } + } + } + }, + "fi" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld päivä" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld päivää" + } + } + } + } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld jour" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld jours" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld jours" + } + } + } + } + }, + "he" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld יום" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld ימים" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld ימים" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld ימים" + } + } + } + } + }, + "hu" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld nap" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld nap" + } + } + } + } + }, + "it" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld giorno" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld giorni" + } + } + } + } + }, + "ja" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld日" + } + } + } + } + }, + "nb" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dag" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dager" + } + } + } + } + }, + "nl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dag" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dagen" + } + } + } + } + }, + "pl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dzień" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dni" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dni" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dnia" + } + } + } + } + }, + "pt-BR" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dia" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dias" + } + } + } + } + }, + "ro" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld zi" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld zile" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld de zile" + } + } + } + } + }, + "ru" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld день" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld дня" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld дней" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld дня" + } + } + } + } + }, + "sk" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld deň" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dni" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dňa" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dní" + } + } + } + } + }, + "sv" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dag" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dagar" + } + } + } + } + }, + "tr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld gün" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld gün" + } + } + } + } + }, + "uk" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld день" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld дні" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld днів" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld дня" + } + } + } + } + }, + "vi" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld ngày" + } + } + } + } + }, + "zh-Hans" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld天" + } + } + } + } + } + } + }, + "%lld entries" : { + "comment" : "Trends Carbs tile detail for a one-day period: how many carb entries were made that day. (1: number)", + "localizations" : { + "ar" : { + "variations" : { + "plural" : { + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld إدخالات" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "إدخال واحد" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "إدخالان" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld إدخالات" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld إدخالًا" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld إدخال" + } + } + } + } + }, + "cs" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld záznam" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld záznamy" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld záznamu" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld záznamů" + } + } + } + } + }, + "da" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registrering" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registreringer" + } + } + } + } + }, + "de" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Eintrag" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Einträge" + } + } + } + } + }, + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld entry" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld entries" + } + } + } + } + }, + "es" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld entrada" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld entradas" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld entradas" + } + } + } + } + }, + "fi" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kirjaus" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kirjausta" + } + } + } + } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld entrée" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld entrées" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld entrées" + } + } + } + } + }, + "he" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "רשומה אחת" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld רשומות" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld רשומות" + } + } + } + } + }, + "hu" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld bejegyzés" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld bejegyzés" + } + } + } + } + }, + "it" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld inserimento" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld inserimenti" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld inserimenti" + } + } + } + } + }, + "ja" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld件" + } + } + } + } + }, + "nb" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registrering" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registreringer" + } + } + } + } + }, + "nl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registratie" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registraties" + } + } + } + } + }, + "pl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld wpis" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld wpisy" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld wpisów" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld wpisu" + } + } + } + } + }, + "pt-BR" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registro" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registros" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registros" + } + } + } + } + }, + "ro" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld intrare" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld intrări" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld de intrări" + } + } + } + } + }, + "ru" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld запись" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld записи" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld записей" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld записи" + } + } + } + } + }, + "sk" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld záznam" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld záznamy" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld záznamu" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld záznamov" + } + } + } + } + }, + "sv" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registrering" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld registreringar" + } + } + } + } + }, + "tr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld giriş" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld giriş" + } + } + } + } + }, + "uk" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld запис" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld записи" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld записів" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld запису" + } + } + } + } + }, + "vi" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld lần nhập" + } + } + } + } + }, + "zh-Hans" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld条" + } + } + } + } + } + } + }, + "%lld shorter gaps" : { + "comment" : "Chart note: how many other, shorter gaps in glucose readings there were; follows the longest gap. (1: number of gaps)", + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld shorter gap" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld shorter gaps" + } + } + } + } + }, + "ar" : { + "variations" : { + "plural" : { + "zero" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld فجوة أقصر" + } + }, + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld فجوة أقصر" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld فجوة أقصر" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld فجوات أقصر" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld فجوة أقصر" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld فجوة أقصر" + } + } + } + } + }, + "cs" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kratší výpadek" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kratší výpadky" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kratšího výpadku" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kratších výpadků" + } + } + } + } + }, + "da" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kortere hul" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kortere huller" + } + } + } + } + }, + "de" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kürzere Lücke" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kürzere Lücken" + } + } + } + } + }, + "es" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld interrupción más corta" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld interrupciones más cortas" + } + } + } + } + }, + "fi" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld lyhyempi katko" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld lyhyempää katkoa" + } + } + } + } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld interruption plus courte" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld interruptions plus courtes" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld interruptions plus courtes" + } + } + } + } + }, + "he" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld פער קצר יותר" + } + }, + "two" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld פערים קצרים יותר" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld פערים קצרים יותר" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld פערים קצרים יותר" + } + } + } + } + }, + "hu" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld rövidebb kimaradás" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld rövidebb kimaradás" + } + } + } + } + }, + "it" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld interruzione più breve" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld interruzioni più brevi" + } + } + } + } + }, + "ja" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "ほか短い欠損%lld件" + } + } + } + } + }, + "nb" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kortere hull" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kortere hull" + } + } + } + } + }, + "nl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kortere onderbreking" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kortere onderbrekingen" + } + } + } + } + }, + "pl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld krótsza przerwa" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld krótsze przerwy" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld krótszych przerw" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld krótszej przerwy" + } + } + } + } + }, + "pt-BR" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld interrupção mais curta" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld interrupções mais curtas" + } + } + } + } + }, + "ro" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld întrerupere mai scurtă" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld întreruperi mai scurte" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld de întreruperi mai scurte" + } + } + } + } + }, + "ru" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld короткий пропуск" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld коротких пропуска" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld коротких пропусков" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld коротких пропуска" + } + } + } + } + }, + "sk" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kratší výpadok" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kratšie výpadky" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kratšieho výpadku" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kratších výpadkov" + } + } + } + } + }, + "sv" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kortare lucka" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld kortare luckor" + } + } + } + } + }, + "tr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld daha kısa boşluk" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld daha kısa boşluk" + } + } + } + } + }, + "uk" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld коротший пропуск" + } + }, + "few" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld коротші пропуски" + } + }, + "many" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld коротших пропусків" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld коротшого пропуску" + } + } + } + } + }, + "vi" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld khoảng trống ngắn hơn" + } + } + } + } + }, + "zh-Hans" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "另有%lld段较短中断" + } + } + } + } + } + } + }, + "+ %1$@ automatic" : { + "comment" : "Trends Boluses per day tile detail: average number of automatic boluses per day on top of the manual ones, a fractional value such as 4.2. (1: number with one decimal)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ تلقائي" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automat." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatiske" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatisch" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automáticos" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automaattista" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatiques" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ אוטומטיים" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatikus" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatici" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ 自動 %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatiske" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatisch" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automat." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automáticos" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automate" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ авто" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automat." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatiska" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ otomatik" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ авто" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ tự động" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ 自动 %1$@" + } + } + } + }, + "25–75%" : { + "comment" : "Trends Daily pattern legend: band between the 25th and 75th percentiles of glucose", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75 %" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75 %" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75 %" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75 %" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%25–75" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "25–75%" + } + } + } + }, + "5–95%" : { + "comment" : "Trends Daily pattern legend: band between the 5th and 95th percentiles of glucose", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95 %" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95 %" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95 %" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95 %" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%5–95" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "5–95%" + } + } + } + }, + "Active insulin %1$@, active carbs %2$@" : { + "comment" : "VoiceOver summary of the active insulin and carbs chart. (1: insulin on board, e.g. 1.25 U, or unknown; 2: carbs on board, e.g. 20 g, or unknown)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الأنسولين النشط %1$@، الكارب النشط %2$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivní inzulín %1$@, aktivní sacharidy %2$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivt insulin %1$@, aktive kulhydrater %2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktives Insulin %1$@, aktive KH %2$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina activa %1$@, carbohidratos activos %2$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Akt. insuliini %1$@, akt. hiilari %2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline active %1$@, glucides actifs %2$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אינסולין פעיל %1$@, פחמימות פעילות %2$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktív inzulin %1$@, aktív szénhidrát %2$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina attiva %1$@, carboidrati attivi %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "残存インスリン %1$@、残存糖質 %2$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivt insulin %1$@, aktive karbohydrater %2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actieve insuline %1$@, actieve koolhydraten %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktywna insulina %1$@, aktywne węglowodany %2$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina ativa %1$@, carboidratos ativos %2$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulină activă %1$@, carbohidrați activi %2$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активный инсулин %1$@, активные углеводы %2$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktívny inzulín %1$@, aktívne sacharidy %2$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivt insulin %1$@, aktiva kolhydrater %2$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktif insülin %1$@, aktif karb %2$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активний інсулін %1$@, активні вуглеводи %2$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin còn hoạt động %1$@, Carbs còn hoạt động %2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "活性胰岛素 %1$@,活性碳水 %2$@" + } + } + } + }, + "Active insulin and carbs" : { + "comment" : "Title of the day page card charting insulin and carbs on board", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الأنسولين والكارب النشطان" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivní inzulín a sacharidy" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivt insulin og kulhydrater" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktives Insulin und KH" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina y carbohidratos activos" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktiivinen insuliini ja hiilihydraatit" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline et glucides actifs" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אינסולין ופחמימות פעילים" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktív inzulin és szénhidrát" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina e carboidrati attivi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "残存インスリンと残存糖質" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivt insulin og karbohydrater" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actieve insuline en koolhydraten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktywna insulina i węglowodany" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina e carboidratos ativos" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulină activă și carbohidrați activi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активный инсулин и углеводы" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktívny inzulín a sacharidy" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivt insulin och kolhydrater" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktif insülin ve karb" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Активний інсулін і вуглеводи" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin và Carbs còn hoạt động" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "活性胰岛素和碳水" + } + } + } + }, + "Aim: over 70%% in range, under 4%% low." : { + "comment" : "Trends Time in range card footnote: the consensus targets for time in range and time low", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الهدف: أكثر من 70%% ضمن النطاق، وأقل من 4%% منخفض." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cíl: přes 70%% v rozsahu, pod 4%% nízko." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mål: over 70 %% i området, under 4 %% lavt." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ziel: über 70 %% im Zielbereich, unter 4 %% niedrig." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Objetivo: más del 70%% en rango, menos del 4%% bajo." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tavoite: yli 70 %% tavoitealueella, alle 4 %% matalalla." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Objectif : plus de 70 %% dans la cible, moins de 4 %% en hypo." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "יעד: מעל 70%% בטווח, מתחת ל-4%% נמוך." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cél: több mint 70%% tartományban, kevesebb mint 4%% alacsony." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obiettivo: oltre il 70%% nell'intervallo, meno del 4%% bassa." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "目標:範囲内70%%超、低値4%%未満。" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mål: over 70 %% i målområdet, under 4 %% lavt." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doel: meer dan 70%% binnen bereik, minder dan 4%% laag." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cel: ponad 70%% w zakresie, poniżej 4%% niskich." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meta: mais de 70%% no alvo, menos de 4%% baixo." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Țintă: peste 70%% în interval, sub 4%% scăzut." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цель: в диапазоне более 70%%, низкая — менее 4%%." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cieľ: nad 70%% v rozsahu, pod 4%% nízko." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mål: över 70 %% inom målområdet, under 4 %% lågt." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef: aralıkta %%70'in üzeri, düşükte %%4'ün altı." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мета: у діапазоні понад 70%%, низька — менше 4%%." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mục tiêu: trên 70%% trong khoảng, dưới 4%% thấp." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "目标:范围内超过70%%,低血糖低于4%%。" + } + } + } + }, + "Automatic" : { + "comment" : "Chart legend: hatched bars are automatic boluses given by Loop", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تلقائي" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatické" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisk" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisch" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automático" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automaattinen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatique" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אוטומטי" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatikus" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatico" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisk" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisch" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatyczne" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automático" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматические" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatické" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisk" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otomatik" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматичні" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tự động" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动" + } + } + } + }, + "Automatic %1$@" : { + "comment" : "Basal and bolus: bolus Loop gave automatically. (1: units, e.g. 5.4 U)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تلقائي %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automaticky %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisk %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisch %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automático %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automaattinen %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatique %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אוטומטי %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatikus %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatico %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動 %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisk %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisch %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatycznie %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automático %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automat %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматически %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automaticky %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisk %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otomatik %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматично %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tự động %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动 %1$@" + } + } + } + }, + "Automatic bolus" : { + "comment" : "Chart callout label for a bolus Loop delivered automatically near the touched time Logbook row title: a bolus Loop delivered automatically", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus تلقائي" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatický bolus" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisk bolus" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatischer Bolus" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolo automático" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automaattinen bolus" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus automatique" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus אוטומטי" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatikus bólus" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolo automatico" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "自動ボーラス" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisk bolus" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatische bolus" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus automatyczny" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus automático" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus automat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматический болюс" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatický bolus" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisk bolus" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otomatik bolus" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматичний болюс" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus tự động" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动大剂量" + } + } + } + }, + "Average %1$@ U" : { + "comment" : "Trends Insulin per day legend: the dashed line showing average total insulin per day. (1: units)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المتوسط %1$@ وحدة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Průměr %1$@ U" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gennemsnit %1$@ E" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Durchschnitt %1$@ IE" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promedio %1$@ U" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keskiarvo %1$@ U" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moyenne %1$@ U" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ממוצע U %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Átlag %1$@ U" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Media %1$@ U" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "平均 %1$@ 単位" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gjennomsnitt %1$@ E" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gemiddeld %1$@ E" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Średnio %1$@ J" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Média %1$@ U" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medie %1$@ U" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Среднее %1$@ Ед." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Priemer %1$@ j" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medel %1$@ E" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ortalama %1$@ Ü" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Середнє %1$@ U" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trung bình %1$@ U" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "平均 %1$@ U" + } + } + } + }, + "average per day" : { + "comment" : "Trends Basal and bolus card subtitle when the figures are averaged per day over the period", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المتوسط اليومي" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "průměr za den" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "gennemsnit pr. dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Durchschnitt pro Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "promedio por día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "keskiarvo päivässä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "moyenne par jour" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ממוצע ליום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "napi átlag" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "media al giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1日あたりの平均" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "gjennomsnitt per dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "gemiddeld per dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "średnio dziennie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "média por dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "medie pe zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "в среднем за день" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "priemer za deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "medel per dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "günlük ortalama" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "у середньому за день" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "trung bình mỗi ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日平均" + } + } + } + }, + "Basal" : { + "comment" : "Chart callout label for the basal rate at the touched time Label of a tile showing the current basal rate (Now screen) or the day's basal insulin total (Today so far totals) Logbook filter chip: show only basal changes", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "القاعدية" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazál" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basaalinen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベーサル" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basaal" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podstawowy" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazală" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Базал" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazálna" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazal" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Базал" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "基础" + } + } + } + }, + "Basal %1$@, %2$@ percent" : { + "comment" : "VoiceOver: basal units and share. (1: units, 2: percentage number)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "القاعدية %1$@، %2$@ بالمئة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazál %1$@, %2$@ procent" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal %1$@, %2$@ procent" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal %1$@, %2$@ Prozent" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal %1$@, %2$@ por ciento" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basaali %1$@, %2$@ prosenttia" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal %1$@, %2$@ pour cent" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal %1$@, %2$@ אחוז" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal %1$@, %2$@ százalék" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basale %1$@, %2$@ percento" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベーサル %1$@、%2$@ パーセント" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal %1$@, %2$@ prosent" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basaal %1$@, %2$@ procent" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podstawowy %1$@, %2$@ procent" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal %1$@, %2$@ por cento" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazală %1$@, %2$@ la sută" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Базал %1$@, %2$@ процентов" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazál %1$@, %2$@ percent" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal %1$@, %2$@ procent" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazal %1$@, yüzde %2$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Базал %1$@, %2$@ відсотків" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal %1$@, %2$@ phần trăm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "基础 %1$@,百分之 %2$@" + } + } + } + }, + "Basal and bolus" : { + "comment" : "Trends and day page card title: insulin split into basal and bolus", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "القاعدية والبلعة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazál a bolus" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal og bolus" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal und Bolus" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal y bolo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basaali ja bolus" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal et bolus" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal ו-Bolus" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal és bólus" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basale e bolo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベーサルとボーラス" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal og bolus" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basaal en bolus" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina podstawowa i bolus" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal e bolus" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazală și bolus" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Базал и болюс" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazál a bolus" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal och bolus" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazal ve bolus" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Базал і болюс" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal và bolus" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "基础与大剂量" + } + } + } + }, + "basal and bolus" : { + "comment" : "Trends Insulin per day card subtitle when each bar is one day's basal and bolus", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "القاعدية و Bolus" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "bazál a bolus" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "basal og bolus" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal und Bolus" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "basal y bolo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "basaali ja bolus" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "basal et bolus" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal ו-Bolus" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "basal és bólus" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "basale e bolo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ベーサルとボーラス" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "basal og bolus" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "basaal en bolus" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "baza i bolus" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "basal e bolus" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "bazală și bolus" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "базал и болюс" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "bazál a bolus" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "basal och bolus" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "bazal ve bolus" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "базал і болюс" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal và Bolus" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "基础和大剂量" + } + } + } + }, + "Basal given" : { + "comment" : "Chart legend: filled area is basal insulin actually delivered", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "القاعدية المُعطاة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podaný bazál" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Givet basal" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abgegebene Basalrate" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal administrada" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annettu basaali" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal administré" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal שניתן" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leadott basal" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basale erogata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "注入済みベーサル" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gitt basal" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toegediend basaal" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podana baza" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal entregue" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazală administrată" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поданный базал" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podaný bazál" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Given basal" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verilen bazal" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поданий базал" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal đã tiêm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已输注基础" + } + } + } + }, + "Bolus %1$@, %2$@ percent" : { + "comment" : "VoiceOver: bolus units and share. (1: units, 2: percentage number)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "البلعة %1$@، %2$@ بالمئة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ procent" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ procent" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ Prozent" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolo %1$@, %2$@ por ciento" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ prosenttia" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ pour cent" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ אחוז" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bólus %1$@, %2$@ százalék" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolo %1$@, %2$@ percento" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボーラス %1$@、%2$@ パーセント" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ prosent" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ procent" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ procent" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ por cento" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ la sută" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Болюс %1$@, %2$@ процентов" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ percent" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ procent" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, yüzde %2$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Болюс %1$@, %2$@ відсотків" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus %1$@, %2$@ phần trăm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大剂量 %1$@,百分之 %2$@" + } + } + } + }, + "Boluses" : { + "comment" : "Trends summary tile for a one-day period: how many manual boluses were given that day", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الجرعات" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusy" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boluser" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boli" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolos" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boluksia" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בולוסים" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bóluszok" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boli" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ボーラス" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boluser" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolussen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusy" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusuri" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Болюсы" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusy" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusdoser" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Болюси" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liều bolus" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "大剂量次数" + } + } + } + }, + "Boluses per day" : { + "comment" : "Trends summary tile: average number of manual boluses per day", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "جرعات Bolus يوميًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusů denně" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boluser pr. dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boli pro Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolos por día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boluksia päivässä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus par jour" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus ליום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bólusok naponta" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boli al giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1日のボーラス回数" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boluser per dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolussen per dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusy dziennie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus por dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusuri pe zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Болюсов в день" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusy za deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusar per dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlük bolus sayısı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Болюсів на день" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Số lần Bolus mỗi ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日大剂量次数" + } + } + } + }, + "Carbs" : { + "comment" : "Label of a tile showing the day's total carbohydrate Logbook filter chip: show only carb entries Logbook row title: a carb entry", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carbs" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sacharidy" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kulhydrater" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "KH" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carbohidratos" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiilihydraatit" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucides" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carbs" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Szénhidrát" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carb." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "カーボ" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbohydrater" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koolhydraten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Węglowodany" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carbs" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carbohidrați" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Углеводы" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sacharidy" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kolhydrater" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karb" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вуглеводи" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carbs" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "碳水化合物" + } + } + } + }, + "Carbs per day" : { + "comment" : "Trends summary tile: average grams of carbohydrate entered per day", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الكارب يوميًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sacharidy denně" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kulhydrater pr. dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "KH pro Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carbohidratos por día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiilihydraatit päivässä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucides par jour" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "פחמימות ליום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Szénhidrát naponta" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carboidrati al giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1日のカーボ" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbohydrater per dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koolhydraten per dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Węglowodany dziennie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carbs por dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carbohidrați pe zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Углеводов в день" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sacharidy za deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kolhydrater per dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlük karb" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вуглеводів на день" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Carbs mỗi ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日碳水" + } + } + } + }, + "Closed loop" : { + "comment" : "Trends Loop status: time Loop was running in closed loop", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حلقة مغلقة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzavřená smyčka" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lukket Loop" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Closed Loop" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Circuito cerrado" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suljettu säätö" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boucle fermée" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Closed Loop" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zárt hurok" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop chiuso" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クローズドループ" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lukket Loop" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actieve loop" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pętla zamknięta" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ciclo fechado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buclă închisă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Замкнутый цикл" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzavretá slučka" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sluten loop" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapalı döngü" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Замкнутий цикл" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vòng lặp kín" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "闭环" + } + } + } + }, + "Closed loop off" : { + "comment" : "Reason shown for a stretch of time when Loop was in open loop because closed loop was turned off", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الحلقة المغلقة متوقفة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzavřená smyčka vypnuta" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lukket Loop slået fra" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Closed Loop aus" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Circuito cerrado desactivado" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suljettu säätö pois" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boucle fermée désactivée" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Closed Loop כבוי" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zárt hurok kikapcsolva" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop chiuso disattivato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クローズドループ オフ" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lukket Loop av" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Actieve loop uit" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pętla zamknięta wyłączona" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ciclo fechado desligado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buclă închisă oprită" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Замкнутый цикл выключен" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzavretá slučka vypnutá" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sluten loop av" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapalı döngü kapalı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Замкнутий цикл вимкнено" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vòng lặp kín tắt" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "闭环已关闭" + } + } + } + }, + "COB" : { + "comment" : "Abbreviation for carbs on board (active carbohydrate) Abbreviation for carbs on board; label of a tile on the Now screen", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GA" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + } + } + }, + "COB (g, right)" : { + "comment" : "Chart legend: carbs on board in grams, on the right axis", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g، يمين)" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, vpravo)" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, højre)" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, rechts)" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, derecha)" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, oikea)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GA (g, droite)" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, ימין)" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, jobb)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, destra)" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB(g、右)" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, høyre)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, rechts)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, prawa oś)" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, direita)" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, dreapta)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (г, справа)" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, vpravo)" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, höger)" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (gr, sağ)" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (г, праворуч)" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB (g, bên phải)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB(克,右轴)" + } + } + } + }, + "Configuration error" : { + "comment" : "Reason Loop could not run: its settings are incomplete", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "خطأ في الإعدادات" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba konfigurace" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurationsfejl" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurationsfehler" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error de configuración" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Määritysvirhe" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur de configuration" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שגיאת תצורה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurációs hiba" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errore di configurazione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "設定エラー" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurasjonsfeil" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configuratiefout" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Błąd konfiguracji" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro de configuração" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eroare de configurare" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка настроек" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba konfigurácie" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konfigurationsfel" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yapılandırma hatası" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помилка налаштувань" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi cấu hình" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "配置错误" + } + } + } + }, + "Connection error" : { + "comment" : "Reason Loop could not run: no connected devices, or a device connection failed", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "خطأ في الاتصال" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba připojení" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forbindelsesfejl" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbindungsfehler" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error de conexión" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yhteysvirhe" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur de connexion" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שגיאת חיבור" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapcsolódási hiba" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errore di connessione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "接続エラー" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tilkoblingsfeil" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verbindingsfout" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Błąd połączenia" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro de conexão" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eroare de conexiune" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка подключения" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba pripojenia" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anslutningsfel" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlantı hatası" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помилка підключення" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi kết nối" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接错误" + } + } + } + }, + "Couldn't export" : { + "comment" : "Alert title when exporting the Trends PDF or CSV fails", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّر التصدير" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze exportovat" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke eksportere" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Export fehlgeschlagen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo exportar" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vienti epäonnistui" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportation impossible" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "לא ניתן היה לייצא" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nem sikerült exportálni" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile esportare" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "書き出せませんでした" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke eksportere" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exporteren mislukt" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie udało się wyeksportować" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível exportar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu s-a putut exporta" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось экспортировать" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nedá sa exportovať" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunde inte exportera" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dışa aktarılamadı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не вдалося експортувати" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể xuất" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法导出" + } + } + } + }, + "Couldn't load this day: %1$@" : { + "comment" : "Day page error when the day's history can't be read. (1: system error description)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّر تحميل هذا اليوم: %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze načíst tento den: %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke indlæse denne dag: %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dieser Tag konnte nicht geladen werden: %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudo cargar este día: %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Päivää ei voitu ladata: %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de charger cette journée : %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "לא ניתן היה לטעון יום זה: %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nem sikerült betölteni ezt a napot: %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile caricare questo giorno: %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この日を読み込めませんでした:%1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke laste inn denne dagen: %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deze dag kan niet worden geladen: %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie udało się wczytać tego dnia: %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível carregar este dia: %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu s-a putut încărca această zi: %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось загрузить этот день: %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tento deň sa nedá načítať: %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunde inte läsa in dagen: %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu gün yüklenemedi: %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не вдалося завантажити цей день: %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể tải ngày này: %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法载入这一天:%1$@" + } + } + } + }, + "Couldn't load Trends: %1$@" : { + "comment" : "Trends error message when the history can't be read. (1: error description)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّر تحميل الاتجاهات: %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nelze načíst trendy: %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke indlæse Tendenser: %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trends konnten nicht geladen werden: %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se pudieron cargar las tendencias: %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendejä ei voitu ladata: %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de charger les tendances : %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "לא ניתן היה לטעון מגמות: %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nem sikerült betölteni a trendeket: %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile caricare le tendenze: %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレンドを読み込めませんでした:%1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunne ikke laste inn Trender: %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trends kunnen niet worden geladen: %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie udało się wczytać trendów: %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível carregar as tendências: %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu s-au putut încărca tendințele: %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось загрузить тренды: %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendy sa nedajú načítať: %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kunde inte läsa in Trender: %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eğilimler yüklenemedi: %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не вдалося завантажити тренди: %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không thể tải Xu hướng: %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法载入趋势:%1$@" + } + } + } + }, + "Covers %1$@: Loop keeps 7 days of runs, and closed or open loop is recorded from when this version of Loop first ran." : { + "comment" : "Trends Loop status footnote when the status covers only part of the period. (1: date range covered)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يغطي %1$@: يحتفظ Loop بسجل تشغيلات 7 أيام، وتُسجَّل الحلقة المغلقة أو المفتوحة منذ أول تشغيل لهذا الإصدار من Loop." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokrývá %1$@: Loop uchovává běhy za 7 dní a uzavřená či otevřená smyčka se zaznamenává od prvního spuštění této verze Loop." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dækker %1$@: Loop gemmer kørsler i 7 dage, og lukket eller åbent Loop registreres fra første gang denne version af Loop kørte." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Umfasst %1$@: Loop speichert Durchläufe 7 Tage lang, und Closed oder Open Loop wird erst ab dem ersten Start dieser Loop-Version aufgezeichnet." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cubre %1$@: Loop guarda 7 días de ejecuciones, y el circuito cerrado o abierto se registra desde la primera vez que se ejecutó esta versión de Loop." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kattaa %1$@: Loop säilyttää 7 päivän ajot, ja suljettu tai avoin säätö tallennetaan siitä lähtien, kun tämä Loopin versio käynnistettiin ensimmäisen kerran." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Couvre %1$@ : Loop conserve 7 jours d'exécutions, et la boucle fermée ou ouverte est enregistrée depuis le premier lancement de cette version de Loop." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מכסה %1$@: Loop שומר 7 ימים של הרצות, ו-Closed Loop או Open Loop נרשמים מאז ההפעלה הראשונה של גרסה זו של Loop." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lefedett időszak: %1$@. A Loop 7 napnyi futást őriz meg, a zárt vagy nyitott hurok pedig a Loop ezen verziójának első futtatásától kerül rögzítésre." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copre %1$@: Loop conserva 7 giorni di esecuzioni e il loop chiuso o aperto è registrato dal primo avvio di questa versione di Loop." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "対象期間 %1$@:Loopは実行記録を7日間保持し、クローズド/オープンループはこのバージョンのLoopを初めて起動した時点から記録されます。" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dekker %1$@: Loop lagrer kjøringer i 7 dager, og lukket eller åpen Loop registreres fra første gang denne versjonen av Loop kjørte." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omvat %1$@: Loop bewaart 7 dagen aan loop-runs, en actieve of open loop wordt vastgelegd vanaf de eerste keer dat deze versie van Loop draaide." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obejmuje %1$@: Loop przechowuje cykle z 7 dni, a pętla zamknięta lub otwarta jest zapisywana od pierwszego uruchomienia tej wersji Loop." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cobre %1$@: o Loop guarda 7 dias de execuções, e o ciclo fechado ou aberto é registrado desde a primeira vez que esta versão do Loop foi executada." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acoperă %1$@: Loop păstrează 7 zile de rulări, iar bucla închisă sau deschisă este înregistrată de la prima rulare a acestei versiuni de Loop." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Охвачено: %1$@. Loop хранит запуски за 7 дней, а замкнутый или открытый цикл записывается с первого запуска этой версии Loop." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pokrýva %1$@: Loop uchováva behy za 7 dní a uzavretá či otvorená slučka sa zaznamenáva od prvého spustenia tejto verzie Loop." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omfattar %1$@: Loop sparar 7 dagars körningar, och sluten eller öppen loop registreras från när den här versionen av Loop först kördes." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapsanan dönem %1$@: Loop 7 günlük çalışma kaydı tutar; kapalı veya açık döngü, Loop'un bu sürümünün ilk çalıştığı andan itibaren kaydedilir." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Охоплено: %1$@. Loop зберігає запуски за 7 днів, а замкнутий чи відкритий цикл записується з першого запуску цієї версії Loop." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bao gồm %1$@: Loop lưu các lần chạy trong 7 ngày, và trạng thái vòng lặp kín hoặc mở được ghi lại từ khi phiên bản Loop này chạy lần đầu." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "覆盖 %1$@:Loop 保留 7 天的运行记录,闭环或开环状态从此版本 Loop 首次运行时开始记录。" + } + } + } + }, + "CSV, one row per day" : { + "comment" : "Trends export menu item: share a CSV spreadsheet file with one row for each day", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV، صف واحد لكل يوم" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, jeden řádek na den" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, én række pr. dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, eine Zeile pro Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, una fila por día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, yksi rivi päivää kohden" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, une ligne par jour" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, שורה אחת לכל יום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, naponta egy sor" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, una riga al giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV(1日1行)" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, én rad per dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, één rij per dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, jeden wiersz na dzień" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, uma linha por dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, un rând pe zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, одна строка на день" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, jeden riadok na deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, en rad per dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, gün başına bir satır" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, один рядок на день" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV, mỗi ngày một dòng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "CSV,每天一行" + } + } + } + }, + "Daily pattern" : { + "comment" : "Trends card title: the ambulatory glucose profile, glucose by time of day across the period", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "النمط اليومي" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denní profil" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Døgnmønster" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tagesprofil" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Patrón diario" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vuorokausiprofiili" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil journalier" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "דפוס יומי" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Napi mintázat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Andamento giornaliero" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日内パターン" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Døgnmønster" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dagpatroon" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil dobowy" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Padrão diário" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tipar zilnic" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Суточный профиль" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denný profil" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dygnsmönster" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlük örüntü" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добовий профіль" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mẫu hằng ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日模式" + } + } + } + }, + "Daily pattern over %1$@. Median highest %2$@ at %3$@, lowest %4$@ at %5$@" : { + "comment" : "VoiceOver label for the Trends Daily pattern chart. (1: days with readings, e.g. 14 days; 2: highest median glucose; 3: time of day; 4: lowest median glucose; 5: time of day)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "النمط اليومي خلال %1$@. أعلى وسيط %2$@ عند %3$@، وأدنى وسيط %4$@ عند %5$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denní profil za %1$@. Nejvyšší medián %2$@ v %3$@, nejnižší %4$@ v %5$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Døgnmønster over %1$@. Median højest %2$@ kl. %3$@, lavest %4$@ kl. %5$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tagesprofil über %1$@. Median am höchsten %2$@ um %3$@, am niedrigsten %4$@ um %5$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Patrón diario de %1$@. Mediana más alta %2$@ a las %3$@, más baja %4$@ a las %5$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vuorokausiprofiili %1$@ ajalta. Korkein mediaani %2$@ klo %3$@, matalin %4$@ klo %5$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil journalier sur %1$@. Médiane la plus haute %2$@ à %3$@, la plus basse %4$@ à %5$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "דפוס יומי במשך %1$@. חציון גבוה ביותר %2$@ ב-%3$@, נמוך ביותר %4$@ ב-%5$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Napi mintázat, %1$@. Legmagasabb medián %2$@ ekkor: %3$@, legalacsonyabb %4$@ ekkor: %5$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Andamento giornaliero su %1$@. Mediana più alta %2$@ alle %3$@, più bassa %4$@ alle %5$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@の日内パターン。中央値の最高は%3$@の%2$@、最低は%5$@の%4$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Døgnmønster over %1$@. Median høyest %2$@ kl. %3$@, lavest %4$@ kl. %5$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dagpatroon over %1$@. Mediaan hoogst %2$@ om %3$@, laagst %4$@ om %5$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil dobowy z %1$@. Najwyższa mediana %2$@ o %3$@, najniższa %4$@ o %5$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Padrão diário em %1$@. Mediana mais alta %2$@ às %3$@, mais baixa %4$@ às %5$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tipar zilnic pe %1$@. Mediana cea mai mare %2$@ la %3$@, cea mai mică %4$@ la %5$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Суточный профиль за %1$@. Наибольшая медиана %2$@ в %3$@, наименьшая %4$@ в %5$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denný profil za %1$@. Najvyšší medián %2$@ o %3$@, najnižší %4$@ o %5$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dygnsmönster över %1$@. Högsta median %2$@ kl. %3$@, lägsta %4$@ kl. %5$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ boyunca günlük örüntü. En yüksek medyan %3$@ saatinde %2$@, en düşük %5$@ saatinde %4$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добовий профіль за %1$@. Найвища медіана %2$@ о %3$@, найнижча %4$@ о %5$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mẫu hằng ngày trong %1$@. Trung vị cao nhất %2$@ lúc %3$@, thấp nhất %4$@ lúc %5$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@的每日模式。中位数最高 %2$@(%3$@),最低 %4$@(%5$@)" + } + } + } + }, + "Daily pattern: not enough days" : { + "comment" : "VoiceOver label for the Trends Daily pattern chart when fewer than 5 days have readings", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "النمط اليومي: لا توجد أيام كافية" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denní profil: málo dní" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Døgnmønster: ikke nok dage" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tagesprofil: nicht genug Tage" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Patrón diario: no hay suficientes días" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vuorokausiprofiili: liian vähän päiviä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil journalier : pas assez de jours" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "דפוס יומי: אין מספיק ימים" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Napi mintázat: nincs elég nap" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Andamento giornaliero: giorni insufficienti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日内パターン:日数が不足しています" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Døgnmønster: ikke nok dager" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dagpatroon: niet genoeg dagen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil dobowy: za mało dni" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Padrão diário: dias insuficientes" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tipar zilnic: prea puține zile" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Суточный профиль: недостаточно дней" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denný profil: málo dní" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dygnsmönster: för få dagar" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlük örüntü: yeterli gün yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Добовий профіль: замало днів" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mẫu hằng ngày: không đủ số ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日模式:天数不足" + } + } + } + }, + "Day" : { + "comment" : "Appearance option and app icon name: the light (daytime) version", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "نهار" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Den" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Päivä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jour" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "יום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nap" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dzień" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "День" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gündüz" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "День" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日" + } + } + } + }, + "Day (Trends period)" : { + "comment" : "Trends period picker: a period of one calendar day (not daytime)", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Day" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يوم" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Den" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Päivä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jour" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "יום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nap" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dzień" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "День" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gün" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "День" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日" + } + } + } + }, + "Export" : { + "comment" : "Trends menu title: export the period as a PDF report or CSV file", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تصدير" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportovat" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eksporter" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportieren" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportar" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vie" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exporter" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ייצוא" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportálás" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Esporta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "書き出し" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eksporter" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exporteer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eksportuj" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportar" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportați" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Экспорт" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportovať" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exportera" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dışa Aktar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Експорт" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xuất" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出" + } + } + } + }, + "From Apple Health: Loop keeps 7 days, so this day has no temp basal or schedule detail." : { + "comment" : "Day page note when the day is older than Loop's 7-day history and was read from Apple Health", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "من Apple Health: يحتفظ Loop بـ 7 أيام فقط، لذا لا توجد تفاصيل Temp Basal أو الجدول لهذا اليوم." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Z aplikace Zdraví: Loop uchovává 7 dní, takže pro tento den chybí podrobnosti o dočasném bazálu a plánu." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fra Apple Sundhed: Loop gemmer 7 dage, så denne dag har ingen detaljer om midlertidig basal eller tidsplan." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aus Apple Health: Loop speichert 7 Tage, daher gibt es für diesen Tag keine Details zu temporären Basalraten oder zum Zeitplan." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "De Apple Salud: Loop guarda 7 días, así que este día no tiene detalle de basal temporal ni de programación." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Terveydestä: Loop säilyttää 7 päivää, joten tältä päivältä ei ole tilapäisen basaalin tai ohjelman tietoja." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Depuis Apple Santé : Loop conserve 7 jours, donc ce jour n'a pas de détail de basal temp. ni de programmation." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מ-Apple Health: Loop שומר 7 ימים, ולכן אין ליום זה פרטי Temp Basal או תוכנית." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Az Apple Egészség appból: a Loop 7 napot őriz meg, így ehhez a naphoz nincsenek ideiglenes basal- és ütemezési részletek." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Da Apple Salute: Loop conserva 7 giorni, quindi questo giorno non ha dettagli su basale temporanea o programmazione." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appleヘルスケアから取得:Loopは7日間のみ保持するため、この日には一時ベーサルやスケジュールの詳細がありません。" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fra Apple Helse: Loop lagrer 7 dager, så denne dagen har ingen detaljer om midlertidig basal eller tidsplan." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uit Apple Health: Loop bewaart 7 dagen, dus deze dag heeft geen details over tijdelijk basaal of het schema." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Z aplikacji Zdrowie: Loop przechowuje 7 dni, więc dla tego dnia brak szczegółów bazy tymczasowej i harmonogramu." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do app Saúde: o Loop guarda 7 dias, então este dia não tem detalhes de basal temporária nem da programação." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Din Apple Sănătate: Loop păstrează 7 zile, așa că această zi nu are detalii despre bazala temporară sau program." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Из приложения «Здоровье»: Loop хранит 7 дней, поэтому для этого дня нет данных о временном базале и расписании." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Z aplikácie Zdravie: Loop uchováva 7 dní, preto pre tento deň chýbajú podrobnosti o dočasnom bazáli a pláne." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Från Apple Hälsa: Loop sparar 7 dagar, så den här dagen saknar detaljer om temporär basal och schema." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple Health'ten: Loop 7 gün saklar, bu nedenle bu gün için geçici bazal veya program ayrıntısı yok." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "З програми «Здоровʼя»: Loop зберігає 7 днів, тому для цього дня немає даних про тимчасовий базал і розклад." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Từ Apple Health: Loop chỉ lưu 7 ngày, nên ngày này không có chi tiết Temp Basal hoặc lịch biểu." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "来自 Apple 健康:Loop 只保留 7 天,因此这一天没有临时基础率或预设详情。" + } + } + } + }, + "Gaps" : { + "comment" : "Trends Loop status button and screen title: list of times Loop wasn't in closed loop", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الفجوات" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Výpadky" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Huller" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lücken" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Interrupciones" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Katkot" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Interruptions" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "פערים" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kimaradások" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Interruzioni" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中断" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hull" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onderbrekingen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przerwy" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Interrupções" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Întreruperi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пропуски" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Výpadky" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luckor" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boşluklar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пропуски" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gián đoạn" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "中断" + } + } + } + }, + "Glucose too old" : { + "comment" : "Reason Loop could not run: the latest glucose reading was too old", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قراءة السكر قديمة جدًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukóza je příliš stará" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukosemåling for gammel" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blutzuckerwert zu alt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucosa demasiado antigua" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoosi liian vanha" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glycémie trop ancienne" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "קריאת הסוכר ישנה מדי" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Túl régi glükózérték" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicemia troppo vecchia" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "血糖値が古すぎます" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Blodsukkermåling for gammel" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucosewaarde te oud" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoza zbyt stara" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicose muito antiga" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicemie prea veche" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Данные глюкозы устарели" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glykémia je príliš stará" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukosvärdet är för gammalt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kan şekeri verisi çok eski" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дані глюкози застаріли" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dữ liệu đường huyết quá cũ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "血糖数据过旧" + } + } + } + }, + "In range" : { + "comment" : "Time in range band: glucose 70–180 mg/dL (3.9–10.0 mmol/L) Time-in-range band name: glucose 70–180 mg/dL (3.9–10.0 mmol/L)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "ضمن النطاق" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "V rozsahu" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "I området" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Im Zielbereich" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "En rango" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tavoitealueella" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dans la cible" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בטווח" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tartományban" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nell'intervallo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "範囲内" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "I målområdet" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Binnen bereik" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "W zakresie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "No alvo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "În interval" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "В диапазоне" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "V rozsahu" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inom målområdet" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aralıkta" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "У діапазоні" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trong khoảng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "范围内" + } + } + } + }, + "Insulin per day" : { + "comment" : "Trends tile and card title: average total insulin per day", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الأنسولين يوميًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulín za den" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin pr. dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin pro Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina por día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuliini päivässä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline par jour" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אינסולין ליום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulin naponta" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina al giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1日のインスリン量" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin per dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline per dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina dziennie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina por dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulină pe zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Инсулин в день" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulín za deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin per dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlük insülin" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Інсулін на день" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin mỗi ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日胰岛素" + } + } + } + }, + "Insulin per day, average %1$@" : { + "comment" : "VoiceOver label for the Trends Insulin per day chart. (1: average insulin per day, e.g. 32.1 U)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الأنسولين يوميًا، المتوسط %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulín za den, průměr %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin pr. dag, gennemsnit %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin pro Tag, Durchschnitt %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina por día, promedio %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuliini päivässä, keskiarvo %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline par jour, moyenne %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אינסולין ליום, ממוצע %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulin naponta, átlag %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina al giorno, media %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1日のインスリン量、平均 %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin per dag, gjennomsnitt %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline per dag, gemiddeld %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina dziennie, średnio %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina por dia, média %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulină pe zi, medie %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Инсулин в день, в среднем %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulín za deň, priemer %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin per dag, medel %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlük insülin, ortalama %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Інсулін на день, у середньому %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin mỗi ngày, trung bình %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日胰岛素,平均 %1$@" + } + } + } + }, + "Insulin per day, weekly average, average %1$@" : { + "comment" : "VoiceOver label for the Trends Insulin per day chart when bars are weekly averages. (1: average insulin per day, e.g. 32.1 U)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الأنسولين يوميًا، متوسط أسبوعي، المتوسط %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulín za den, týdenní průměr, průměr %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin pr. dag, ugegennemsnit, gennemsnit %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin pro Tag, Wochendurchschnitt, Durchschnitt %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina por día, promedio semanal, promedio %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuliini päivässä, viikkokeskiarvo, keskiarvo %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline par jour, moyenne hebdomadaire, moyenne %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אינסולין ליום, ממוצע שבועי, ממוצע %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulin naponta, heti átlag, átlag %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina al giorno, media settimanale, media %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1日のインスリン量、週平均、平均 %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin per dag, ukesgjennomsnitt, gjennomsnitt %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuline per dag, weekgemiddelde, gemiddeld %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina dziennie, średnia tygodniowa, średnio %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulina por dia, média semanal, média %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulină pe zi, medie săptămânală, medie %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Инсулин в день, среднее за неделю, в среднем %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulín za deň, týždenný priemer, priemer %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin per dag, veckomedel, medel %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlük insülin, haftalık ortalama, ortalama %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Інсулін на день, середнє за тиждень, у середньому %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin mỗi ngày, trung bình theo tuần, trung bình %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日胰岛素,按周平均,平均 %1$@" + } + } + } + }, + "Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@" : { + "comment" : "VoiceOver summary of the day's insulin timeline chart with no glucose readings. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مخطط الأنسولين، %1$@، Bolus %2$@، القاعدية %3$@، الكارب %4$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Časová osa inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kulhydrater %4$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin-Zeitleiste, %1$@, Bolus %2$@, Basal %3$@, KH %4$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cronología de insulina, %1$@, bolo %2$@, basal %3$@, carbohidratos %4$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuliinin aikajana, %1$@, bolus %2$@, basaali %3$@, hiilihydraatit %4$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chronologie de l'insuline, %1$@, bolus %2$@, basal %3$@, glucides %4$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ציר זמן אינסולין, %1$@, Bolus %2$@, Basal %3$@, פחמימות %4$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulin idővonal, %1$@, bólus %2$@, basal %3$@, szénhidrát %4$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cronologia insulina, %1$@, bolo %2$@, basale %3$@, carboidrati %4$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "インスリンのタイムライン、%1$@、ボーラス %2$@、ベーサル %3$@、カーボ %4$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, karbohydrater %4$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulinetijdlijn, %1$@, bolus %2$@, basaal %3$@, koolhydraten %4$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oś czasu insuliny, %1$@, bolus %2$@, baza %3$@, węglowodany %4$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linha do tempo de insulina, %1$@, bolus %2$@, basal %3$@, carbs %4$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cronologie insulină, %1$@, bolus %2$@, bazală %3$@, carbohidrați %4$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хронология инсулина, %1$@, болюс %2$@, базал %3$@, углеводы %4$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Časová os inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kolhydrater %4$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin zaman çizelgesi, %1$@, bolus %2$@, bazal %3$@, karb %4$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хронологія інсуліну, %1$@, болюс %2$@, базал %3$@, вуглеводи %4$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dòng thời gian insulin, %1$@, Bolus %2$@, Basal %3$@, Carbs %4$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素时间线,%1$@,大剂量 %2$@,基础 %3$@,碳水 %4$@" + } + } + } + }, + "Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@, latest glucose %5$@ %6$@" : { + "comment" : "VoiceOver summary of the day's insulin timeline chart. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g; 5: latest glucose value; 6: glucose unit, mmol/L or mg/dL)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مخطط الأنسولين، %1$@، Bolus %2$@، القاعدية %3$@، الكارب %4$@، آخر قراءة سكر %5$@ %6$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Časová osa inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@, poslední glukóza %5$@ %6$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kulhydrater %4$@, seneste glukose %5$@ %6$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulin-Zeitleiste, %1$@, Bolus %2$@, Basal %3$@, KH %4$@, letzter Blutzucker %5$@ %6$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cronología de insulina, %1$@, bolo %2$@, basal %3$@, carbohidratos %4$@, última glucosa %5$@ %6$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insuliinin aikajana, %1$@, bolus %2$@, basaali %3$@, hiilihydraatit %4$@, viimeisin glukoosi %5$@ %6$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chronologie de l'insuline, %1$@, bolus %2$@, basal %3$@, glucides %4$@, dernière glycémie %5$@ %6$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ציר זמן אינסולין, %1$@, Bolus %2$@, Basal %3$@, פחמימות %4$@, סוכר אחרון %5$@ %6$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulin idővonal, %1$@, bólus %2$@, basal %3$@, szénhidrát %4$@, legutóbbi glükóz %5$@ %6$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cronologia insulina, %1$@, bolo %2$@, basale %3$@, carboidrati %4$@, ultima glicemia %5$@ %6$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "インスリンのタイムライン、%1$@、ボーラス %2$@、ベーサル %3$@、カーボ %4$@、最新の血糖値 %5$@ %6$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, karbohydrater %4$@, siste blodsukker %5$@ %6$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulinetijdlijn, %1$@, bolus %2$@, basaal %3$@, koolhydraten %4$@, laatste glucose %5$@ %6$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oś czasu insuliny, %1$@, bolus %2$@, baza %3$@, węglowodany %4$@, ostatnia glukoza %5$@ %6$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Linha do tempo de insulina, %1$@, bolus %2$@, basal %3$@, carbs %4$@, última glicose %5$@ %6$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cronologie insulină, %1$@, bolus %2$@, bazală %3$@, carbohidrați %4$@, ultima glicemie %5$@ %6$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хронология инсулина, %1$@, болюс %2$@, базал %3$@, углеводы %4$@, последняя глюкоза %5$@ %6$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Časová os inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@, posledná glykémia %5$@ %6$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kolhydrater %4$@, senaste glukos %5$@ %6$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin zaman çizelgesi, %1$@, bolus %2$@, bazal %3$@, karb %4$@, son KŞ %5$@ %6$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Хронологія інсуліну, %1$@, болюс %2$@, базал %3$@, вуглеводи %4$@, остання глюкоза %5$@ %6$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dòng thời gian insulin, %1$@, Bolus %2$@, Basal %3$@, Carbs %4$@, đường huyết mới nhất %5$@ %6$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素时间线,%1$@,大剂量 %2$@,基础 %3$@,碳水 %4$@,最新血糖 %5$@ %6$@" + } + } + } + }, + "Invalid future glucose" : { + "comment" : "Reason Loop could not run: a glucose reading had a timestamp in the future", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قراءة سكر مستقبلية غير صالحة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukóza s budoucím časem" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ugyldig glukosemåling i fremtiden" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ungültiger Blutzuckerwert in der Zukunft" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glucosa futura no válida" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Virheellinen tulevaisuuden glukoosiarvo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glycémie future non valide" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "קריאת סוכר עתידית לא תקינה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Érvénytelen, jövőbeli glükózérték" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicemia futura non valida" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "未来時刻の無効な血糖値" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ugyldig blodsukkermåling i fremtiden" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ongeldige glucosewaarde in de toekomst" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoza z przyszłą datą" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicose futura inválida" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glicemie invalidă, din viitor" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Глюкоза с временем в будущем" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glykémia s budúcim časom" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ogiltigt glukosvärde i framtiden" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz, ileri tarihli KŞ" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Глюкоза з часом у майбутньому" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đường huyết có thời gian trong tương lai không hợp lệ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无效的未来血糖" + } + } + } + }, + "IOB" : { + "comment" : "Abbreviation for insulin on board (active insulin) Abbreviation for insulin on board; label of a tile on the Now screen", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IA" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + } + } + }, + "IOB (U, left)" : { + "comment" : "Chart legend: insulin on board in units, on the left axis", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (وحدة، يسار)" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (U, vlevo)" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (E, venstre)" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (IE, links)" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (U, izquierda)" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (U, vasen)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IA (U, gauche)" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (U, שמאל)" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (U, bal)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (U, sinistra)" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB(U、左)" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (E, venstre)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (E, links)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (J, lewa oś)" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (U, esquerda)" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (U, stânga)" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (Ед, слева)" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (j, vľavo)" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (E, vänster)" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (Ü, sol)" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (U, ліворуч)" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB (U, bên trái)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB(U,左轴)" + } + } + } + }, + "Limited" : { + "comment" : "Trends Loop status: time closed loop was on but Loop wasn't completing runs", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "محدود" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omezeno" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Begrænset" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eingeschränkt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitado" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rajoitettu" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitée" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מוגבל" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Korlátozott" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "制限あり" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Begrenset" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beperkt" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ograniczona" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limitat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ограничен" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obmedzená" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Begränsad" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sınırlı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обмежено" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hạn chế" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "受限" + } + } + } + }, + "Loop active" : { + "comment" : "Trends summary tile: share of the period Loop spent in closed loop", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop نشط" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktivní" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktiv" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktiv" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop activo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktiivinen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop actif" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop פעיל" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktív" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop attivo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop稼働" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktiv" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop actief" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktywny" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop ativo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop activ" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop активен" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktívny" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktiv" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktif" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop активний" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop hoạt động" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop 运行" + } + } + } + }, + "Loop didn't run" : { + "comment" : "Reason shown for a stretch of time when Loop did not complete a run and recorded no error", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لم يعمل Loop" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop neproběhl" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop kørte ikke" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop ist nicht gelaufen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop no se ejecutó" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop ei ajanut" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop ne s'est pas exécuté" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop לא רץ" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "A Loop nem futott" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop non eseguito" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopが実行されませんでした" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop kjørte ikke" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop heeft niet gedraaid" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop nie działał" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O Loop não executou" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop nu a rulat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop не запускался" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop neprebehol" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop kördes inte" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop çalışmadı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop не запускався" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop không chạy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop 未运行" + } + } + } + }, + "Loop only works out active insulin and carbs for the days it keeps" : { + "comment" : "Day page placeholder for active insulin and carbs on a day older than Loop's 7-day history", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يحسب Loop الأنسولين والكارب النشطين فقط للأيام التي يحتفظ بها" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop počítá aktivní inzulín a sacharidy jen pro dny, které uchovává" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop beregner kun aktivt insulin og kulhydrater for de dage, den gemmer" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop berechnet aktives Insulin und aktive KH nur für die gespeicherten Tage" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop solo calcula la insulina y los carbohidratos activos de los días que guarda" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop laskee aktiivisen insuliinin ja hiilihydraatit vain säilyttämilleen päiville" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop calcule l'insuline et les glucides actifs uniquement pour les jours qu'il conserve" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop מחשב אינסולין ופחמימות פעילים רק עבור הימים שהוא שומר" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "A Loop csak a megőrzött napokra számolja ki az aktív inzulint és szénhidrátot" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop calcola insulina e carboidrati attivi solo per i giorni che conserva" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopが残存インスリンと残存糖質を計算するのは、保持している日のみです" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop beregner bare aktivt insulin og karbohydrater for dagene den lagrer" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop berekent actieve insuline en koolhydraten alleen voor de dagen die het bewaart" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop oblicza aktywną insulinę i węglowodany tylko dla przechowywanych dni" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O Loop só calcula a insulina e os carboidratos ativos dos dias que guarda" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop calculează insulina și carbohidrații activi doar pentru zilele pe care le păstrează" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop рассчитывает активный инсулин и углеводы только за хранимые дни" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop počíta aktívny inzulín a sacharidy len pre dni, ktoré uchováva" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop beräknar bara aktivt insulin och kolhydrater för de dagar som sparas" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aktif insülini ve karbı yalnızca sakladığı günler için hesaplar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop розраховує активний інсулін і вуглеводи лише за дні, які зберігає" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop chỉ tính insulin và Carbs còn hoạt động cho những ngày nó lưu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop 只为其保留的日期计算活性胰岛素和碳水" + } + } + } + }, + "Loop status" : { + "comment" : "Trends card title: how long Loop spent in closed loop, limited or open loop", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حالة Loop" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stav Loop" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-status" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-Status" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Estado de Loop" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopin tila" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "État de Loop" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מצב Loop" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop állapota" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stato di Loop" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopの状態" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-status" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopstatus" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stan Loop" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Status do Loop" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Starea Loop" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Статус Loop" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stav Loop" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopstatus" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop durumu" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стан Loop" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trạng thái Loop" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop 状态" + } + } + } + }, + "Loop status is recorded from when this version of Loop first ran." : { + "comment" : "Trends Loop status footnote when there is no Loop status history yet", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تُسجَّل حالة Loop منذ أول تشغيل لهذا الإصدار من Loop." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stav Loop se zaznamenává od prvního spuštění této verze Loop." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-status registreres fra første gang denne version af Loop kørte." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Der Loop-Status wird ab dem ersten Start dieser Loop-Version aufgezeichnet." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "El estado de Loop se registra desde la primera vez que se ejecutó esta versión de Loop." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopin tila tallennetaan siitä lähtien, kun tämä Loopin versio käynnistettiin ensimmäisen kerran." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "L'état de Loop est enregistré depuis le premier lancement de cette version de Loop." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מצב Loop נרשם מאז ההפעלה הראשונה של גרסה זו של Loop." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "A Loop állapota a Loop ezen verziójának első futtatásától kerül rögzítésre." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lo stato di Loop è registrato dal primo avvio di questa versione di Loop." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopの状態は、このバージョンのLoopを初めて起動した時点から記録されます。" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-status registreres fra første gang denne versjonen av Loop kjørte." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "De loopstatus wordt vastgelegd vanaf de eerste keer dat deze versie van Loop draaide." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stan Loop jest zapisywany od pierwszego uruchomienia tej wersji Loop." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "O status do Loop é registrado desde a primeira vez que esta versão do Loop foi executada." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Starea Loop este înregistrată de la prima rulare a acestei versiuni de Loop." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Статус Loop записывается с первого запуска этой версии Loop." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stav Loop sa zaznamenáva od prvého spustenia tejto verzie Loop." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopstatus registreras från när den här versionen av Loop först kördes." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop durumu, Loop'un bu sürümünün ilk çalıştığı andan itibaren kaydedilir." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Стан Loop записується з першого запуску цієї версії Loop." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trạng thái Loop được ghi lại từ khi phiên bản Loop này chạy lần đầu." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop 状态从此版本 Loop 首次运行时开始记录。" + } + } + } + }, + "Loop trends" : { + "comment" : "Title of the exported Trends PDF report", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اتجاهات Loop" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendy Loop" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-tendenser" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-Trends" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendencias de Loop" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-trendit" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendances Loop" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מגמות Loop" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-trendek" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendenze Loop" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopトレンド" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-trender" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-trends" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendy Loop" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendências do Loop" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendințe Loop" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренды Loop" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendy Loop" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-trender" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop eğilimleri" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренди Loop" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xu hướng Loop" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop 趋势" + } + } + } + }, + "Loop trends %1$@" : { + "comment" : "File name of an exported Trends report or CSV, without the extension. (1: date range of the period)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اتجاهات Loop %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendy Loop %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-tendenser %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-Trends %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendencias de Loop %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-trendit %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendances Loop %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מגמות Loop %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-trendek %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendenze Loop %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loopトレンド %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-trender %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-trends %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendy Loop %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendências do Loop %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendințe Loop %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренды Loop %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendy Loop %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop-trender %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop eğilimleri %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренди Loop %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xu hướng Loop %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop 趋势 %1$@" + } + } + } + }, + "Manual %1$@" : { + "comment" : "Basal and bolus: bolus the person gave. (1: units, e.g. 16.2 U)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يدوي %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ručně %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manuel %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manuell %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manual %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manuaalinen %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manuel %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ידני %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kézi %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manuale %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "手動 %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manuell %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Handmatig %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ręcznie %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manual %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manual %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вручную %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ručne %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manuell %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manuel %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вручну %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thủ công %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "手动 %1$@" + } + } + } + }, + "Median" : { + "comment" : "Trends Daily pattern legend: the median glucose line", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الوسيط" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medián" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Median" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Median" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mediana" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mediaani" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Médiane" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "חציון" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medián" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mediana" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中央値" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Median" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mediaan" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mediana" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mediana" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mediană" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Медиана" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medián" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Median" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medyan" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Медіана" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trung vị" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "中位数" + } + } + } + }, + "Missing data error" : { + "comment" : "Reason Loop could not run: data it needs was missing", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "خطأ بيانات مفقودة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba chybějících dat" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fejl: manglende data" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fehler: fehlende Daten" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error de datos faltantes" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tietoja puuttuu" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur de données manquantes" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שגיאת נתונים חסרים" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiányzó adat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errore dati mancanti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "データ欠落エラー" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Feil: manglende data" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fout: ontbrekende gegevens" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Błąd brakujących danych" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro de dados ausentes" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eroare: date lipsă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка: нет данных" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba chýbajúcich údajov" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data saknas" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eksik veri hatası" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помилка: немає даних" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi thiếu dữ liệu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据缺失错误" + } + } + } + }, + "Needs 10 days of CGM" : { + "comment" : "Trends GMI tile detail when fewer than 10 days of CGM data are available", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يحتاج إلى 10 أيام من CGM" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vyžaduje 10 dní CGM" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kræver 10 dages CGM" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Benötigt 10 Tage CGM" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Requiere 10 días de CGM" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vaatii 10 päivää CGM-dataa" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nécessite 10 jours de CGM" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "נדרשים 10 ימי CGM" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "10 nap CGM-adat szükséges" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Servono 10 giorni di CGM" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGMデータが10日分必要" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Krever 10 dager med CGM" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vereist 10 dagen CGM" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wymaga 10 dni CGM" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Requer 10 dias de CGM" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Necesită 10 zile de CGM" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нужно 10 дней CGM" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vyžaduje 10 dní CGM" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kräver 10 dagars CGM" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "10 günlük CGM gerekir" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Потрібно 10 днів CGM" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cần 10 ngày dữ liệu CGM" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要 10 天 CGM 数据" + } + } + } + }, + "Next day" : { + "comment" : "Accessibility label for the day page button that steps forward a day", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اليوم التالي" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Další den" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Næste dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nächster Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Día siguiente" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seuraava päivä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jour suivant" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "היום הבא" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Következő nap" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giorno successivo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "翌日" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neste dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volgende dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Następny dzień" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Próximo dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ziua următoare" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Следующий день" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ďalší deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nästa dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sonraki gün" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Наступний день" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ngày sau" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "后一天" + } + } + } + }, + "Next period" : { + "comment" : "VoiceOver label for the Trends button that moves forward one period", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الفترة التالية" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Další období" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Næste periode" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nächster Zeitraum" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Período siguiente" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seuraava jakso" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Période suivante" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "התקופה הבאה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Következő időszak" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periodo successivo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "次の期間" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neste periode" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Volgende periode" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Następny okres" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Próximo período" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perioada următoare" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Следующий период" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ďalšie obdobie" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nästa period" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sonraki dönem" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Наступний період" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khoảng thời gian sau" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一时段" + } + } + } + }, + "No active insulin or carbs on this day" : { + "comment" : "Day page placeholder when there was no insulin or carbs on board all day", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا يوجد أنسولين أو كارب نشط في هذا اليوم" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "V tento den nebyl žádný aktivní inzulín ani sacharidy" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intet aktivt insulin eller kulhydrater denne dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kein aktives Insulin und keine aktiven KH an diesem Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin insulina ni carbohidratos activos este día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei aktiivista insuliinia tai hiilihydraatteja tänä päivänä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune insuline ni glucides actifs ce jour-là" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אין אינסולין או פחמימות פעילים ביום זה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ezen a napon nem volt aktív inzulin vagy szénhidrát" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessuna insulina o carboidrati attivi in questo giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この日は残存インスリンも残存糖質もありません" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen aktivt insulin eller karbohydrater denne dagen" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen actieve insuline of koolhydraten op deze dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak aktywnej insuliny i węglowodanów tego dnia" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sem insulina ou carboidratos ativos neste dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fără insulină sau carbohidrați activi în această zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "В этот день не было активного инсулина или углеводов" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "V tento deň nebol žiadny aktívny inzulín ani sacharidy" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inget aktivt insulin eller kolhydrater denna dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu gün aktif insülin veya karb yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цього дня не було активного інсуліну чи вуглеводів" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có insulin hoặc Carbs còn hoạt động trong ngày này" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "这一天没有活性胰岛素或碳水" + } + } + } + }, + "No CGM data" : { + "comment" : "Trends Average tile detail when the period has no glucose readings", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد بيانات CGM" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádná data CGM" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine CGM-Daten" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin datos del CGM" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei CGM-dataa" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune donnée CGM" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אין נתוני CGM" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nincs CGM-adat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessun dato CGM" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGMデータなし" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen CGM-gegevens" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak danych CGM" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sem dados do CGM" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fără date CGM" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет данных CGM" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žiadne údaje CGM" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inga CGM-data" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM verisi yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Немає даних CGM" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có dữ liệu CGM" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无 CGM 数据" + } + } + } + }, + "No CGM data %1$@ – %2$@" : { + "comment" : "Chart note: a gap in glucose readings. (1: start time; 2: end time)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد بيانات CGM %1$@ – %2$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádná data CGM %1$@ – %2$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data %1$@ – %2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine CGM-Daten %1$@ – %2$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin datos del CGM %1$@ – %2$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei CGM-dataa %1$@ – %2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune donnée CGM %1$@ – %2$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אין נתוני CGM %1$@ – %2$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nincs CGM-adat %1$@ – %2$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessun dato CGM %1$@ – %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGMデータなし %1$@ – %2$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data %1$@ – %2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen CGM-gegevens %1$@ – %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak danych CGM %1$@ – %2$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sem dados do CGM %1$@ – %2$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fără date CGM %1$@ – %2$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет данных CGM %1$@ – %2$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žiadne údaje CGM %1$@ – %2$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inga CGM-data %1$@ – %2$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM verisi yok %1$@ – %2$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Немає даних CGM %1$@ – %2$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có dữ liệu CGM %1$@ – %2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无 CGM 数据 %1$@ – %2$@" + } + } + } + }, + "No CGM data %1$@ – %2$@ and %3$@" : { + "comment" : "Chart note: the longest gap in glucose readings, plus the shorter ones. (1: start time; 2: end time; 3: number of shorter gaps, e.g. 2 shorter gaps)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد بيانات CGM %1$@ – %2$@ و %3$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádná data CGM %1$@ – %2$@ a %3$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data %1$@ – %2$@ og %3$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine CGM-Daten %1$@ – %2$@ und %3$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin datos del CGM %1$@ – %2$@ y %3$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei CGM-dataa %1$@ – %2$@ ja %3$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune donnée CGM %1$@ – %2$@ et %3$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אין נתוני CGM %1$@ – %2$@ ו-%3$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nincs CGM-adat %1$@ – %2$@, és %3$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessun dato CGM %1$@ – %2$@ e %3$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGMデータなし %1$@ – %2$@(%3$@)" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data %1$@ – %2$@ og %3$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen CGM-gegevens %1$@ – %2$@ en %3$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak danych CGM %1$@ – %2$@ oraz %3$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sem dados do CGM %1$@ – %2$@ e %3$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fără date CGM %1$@ – %2$@ și %3$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет данных CGM %1$@ – %2$@ и ещё %3$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žiadne údaje CGM %1$@ – %2$@ a %3$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inga CGM-data %1$@ – %2$@ och %3$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM verisi yok %1$@ – %2$@ ve %3$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Немає даних CGM %1$@ – %2$@ і ще %3$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có dữ liệu CGM %1$@ – %2$@ và %3$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无 CGM 数据 %1$@ – %2$@,%3$@" + } + } + } + }, + "No CGM data in this period" : { + "comment" : "Trends chart placeholder when the period has no glucose readings", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد بيانات CGM في هذه الفترة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádná data CGM v tomto období" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data i denne periode" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine CGM-Daten in diesem Zeitraum" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin datos del CGM en este período" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei CGM-dataa tällä jaksolla" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune donnée CGM sur cette période" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אין נתוני CGM בתקופה זו" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nincs CGM-adat ebben az időszakban" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessun dato CGM in questo periodo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この期間のCGMデータなし" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data i denne perioden" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen CGM-gegevens in deze periode" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak danych CGM w tym okresie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sem dados do CGM neste período" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fără date CGM în această perioadă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет данных CGM за этот период" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žiadne údaje CGM v tomto období" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inga CGM-data under denna period" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu dönemde CGM verisi yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Немає даних CGM за цей період" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có dữ liệu CGM trong khoảng thời gian này" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此时段无 CGM 数据" + } + } + } + }, + "No CGM data on this day" : { + "comment" : "Chart note: there are no glucose readings for the whole day", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد بيانات CGM في هذا اليوم" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádná data CGM v tento den" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data denne dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine CGM-Daten an diesem Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin datos del CGM este día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei CGM-dataa tänä päivänä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune donnée CGM ce jour-là" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אין נתוני CGM ביום זה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nincs CGM-adat ezen a napon" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessun dato CGM in questo giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この日のCGMデータなし" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data denne dagen" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen CGM-gegevens op deze dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak danych CGM tego dnia" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sem dados do CGM neste dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fără date CGM în această zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет данных CGM за этот день" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žiadne údaje CGM v tento deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inga CGM-data denna dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu günde CGM verisi yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Немає даних CGM за цей день" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có dữ liệu CGM trong ngày này" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "这一天无 CGM 数据" + } + } + } + }, + "No CGM data yet today" : { + "comment" : "Chart note: there are no glucose readings so far today", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد بيانات CGM اليوم حتى الآن" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dnes zatím žádná data CGM" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data endnu i dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heute noch keine CGM-Daten" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aún no hay datos del CGM hoy" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei vielä CGM-dataa tänään" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune donnée CGM aujourd'hui pour l'instant" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אין עדיין נתוני CGM היום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ma még nincs CGM-adat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ancora nessun dato CGM oggi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日はまだCGMデータなし" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen CGM-data ennå i dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vandaag nog geen CGM-gegevens" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dziś jeszcze brak danych CGM" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ainda sem dados do CGM hoje" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Încă nu există date CGM azi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сегодня пока нет данных CGM" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dnes zatiaľ žiadne údaje CGM" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inga CGM-data än i dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bugün henüz CGM verisi yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сьогодні ще немає даних CGM" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hôm nay chưa có dữ liệu CGM" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今天尚无 CGM 数据" + } + } + } + }, + "No gaps: Loop ran closed loop all through this period." : { + "comment" : "Trends Gaps list when Loop was in closed loop for the whole period", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا فجوات: عمل Loop بالحلقة المغلقة طوال هذه الفترة." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádné výpadky: Loop běžel v uzavřené smyčce celé období." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen huller: Loop kørte lukket Loop i hele perioden." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Lücken: Loop lief im gesamten Zeitraum im Closed Loop." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sin interrupciones: Loop funcionó en circuito cerrado durante todo el período." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei katkoja: Loop toimi suljetulla säädöllä koko jakson ajan." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune interruption : Loop est resté en boucle fermée toute la période." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אין פערים: Loop פעל ב-Closed Loop לאורך כל התקופה." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nincs kimaradás: a Loop az egész időszakban zárt hurokban futott." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessuna interruzione: Loop è rimasto in loop chiuso per tutto il periodo." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "中断なし:この期間中、Loopは常にクローズドループで動作しました。" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen hull: Loop kjørte lukket Loop gjennom hele perioden." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen onderbrekingen: Loop draaide de hele periode in actieve loop." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak przerw: Loop działał w pętli zamkniętej przez cały okres." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sem interrupções: o Loop ficou em ciclo fechado durante todo o período." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fără întreruperi: Loop a rulat în buclă închisă toată perioada." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пропусков нет: Loop работал в замкнутом цикле весь период." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žiadne výpadky: Loop bežal v uzavretej slučke celé obdobie." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inga luckor: Loop körde sluten loop under hela perioden." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boşluk yok: Loop bu dönem boyunca kapalı döngüde çalıştı." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Пропусків немає: Loop працював у замкнутому циклі весь період." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không gián đoạn: Loop chạy vòng lặp kín suốt khoảng thời gian này." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无中断:此时段内 Loop 一直以闭环运行。" + } + } + } + }, + "No insulin recorded in this period" : { + "comment" : "Trends Insulin per day chart placeholder when no insulin was delivered in the period", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لم يُسجَّل أنسولين في هذه الفترة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "V tomto období nebyl zaznamenán žádný inzulín" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Intet insulin registreret i denne periode" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kein Insulin in diesem Zeitraum erfasst" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se registró insulina en este período" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei kirjattua insuliinia tällä jaksolla" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune insuline enregistrée sur cette période" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "לא נרשם אינסולין בתקופה זו" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nincs rögzített inzulin ebben az időszakban" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessuna insulina registrata in questo periodo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この期間のインスリン記録なし" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen insulin registrert i denne perioden" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen insuline vastgelegd in deze periode" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak zapisanej insuliny w tym okresie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nenhuma insulina registrada neste período" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nicio insulină înregistrată în această perioadă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "За этот период инсулин не записан" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "V tomto období nebol zaznamenaný žiadny inzulín" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inget insulin registrerat under denna period" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu dönemde kaydedilmiş insülin yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "За цей період інсулін не записано" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có insulin được ghi lại trong khoảng thời gian này" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此时段内无胰岛素记录" + } + } + } + }, + "No loop runs kept" : { + "comment" : "Trends Loop active tile detail when no Loop run history is available for the period", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد تشغيلات Loop محفوظة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žádné uložené běhy" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen Loop-kørsler gemt" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Loop-Durchläufe gespeichert" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se guardaron ejecuciones de Loop" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei tallennettuja Loop-ajoja" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune exécution de Loop conservée" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "לא נשמרו הרצות Loop" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nincs megőrzött Loop-futás" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessuna esecuzione di Loop conservata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop実行記録なし" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen Loop-kjøringer lagret" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen loop-runs bewaard" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak zapisanych cykli" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nenhuma execução do Loop guardada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu sunt păstrate rulări Loop" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет сохранённых запусков" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Žiadne uložené behy" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inga loopkörningar sparade" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saklanan Loop çalışması yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Немає збережених запусків" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không lưu lần chạy Loop nào" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无 Loop 运行记录" + } + } + } + }, + "No loop runs recorded in this period" : { + "comment" : "Trends Loop status placeholder when there is no Loop run history for the period", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لم تُسجَّل تشغيلات Loop في هذه الفترة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "V tomto období nebyly zaznamenány žádné běhy Loop" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen Loop-kørsler registreret i denne periode" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Loop-Durchläufe in diesem Zeitraum erfasst" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se registraron ejecuciones de Loop en este período" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei Loop-ajoja tällä jaksolla" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune exécution de Loop enregistrée sur cette période" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "לא נרשמו הרצות Loop בתקופה זו" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nincs rögzített Loop-futás ebben az időszakban" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessuna esecuzione di Loop registrata in questo periodo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この期間のLoop実行記録なし" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ingen Loop-kjøringer registrert i denne perioden" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen loop-runs vastgelegd in deze periode" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak zapisanych cykli Loop w tym okresie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nenhuma execução do Loop registrada neste período" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nicio rulare Loop înregistrată în această perioadă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "За этот период запуски Loop не записаны" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "V tomto období neboli zaznamenané žiadne behy Loop" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inga loopkörningar registrerade under denna period" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu dönemde kaydedilmiş Loop çalışması yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "За цей період запуски Loop не записано" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có lần chạy Loop nào được ghi lại trong khoảng thời gian này" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此时段内无 Loop 运行记录" + } + } + } + }, + "Not enough days for a pattern: %1$@ of 5" : { + "comment" : "Trends Daily pattern footnote when fewer than 5 days have readings; the chart shows raw readings instead. (1: days with readings)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "أيام غير كافية لنمط: %1$@ من 5" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Málo dní pro profil: %1$@ z 5" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ikke nok dage til et mønster: %1$@ af 5" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nicht genug Tage für ein Profil: %1$@ von 5" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No hay suficientes días para un patrón: %1$@ de 5" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Liian vähän päiviä profiiliin: %1$@ / 5" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pas assez de jours pour un profil : %1$@ sur 5" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "אין מספיק ימים לדפוס: %1$@ מתוך 5" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nincs elég nap a mintázathoz: %1$@ / 5" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giorni insufficienti per un andamento: %1$@ su 5" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "パターン作成には日数不足:%1$@ / 5日" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ikke nok dager for et mønster: %1$@ av 5" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niet genoeg dagen voor een patroon: %1$@ van 5" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Za mało dni na profil: %1$@ z 5" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dias insuficientes para um padrão: %1$@ de 5" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prea puține zile pentru un tipar: %1$@ din 5" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мало дней для профиля: %1$@ из 5" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Málo dní pre profil: %1$@ z 5" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "För få dagar för ett mönster: %1$@ av 5" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Örüntü için yeterli gün yok: %1$@ / 5" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Замало днів для профілю: %1$@ з 5" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không đủ ngày để tạo mẫu: %1$@ / 5" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "天数不足以生成模式:%1$@ / 5" + } + } + } + }, + "Now" : { + "comment" : "Title at the top of the Now screen, showing current glucose and Loop status Title of the Now tab in the redesigned interface, showing current glucose and Loop status", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الآن" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nyní" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jetzt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ahora" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nyt" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maintenant" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "עכשיו" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Most" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ora" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nå" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teraz" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Agora" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acum" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сейчас" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Teraz" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nu" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şimdi" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зараз" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiện tại" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前" + } + } + } + }, + "Open loop / manual" : { + "comment" : "Trends Loop status: time closed loop was switched off", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "حلقة مفتوحة / يدوي" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otevřená smyčka / ručně" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Åbent Loop / manuel" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open Loop / manuell" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Circuito abierto / manual" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avoin säätö / manuaalinen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boucle ouverte / manuel" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open Loop / ידני" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nyitott hurok / kézi" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop aperto / manuale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "オープンループ/手動" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Åpen Loop / manuell" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open loop / handmatig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pętla otwarta / ręcznie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ciclo aberto / manual" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Buclă deschisă / manual" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открытый цикл / вручную" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otvorená slučka / ručne" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öppen loop / manuell" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açık döngü / manuel" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відкритий цикл / вручну" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vòng lặp mở / thủ công" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "开环 / 手动" + } + } + } + }, + "Opens the full day" : { + "comment" : "VoiceOver hint for the Today so far card on the Now screen", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يفتح اليوم كاملًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otevře celý den" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Åbner hele dagen" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öffnet den ganzen Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abre el día completo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avaa koko päivän" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvre la journée complète" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "פותח את היום המלא" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Megnyitja a teljes napot" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apre il giorno completo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1日全体を開きます" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Åpner hele dagen" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Opent de hele dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwiera cały dzień" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abre o dia completo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deschide ziua completă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Открывает весь день" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otvorí celý deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öppnar hela dagen" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm günü açar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відкриває весь день" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mở toàn bộ ngày" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开全天视图" + } + } + } + }, + "PDF report" : { + "comment" : "Trends export menu item: share a PDF report of the period", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تقرير PDF" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zpráva PDF" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF-rapport" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF-Bericht" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Informe PDF" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF-raportti" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rapport PDF" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "דוח PDF" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF-jelentés" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Report PDF" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDFレポート" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF-rapport" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF-rapport" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Raport PDF" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Relatório PDF" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Raport PDF" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Отчёт PDF" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Správa PDF" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF-rapport" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF raporu" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Звіт PDF" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Báo cáo PDF" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "PDF 报告" + } + } + } + }, + "per day, weekly average" : { + "comment" : "Trends Insulin per day card subtitle when each bar is a week's average daily total", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يوميًا، متوسط أسبوعي" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "za den, týdenní průměr" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "pr. dag, ugegennemsnit" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "pro Tag, Wochendurchschnitt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "por día, promedio semanal" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "päivässä, viikkokeskiarvo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "par jour, moyenne hebdomadaire" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ליום, ממוצע שבועי" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "naponta, heti átlag" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "al giorno, media settimanale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1日あたり、週平均" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "per dag, ukesgjennomsnitt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "per dag, weekgemiddelde" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "dziennie, średnia tygodniowa" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "por dia, média semanal" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "pe zi, medie săptămânală" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "в день, среднее за неделю" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "za deň, týždenný priemer" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "per dag, veckomedel" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "günlük, haftalık ortalama" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "на день, середнє за тиждень" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "mỗi ngày, trung bình theo tuần" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日,按周平均" + } + } + } + }, + "Previous day" : { + "comment" : "Accessibility label for the day page button that steps back a day", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اليوم السابق" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Předchozí den" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forrige dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vorheriger Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Día anterior" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edellinen päivä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jour précédent" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "היום הקודם" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Előző nap" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giorno precedente" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "前日" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forrige dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vorige dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Poprzedni dzień" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dia anterior" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ziua anterioară" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предыдущий день" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Predchádzajúci deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Föregående dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önceki gün" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Попередній день" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ngày trước" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "前一天" + } + } + } + }, + "Previous period" : { + "comment" : "VoiceOver label for the Trends button that moves back one period", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الفترة السابقة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Předchozí období" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forrige periode" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vorheriger Zeitraum" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Período anterior" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edellinen jakso" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Période précédente" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "התקופה הקודמת" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Előző időszak" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periodo precedente" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "前の期間" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forrige periode" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vorige periode" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Poprzedni okres" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Período anterior" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perioada anterioară" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Предыдущий период" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Predchádzajúce obdobie" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Föregående period" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önceki dönem" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Попередній період" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khoảng thời gian trước" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上一时段" + } + } + } + }, + "Projected" : { + "comment" : "Chart legend: dashed lines show Loop's projection of insulin and carbs on board", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المتوقع" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Předpověď" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forventet" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prognose" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proyectado" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ennuste" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Projeté" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "צפי" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Előrevetített" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Previsto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "予測" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beregnet" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verwacht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prognozowane" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Projetado" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Proiectat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прогнозируемые" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Predpoveď" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prognos" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öngörülen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Прогнозовані" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dự kiến" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预计" + } + } + } + }, + "Pump data too old" : { + "comment" : "Reason Loop could not run: the latest pump data was too old", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "بيانات المضخة قديمة جدًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data pumpy jsou příliš stará" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpedata for gamle" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpendaten zu alt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos de la microinfusadora demasiado antiguos" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpun tiedot liian vanhoja" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Données de la pompe trop anciennes" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "נתוני המשאבה ישנים מדי" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Túl régi pumpaadatok" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dati pompa troppo vecchi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ポンプデータが古すぎます" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpedata for gamle" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompgegevens te oud" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dane pompy zbyt stare" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dados da bomba muito antigos" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Date pompă prea vechi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Данные помпы устарели" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Údaje pumpy sú príliš staré" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpdata för gamla" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa verisi çok eski" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дані помпи застаріли" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dữ liệu bơm quá cũ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素泵数据过旧" + } + } + } + }, + "Pump manager error" : { + "comment" : "Reason Loop could not run: the pump reported an error", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "خطأ في مدير المضخة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba správce pumpy" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpefejl" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpenfehler" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error del gestor de la microinfusadora" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpunhallinnan virhe" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur du gestionnaire de pompe" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שגיאת מנהל המשאבה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpakezelő-hiba" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errore del gestore pompa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ポンプマネージャのエラー" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpefeil" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompfout" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Błąd menedżera pompy" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro do gerenciador da bomba" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eroare manager pompă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ошибка менеджера помпы" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chyba správcu pumpy" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fel i pumphanteraren" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa yöneticisi hatası" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помилка менеджера помпи" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi trình quản lý bơm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素泵管理器错误" + } + } + } + }, + "Pump suspended" : { + "comment" : "Reason Loop could not run: insulin delivery on the pump was suspended", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المضخة متوقفة مؤقتًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpa pozastavena" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpe suspenderet" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpe unterbrochen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Microinfusadora suspendida" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumppu pysäytetty" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompe suspendue" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "המשאבה מושהית" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpa felfüggesztve" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa sospesa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ポンプ一時停止中" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpe stanset" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pomp onderbroken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa wstrzymana" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bomba suspensa" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompă suspendată" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помпа приостановлена" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pumpa pozastavená" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pump pausad" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa askıya alındı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Помпу призупинено" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bơm đã tạm ngưng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "胰岛素泵已暂停" + } + } + } + }, + "Recommendation expired" : { + "comment" : "Reason Loop could not run: its dosing recommendation expired before it was used", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "انتهت صلاحية التوصية" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doporučení vypršelo" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anbefaling udløbet" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Empfehlung abgelaufen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recomendación caducada" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suositus vanhentunut" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recommandation expirée" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "תוקף ההמלצה פג" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lejárt javaslat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suggerimento scaduto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "推奨の有効期限切れ" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anbefaling utløpt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advies verlopen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zalecenie wygasło" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recomendação expirada" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recomandare expirată" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рекомендация устарела" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Odporúčanie vypršalo" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rekommendationen har gått ut" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önerinin süresi doldu" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Рекомендація застаріла" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khuyến nghị đã hết hạn" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "建议已过期" + } + } + } + }, + "SD %1$@ · CV %2$@%%" : { + "comment" : "Trends Average tile detail: standard deviation and coefficient of variation of glucose. (1: standard deviation in the glucose unit, 2: coefficient of variation as a percentage number)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@ %%" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · VK %2$@ %%" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DE %1$@ · CV %2$@%%" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@ %%" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ET %1$@ · CV %2$@ %%" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DS %1$@ · CV %2$@%%" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@ %%" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "DP %1$@ · CV %2$@%%" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@ %%" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %%%2$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + } + } + }, + "Summary" : { + "comment" : "Trends card title: the summary statistics tiles", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الملخص" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Souhrn" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oversigt" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Übersicht" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resumen" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yhteenveto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Résumé" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סיכום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Összegzés" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Riepilogo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "概要" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oversikt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Overzicht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podsumowanie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Resumo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rezumat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сводка" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Súhrn" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sammanfattning" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Özet" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підсумок" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tóm tắt" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "摘要" + } + } + } + }, + "Suspended" : { + "comment" : "Chart callout value: insulin delivery was suspended at the touched time Shown in the Basal tile on the Now screen when insulin delivery is suspended", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "متوقف مؤقتًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozastaveno" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suspenderet" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unterbrochen" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suspendida" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pysäytetty" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suspendu" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מושהה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Felfüggesztve" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sospesa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "一時停止中" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Stanset" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onderbroken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wstrzymano" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suspensa" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Suspendat" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Приостановлено" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pozastavené" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pausad" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Askıya alındı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Призупинено" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Đã tạm ngưng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已暂停" + } + } + } + }, + "this day" : { + "comment" : "Trends Basal and bolus card subtitle when the period is one complete day", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "هذا اليوم" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "tento den" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "denne dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "dieser Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "este día" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "tämä päivä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ce jour" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "יום זה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "ez a nap" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "questo giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "この日" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "denne dagen" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "deze dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "ten dzień" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "este dia" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "această zi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "этот день" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "tento deň" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "denna dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "bu gün" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "цей день" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ngày này" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当天" + } + } + } + }, + "Time" : { + "comment" : "Carb entry row label: the exact time the food was eaten", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الوقت" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Čas" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tidspunkt" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uhrzeit" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hora" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aika" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heure" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שעה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Időpont" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ora" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "時刻" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tidspunkt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tijd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Godzina" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hora" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ora" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Время" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Čas" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tid" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saat" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Час" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thời gian" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时间" + } + } + } + }, + "Time in range" : { + "comment" : "Trends card title: share of glucose readings in each range", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الوقت ضمن النطاق" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Čas v rozsahu" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tid i området" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeit im Zielbereich" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo en rango" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aika tavoitealueella" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temps dans la cible" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "זמן בטווח" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tartományban töltött idő" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tempo nell'intervallo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "範囲内時間" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tid i målområdet" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tijd binnen bereik" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Czas w zakresie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tempo no alvo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Timp în interval" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Время в диапазоне" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Čas v rozsahu" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tid inom målområdet" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aralıkta geçen süre" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Час у діапазоні" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thời gian trong khoảng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "目标范围内时间" + } + } + } + }, + "Time in range, daily pattern, insulin per day and Loop status" : { + "comment" : "Trends settings descriptive text", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الوقت ضمن النطاق والنمط اليومي والإنسولين اليومي وحالة Loop" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Čas v rozmezí, denní průběh, inzulin za den a stav Loop" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tid i område, døgnmønster, insulin pr. dag og Loop-status" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeit im Zielbereich, Tagesverlauf, Insulin pro Tag und Loop-Status" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tiempo en rango, patrón diario, insulina por día y estado de Loop" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aika tavoitealueella, päivittäinen kuvio, insuliini päivässä ja Loopin tila" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Temps dans la cible, profil quotidien, insuline par jour et état de Loop" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "זמן בטווח, דפוס יומי, אינסולין ליום ומצב Loop" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Céltartományban töltött idő, napi mintázat, napi inzulin és Loop állapot" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tempo nell'intervallo, andamento giornaliero, insulina al giorno e stato di Loop" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "範囲内時間、日内パターン、1日のインスリン、Loopの状態" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tid i målområdet, døgnmønster, insulin per dag og Loop-status" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tijd in bereik, dagpatroon, insuline per dag en loopstatus" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Czas w zakresie, profil dobowy, insulina na dzień i stan Loop" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tempo no alvo, padrão diário, insulina por dia e status do Loop" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Timp în interval, tipar zilnic, insulină pe zi și starea Loop" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Время в диапазоне, суточный профиль, инсулин в день и статус Loop" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Čas v rozmedzí, denný priebeh, inzulín za deň a stav Loop" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tid inom målområdet, dygnsmönster, insulin per dag och Loopstatus" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef aralıkta geçen süre, günlük örüntü, günlük insülin ve Loop durumu" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Час у діапазоні, добовий профіль, інсулін на день і стан Loop" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thời gian trong ngưỡng, mẫu hình hằng ngày, insulin mỗi ngày và trạng thái Loop" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "目标范围内时间、每日模式、每日胰岛素和 Loop 状态" + } + } + } + }, + "Today" : { + "comment" : "Day page title when showing today", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اليوم" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dnes" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "I dag" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heute" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoy" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tänään" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aujourd'hui" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "היום" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ma" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oggi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "I dag" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vandaag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dziś" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoje" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Azi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сегодня" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dnes" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "I dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bugün" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сьогодні" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hôm nay" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今天" + } + } + } + }, + "today so far" : { + "comment" : "Trends Basal and bolus card subtitle when the figures are today's total so far", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اليوم حتى الآن" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "dnes zatím" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "i dag indtil nu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "heute bisher" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "hoy hasta ahora" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "tänään tähän mennessä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "aujourd'hui jusqu'ici" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "היום עד כה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "ma eddig" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "oggi finora" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日のこれまで" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "i dag så langt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "vandaag tot nu toe" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "dziś do tej pory" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "hoje até agora" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "azi până acum" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "сегодня на данный момент" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "dnes zatiaľ" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "hittills i dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "bugün şu ana kadar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "сьогодні наразі" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "hôm nay đến giờ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日至今" + } + } + } + }, + "Today so far" : { + "comment" : "Title of the card on the Now screen showing today's insulin and carbs", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اليوم حتى الآن" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dnes zatím" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "I dag indtil nu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heute bisher" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoy hasta ahora" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tänään tähän mennessä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aujourd'hui jusqu'ici" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "היום עד כה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ma eddig" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oggi finora" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日のこれまで" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "I dag så langt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vandaag tot nu toe" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dziś do tej pory" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoje até agora" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Azi până acum" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сегодня на данный момент" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dnes zatiaľ" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hittills i dag" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bugün şu ana kadar" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сьогодні наразі" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hôm nay đến giờ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日至今" + } + } + } + }, + "Total" : { + "comment" : "Basal and bolus total row: basal plus bolus", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الإجمالي" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Celkem" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "I alt" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gesamt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yhteensä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סה״כ" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Összesen" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "合計" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totalt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totaal" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Razem" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всего" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spolu" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totalt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toplam" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Усього" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tổng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "总计" + } + } + } + }, + "Total %1$@" : { + "comment" : "VoiceOver: total insulin. (1: units)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الإجمالي %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Celkem %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "I alt %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gesamt %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yhteensä %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סה״כ %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Összesen %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totale %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "合計 %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totalt %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totaal %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Razem %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всего %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spolu %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totalt %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toplam %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Усього %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tổng %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "总计 %1$@" + } + } + } + }, + "Total so far" : { + "comment" : "Basal and bolus total row for today, which isn't over yet", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الإجمالي حتى الآن" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Celkem zatím" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "I alt indtil nu" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gesamt bisher" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total hasta ahora" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yhteensä tähän mennessä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total jusqu'ici" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סה״כ עד כה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Összesen eddig" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totale finora" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これまでの合計" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totalt så langt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totaal tot nu toe" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Razem do tej pory" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total até agora" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total până acum" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всего на данный момент" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spolu zatiaľ" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totalt hittills" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şu ana kadar toplam" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Усього наразі" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tổng đến giờ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "至今总计" + } + } + } + }, + "Total so far %1$@" : { + "comment" : "VoiceOver: today's total insulin so far. (1: units)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الإجمالي حتى الآن %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Celkem zatím %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "I alt indtil nu %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gesamt bisher %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total hasta ahora %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yhteensä tähän mennessä %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total jusqu'ici %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סה״כ עד כה %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Összesen eddig %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totale finora %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これまでの合計 %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totalt så langt %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totaal tot nu toe %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Razem do tej pory %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total até agora %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Total până acum %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всего на данный момент %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spolu zatiaľ %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Totalt hittills %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şu ana kadar toplam %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Усього наразі %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tổng đến giờ %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "至今总计 %1$@" + } + } + } + }, + "Touch and hold a chart to see exact values." : { + "comment" : "Hint under the day page charts explaining how to scrub them", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المس مخططًا مع الاستمرار لرؤية القيم الدقيقة." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podržte prst na grafu a uvidíte přesné hodnoty." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tryk og hold på et diagram for at se præcise værdier." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Halte ein Diagramm gedrückt, um genaue Werte zu sehen." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mantén pulsado un gráfico para ver los valores exactos." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pidä kaaviota painettuna nähdäksesi tarkat arvot." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touchez un graphique de manière prolongée pour voir les valeurs exactes." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "גע והחזק תרשים כדי לראות ערכים מדויקים." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Érints meg és tarts lenyomva egy diagramot a pontos értékekért." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tieni premuto su un grafico per vedere i valori esatti." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "グラフを長押しすると正確な値が表示されます。" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trykk og hold på et diagram for å se nøyaktige verdier." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Houd een grafiek ingedrukt om exacte waarden te zien." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dotknij i przytrzymaj wykres, aby zobaczyć dokładne wartości." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toque e segure um gráfico para ver os valores exatos." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Țineți apăsat pe un grafic pentru a vedea valorile exacte." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нажмите и удерживайте график, чтобы увидеть точные значения." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podržte prst na grafe a uvidíte presné hodnoty." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tryck och håll på ett diagram för att se exakta värden." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tam değerleri görmek için bir grafiğe dokunup basılı tutun." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Торкніться й утримуйте графік, щоб побачити точні значення." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chạm và giữ biểu đồ để xem giá trị chính xác." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "按住图表可查看精确数值。" + } + } + } + }, + "Trends" : { + "comment" : "Title of the Trends tab Title of the Trends tab in the redesigned interface", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الاتجاهات" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendy" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendenser" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trends" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendencias" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendit" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendances" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "מגמות" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendek" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendenze" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "トレンド" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trender" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trends" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendy" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendências" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tendințe" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренды" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendy" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trender" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eğilimler" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Тренди" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xu hướng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "趋势" + } + } + } + }, + "unknown" : { + "comment" : "VoiceOver value when active insulin or carbs at the current time isn't known", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "غير معروف" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "neznámé" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "ukendt" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "unbekannt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "desconocido" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "tuntematon" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "inconnu" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "לא ידוע" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "ismeretlen" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "sconosciuto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "ukjent" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "onbekend" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "nieznane" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "desconhecido" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "necunoscut" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "неизвестно" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "neznáme" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "okänt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "bilinmiyor" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "невідомо" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "không rõ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未知" + } + } + } + }, + "Unknown error" : { + "comment" : "Reason Loop could not run: an unexpected error", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "خطأ غير معروف" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neznámá chyba" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukendt fejl" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unbekannter Fehler" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error desconocido" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tuntematon virhe" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erreur inconnue" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שגיאה לא ידועה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ismeretlen hiba" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Errore sconosciuto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明なエラー" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ukjent feil" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onbekende fout" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nieznany błąd" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erro desconhecido" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eroare necunoscută" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Неизвестная ошибка" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neznáma chyba" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Okänt fel" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilinmeyen hata" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Невідома помилка" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lỗi không xác định" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未知错误" + } + } + } + }, + "Very high" : { + "comment" : "Time in range band: glucose above 250 mg/dL (13.9 mmol/L) Time-in-range band name: glucose above 250 mg/dL (13.9 mmol/L)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مرتفع جدًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velmi vysoká" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meget høj" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sehr hoch" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Muy alta" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erittäin korkea" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Très élevée" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "גבוה מאוד" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nagyon magas" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Molto alta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "非常に高い" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Svært høy" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeer hoog" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bardzo wysoka" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Muito alta" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Foarte ridicată" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очень высокая" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veľmi vysoká" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mycket högt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çok yüksek" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дуже висока" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rất cao" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "非常高" + } + } + } + }, + "Very low" : { + "comment" : "Time in range band: glucose below 54 mg/dL (3.0 mmol/L) Time-in-range band name: glucose below 54 mg/dL (3.0 mmol/L)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "منخفض جدًا" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Velmi nízká" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meget lav" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sehr niedrig" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Muy baja" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erittäin matala" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Très basse" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "נמוך מאוד" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nagyon alacsony" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Molto bassa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "非常に低い" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Svært lav" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeer laag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bardzo niska" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Muito baixa" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Foarte scăzută" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Очень низкая" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veľmi nízka" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mycket lågt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çok düşük" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дуже низька" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rất thấp" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "非常低" + } + } + } }, " - " : { @@ -2423,6 +21325,48 @@ "state" : "translated", "value" : "%1$@ U" } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ وحدة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ 単位" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ U" + } } } }, @@ -9054,7 +27998,147 @@ "comment" : "title for when automation was unsuccessful" }, "Average" : { - "comment" : "Average glucose metric title" + "comment" : "Average glucose metric title", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المتوسط" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Průměr" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gennemsnit" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Durchschnitt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promedio" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keskiarvo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moyenne" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ממוצע" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Átlag" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Media" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "平均" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gjennomsnitt" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gemiddeld" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Średnia" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Média" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medie" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Среднее" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Priemer" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Medelvärde" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ortalama" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Середнє" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trung bình" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "平均值" + } + } + } }, "Average Glucose" : { @@ -9797,6 +28881,24 @@ "state" : "translated", "value" : "大剂量" } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bólus" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Болюс" + } } } }, @@ -20530,6 +39632,24 @@ "state" : "translated", "value" : "克" } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "g" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "g" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "г" + } } } }, @@ -20742,6 +39862,18 @@ "state" : "translated", "value" : "血糖" } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glükóz" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Глюкоза" + } } } }, @@ -21181,7 +40313,147 @@ "comment" : "Apple Health settings descriptive text" }, "GMI" : { - "comment" : "Glucose Management Indicator metric title" + "comment" : "Glucose Management Indicator metric title", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + } + } }, "Go to Settings > Focus." : { "comment" : "Focus modes step 1" @@ -21279,7 +40551,147 @@ }, "High" : { - "comment" : "Time-in-range band label (181–250 mg/dL)" + "comment" : "Time-in-range band label (181–250 mg/dL)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مرتفع" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vysoká" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Høj" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoch" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alta" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Korkea" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Élevée" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "גבוה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Magas" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alta" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "高値" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Høy" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoog" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wysoka" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alta" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ridicată" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Высокая" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vysoká" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Högt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksek" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Висока" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cao" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "高" + } + } + } }, "High Glucose" : { "localizations" : { @@ -22241,6 +41653,30 @@ "state" : "translated", "value" : "胰岛素" } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulín" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulin" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inzulín" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Інсулін" + } } } }, @@ -25868,7 +45304,147 @@ } }, "Low" : { - "comment" : "Time-in-range band label (54–69 mg/dL)" + "comment" : "Time-in-range band label (54–69 mg/dL)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "منخفض" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nízká" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lav" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niedrig" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Baja" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Matala" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basse" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "נמוך" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alacsony" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bassa" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "低値" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lav" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Laag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Niska" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Baixa" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scăzută" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Низкая" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nízka" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lågt" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşük" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Низька" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thấp" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "低" + } + } + } }, "Low Glucose" : { "comment" : "Title for bolus screen warning when glucose is below glucose warning limit.\nTitle for bolus screen warning when glucose is below suspend threshold, but a bolus is recommended", @@ -29333,6 +48909,18 @@ "state" : "translated", "value" : "OK" } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } } } }, @@ -33281,6 +52869,18 @@ "state" : "translated", "value" : "重试" } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Újra" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Спробувати ще раз" + } } } }, @@ -33626,6 +53226,66 @@ "state" : "translated", "value" : "预设" } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المجدول" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plánovaný" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ohjelmoitu" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ütemezett" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "スケジュール" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Programada" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plánovaný" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schemalagd" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "За розкладом" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Theo lịch" + } } } }, @@ -38219,6 +57879,24 @@ "state" : "translated", "value" : "U" } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "U" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "U" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "U" + } } } }, diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 47f1b67d7a..6c95612287 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -518,6 +518,14 @@ class LoopAppManager: NSObject { analyticsServicesManager.application(didFinishLaunchingWithOptions: launchOptions) + // Loop's automation history only goes back 36 hours, so the Trends screen keeps its own longer record. + // The debounce keeps a quick off-and-on from being recorded as open loop. + ObservablePublishers.tracking(settingsManager, keyPath: \.dosingEnabled) + .debounce(for: .seconds(15), scheduler: RunLoop.main) + .removeDuplicates() + .sink { LoopModeHistory.shared.record(closedLoop: $0) } + .store(in: &cancellables) + state = state.next await loopDataManager.updateDisplayState() @@ -619,6 +627,8 @@ class LoopAppManager: NSObject { return SetupHistory(settings: settings, experiments: experiments) } viewModel.outcomesPackageBuilder = criticalEventLogExportManager.map { OutcomesPackageBuilder(logs: $0.logs) } + viewModel.historyProvider = LoopHistoryProvider(deviceManager: deviceDataManager, loopDataManager: loopDataManager, + settingsManager: settingsManager, temporaryPresetsManager: temporaryPresetsManager) return viewModel }() diff --git a/Loop/Redesign/DayCharts.swift b/Loop/Redesign/DayCharts.swift new file mode 100644 index 0000000000..83e2032d74 --- /dev/null +++ b/Loop/Redesign/DayCharts.swift @@ -0,0 +1,548 @@ +// +// DayCharts.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import Charts +import UIKit + +private typealias P = LoopDesign.Palette + +// MARK: - Shared axis helpers + +enum DayAxis { + /// Midnight, 6 AM, noon and 6 PM inside `window`, by the clock, so a day when the clocks change still marks 6 AM. + static func quarterDayMarks(in window: DateInterval, calendar: Calendar = .current) -> [Date] { + var marks: [Date] = [] + var day = calendar.startOfDay(for: window.start) + while day <= window.end { + for hour in [0, 6, 12, 18] { + if let mark = calendar.date(bySettingHour: hour, minute: 0, second: 0, of: day), window.contains(mark) { + marks.append(mark) + } + } + guard let next = calendar.date(byAdding: .day, value: 1, to: day) else { break } + day = next + } + return marks + } + + static func hourLabel(_ date: Date) -> String { + date.formatted(.dateTime.hour()) + } + + /// Gaps of more than 30 minutes between readings, so a chart can say where data is missing. + static func gaps(in points: [GlucosePoint], window: DateInterval, until end: Date) -> [DateInterval] { + let limit: TimeInterval = 30 * 60 + var gaps: [DateInterval] = [] + var previous = window.start + for point in points.sorted(by: { $0.date < $1.date }) where window.contains(point.date) { + if point.date.timeIntervalSince(previous) > limit { + gaps.append(DateInterval(start: previous, end: point.date)) + } + previous = point.date + } + let last = min(end, window.end) + if last.timeIntervalSince(previous) > limit { + gaps.append(DateInterval(start: previous, end: last)) + } + return gaps + } + + static func gapMessage(_ gaps: [DateInterval], window: DateInterval, until end: Date) -> String? { + guard let longest = gaps.max(by: { $0.duration < $1.duration }) else { return nil } + // A gap over the whole period would read "0:00 – 0:00". + if gaps.count == 1, longest.start == window.start, longest.end == min(end, window.end) { + return window.contains(end) + ? NSLocalizedString("No CGM data yet today", comment: "Chart note: there are no glucose readings so far today") + : NSLocalizedString("No CGM data on this day", comment: "Chart note: there are no glucose readings for the whole day") + } + let start = LoopFormat.time(longest.start) + let end = LoopFormat.time(longest.end) + if gaps.count > 1 { + let others = String.localizedStringWithFormat(NSLocalizedString("%lld shorter gaps", comment: "Chart note: how many other, shorter gaps in glucose readings there were; follows the longest gap. (1: number of gaps)"), gaps.count - 1) + return String(format: NSLocalizedString("No CGM data %1$@ – %2$@ and %3$@", comment: "Chart note: the longest gap in glucose readings, plus the shorter ones. (1: start time; 2: end time; 3: number of shorter gaps, e.g. 2 shorter gaps)"), start, end, others) + } + return String(format: NSLocalizedString("No CGM data %1$@ – %2$@", comment: "Chart note: a gap in glucose readings. (1: start time; 2: end time)"), start, end) + } +} + +// MARK: - InsulinTimeline + +/// Carbs, glucose and insulin for one day on one shared time axis. +/// +/// The lanes are drawn in a single chart with stacked y ranges so they can't drift apart: insulin 0–30, +/// glucose 36–92 (the fixed 2–20 mmol/L scale), carb markers at 97. +struct InsulinTimelineView: View { + let history: DayHistory + let unit: GlucoseUnitStyle + let targetBands: [TargetBand] + var now: Date = Date() + var height: CGFloat = 260 + + @State private var selection: Date? + @Environment(\.colorScheme) private var colorScheme + + private static let insulinTop = 30.0 + private static let basalTop = 12.0 + private static let glucoseRange = 36.0...92.0 + private static let carbLane = 97.0 + + private var window: DateInterval { history.window } + + private func y(glucose mgdl: Double) -> Double { + let domain = unit.dayDomain + let value = min(max(unit.value(mgdl), domain.lowerBound), domain.upperBound) + let fraction = (value - domain.lowerBound) / (domain.upperBound - domain.lowerBound) + return Self.glucoseRange.lowerBound + fraction * (Self.glucoseRange.upperBound - Self.glucoseRange.lowerBound) + } + + private var maxBasal: Double { + max(0.5, (history.basal.map(\.rate) + history.scheduledBasal.map(\.rate)).max() ?? 0) + } + + private var maxBolus: Double { + max(1, history.boluses.map(\.units).max() ?? 0) + } + + private func y(basal rate: Double) -> Double { rate / maxBasal * Self.basalTop } + private func y(bolus units: Double) -> Double { max(1.5, units / maxBolus * (Self.insulinTop - 4)) } + + private var gaps: [DateInterval] { DayAxis.gaps(in: history.glucose, window: window, until: now) } + + /// Labels closer than this to the previous label are dropped so they don't overprint; the marks still draw. + private var labelSpacing: TimeInterval { window.duration / 7 } + + private func spacedIDs(_ items: [T], date: (T) -> Date, id: (T) -> String) -> Set { + var kept = Set() + var last: Date? + for item in items.sorted(by: { date($0) < date($1) }) { + if let last, date(item).timeIntervalSince(last) < labelSpacing { continue } + kept.insert(id(item)) + last = date(item) + } + return kept + } + + /// One label per run of carb entries closer together than `labelSpacing`, giving the run's total. + /// It sits after the run's last marker, so no later marker in the run draws over it. + private var carbLabels: [String: Double] { + var labels: [String: Double] = [:] + var runStart: Date? + var runLast: (id: String, total: Double)? + for carb in history.carbs.sorted(by: { $0.date < $1.date }) { + if let start = runStart, let last = runLast, carb.date.timeIntervalSince(start) < labelSpacing { + runLast = (carb.id, last.total + carb.grams) + } else { + if let last = runLast { labels[last.id] = last.total } + runStart = carb.date + runLast = (carb.id, carb.grams) + } + } + if let last = runLast { labels[last.id] = last.total } + return labels + } + private var labelledBoluses: Set { spacedIDs(history.boluses.filter { !$0.automatic }, date: \.date, id: \.id) } + + var body: some View { + VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { + chart + .frame(height: height) + if let message = DayAxis.gapMessage(gaps, window: window, until: now), history.source == .loop || !history.glucose.isEmpty { + Text(message) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(LoopDesign.Palette.inkMuted) + } + } + } + + private var chart: some View { + Chart { + // Quarter-day rules + ForEach(DayAxis.quarterDayMarks(in: window), id: \.self) { mark in + RuleMark(x: .value("Time", mark)) + .foregroundStyle(P.grid) + .lineStyle(StrokeStyle(lineWidth: 1)) + } + + // Glucose lane + ForEach(Array(targetBands.enumerated()), id: \.offset) { _, band in + RectangleMark(xStart: .value("Start", band.start), xEnd: .value("End", band.end), + yStart: .value("Low", y(glucose: band.range.lowerBound)), yEnd: .value("High", y(glucose: band.range.upperBound))) + .foregroundStyle(P.targetBand) + } + ForEach(Array(history.glucose.enumerated()), id: \.offset) { _, point in + PointMark(x: .value("Time", point.date), y: .value("Glucose", y(glucose: point.mgdl))) + .symbolSize(10) + .foregroundStyle(P.glucose) + } + + // Carb markers + let carbLabels = carbLabels + ForEach(history.carbs) { carb in + PointMark(x: .value("Time", carb.date), y: .value("Carbs", Self.carbLane)) + .symbolSize(60) + .foregroundStyle(P.carbs) + .annotation(position: .trailing, spacing: 2) { + if let grams = carbLabels[carb.id] { + Text(LoopFormat.grams(grams)) + .font(LoopDesign.Typeface.caption) + .foregroundStyle(P.carbsInk) + } + } + } + + // Delivered basal, edged, then the schedule dashed over it + // Draw each recorded segment independently; a gap must not become an interpolated delivery area. + ForEach(Array(history.basal.enumerated()), id: \.offset) { _, segment in + RectangleMark(xStart: .value("Start", segment.start), xEnd: .value("End", segment.end), + yStart: .value("Base", 0), yEnd: .value("Basal", y(basal: segment.rate))) + .foregroundStyle(P.basal) + RuleMark(xStart: .value("Start", segment.start), xEnd: .value("End", segment.end), y: .value("Basal", y(basal: segment.rate))) + .foregroundStyle(P.basalEdge) + .lineStyle(StrokeStyle(lineWidth: 1.5)) + } + ForEach(Array(history.scheduledBasal.enumerated()), id: \.offset) { _, value in + RuleMark(xStart: .value("Start", value.start), xEnd: .value("End", value.end), y: .value("Scheduled", y(basal: value.rate))) + .foregroundStyle(P.basalScheduled) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3])) + } + + // Boluses over basal: solid for manual with a label, hatched for automatic. + // Each sits in a card-coloured slot a point wider, so it never touches the basal behind it. + ForEach(history.boluses) { bolus in + RectangleMark(x: .value("Time", bolus.date), yStart: .value("Base", 0), yEnd: .value("Bolus", y(bolus: bolus.units)), width: .fixed(7)) + .foregroundStyle(P.surfaceRaised) + } + let bolusLabels = labelledBoluses + ForEach(history.boluses) { bolus in + if bolus.automatic { + RectangleMark(x: .value("Time", bolus.date), yStart: .value("Base", 0), yEnd: .value("Bolus", y(bolus: bolus.units)), width: .fixed(5)) + .foregroundStyle(Hatch.paint(P.bolus, scheme: colorScheme)) + } else { + RectangleMark(x: .value("Time", bolus.date), yStart: .value("Base", 0), yEnd: .value("Bolus", y(bolus: bolus.units)), width: .fixed(5)) + .foregroundStyle(P.bolus) + .annotation(position: .top, spacing: 2) { + if bolusLabels.contains(bolus.id) { + Text(LoopFormat.number(bolus.units, decimals: 1)) + .font(LoopDesign.Typeface.caption) + .foregroundStyle(P.insulinInk) + } + } + } + } + + // Now + if window.contains(now) { + RuleMark(x: .value("Now", now)) + .foregroundStyle(P.inkMuted) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [2, 3])) + } + + if let selection { + RuleMark(x: .value("Selected", selection)) + .foregroundStyle(P.ink.opacity(0.6)) + .lineStyle(StrokeStyle(lineWidth: 1)) + } + } + .chartXScale(domain: window.start...window.end) + .chartYScale(domain: 0...104) + .chartXAxis { + AxisMarks(values: DayAxis.quarterDayMarks(in: window)) { value in + AxisValueLabel { + if let date = value.as(Date.self) { + Text(DayAxis.hourLabel(date)) + } + } + .font(LoopDesign.Typeface.caption) + .foregroundStyle(P.inkMuted) + } + } + .chartYAxis { + AxisMarks(position: .leading, values: unit.dayTicks.map { y(glucose: unit.mgdl($0)) }) { value in + AxisGridLine().foregroundStyle(P.grid) + AxisValueLabel { + if let mapped = value.as(Double.self), let tick = unit.dayTicks.first(where: { abs(y(glucose: unit.mgdl($0)) - mapped) < 0.01 }) { + Text(tick.formatted()) + } + } + .font(LoopDesign.Typeface.caption) + .foregroundStyle(P.inkMuted) + } + } + .chartOverlay { proxy in + ScrubOverlay(proxy: proxy, selection: $selection, clampTo: window) + } + .overlay(alignment: .top) { + if let selection { + ScrubCallout(rows: scrubRows(at: selection)) + .padding(.top, 2) + .allowsHitTesting(false) + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilitySummary) + } + + private func scrubRows(at date: Date) -> [ScrubCallout.Row] { + var rows: [ScrubCallout.Row] = [.init(label: nil, value: LoopFormat.time(date), color: P.ink)] + if let glucose = nearest(history.glucose.map { ($0.date, $0.mgdl) }, to: date, within: 10 * 60) { + rows.append(.init(label: NSLocalizedString("Glucose", comment: "Chart callout label for the glucose reading at the touched time"), value: "\(unit.format(glucose)) \(unit.unitName)", color: P.glucoseInk)) + } + if let iob = nearest(history.iob.map { ($0.date, $0.value) }, to: date, within: 10 * 60) { + rows.append(.init(label: NSLocalizedString("IOB", comment: "Abbreviation for insulin on board (active insulin)"), value: LoopFormat.doseUnits(iob), color: P.insulinInk)) + } + if let cob = nearest(history.cob.map { ($0.date, $0.value) }, to: date, within: 10 * 60) { + rows.append(.init(label: NSLocalizedString("COB", comment: "Abbreviation for carbs on board (active carbohydrate)"), value: LoopFormat.grams(cob), color: P.carbsInk)) + } + if let segment = LoopHistoryMath.basalRate(at: date, in: history.basal) { + let value = segment.kind == .suspended + ? NSLocalizedString("Suspended", comment: "Chart callout value: insulin delivery was suspended at the touched time") + : LoopFormat.rate(segment.rate) + rows.append(.init(label: NSLocalizedString("Basal", comment: "Chart callout label for the basal rate at the touched time"), value: value, color: P.ink)) + } + if let bolus = history.boluses.min(by: { abs($0.date.timeIntervalSince(date)) < abs($1.date.timeIntervalSince(date)) }), + abs(bolus.date.timeIntervalSince(date)) < 5 * 60 { + rows.append(.init(label: bolus.automatic + ? NSLocalizedString("Automatic bolus", comment: "Chart callout label for a bolus Loop delivered automatically near the touched time") + : NSLocalizedString("Bolus", comment: "Chart callout label for a bolus near the touched time"), + value: LoopFormat.doseUnits(bolus.units), color: P.insulinInk)) + } + return rows + } + + private var accessibilitySummary: String { + let day = LoopFormat.weekdayDay(window.start) + let bolus = LoopFormat.totalUnits(history.bolusUnits) + let basal = LoopFormat.totalUnits(history.basalUnits) + let carbs = LoopFormat.grams(history.carbGrams) + if let last = history.glucose.last { + return String(format: NSLocalizedString("Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@, latest glucose %5$@ %6$@", comment: "VoiceOver summary of the day's insulin timeline chart. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g; 5: latest glucose value; 6: glucose unit, mmol/L or mg/dL)"), day, bolus, basal, carbs, unit.format(last.mgdl), unit.unitName) + } + return String(format: NSLocalizedString("Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@", comment: "VoiceOver summary of the day's insulin timeline chart with no glucose readings. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g)"), day, bolus, basal, carbs) + } +} + +func nearest(_ values: [(Date, Double)], to date: Date, within limit: TimeInterval) -> Double? { + guard let best = values.min(by: { abs($0.0.timeIntervalSince(date)) < abs($1.0.timeIntervalSince(date)) }), + abs(best.0.timeIntervalSince(date)) <= limit else { + return nil + } + return best.1 +} + +// MARK: - Scrubbing + +/// Touch and hold then drag to scrub; a plain swipe still scrolls the page. +/// +/// A UIKit long-press recogniser is used because a SwiftUI long-press-then-drag gesture inside a table cell +/// takes every drag that starts on the chart, so the page couldn't scroll past it. +struct ScrubOverlay: View { + let proxy: ChartProxy + @Binding var selection: Date? + let clampTo: DateInterval + + var body: some View { + GeometryReader { geometry in + if #available(iOS 18.0, *) { + Color.clear + .contentShape(Rectangle()) + .gesture(ScrubPressRecognizer { location in + update(location, in: geometry) + } onEnd: { + selection = nil + }) + } else { + Color.clear + } + } + } + + private func update(_ location: CGPoint, in geometry: GeometryProxy) { + guard let frame = proxy.plotFrame else { return } + let origin = geometry[frame].origin + if let date: Date = proxy.value(atX: location.x - origin.x) { + selection = min(max(date, clampTo.start), clampTo.end) + } + } +} + +@available(iOS 18.0, *) +private struct ScrubPressRecognizer: UIGestureRecognizerRepresentable { + let onChange: (CGPoint) -> Void + let onEnd: () -> Void + + func makeUIGestureRecognizer(context: Context) -> UILongPressGestureRecognizer { + let recognizer = UILongPressGestureRecognizer() + recognizer.minimumPressDuration = 0.3 + recognizer.allowableMovement = 12 + return recognizer + } + + func handleUIGestureRecognizerAction(_ recognizer: UILongPressGestureRecognizer, context: Context) { + switch recognizer.state { + case .began, .changed: + onChange(context.converter.localLocation) + case .ended, .cancelled, .failed: + onEnd() + default: + break + } + } +} + +struct ScrubCallout: View { + struct Row: Hashable { + let label: String? + let value: String + let color: Color + } + + let rows: [Row] + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + ForEach(rows, id: \.self) { row in + HStack(spacing: LoopDesign.Space.s1) { + if let label = row.label { + Text(label).foregroundStyle(P.inkMuted) + } + Text(row.value).foregroundStyle(row.color).fontWeight(.semibold) + } + } + } + .font(LoopDesign.Typeface.footnote.monospacedDigit()) + .padding(.horizontal, LoopDesign.Space.s2) + .padding(.vertical, LoopDesign.Space.s1) + .background(P.surfaceRaised, in: RoundedRectangle(cornerRadius: LoopDesign.Radius.sm)) + .overlay(RoundedRectangle(cornerRadius: LoopDesign.Radius.sm).strokeBorder(P.separator)) + } +} + +/// Insulin and carbs on board over the same window, each against its own coloured axis. +struct IOBCOBChartView: View { + let iob: [TimedValue] + let cob: [TimedValue] + let window: DateInterval + var now: Date = Date() + var height: CGFloat = 140 + + @State private var selection: Date? + + private var iobMax: Double { max(1, (iob.map(\.value).max() ?? 0) * 1.1) } + private var iobMin: Double { min(0, (iob.map(\.value).min() ?? 0) * 1.1) } + private var cobMax: Double { max(10, (cob.map(\.value).max() ?? 0) * 1.1) } + + /// COB drawn on the IOB scale. + private func scaled(cob value: Double) -> Double { value / cobMax * iobMax } + + private var iobTicks: [Double] { + let step = Self.niceStep(iobMax / 2) + return stride(from: (iobMin / step).rounded(.down) * step, through: iobMax, by: step).map { $0 } + } + + private var cobTicks: [Double] { + let step = Self.niceStep(cobMax / 2) + return stride(from: 0, through: cobMax, by: step).map { $0 } + } + + static func niceStep(_ raw: Double) -> Double { + guard raw > 0 else { return 1 } + let magnitude = pow(10, floor(log10(raw))) + let normalized = raw / magnitude + let nice: Double = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10 + return nice * magnitude + } + + var body: some View { + Chart { + RuleMark(y: .value("Zero", 0)) + .foregroundStyle(P.inkMuted) + .lineStyle(StrokeStyle(lineWidth: 1)) + + ForEach(Array(cob.enumerated()), id: \.offset) { _, value in + AreaMark(x: .value("Time", value.date), yStart: .value("Base", 0), yEnd: .value("COB", scaled(cob: value.value)), series: .value("Series", "cob-fill")) + .foregroundStyle(P.carbs.opacity(0.14)) + LineMark(x: .value("Time", value.date), y: .value("COB", scaled(cob: value.value)), series: .value("Series", value.date > now ? "cob-projected" : "cob")) + .foregroundStyle(P.carbs) + .lineStyle(StrokeStyle(lineWidth: 1.5, dash: value.date > now ? [4, 3] : [])) + } + ForEach(Array(iob.enumerated()), id: \.offset) { _, value in + AreaMark(x: .value("Time", value.date), yStart: .value("Base", 0), yEnd: .value("IOB", value.value), series: .value("Series", "iob-fill")) + .foregroundStyle(P.insulin.opacity(0.14)) + LineMark(x: .value("Time", value.date), y: .value("IOB", value.value), series: .value("Series", value.date > now ? "iob-projected" : "iob")) + .foregroundStyle(P.insulin) + .lineStyle(StrokeStyle(lineWidth: 1.5, dash: value.date > now ? [4, 3] : [])) + } + if window.contains(now) { + RuleMark(x: .value("Now", now)) + .foregroundStyle(P.inkMuted) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [1, 3])) + } + if let selection { + RuleMark(x: .value("Selected", selection)).foregroundStyle(P.ink.opacity(0.6)) + } + } + .frame(height: height) + .chartXScale(domain: window.start...window.end) + .chartYScale(domain: iobMin...iobMax) + .chartXAxis { + AxisMarks(values: DayAxis.quarterDayMarks(in: window)) { value in + AxisGridLine().foregroundStyle(P.grid) + AxisValueLabel { + if let date = value.as(Date.self) { Text(DayAxis.hourLabel(date)) } + } + .font(LoopDesign.Typeface.caption) + .foregroundStyle(P.inkMuted) + } + } + .chartYAxis { + AxisMarks(position: .leading, values: iobTicks) { value in + AxisGridLine().foregroundStyle(P.grid) + AxisValueLabel { + if let v = value.as(Double.self) { Text(v.formatted(.number.precision(.fractionLength(0...1)))) } + } + .font(LoopDesign.Typeface.caption) + .foregroundStyle(P.insulinInk) + } + AxisMarks(position: .trailing, values: cobTicks.map { scaled(cob: $0) }) { value in + AxisValueLabel { + if let v = value.as(Double.self) { Text((v / iobMax * cobMax).formatted(.number.precision(.fractionLength(0)))) } + } + .font(LoopDesign.Typeface.caption) + .foregroundStyle(P.carbsInk) + } + } + .chartOverlay { proxy in + ScrubOverlay(proxy: proxy, selection: $selection, clampTo: window) + } + .overlay(alignment: .top) { + if let selection { + ScrubCallout(rows: scrubRows(at: selection)).allowsHitTesting(false) + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilitySummary) + } + + private func scrubRows(at date: Date) -> [ScrubCallout.Row] { + var rows: [ScrubCallout.Row] = [.init(label: nil, value: LoopFormat.time(date), color: P.ink)] + if let value = nearest(iob.map { ($0.date, $0.value) }, to: date, within: 10 * 60) { + rows.append(.init(label: NSLocalizedString("IOB", comment: "Abbreviation for insulin on board (active insulin)"), value: LoopFormat.doseUnits(value), color: P.insulinInk)) + } + if let value = nearest(cob.map { ($0.date, $0.value) }, to: date, within: 10 * 60) { + rows.append(.init(label: NSLocalizedString("COB", comment: "Abbreviation for carbs on board (active carbohydrate)"), value: LoopFormat.grams(value), color: P.carbsInk)) + } + return rows + } + + private var accessibilitySummary: String { + let iobNow = nearest(iob.map { ($0.date, $0.value) }, to: now, within: 10 * 60) + let cobNow = nearest(cob.map { ($0.date, $0.value) }, to: now, within: 10 * 60) + let unknown = NSLocalizedString("unknown", comment: "VoiceOver value when active insulin or carbs at the current time isn't known") + return String(format: NSLocalizedString("Active insulin %1$@, active carbs %2$@", comment: "VoiceOver summary of the active insulin and carbs chart. (1: insulin on board, e.g. 1.25 U, or unknown; 2: carbs on board, e.g. 20 g, or unknown)"), iobNow.map(LoopFormat.doseUnits) ?? unknown, cobNow.map(LoopFormat.grams) ?? unknown) + } +} diff --git a/Loop/Redesign/DayDetailView.swift b/Loop/Redesign/DayDetailView.swift new file mode 100644 index 0000000000..0f8186b71b --- /dev/null +++ b/Loop/Redesign/DayDetailView.swift @@ -0,0 +1,198 @@ +// +// DayDetailView.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import SwiftUI + +private typealias P = LoopDesign.Palette + +/// One full day on InsulinTimeline with IOB and COB under it, stepping a day at a time. +struct DayDetailView: View { + let provider: LoopHistoryProvider + + @State private var day: Date + @State private var history: DayHistory? + @State private var targetBands: [TargetBand] = [] + @State private var now = Date() + @State private var loadError: String? + + init(provider: LoopHistoryProvider, day: Date) { + self.provider = provider + _day = State(initialValue: Calendar.current.startOfDay(for: day)) + } + + private var window: DateInterval { + DateInterval(start: day, end: Calendar.current.date(byAdding: .day, value: 1, to: day)!) + } + + private var isToday: Bool { Calendar.current.isDate(day, inSameDayAs: now) } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: LoopDesign.Space.s6) { + dayStepper + if let history { + LoopCard { + CardHeader(title: NSLocalizedString("Insulin", comment: "Title of the day page card showing insulin, carbs and glucose on one timeline")) { + HStack(spacing: 4) { + Text(LoopFormat.totalUnits(history.totalUnits)).foregroundStyle(P.insulinInk) + Text("·") + Text(LoopFormat.grams(history.carbGrams)).foregroundStyle(P.carbsInk) + } + } + InsulinTimelineView(history: history, unit: provider.unitStyle, targetBands: targetBands, + now: now, height: 300) + ChartLegend(items: DayTotalsTiles.legend) + if history.source == .health { + Text(NSLocalizedString("From Apple Health: Loop keeps 7 days, so this day has no temp basal or schedule detail.", comment: "Day page note when the day is older than Loop's 7-day history and was read from Apple Health")) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + } + if #available(iOS 18.0, *) { + Text(NSLocalizedString("Touch and hold a chart to see exact values.", comment: "Hint under the day page charts explaining how to scrub them")) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + } + } + LoopCard { + CardHeader(title: NSLocalizedString("Basal and bolus", comment: "Trends card title: insulin split into basal and bolus")) + InsulinSplitView(split: history.insulinSplit, partial: isToday) + ValueTile(label: NSLocalizedString("Carbs", comment: "Label of a tile showing the day's total carbohydrate")) { + Text(LoopFormat.grams(history.carbGrams)) + .font(LoopDesign.Typeface.statValueSmall) + .foregroundStyle(P.carbsInk) + } + .fixedSize(horizontal: false, vertical: true) + } + LoopCard { + CardHeader(title: NSLocalizedString("Active insulin and carbs", comment: "Title of the day page card charting insulin and carbs on board")) + if history.iob.isEmpty && history.cob.isEmpty { + ChartPlaceholder(message: history.source == .health + ? NSLocalizedString("Loop only works out active insulin and carbs for the days it keeps", comment: "Day page placeholder for active insulin and carbs on a day older than Loop's 7-day history") + : NSLocalizedString("No active insulin or carbs on this day", comment: "Day page placeholder when there was no insulin or carbs on board all day")) + } else { + IOBCOBChartView(iob: history.iob, cob: history.cob, window: window, now: now) + } + ChartLegend(items: [ + LegendItem(swatch: .solid(P.insulin), label: NSLocalizedString("IOB (U, left)", comment: "Chart legend: insulin on board in units, on the left axis")), + LegendItem(swatch: .solid(P.carbs), label: NSLocalizedString("COB (g, right)", comment: "Chart legend: carbs on board in grams, on the right axis")), + LegendItem(swatch: .dash(P.inkMuted), label: NSLocalizedString("Projected", comment: "Chart legend: dashed lines show Loop's projection of insulin and carbs on board")), + ]) + } + } else if let loadError { + ChartPlaceholder(message: String(format: NSLocalizedString("Couldn't load this day: %1$@", comment: "Day page error when the day's history can't be read. (1: system error description)"), loadError)) + } else { + ProgressView() + .frame(maxWidth: .infinity, minHeight: 300) + } + } + .padding(LoopDesign.Space.s4) + } + .background(P.surface) + .navigationTitle(isToday ? NSLocalizedString("Today", comment: "Day page title when showing today") : LoopFormat.weekdayDay(day)) + .navigationBarTitleDisplayMode(.inline) + .task(id: day) { + now = Date() + let requestedWindow = window + history = nil + loadError = nil + do { + async let historyFetch = provider.dayHistory(requestedWindow) + async let targetsFetch = provider.glucoseTargetBands(in: requestedWindow) + let (result, targets) = try await (historyFetch, targetsFetch) + guard !Task.isCancelled, window == requestedWindow else { return } + history = result + targetBands = targets + } catch { + guard !Task.isCancelled, window == requestedWindow else { return } + loadError = error.localizedDescription + } + } + } + + private var dayStepper: some View { + HStack { + StepButton(systemName: "chevron.left", label: NSLocalizedString("Previous day", comment: "Accessibility label for the day page button that steps back a day")) { + day = Calendar.current.date(byAdding: .day, value: -1, to: day)! + } + Spacer() + Text(LoopFormat.day(day)) + .font(LoopDesign.Typeface.headline) + .foregroundStyle(P.ink) + Spacer() + StepButton(systemName: "chevron.right", label: NSLocalizedString("Next day", comment: "Accessibility label for the day page button that steps forward a day"), enabled: !isToday) { + day = Calendar.current.date(byAdding: .day, value: 1, to: day)! + } + } + } +} + +/// A day on one timeline with its totals, for a one-day period on Trends. +struct DayTimelineCard: View { + let title: String + let history: DayHistory + let unit: GlucoseUnitStyle + let targetBands: [TargetBand] + var now: Date = Date() + var height: CGFloat = 230 + /// The bolus, basal and carb tiles; off where a Basal and bolus card follows. + var showsTotals = true + + static var todayTitle: String { + NSLocalizedString("Today so far", comment: "Title of the card on the Now screen showing today's insulin and carbs") + } + + var body: some View { + LoopCard { + CardHeader(title: title) { + HStack(spacing: 4) { + Text(LoopFormat.totalUnits(history.totalUnits)).foregroundStyle(P.insulinInk) + Text(verbatim: "·") + Text(LoopFormat.grams(history.carbGrams)).foregroundStyle(P.carbsInk) + } + } + InsulinTimelineView(history: history, unit: unit, targetBands: targetBands, now: now, height: height) + if showsTotals { + DayTotalsTiles(history: history) + } + ChartLegend(items: DayTotalsTiles.legend) + } + } +} + +/// Bolus, basal and carb totals under a timeline. +struct DayTotalsTiles: View { + let history: DayHistory + + static let legend: [LegendItem] = [ + LegendItem(swatch: .solid(P.bolus), label: NSLocalizedString("Bolus", comment: "Chart legend: solid bars are manual boluses")), + LegendItem(swatch: .hatch(P.bolus), label: NSLocalizedString("Automatic", comment: "Chart legend: hatched bars are automatic boluses given by Loop")), + LegendItem(swatch: .basal, label: NSLocalizedString("Basal given", comment: "Chart legend: filled area is basal insulin actually delivered")), + LegendItem(swatch: .dash(P.basalScheduled), label: NSLocalizedString("Scheduled", comment: "Chart legend: dashed line is the scheduled basal rate")), + ] + + var body: some View { + AdaptiveRow { + ValueTile(label: NSLocalizedString("Bolus", comment: "Label of a tile showing the day's total bolus insulin")) { + Text(LoopFormat.totalUnits(history.bolusUnits)) + .font(LoopDesign.Typeface.statValueSmall) + .foregroundStyle(P.insulinInk) + } + ValueTile(label: NSLocalizedString("Basal", comment: "Label of a tile showing the current basal rate (Now screen) or the day's basal insulin total (Today so far totals)")) { + // Plain ink: basal is the pale fill on the chart, and orange here read as bolus. + Text(LoopFormat.totalUnits(history.basalUnits)) + .font(LoopDesign.Typeface.statValueSmall) + .foregroundStyle(P.ink) + } + ValueTile(label: NSLocalizedString("Carbs", comment: "Label of a tile showing the day's total carbohydrate")) { + Text(LoopFormat.grams(history.carbGrams)) + .font(LoopDesign.Typeface.statValueSmall) + .foregroundStyle(P.carbsInk) + } + } + .fixedSize(horizontal: false, vertical: true) + } +} diff --git a/Loop/Redesign/LoopDesign.swift b/Loop/Redesign/LoopDesign.swift new file mode 100644 index 0000000000..0fb74edffd --- /dev/null +++ b/Loop/Redesign/LoopDesign.swift @@ -0,0 +1,341 @@ +// +// LoopDesign.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import UIKit + +/// The tokens from the Loop design system (`design/project/tokens.json`). +/// +/// Quantity colours reuse Loop's existing assets (`glucose`, `insulin`, `carbs`, `fresh`, `warning`, `accent`) +/// so a DerivedAssetsOverride still recolours them. Grounds and text map onto the iOS system colours they +/// were taken from; the rest are colour sets in DerivedAssetsBase.xcassets named after their token. +/// +/// Insulin and carb amounts are set in `insulinInk` and `carbsInk` wherever they stand alone. Glucose, basal, +/// counts and figures beside a legend swatch stay `ink`. Red is only for something that needs action now. +enum LoopDesign { + + enum Palette { + // Grounds + static let surface = Color(uiColor: .systemGroupedBackground) + static let surfaceRaised = Color(uiColor: .secondarySystemGroupedBackground) + static let surfaceSunken = Color(uiColor: .systemGray5) + static let controlThumb = Color("control-thumb") + static let separator = Color(uiColor: .opaqueSeparator) + static let grid = Color(uiColor: .systemGray5) + + // Text + static let ink = Color(uiColor: .label) + static let inkMuted = Color("ink-muted") + static let accentInk = Color("accent-ink") + + // Quantities + static let glucose = Color("glucose") + static let glucoseInk = Color("glucose-ink") + static let targetBand = Color("target-band") + static let agpOuter = Color("agp-outer") + static let agpInner = Color("agp-inner") + /// Insulin as a quantity and an action: the Bolus button and the active insulin curve. + static let insulin = Color("insulin") + /// Every insulin amount set as text, whether a bolus, a day's total or active insulin. + static let insulinInk = Color("insulin-ink") + static let basal = Color("basal") + static let basalEdge = Color("basal-edge") + /// Basal is a pale, quiet orange with a tan edge; bolus is insulin's orange made deep. Both stay orange, + /// and their lightness is far enough apart (3:1 even where they touch) to tell them apart at a glance. + static let bolus = Color("bolus") + static let basalScheduled = inkMuted + static let carbs = Color("carbs") + static let carbsInk = Color("carbs-ink") + + // Time in range ladder, top to bottom + static let rangeVeryHigh = Color("range-very-high") + static let rangeHigh = Color("range-high") + static let rangeIn = Color("range-in") + static let rangeLow = Color("range-low") + static let rangeVeryLow = Color("range-very-low") + + // Loop state + static let statusAutomated = Color("fresh") + static let statusWarning = Color("warning") + } + + enum Space { + static let s1: CGFloat = 4 + static let s2: CGFloat = 8 + static let s3: CGFloat = 12 + static let s4: CGFloat = 16 + static let s6: CGFloat = 24 + static let s8: CGFloat = 32 + } + + enum Radius { + static let sm: CGFloat = 6 + static let md: CGFloat = 10 + static let lg: CGFloat = 14 + } + + /// Text styles follow Dynamic Type; the large numbers are fixed sizes in SF Pro Rounded with tabular figures. + enum Typeface { + static let statValue = Font.system(.title, design: .rounded).weight(.semibold).monospacedDigit() + static let statValueSmall = Font.system(.title3, design: .rounded).weight(.semibold).monospacedDigit() + static let title = Font.title.bold() + static let headline = Font.headline + static let body = Font.body + static let subhead = Font.subheadline + static let footnote = Font.footnote + static let caption = Font.caption2.weight(.medium).monospacedDigit() + } +} + +// MARK: - Card + +struct LoopCard: View { + var padding: CGFloat = LoopDesign.Space.s4 + @ViewBuilder var content: Content + + var body: some View { + VStack(alignment: .leading, spacing: LoopDesign.Space.s3) { + content + } + .padding(padding) + .frame(maxWidth: .infinity, alignment: .leading) + .background(LoopDesign.Palette.surfaceRaised, in: RoundedRectangle(cornerRadius: LoopDesign.Radius.lg, style: .continuous)) + } +} + +/// A card title with an optional trailing detail or action, as on every card in the mock-ups. +struct CardHeader: View { + let title: String + @ViewBuilder var trailing: Trailing + + var body: some View { + HStack(alignment: .firstTextBaseline) { + Text(title) + .font(LoopDesign.Typeface.headline) + .foregroundStyle(LoopDesign.Palette.ink) + .accessibilityAddTraits(.isHeader) + Spacer(minLength: LoopDesign.Space.s2) + trailing + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(LoopDesign.Palette.inkMuted) + } + } +} + +extension CardHeader where Trailing == EmptyView { + init(title: String) { + self.init(title: title) { EmptyView() } + } +} + +// MARK: - Legend + +struct LegendItem: Identifiable { + enum Swatch { + case solid(Color) + case outline(Color) + case dash(Color) + case dot(Color) + case hatch(Color) + case basal + } + + let id = UUID() + let swatch: Swatch + let label: String +} + +struct ChartLegend: View { + let items: [LegendItem] + + var body: some View { + FlowLayout(spacing: LoopDesign.Space.s3, lineSpacing: LoopDesign.Space.s1) { + ForEach(items) { item in + HStack(spacing: LoopDesign.Space.s1) { + LegendSwatch(swatch: item.swatch) + Text(item.label) + } + .accessibilityElement(children: .combine) + } + } + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(LoopDesign.Palette.inkMuted) + } +} + +/// The 10-point key for one kind of mark, shared by legends and the basal and bolus rows. +struct LegendSwatch: View { + let swatch: LegendItem.Swatch + @Environment(\.colorScheme) private var scheme + + var body: some View { + switch swatch { + case .solid(let color): + RoundedRectangle(cornerRadius: 2).fill(color).frame(width: 10, height: 10) + case .outline(let color): + RoundedRectangle(cornerRadius: 2).strokeBorder(color, lineWidth: 1.5).frame(width: 10, height: 10) + case .dash(let color): + Path { path in + path.move(to: CGPoint(x: 0, y: 1)) + path.addLine(to: CGPoint(x: 14, y: 1)) + } + .stroke(color, style: StrokeStyle(lineWidth: 2, dash: [4, 2])) + .frame(width: 14, height: 2) + case .dot(let color): + Circle().fill(color).frame(width: 8, height: 8) + case .hatch(let color): + RoundedRectangle(cornerRadius: 2).fill(Hatch.paint(color, scheme: scheme)).frame(width: 10, height: 10) + case .basal: + VStack(spacing: 0) { + Rectangle().fill(LoopDesign.Palette.basalEdge).frame(height: 2) + Rectangle().fill(LoopDesign.Palette.basal) + } + .frame(width: 10, height: 10) + .clipShape(RoundedRectangle(cornerRadius: 2)) + } + } +} + +/// Diagonal stripes over a pale tint of a colour: automatic bolus, wherever a mark is wide enough to show it. +enum Hatch { + static func paint(_ color: Color, scheme: ColorScheme) -> ImagePaint { + let resolved = UIColor(color).resolvedColor(with: UITraitCollection(userInterfaceStyle: scheme == .dark ? .dark : .light)) + let size: CGFloat = 5 + let image = UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { context in + resolved.withAlphaComponent(scheme == .dark ? 0.3 : 0.22).setFill() + context.fill(CGRect(x: 0, y: 0, width: size, height: size)) + resolved.setStroke() + // One stripe corner to corner, plus the ends of its neighbours, so tiles join into unbroken lines. + let path = UIBezierPath() + path.lineWidth = 1.4 + for offset in [-size, 0, size] { + path.move(to: CGPoint(x: offset, y: size)) + path.addLine(to: CGPoint(x: offset + size, y: 0)) + } + path.stroke() + } + return ImagePaint(image: Image(uiImage: image)) + } +} + +/// Wraps its children onto new lines, for legends and chips. +struct FlowLayout: Layout { + var spacing: CGFloat + var lineSpacing: CGFloat + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let width = proposal.width ?? .infinity + var x: CGFloat = 0, y: CGFloat = 0, lineHeight: CGFloat = 0, maxX: CGFloat = 0 + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if x > 0 && x + size.width > width { + x = 0 + y += lineHeight + lineSpacing + lineHeight = 0 + } + x += size.width + spacing + maxX = max(maxX, x - spacing) + lineHeight = max(lineHeight, size.height) + } + return CGSize(width: maxX, height: y + lineHeight) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + var x = bounds.minX, y = bounds.minY, lineHeight: CGFloat = 0 + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + if x > bounds.minX && x + size.width > bounds.maxX { + x = bounds.minX + y += lineHeight + lineSpacing + lineHeight = 0 + } + subview.place(at: CGPoint(x: x, y: y), proposal: ProposedViewSize(size)) + x += size.width + spacing + lineHeight = max(lineHeight, size.height) + } + } +} + +/// A row that stacks vertically at accessibility text sizes, so tiles and buttons keep their numbers on one line. +struct AdaptiveRow: View { + @Environment(\.dynamicTypeSize) private var typeSize + var spacing: CGFloat = LoopDesign.Space.s2 + @ViewBuilder var content: Content + + var body: some View { + let layout = typeSize.isAccessibilitySize + ? AnyLayout(VStackLayout(alignment: .leading, spacing: spacing)) + : AnyLayout(HStackLayout(spacing: spacing)) + layout { content } + } +} + +// MARK: - Tiles + +/// A small sunken tile with a label over a value, as used under the timeline. +struct ValueTile: View { + let label: String + @ViewBuilder var value: Value + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Text(label) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(LoopDesign.Palette.inkMuted) + .lineLimit(1) + .minimumScaleFactor(0.7) + .allowsTightening(true) + // A value with a long unit ("0.04 単位") shrinks to fit rather than breaking mid-word. + value + .lineLimit(1) + .minimumScaleFactor(0.6) + } + .padding(LoopDesign.Space.s3) + .frame(maxWidth: .infinity, minHeight: 64, maxHeight: .infinity, alignment: .topLeading) + .background(LoopDesign.Palette.surfaceSunken, in: RoundedRectangle(cornerRadius: LoopDesign.Radius.md, style: .continuous)) + .accessibilityElement(children: .combine) + } +} + +/// The round previous and next buttons beside a date or period. +struct StepButton: View { + let systemName: String + let label: String + var enabled = true + let action: () -> Void + + var body: some View { + Button(action: action) { + Image(systemName: systemName) + .font(.footnote.weight(.semibold)) + .frame(width: 30, height: 30) + .background(LoopDesign.Palette.surfaceRaised, in: Circle()) + .overlay(Circle().strokeBorder(LoopDesign.Palette.separator)) + .frame(width: 44, height: 44) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .foregroundStyle(enabled ? LoopDesign.Palette.ink : LoopDesign.Palette.inkMuted.opacity(0.5)) + .disabled(!enabled) + .accessibilityLabel(label) + } +} + +/// A message in place of a chart that has nothing to draw, saying why. +struct ChartPlaceholder: View { + let message: String + var height: CGFloat = 120 + + var body: some View { + Text(message) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(LoopDesign.Palette.inkMuted) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, minHeight: height) + .background(LoopDesign.Palette.surfaceSunken.opacity(0.5), in: RoundedRectangle(cornerRadius: LoopDesign.Radius.md)) + } +} diff --git a/Loop/Redesign/LoopHistoryMath.swift b/Loop/Redesign/LoopHistoryMath.swift new file mode 100644 index 0000000000..0bc1080785 --- /dev/null +++ b/Loop/Redesign/LoopHistoryMath.swift @@ -0,0 +1,352 @@ +// +// LoopHistoryMath.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import Foundation + +/// Pure calculations behind the Trends screen. Nothing here touches a store. +enum LoopHistoryMath { + + // MARK: Glucose statistics + + struct GlucoseStats: Equatable { + let count: Int + let meanMgdl: Double + let sdMgdl: Double + /// Coefficient of variation, 0–1. + let cv: Double + /// Glucose management indicator, as a percentage (Bergenstal 2018). + let gmiPercent: Double + var gmiMmolMol: Double { (gmiPercent - 2.152) * 10.929 } + } + + static func glucoseStats(_ points: [GlucosePoint]) -> GlucoseStats? { + guard points.count >= 2 else { return nil } + let values = points.map(\.mgdl) + let mean = values.reduce(0, +) / Double(values.count) + let variance = values.reduce(0) { $0 + pow($1 - mean, 2) } / Double(values.count - 1) + let sd = variance.squareRoot() + return GlucoseStats(count: values.count, meanMgdl: mean, sdMgdl: sd, cv: sd / mean, gmiPercent: 3.31 + 0.02392 * mean) + } + + /// The share of the period with CGM readings, assuming one reading every 5 minutes. + static func cgmActiveFraction(_ points: [GlucosePoint], over interval: DateInterval) -> Double { + let expected = interval.duration / 300 + guard expected > 0 else { return 0 } + return min(1, Double(points.count) / expected) + } + + // MARK: Time in range + + enum RangeBand: Int, CaseIterable, Identifiable { + case veryHigh, high, inRange, low, veryLow + + var id: Int { rawValue } + + static func band(for mgdl: Double) -> RangeBand { + switch mgdl { + case ..<54: return .veryLow + case ..<70: return .low + case ...180: return .inRange + case ...250: return .high + default: return .veryHigh + } + } + } + + /// Fractions (0–1) per band, in ladder order. Empty input gives all zeros. + static func timeInRange(_ points: [GlucosePoint]) -> [RangeBand: Double] { + var counts: [RangeBand: Int] = [:] + for point in points { + counts[RangeBand.band(for: point.mgdl), default: 0] += 1 + } + let total = Double(points.count) + return Dictionary(uniqueKeysWithValues: RangeBand.allCases.map { band in + (band, total > 0 ? Double(counts[band] ?? 0) / total : 0) + }) + } + + /// Whole-number percentages that add up to 100 (largest remainder), so the labels never sum to 99 or 101. + static func roundedPercentages(_ fractions: [RangeBand: Double]) -> [RangeBand: Int] { + let raw = RangeBand.allCases.map { (band: $0, value: (fractions[$0] ?? 0) * 100) } + let total = raw.reduce(0) { $0 + $1.value } + guard total > 0 else { return Dictionary(uniqueKeysWithValues: RangeBand.allCases.map { ($0, 0) }) } + var floors = raw.map { (band: $0.band, floor: Int($0.value.rounded(.down)), remainder: $0.value - $0.value.rounded(.down)) } + var missing = Int(total.rounded()) - floors.reduce(0) { $0 + $1.floor } + for index in floors.indices.sorted(by: { floors[$0].remainder > floors[$1].remainder }) where missing > 0 { + floors[index].floor += 1 + missing -= 1 + } + return Dictionary(uniqueKeysWithValues: floors.map { ($0.band, $0.floor) }) + } + + // MARK: AGP + + struct AGPSlot: Equatable { + /// Minutes after midnight at the start of the slot. + let minute: Int + let p5: Double + let p25: Double + let p50: Double + let p75: Double + let p95: Double + } + + /// Readings folded onto one day in `slotMinutes` slots, as mg/dL percentiles. Slots with fewer than 3 readings are skipped. + static func agp(_ points: [GlucosePoint], slotMinutes: Int = 15, calendar: Calendar = .current) -> [AGPSlot] { + var slots: [Int: [Double]] = [:] + for point in points { + let parts = calendar.dateComponents([.hour, .minute], from: point.date) + let minute = (parts.hour ?? 0) * 60 + (parts.minute ?? 0) + slots[minute / slotMinutes, default: []].append(point.mgdl) + } + return slots.keys.sorted().compactMap { key in + let values = slots[key]!.sorted() + guard values.count >= 3 else { return nil } + return AGPSlot(minute: key * slotMinutes, + p5: percentile(values, 0.05), p25: percentile(values, 0.25), p50: percentile(values, 0.5), + p75: percentile(values, 0.75), p95: percentile(values, 0.95)) + } + } + + /// Linear-interpolated percentile of sorted values. + static func percentile(_ sorted: [Double], _ p: Double) -> Double { + guard !sorted.isEmpty else { return .nan } + let position = p * Double(sorted.count - 1) + let lower = Int(position.rounded(.down)), upper = Int(position.rounded(.up)) + let weight = position - Double(lower) + return sorted[lower] * (1 - weight) + sorted[upper] * weight + } + + /// The number of distinct calendar days with readings. + static func daysWithReadings(_ points: [GlucosePoint], calendar: Calendar = .current) -> Int { + Set(points.map { calendar.startOfDay(for: $0.date) }).count + } + + // MARK: Days + + /// Midnight-to-midnight intervals covering `interval`, oldest first, the last one cut off at `interval.end`. + static func days(in interval: DateInterval, calendar: Calendar = .current) -> [DateInterval] { + var result: [DateInterval] = [] + var start = calendar.startOfDay(for: interval.start) + while start < interval.end { + guard let next = calendar.date(byAdding: .day, value: 1, to: start) else { break } + result.append(DateInterval(start: max(start, interval.start), end: min(next, interval.end))) + start = next + } + return result + } + + struct DayTotal: Equatable, Identifiable { + let day: Date + let split: InsulinSplit + /// False for a day still in progress, so its partial total doesn't pull per-day averages down. + var isComplete = true + var id: Date { day } + var basal: Double { split.basal } + var bolus: Double { split.bolus } + var total: Double { split.total } + + init(day: Date, split: InsulinSplit, isComplete: Bool = true) { + self.day = day + self.split = split + self.isComplete = isComplete + } + + /// A total with no manual or automatic detail: all of its bolus counts as manual. + init(day: Date, basal: Double, bolus: Double, isComplete: Bool = true) { + self.init(day: day, split: InsulinSplit(basal: basal, manualBolus: bolus), isComplete: isComplete) + } + } + + /// The days per-day insulin figures average over: days with insulin, leaving out a day still in progress + /// unless no complete day has any. + static func averagingDays(_ totals: [DayTotal]) -> [DayTotal] { + let recorded = totals.filter { $0.total > 0 } + let complete = recorded.filter(\.isComplete) + return complete.isEmpty ? recorded : complete + } + + /// One source for the summary, legend and chart rule, before any weekly aggregation. + static func averageDailyInsulin(_ totals: [DayTotal]) -> Double { + let days = averagingDays(totals) + guard !days.isEmpty else { return 0 } + return days.reduce(0) { $0 + $1.total } / Double(days.count) + } + + /// Averages daily totals by calendar week (Monday start), for periods past 30 days. + static func weekly(_ totals: [DayTotal], calendar: Calendar = .current) -> [DayTotal] { + var cal = calendar + cal.firstWeekday = 2 + let groups = Dictionary(grouping: totals) { cal.dateInterval(of: .weekOfYear, for: $0.day)?.start ?? $0.day } + return groups.keys.sorted().map { week in + DayTotal(day: week, split: InsulinSplit.average(averagingDays(groups[week]!).map(\.split))) + } + } + + // MARK: Basal + + struct ScheduleValue { + let start: Date + let end: Date + let rate: Double + } + + /// A basal record from the pump: scheduled delivery, a temp basal or a suspend. + struct BasalDelivery { + let start: Date + let end: Date + /// U/h; 0 for a suspend. + let rate: Double + let kind: BasalSegment.Kind + let automatic: Bool + } + + /// Delivered basal over `window` from the pump's records (the latest-starting one wins where they overlap). + /// + /// From `recordsStart` on, time without a basal record counts at `programmedRates`, since that is what the pump + /// delivers on its own; this matches Loop's own insulin totals (`InsulinDeliveryHistory`). Before it, or without + /// it, gaps stay unknown. + static func basalSegments(window: DateInterval, schedule: [ScheduleValue], deliveries temps: [BasalDelivery], + programmedRates: [ScheduleValue] = [], recordsStart: Date? = nil) -> [BasalSegment] { + var cuts = Set([window.start, window.end]) + if let recordsStart, window.contains(recordsStart) { cuts.insert(recordsStart) } + for value in schedule + programmedRates { + if window.contains(value.start) { cuts.insert(value.start) } + if window.contains(value.end) { cuts.insert(value.end) } + } + for temp in temps { + if window.contains(temp.start) { cuts.insert(temp.start) } + if window.contains(temp.end) { cuts.insert(temp.end) } + } + let sorted = cuts.sorted() + let sortedTemps = temps.sorted { $0.start < $1.start } + // The furthest end among records up to each index bounds the backward search, so a week of five-minute + // records stays linear rather than scanning every record for every cut. + var furthestEnd: [Date] = [] + furthestEnd.reserveCapacity(sortedTemps.count) + for temp in sortedTemps { furthestEnd.append(max(furthestEnd.last ?? temp.end, temp.end)) } + func latestRecord(covering mid: Date) -> BasalDelivery? { + var low = 0, high = sortedTemps.count + while low < high { + let middle = (low + high) / 2 + if sortedTemps[middle].start <= mid { low = middle + 1 } else { high = middle } + } + var index = low - 1 + while index >= 0, furthestEnd[index] > mid { + if mid < sortedTemps[index].end { return sortedTemps[index] } + index -= 1 + } + return nil + } + var segments: [BasalSegment] = [] + for (a, b) in zip(sorted, sorted.dropFirst()) where b > a { + let mid = a.addingTimeInterval(b.timeIntervalSince(a) / 2) + let scheduled = schedule.first { $0.start <= mid && mid < $0.end }?.rate + let segment: BasalSegment + if let temp = latestRecord(covering: mid) { + segment = BasalSegment(start: a, end: b, rate: temp.kind == .suspended ? 0 : temp.rate, scheduledRate: scheduled, + kind: temp.kind, automatic: temp.automatic) + } else if let recordsStart, a >= recordsStart, + let programmed = programmedRates.first(where: { $0.start <= mid && mid < $0.end })?.rate { + segment = BasalSegment(start: a, end: b, rate: programmed, scheduledRate: scheduled, kind: .scheduled, automatic: false) + } else { + continue + } + if let last = segments.last, last.end == segment.start, last.rate == segment.rate, last.kind == segment.kind, + last.scheduledRate == segment.scheduledRate, last.automatic == segment.automatic { + segments[segments.count - 1] = BasalSegment(start: last.start, end: segment.end, rate: last.rate, + scheduledRate: last.scheduledRate, kind: last.kind, automatic: last.automatic) + } else { + segments.append(segment) + } + } + return segments + } + + /// Basal rate at a moment, from delivered segments. + static func basalRate(at date: Date, in segments: [BasalSegment]) -> BasalSegment? { + segments.last { $0.start <= date && date < $0.end } + } + + // MARK: Loop modes + + struct LoopRun { + let date: Date + let succeeded: Bool + let error: String? + } + + struct ModeChange: Codable, Equatable { + let date: Date + let closedLoop: Bool + } + + /// Splits `interval` into closed, limited and open stretches. + /// + /// Each 5-minute slot is open when closed loop was switched off (from `modeChanges`), closed when Loop completed a + /// run in the 15 minutes before the slot ends, and limited otherwise. Before the first mode change the setting is + /// unknown, so the result starts at the later of `coverageStart` and the first mode change. + static func loopModeSpans(interval: DateInterval, runs: [LoopRun], modeChanges: [ModeChange], coverageStart: Date?, + recency: TimeInterval = 15 * 60, slot: TimeInterval = 5 * 60) -> [LoopModeSpan] { + guard let coverageStart, let firstChange = modeChanges.map(\.date).min() else { return [] } + let start = max(interval.start, coverageStart, firstChange) + guard interval.end > start else { return [] } + + let sortedRuns = runs.sorted { $0.date < $1.date } + let changes = modeChanges.sorted { $0.date < $1.date } + var spans: [LoopModeSpan] = [] + var runIndex = 0 + var lastSuccess: Date? = nil + var lastError: String? = nil + var changeIndex = 0 + var closedSetting: Bool? = nil + + var t = start + while t < interval.end { + let slotEnd = min(t.addingTimeInterval(slot), interval.end) + while runIndex < sortedRuns.count && sortedRuns[runIndex].date <= slotEnd { + let run = sortedRuns[runIndex] + if run.succeeded { + lastSuccess = run.date + lastError = nil + } else { + lastError = run.error ?? lastError + } + runIndex += 1 + } + while changeIndex < changes.count && changes[changeIndex].date <= slotEnd { + closedSetting = changes[changeIndex].closedLoop + changeIndex += 1 + } + + let mode: LoopModeSpan.Mode + let cause: String? + if closedSetting == false { + mode = .open + cause = NSLocalizedString("Closed loop off", comment: "Reason shown for a stretch of time when Loop was in open loop because closed loop was turned off") + } else if let lastSuccess, slotEnd.timeIntervalSince(lastSuccess) <= recency { + mode = .closed + cause = nil + } else { + mode = .limited + cause = lastError ?? NSLocalizedString("Loop didn't run", comment: "Reason shown for a stretch of time when Loop did not complete a run and recorded no error") + } + + if let last = spans.last, last.mode == mode, last.cause == cause, last.end == t { + spans[spans.count - 1] = LoopModeSpan(start: last.start, end: slotEnd, mode: mode, cause: cause) + } else { + spans.append(LoopModeSpan(start: t, end: slotEnd, mode: mode, cause: cause)) + } + t = slotEnd + } + return spans + } + + static func duration(of mode: LoopModeSpan.Mode, in spans: [LoopModeSpan]) -> TimeInterval { + spans.filter { $0.mode == mode }.reduce(0) { $0 + $1.duration } + } +} diff --git a/Loop/Redesign/LoopHistoryModels.swift b/Loop/Redesign/LoopHistoryModels.swift new file mode 100644 index 0000000000..e1acc34c89 --- /dev/null +++ b/Loop/Redesign/LoopHistoryModels.swift @@ -0,0 +1,342 @@ +// +// LoopHistoryModels.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import Foundation +import HealthKit +import LoopKit +import LoopAlgorithm + +/// Plain value types the redesigned screens draw from. Glucose is kept in mg/dL and converted only for display, +/// so the statistics don't depend on the display unit. + +struct GlucosePoint: Equatable { + let date: Date + let mgdl: Double +} + +struct BolusEvent: Equatable, Identifiable { + let id: String + let date: Date + let units: Double + /// Issued by Loop (automatic bolus) rather than by the person. + let automatic: Bool + /// Logged in Loop by hand rather than delivered by the pump; these can be deleted. + let manuallyEntered: Bool + /// Kept so a manually entered dose can be deleted from the dose store. + var dose: DoseEntry? = nil + /// False when the record doesn't say whether Loop or the person gave it, such as another app's doses in Apple Health. + var automaticKnown = true + + static func == (lhs: BolusEvent, rhs: BolusEvent) -> Bool { + lhs.id == rhs.id && lhs.date == rhs.date && lhs.units == rhs.units && lhs.automatic == rhs.automatic + } +} + +struct BasalSegment: Equatable { + enum Kind: Equatable { + /// The scheduled rate, with no temp basal running. + case scheduled + /// A temp basal, set by Loop or by hand. + case temp + case suspended + } + + let start: Date + let end: Date + /// The rate actually delivered, in U/h. + let rate: Double + /// The profile rate at the time, in U/h, if known. + let scheduledRate: Double? + let kind: Kind + let automatic: Bool + + var hours: Double { end.timeIntervalSince(start) / 3600 } + var units: Double { rate * hours } + /// Units delivered above (positive) or below (negative) the schedule. + var netUnits: Double { + guard let scheduledRate else { return 0 } + return (rate - scheduledRate) * hours + } + + func clamped(to interval: DateInterval) -> BasalSegment? { + let s = max(start, interval.start), e = min(end, interval.end) + guard e > s else { return nil } + return BasalSegment(start: s, end: e, rate: rate, scheduledRate: scheduledRate, kind: kind, automatic: automatic) + } +} + +struct ScheduledBasalPoint: Equatable { + let start: Date + let end: Date + let rate: Double +} + +/// The correction range over part of a chart, in mg/dL. +struct TargetBand: Equatable { + let start: Date + let end: Date + let range: ClosedRange +} + +struct CarbEvent: Equatable, Identifiable { + let id: String + let date: Date + let grams: Double + let absorptionHours: Double? + let foodType: String? + let createdByLoop: Bool + /// Kept so the entry can be edited or deleted. + var entry: StoredCarbEntry? = nil + + static func == (lhs: CarbEvent, rhs: CarbEvent) -> Bool { + lhs.id == rhs.id && lhs.date == rhs.date && lhs.grams == rhs.grams + } +} + +struct TimedValue: Equatable { + let date: Date + let value: Double +} + +/// Everything known about one day or reporting period, for InsulinTimeline. +struct DayHistory { + var window: DateInterval + var glucose: [GlucosePoint] = [] + var boluses: [BolusEvent] = [] + var basal: [BasalSegment] = [] + var scheduledBasal: [ScheduledBasalPoint] = [] + var carbs: [CarbEvent] = [] + var iob: [TimedValue] = [] + var cob: [TimedValue] = [] + /// Where the data came from, so older days can say they are missing Loop's own detail. + var source: HistorySource = .loop + + var bolusUnits: Double { boluses.reduce(0) { $0 + $1.units } } + var basalUnits: Double { basal.reduce(0) { $0 + $1.units } } + var carbGrams: Double { carbs.reduce(0) { $0 + $1.grams } } + var totalUnits: Double { bolusUnits + basalUnits } + var insulinSplit: InsulinSplit { InsulinSplit(basal: basalUnits, boluses: boluses) } + + /// Only what falls inside `interval`, with basal cut at its edges. + func clipped(to interval: DateInterval) -> DayHistory { + var history = self + let inside: (Date) -> Bool = { $0 >= interval.start && $0 < interval.end } + history.window = interval + history.glucose = glucose.filter { inside($0.date) } + history.boluses = boluses.filter { inside($0.date) } + history.basal = basal.compactMap { $0.clamped(to: interval) } + history.carbs = carbs.filter { inside($0.date) } + history.iob = iob.filter { inside($0.date) } + history.cob = cob.filter { inside($0.date) } + return history + } +} + +/// Basal and bolus for one day, or averaged per day, with bolus split into manual and automatic. +struct InsulinSplit: Equatable { + var basal = 0.0 + var manualBolus = 0.0 + var automaticBolus = 0.0 + /// Bolus that doesn't say whether it was manual or automatic; while there is any, the split isn't shown. + var unclassifiedBolus = 0.0 + + var bolus: Double { manualBolus + automaticBolus + unclassifiedBolus } + var total: Double { basal + bolus } + var isClassified: Bool { unclassifiedBolus < 0.005 } + + /// Shares in whole percent that add up to 100. + var bolusPercent: Int { total > 0 ? Int((bolus / total * 100).rounded()) : 0 } + var basalPercent: Int { total > 0 ? 100 - bolusPercent : 0 } +} + +extension InsulinSplit { + init(basal: Double, boluses: [BolusEvent]) { + self.basal = basal + for bolus in boluses { + if !bolus.automaticKnown { + unclassifiedBolus += bolus.units + } else if bolus.automatic { + automaticBolus += bolus.units + } else { + manualBolus += bolus.units + } + } + } + + /// The per-day average of several days' splits. + static func average(_ splits: [InsulinSplit]) -> InsulinSplit { + guard !splits.isEmpty else { return InsulinSplit() } + let n = Double(splits.count) + return InsulinSplit(basal: splits.reduce(0) { $0 + $1.basal } / n, + manualBolus: splits.reduce(0) { $0 + $1.manualBolus } / n, + automaticBolus: splits.reduce(0) { $0 + $1.automaticBolus } / n, + unclassifiedBolus: splits.reduce(0) { $0 + $1.unclassifiedBolus } / n) + } +} + +enum HistorySource { + /// Loop's own stores: manual versus automatic, temp basals and the schedule are all known. + case loop + /// Apple Health, for days older than Loop keeps. Doses are split into basal and bolus only. + case health +} + +/// One stretch of the period in a single loop mode. +struct LoopModeSpan: Equatable { + enum Mode: Equatable { + case closed + case limited + case open + } + + let start: Date + let end: Date + let mode: Mode + let cause: String? + + var duration: TimeInterval { end.timeIntervalSince(start) } +} + +// MARK: - Glucose unit + +struct GlucoseUnitStyle: Equatable { + let isMmol: Bool + + init(unit: LoopUnit) { + isMmol = unit == .millimolesPerLiter + } + + init(isMmol: Bool) { + self.isMmol = isMmol + } + + static let mgdlPerMmol = 18.0182 + + var unitName: String { isMmol ? "mmol/L" : "mg/dL" } + + func value(_ mgdl: Double) -> Double { isMmol ? mgdl / Self.mgdlPerMmol : mgdl } + func mgdl(_ displayValue: Double) -> Double { isMmol ? displayValue * Self.mgdlPerMmol : displayValue } + + func format(_ mgdl: Double) -> String { + isMmol ? LoopFormat.number(mgdl / Self.mgdlPerMmol, decimals: 1) : LoopFormat.number(mgdl, decimals: 0) + } + + /// Fixed chart scales from the design: 2–20 mmol/L on day views, 0–22 on AGP. + var dayDomain: ClosedRange { isMmol ? 2...20 : 36...360 } + var agpDomain: ClosedRange { isMmol ? 0...22 : 0...400 } + var dayTicks: [Double] { isMmol ? [4, 10, 16] : [70, 180, 300] } + var agpTicks: [Double] { isMmol ? [0, 4, 10, 16, 22] : [0, 70, 180, 250, 400] } +} + +// MARK: - Formatting + +enum LoopFormat { + /// A number in the user's locale, e.g. "7.3" or "7,3", with no grouping so values read like Loop's own screens. + static func number(_ value: Double, decimals: Int) -> String { + value.formatted(.number.precision(.fractionLength(decimals)).grouping(.never)) + } + + static func doseUnits(_ units: Double) -> String { + String(format: NSLocalizedString("%1$@ U", comment: "Format string for an amount of insulin in units. (1: amount)"), number(units, decimals: 2)) + } + + static func totalUnits(_ units: Double) -> String { + String(format: NSLocalizedString("%1$@ U", comment: "Format string for an amount of insulin in units. (1: amount)"), number(units, decimals: 1)) + } + + static func rate(_ rate: Double) -> String { + String(format: NSLocalizedString("%1$@ U/h", comment: "Format string for an insulin rate in units per hour. (1: rate)"), number(rate, decimals: 2)) + } + + static func grams(_ grams: Double) -> String { + String(format: NSLocalizedString("%1$@ g", comment: "Format string for an amount of carbohydrate in grams. (1: grams)"), number(grams, decimals: 0)) + } + + static func signed(_ value: Double, decimals: Int = 2) -> String { + let s = number(abs(value), decimals: decimals) + if abs(value) < pow(10, -Double(decimals)) / 2 { return "+" + s } + return (value < 0 ? "−" : "+") + s + } + + static func time(_ date: Date) -> String { + date.formatted(date: .omitted, time: .shortened) + } + + /// "25 Oct 2026" + static func day(_ date: Date) -> String { + date.formatted(.dateTime.day().month(.abbreviated).year()) + } + + /// "Sun 25 Oct" + static func weekdayDay(_ date: Date) -> String { + date.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated)) + } + + /// "12 – 25 Oct 2026", "28 Sep – 25 Oct 2026", "20 Dec 2025 – 2 Jan 2026", in the user's locale. + static func range(_ start: Date, _ end: Date, calendar: Calendar = .current) -> String { + if calendar.isDate(start, inSameDayAs: end) { + return day(end) + } + let formatter = DateIntervalFormatter() + formatter.calendar = calendar + formatter.dateTemplate = "dMMMy" + return formatter.string(from: start, to: end) + } + + /// "13d 11h", "9h 45m", "40m" in English; abbreviated in the user's language. + static func duration(_ interval: TimeInterval) -> String { + let minutes = Int((interval / 60).rounded()) + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .abbreviated + formatter.allowedUnits = minutes >= 1440 ? [.day, .hour] : minutes >= 60 ? [.hour, .minute] : [.minute] + formatter.zeroFormattingBehavior = .dropAll + return formatter.string(from: TimeInterval(minutes * 60)) ?? "" + } +} + +// MARK: - Store conversion boundaries + +extension BolusEvent { + init?(dose: DoseEntry, in interval: DateInterval) { + guard dose.type == .bolus, dose.startDate >= interval.start, dose.startDate < interval.end else { return nil } + self.init(id: dose.syncIdentifier ?? "\(dose.startDate.timeIntervalSince1970)", date: dose.startDate, + units: dose.unitsInDeliverableIncrements, automatic: dose.automatic ?? false, + manuallyEntered: dose.manuallyEntered, dose: dose) + } +} + +extension LoopHistoryMath.BasalDelivery { + init?(dose: DoseEntry) { + let kind: BasalSegment.Kind + switch dose.type { + case .basal: kind = .scheduled + case .tempBasal: kind = .temp + case .suspend: kind = .suspended + default: return nil + } + let hours = dose.endDate.timeIntervalSince(dose.startDate) / 3600 + guard hours > 0 else { return nil } + self.init(start: dose.startDate, end: dose.endDate, + rate: kind == .suspended ? 0 : dose.unitsInDeliverableIncrements / hours, + kind: kind, automatic: dose.automatic ?? (kind == .temp)) + } +} + +extension BasalSegment { + init?(healthSample sample: HKQuantitySample, in interval: DateInterval) { + let hours = sample.endDate.timeIntervalSince(sample.startDate) / 3600 + guard hours > 0 else { return nil } + let scheduled = (sample.metadata?["com.loopkit.InsulinKit.MetadataKeyScheduledBasalRate"] as? HKQuantity)? + .doubleValue(for: HKUnit.internationalUnit().unitDivided(by: .hour())) + let segment = BasalSegment(start: sample.startDate, end: sample.endDate, + rate: sample.quantity.doubleValue(for: .internationalUnit()) / hours, + scheduledRate: scheduled, kind: .temp, + automatic: (sample.metadata?["com.loopkit.InsulinKit.MetadataKeyAutomaticallyIssued"] as? NSNumber)?.boolValue ?? false) + guard let clamped = segment.clamped(to: interval) else { return nil } + self = clamped + } +} diff --git a/Loop/Redesign/LoopHistoryProvider.swift b/Loop/Redesign/LoopHistoryProvider.swift new file mode 100644 index 0000000000..5bdc47d9f6 --- /dev/null +++ b/Loop/Redesign/LoopHistoryProvider.swift @@ -0,0 +1,450 @@ +// +// LoopHistoryProvider.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import Foundation +import HealthKit +import LoopKit +import LoopCore +import LoopAlgorithm +import os + +/// Reads glucose, insulin, carbs and loop runs for the redesigned screens. +/// +/// Loop keeps `LOOP_LOCAL_CACHE_DURATION_DAYS` (7) days in its own stores. Inside that window everything comes from +/// those stores, so manual and automatic doses, temp basals and the schedule are known. Older days come from Apple +/// Health, which has glucose, carbs and insulin split only into basal and bolus. +final class LoopHistoryProvider { + + private let deviceManager: DeviceDataManager + private let loopDataManager: LoopDataManager + private let settingsManager: SettingsManager + private let temporaryPresetsManager: TemporaryPresetsManager + private let dosingDecisionStore: DosingDecisionStoreProtocol + private let doseStore: DoseStore + private let carbStore: CarbStore + private let glucoseStore: GlucoseStore + private let healthStore = HKHealthStore() + private let log = OSLog(category: "LoopHistoryProvider") + private let recordsStartCache = OSAllocatedUnfairLock<(fetched: Date, value: Date)?>(initialState: nil) + + @MainActor + init(deviceManager: DeviceDataManager, loopDataManager: LoopDataManager, settingsManager: SettingsManager, temporaryPresetsManager: TemporaryPresetsManager) { + self.deviceManager = deviceManager + self.loopDataManager = loopDataManager + self.settingsManager = settingsManager + self.temporaryPresetsManager = temporaryPresetsManager + self.dosingDecisionStore = loopDataManager.dosingDecisionStore + self.doseStore = deviceManager.doseStore + self.carbStore = deviceManager.carbStore + self.glucoseStore = deviceManager.glucoseStore + } + + @MainActor + var unitStyle: GlucoseUnitStyle { + GlucoseUnitStyle(unit: deviceManager.displayGlucosePreference.unit) + } + + /// The start of what Loop keeps locally. + var localHistoryStart: Date { + Date().addingTimeInterval(-Bundle.main.localCacheDuration) + } + + @MainActor + var glucoseTargetRange: ClosedRange? { + guard let range = temporaryPresetsManager.effectiveCorrectionRange() else { return nil } + return range.lowerBound.doubleValue(for: .milligramsPerDeciliter)...range.upperBound.doubleValue(for: .milligramsPerDeciliter) + } + + /// The targets that actually applied, including dated presets and changes to the stored profile. + func glucoseTargetBands(in interval: DateInterval) async throws -> [TargetBand] { + if let store = settingsManager.settingsStore { + let previous = try await store.getStoredSettings(end: interval.start, limit: 1).first + if previous?.glucoseTargetRangeSchedule == nil { + let changes = try await store.getStoredSettings(start: interval.start, end: interval.end) + // LoopKit's historical query requires at least one valid profile, even if settings records exist. + guard changes.contains(where: { $0.glucoseTargetRangeSchedule != nil }) else { return [] } + } + } + return try await loopDataManager.glucoseTargetBands(in: interval) + } + + // MARK: - Days + + /// Everything in `window`; IOB and COB only when asked, as they are the slowest part. + func dayHistory(_ window: DateInterval, includeOnBoard: Bool = true) async throws -> DayHistory { + var history = DayHistory(window: window) + let boundary = localHistoryStart + + if window.end > boundary { + let local = DateInterval(start: max(window.start, boundary), end: window.end) + async let glucose = localGlucose(local) + async let insulin = localInsulin(local) + async let carbs = localCarbs(local) + history.glucose = try await glucose + (history.boluses, history.basal, history.scheduledBasal) = try await insulin + history.carbs = try await carbs + if includeOnBoard { + async let iob = localIOB(local) + async let cob = localCOB(local) + history.iob = try await iob + history.cob = try await cob + } + } + + if window.start < boundary { + let older = DateInterval(start: window.start, end: min(window.end, boundary)) + async let glucose = healthGlucose(older) + async let insulin = healthInsulin(older) + async let carbs = healthCarbs(older) + let (boluses, basal) = try await insulin + history.glucose = try await glucose + history.glucose + history.boluses = boluses + history.boluses + history.basal = basal + history.basal + history.carbs = try await carbs + history.carbs + if window.end <= boundary { + history.source = .health + } + } + try Task.checkCancellation() + return history + } + + // MARK: - Loop runs + + /// Loop runs in the local window, oldest first, with when the record of them begins. + func loopRuns() async throws -> (runs: [LoopHistoryMath.LoopRun], coverageStart: Date?) { + let start = localHistoryStart + var runs: [LoopHistoryMath.LoopRun] + if let store = dosingDecisionStore as? DosingDecisionStore { + // Decode only the fields a run needs, and only Loop's local window. + runs = try await store.getDosingDecisions(start: start, end: Date(), as: LoopRunRecord.self) + .filter { $0.reason == "loop" } + .map { record in + LoopHistoryMath.LoopRun(date: record.date, succeeded: (record.errors ?? []).isEmpty, + error: record.errors?.first.map { Self.describe(issue: $0.id) }) + } + } else { + runs = try await anchoredLoopRuns() + } + try Task.checkCancellation() + runs = runs.filter { $0.date >= start }.sorted { $0.date < $1.date } + return (runs, runs.first?.date) + } + + private struct LoopRunRecord: Decodable { + struct Issue: Decodable { let id: String } + let date: Date + let reason: String + let errors: [Issue]? + } + + /// Fetch each snapshot independently: overlapping reloads must not append the same anchored page twice. + private func anchoredLoopRuns() async throws -> [LoopHistoryMath.LoopRun] { + var anchor: DosingDecisionStore.QueryAnchor? + var runs: [LoopHistoryMath.LoopRun] = [] + while true { + let page: (DosingDecisionStore.QueryAnchor, [StoredDosingDecision]) = try await withCheckedThrowingContinuation { continuation in + dosingDecisionStore.executeDosingDecisionQuery(fromQueryAnchor: anchor, limit: 1000) { result in + switch result { + case .success(let anchor, let decisions): continuation.resume(returning: (anchor, decisions)) + case .failure(let error): continuation.resume(throwing: error) + } + } + } + try Task.checkCancellation() + anchor = page.0 + runs += page.1.filter { $0.reason == "loop" }.map { decision in + LoopHistoryMath.LoopRun(date: decision.date, succeeded: decision.errors.isEmpty, + error: decision.errors.first.map { Self.describe(issue: $0.id) }) + } + if page.1.count < 1000 { break } + } + return runs + } + + /// "glucoseTooOld" → "Glucose too old". Loop's own issue ids are localised; any other id falls back to the + /// id split into English words. + static func describe(issue id: String) -> String { + switch id { + case "configurationError": return NSLocalizedString("Configuration error", comment: "Reason Loop could not run: its settings are incomplete") + case "connectionError": return NSLocalizedString("Connection error", comment: "Reason Loop could not run: no connected devices, or a device connection failed") + case "missingDataError": return NSLocalizedString("Missing data error", comment: "Reason Loop could not run: data it needs was missing") + case "glucoseTooOld": return NSLocalizedString("Glucose too old", comment: "Reason Loop could not run: the latest glucose reading was too old") + case "invalidFutureGlucose": return NSLocalizedString("Invalid future glucose", comment: "Reason Loop could not run: a glucose reading had a timestamp in the future") + case "pumpDataTooOld": return NSLocalizedString("Pump data too old", comment: "Reason Loop could not run: the latest pump data was too old") + case "recommendationExpired": return NSLocalizedString("Recommendation expired", comment: "Reason Loop could not run: its dosing recommendation expired before it was used") + case "pumpSuspended": return NSLocalizedString("Pump suspended", comment: "Reason Loop could not run: insulin delivery on the pump was suspended") + case "pumpManagerError": return NSLocalizedString("Pump manager error", comment: "Reason Loop could not run: the pump reported an error") + case "unknownError": return NSLocalizedString("Unknown error", comment: "Reason Loop could not run: an unexpected error") + default: break + } + var words: [String] = [] + var current = "" + for character in id { + if character.isUppercase, !current.isEmpty { + words.append(current) + current = "" + } + current.append(character) + } + words.append(current) + let sentence = words.joined(separator: " ").lowercased() + return sentence.prefix(1).uppercased() + sentence.dropFirst() + } + + // MARK: - Loop's stores + + private func localGlucose(_ interval: DateInterval) async throws -> [GlucosePoint] { + do { + let samples = try await glucoseStore.getGlucoseSamples(start: interval.start, end: interval.end) + return samples + .filter { !$0.isDisplayOnly } + .map { GlucosePoint(date: $0.startDate, mgdl: $0.quantity.doubleValue(for: .milligramsPerDeciliter)) } + } catch { + os_log("Glucose fetch failed: %{public}@", log: log, type: .error, String(describing: error)) + throw error + } + } + + /// Doses for `interval`, with the start of Loop's insulin records (its earliest local dose). A found start is kept + /// for a few minutes so day pages and periods don't each read the whole local history; when it isn't known, + /// one read from the start of the local history serves both. No dose before `interval.end` says nothing about + /// later days, so that isn't kept. + private func doses(for interval: DateInterval) async throws -> (doses: [DoseEntry], recordsStart: Date?) { + let localStart = localHistoryStart + if interval.start <= localStart { + let doses = try await doseStore.getNormalizedDoseEntries(start: interval.start, end: interval.end) + return (doses, doses.map(\.startDate).min()) + } + if let cached = recordsStartCache.withLock({ $0 }), Date().timeIntervalSince(cached.fetched) < 300 { + return (try await doseStore.getNormalizedDoseEntries(start: interval.start, end: interval.end), cached.value) + } + let local = try await doseStore.getNormalizedDoseEntries(start: localStart, end: interval.end) + let recordsStart = local.map(\.startDate).min() + if let recordsStart { + recordsStartCache.withLock { $0 = (fetched: Date(), value: recordsStart) } + } + return (local.filter { + $0.startDate < interval.end && ($0.type == .bolus ? $0.startDate >= interval.start : $0.endDate > interval.start) + }, recordsStart) + } + + private func localInsulin(_ interval: DateInterval) async throws -> ([BolusEvent], [BasalSegment], [ScheduledBasalPoint]) { + let doses: [DoseEntry] + let recordsStart: Date? + do { + (doses, recordsStart) = try await self.doses(for: interval) + } catch { + os_log("Dose fetch failed: %{public}@", log: log, type: .error, String(describing: error)) + throw error + } + + let boluses = doses.compactMap { BolusEvent(dose: $0, in: interval) } + + let basalHistory = try await loopDataManager.settingsProvider.getBasalHistory(startDate: interval.start, endDate: interval.end) + let temporaryPresetsManager = self.temporaryPresetsManager + let overrides = await MainActor.run { + temporaryPresetsManager.presetHistory.getOverrideHistory(startDate: interval.start, endDate: interval.end) + } + // A resolved daily schedule repeats recent presets on every day. Use the stored profile and dated overrides. + let scheduleValues = overrides.applyBasal(over: basalHistory) + let schedule = scheduleValues.map { LoopHistoryMath.ScheduleValue(start: $0.startDate, end: $0.endDate, rate: $0.value) } + let deliveries = doses.compactMap { LoopHistoryMath.BasalDelivery(dose: $0) } + let now = Date() + let window = DateInterval(start: interval.start, end: max(interval.start, min(interval.end, now))) + // As in Loop's insulin totals: after the first record, unrecorded time runs at the pump's programmed rate. + let programmed = basalHistory.map { LoopHistoryMath.ScheduleValue(start: $0.startDate, end: $0.endDate, rate: $0.value) } + let basal = window.duration > 0 + ? LoopHistoryMath.basalSegments(window: window, schedule: schedule, deliveries: deliveries, + programmedRates: programmed, recordsStart: recordsStart) + : [] + let scheduled = schedule.map { ScheduledBasalPoint(start: max($0.start, interval.start), end: min($0.end, interval.end), rate: $0.rate) } + return (boluses, basal, scheduled) + } + + private func localCarbs(_ interval: DateInterval) async throws -> [CarbEvent] { + do { + let entries = try await carbStore.getCarbEntries(start: interval.start, end: interval.end) + return entries.map { entry in + CarbEvent(id: entry.syncIdentifier ?? "\(entry.startDate.timeIntervalSince1970)", + date: entry.startDate, + grams: entry.quantity.doubleValue(for: .gram), + absorptionHours: entry.absorptionTime.map { $0 / 3600 }, + foodType: entry.foodType, + createdByLoop: entry.createdByCurrentApp, + entry: entry) + } + } catch { + os_log("Carb fetch failed: %{public}@", log: log, type: .error, String(describing: error)) + throw error + } + } + + private func localIOB(_ interval: DateInterval) async throws -> [TimedValue] { + try await loopDataManager.insulinOnBoardTimeline(in: interval).map { TimedValue(date: $0.startDate, value: $0.value) } + } + + private func localCOB(_ interval: DateInterval) async throws -> [TimedValue] { + try await loopDataManager.carbsOnBoardTimeline(in: interval).map { TimedValue(date: $0.startDate, value: $0.quantity.doubleValue(for: .gram)) } + } + + // MARK: - Apple Health + + private func healthSamples(_ type: HKQuantityType, _ interval: DateInterval, options: HKQueryOptions = .strictStartDate) async throws -> [HKQuantitySample] { + guard interval.duration > 0 else { return [] } + let log = self.log + return try await withCheckedThrowingContinuation { continuation in + let predicate = HKQuery.predicateForSamples(withStart: interval.start, end: interval.end, options: options) + let sort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true) + let query = HKSampleQuery(sampleType: type, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sort]) { _, samples, error in + if let error { + os_log("Health query for %{public}@ failed: %{public}@", log: log, type: .error, type.identifier, String(describing: error)) + continuation.resume(throwing: error) + } else { + continuation.resume(returning: (samples as? [HKQuantitySample]) ?? []) + } + } + healthStore.execute(query) + } + } + + /// Several apps can write the same CGM reading to Health; keep one reading per 2½ minutes. + private func healthGlucose(_ interval: DateInterval) async throws -> [GlucosePoint] { + let samples = try await healthSamples(HKQuantityType(.bloodGlucose), interval) + var points: [GlucosePoint] = [] + let own = HKSource.default().bundleIdentifier + for sample in samples where !sample.isDisplayOnly + && (FeatureFlags.observeHealthKitGlucoseSamplesFromOtherApps || sample.sourceRevision.source.bundleIdentifier == own) { + if let last = points.last, sample.startDate.timeIntervalSince(last.date) < 150 { continue } + points.append(GlucosePoint(date: sample.startDate, mgdl: sample.quantity.doubleValue(for: .milligramsPerDeciliter))) + } + return points + } + + private func healthInsulin(_ interval: DateInterval) async throws -> ([BolusEvent], [BasalSegment]) { + let samples = try await healthSamples(HKQuantityType(.insulinDelivery), interval, options: []) + var boluses: [BolusEvent] = [] + var basal: [BasalSegment] = [] + let own = HKSource.default().bundleIdentifier + for sample in samples where FeatureFlags.observeHealthKitDoseSamplesFromOtherApps || sample.sourceRevision.source.bundleIdentifier == own { + let units = sample.quantity.doubleValue(for: .internationalUnit()) + let reason = (sample.metadata?[HKMetadataKeyInsulinDeliveryReason] as? NSNumber)?.intValue + let automaticFlag = (sample.metadata?["com.loopkit.InsulinKit.MetadataKeyAutomaticallyIssued"] as? NSNumber)?.boolValue + let automatic = automaticFlag ?? false + if reason == HKInsulinDeliveryReason.basal.rawValue { + if let segment = BasalSegment(healthSample: sample, in: interval) { + basal.append(segment) + } + } else if sample.startDate >= interval.start && sample.startDate < interval.end { + boluses.append(BolusEvent(id: sample.uuid.uuidString, date: sample.startDate, units: units, automatic: automatic, manuallyEntered: false, + automaticKnown: automaticFlag != nil)) + } + } + return (boluses, basal) + } + + private func healthCarbs(_ interval: DateInterval) async throws -> [CarbEvent] { + let samples = try await healthSamples(HKQuantityType(.dietaryCarbohydrates), interval) + let own = HKSource.default().bundleIdentifier + return samples + .filter { FeatureFlags.observeHealthKitCarbSamplesFromOtherApps || $0.sourceRevision.source.bundleIdentifier == own } + .map { sample in + CarbEvent(id: sample.uuid.uuidString, date: sample.startDate, grams: sample.quantity.doubleValue(for: .gram()), + absorptionHours: nil, foodType: sample.metadata?[HKMetadataKeyFoodType] as? String, + createdByLoop: sample.sourceRevision.source.bundleIdentifier == own) + } + } +} + +// MARK: - Loop mode history + +/// Records when closed loop is switched on and off, because Loop's stored runs don't say which mode they ran in. +/// The Trends screen can only call time "open loop" from the first record on. +final class LoopModeHistory { + static let shared = LoopModeHistory() + + private let defaults: UserDefaults + private let key = "com.loopkit.Loop.redesign.loopModeChanges" + private let maximumAge: TimeInterval = .days(100) + + init(defaults: UserDefaults = .standard) { + self.defaults = defaults + } + + var changes: [LoopHistoryMath.ModeChange] { + guard let data = defaults.data(forKey: key), + let changes = try? JSONDecoder().decode([LoopHistoryMath.ModeChange].self, from: data) else { + return [] + } + return changes + } + + func record(closedLoop: Bool, at date: Date = Date()) { + var changes = self.changes + guard changes.last?.closedLoop != closedLoop else { return } + changes.append(.init(date: date, closedLoop: closedLoop)) + // Keep the last change before the retention window: it establishes the mode at its start. + let cutoff = date.addingTimeInterval(-maximumAge) + if let anchor = changes.lastIndex(where: { $0.date < cutoff }), anchor > 0 { + changes.removeFirst(anchor) + } + if let data = try? JSONEncoder().encode(changes) { + defaults.set(data, forKey: key) + } + } +} + +// MARK: - Loop's on-board history + +extension LoopDataManager { + /// A historical chart must use the profile and presets from that time, rather than repeat today's targets. + func glucoseTargetBands(in interval: DateInterval) async throws -> [TargetBand] { + guard settingsProvider.settings.glucoseTargetRangeSchedule != nil else { return [] } + let schedule = try await settingsProvider.getTargetRangeHistory(startDate: interval.start, endDate: interval.end) + var bands = schedule.map { + TargetBand(start: max($0.startDate, interval.start), end: min($0.endDate, interval.end), + range: $0.value.lowerBound.doubleValue(for: .milligramsPerDeciliter)...$0.value.upperBound.doubleValue(for: .milligramsPerDeciliter)) + } + let overrides = temporaryPresetsManager.presetHistory.getOverrideHistory(startDate: interval.start, endDate: interval.end) + for override in overrides.sorted(by: { $0.startDate < $1.startDate }) { + guard let target = override.settings.targetRange, + let active = override.activeInterval.intersection(with: interval), active.duration > 0 else { continue } + bands = bands.flatMap { band -> [TargetBand] in + var pieces: [TargetBand] = [] + if band.start < active.start { + pieces.append(TargetBand(start: band.start, end: min(band.end, active.start), range: band.range)) + } + if band.end > active.end { + pieces.append(TargetBand(start: max(band.start, active.end), end: band.end, range: band.range)) + } + return pieces + } + [TargetBand(start: active.start, end: active.end, + range: target.lowerBound.doubleValue(for: .milligramsPerDeciliter)...target.upperBound.doubleValue(for: .milligramsPerDeciliter))] + } + return bands.filter { $0.end > $0.start }.sorted { $0.start < $1.start } + } + + /// Insulin on board across `interval`, from the doses and basal history Loop keeps, as `iobValues` is for the display window. + func insulinOnBoardTimeline(in interval: DateInterval) async throws -> [InsulinValue] { + let dosesStart = interval.start.addingTimeInterval(-InsulinMath.defaultInsulinActivityDuration) + let doses = try await doseStore.getNormalizedDoseEntries(start: dosesStart, end: interval.end) + .map { $0.simpleDose(with: insulinModel(for: $0.insulinType)) } + let basalStart = min(dosesStart, doses.map(\.startDate).min() ?? dosesStart) + let basal = try await settingsProvider.getBasalHistory(startDate: basalStart, endDate: interval.end) + let overrides = temporaryPresetsManager.presetHistory.getOverrideHistory(startDate: basalStart, endDate: interval.end) + return doses.annotated(with: overrides.applyBasal(over: basal).algorithmSchedule()) + .insulinOnBoardTimeline(from: interval.start, to: interval.end) + .map(\.loopKit) + } + + /// Carbs on board across `interval`, using the same absorption review as Loop's carb screen. + func carbsOnBoardTimeline(in interval: DateInterval) async throws -> [CarbValue] { + let review = try await fetchCarbAbsorptionReview(start: interval.start.addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval), end: interval.end) + return review.carbStatuses.dynamicCarbsOnBoard(from: interval.start, to: interval.end, absorptionModel: carbAbsorptionModel.model) + .map(\.loopKit) + } +} diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift new file mode 100644 index 0000000000..5f93573c36 --- /dev/null +++ b/Loop/Redesign/TrendsComponents.swift @@ -0,0 +1,766 @@ +// +// TrendsComponents.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import Charts + +private typealias P = LoopDesign.Palette + +// MARK: - RangeSelector + +enum TrendsPeriod: Int, CaseIterable, Identifiable { + case day = 1 + case week = 7 + case twoWeeks = 14 + case month = 30 + case quarter = 90 + + var id: Int { rawValue } + + var label: String { + self == .day + ? NSLocalizedString("Day (Trends period)", value: "Day", comment: "Trends period picker: a period of one calendar day (not daytime)") + : String(format: NSLocalizedString("%1$@ d", comment: "Trends period picker, short label for a period of several days, e.g. 7 d. (1: number of days)"), + LoopFormat.number(Double(rawValue), decimals: 0)) + } + + var accessibilityName: String { + self == .day + ? NSLocalizedString("Day (Trends period)", value: "Day", comment: "Trends period picker: a period of one calendar day (not daytime)") + : String.localizedStringWithFormat(NSLocalizedString("%lld days", comment: "Trends period picker, VoiceOver name for a period of several days. (1: number of days)"), rawValue) + } + + /// The period ending on `endDay` (inclusive), cut off at `now` when it ends today. + func interval(endingOn endDay: Date, now: Date = Date(), calendar: Calendar = .current) -> DateInterval { + let endOfDay = calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: endDay))! + let start = calendar.date(byAdding: .day, value: -rawValue, to: endOfDay)! + return DateInterval(start: start, end: min(endOfDay, now)) + } +} + +struct RangeSelectorView: View { + @Binding var period: TrendsPeriod + @Binding var endDay: Date + var now: Date = Date() + + private var calendar: Calendar { .current } + + private var canStepForward: Bool { + !calendar.isDate(endDay, inSameDayAs: now) && endDay < now + } + + var body: some View { + VStack(spacing: LoopDesign.Space.s3) { + HStack(spacing: 2) { + ForEach(TrendsPeriod.allCases) { option in + Button { + withAnimation(.snappy(duration: 0.2)) { period = option } + } label: { + Text(option.label) + .font(.footnote.weight(.semibold)) + .foregroundStyle(P.ink) + .frame(maxWidth: .infinity, minHeight: 30) + .background { + if option == period { + Capsule().fill(P.controlThumb).shadow(color: .black.opacity(0.12), radius: 3, y: 1) + } + } + .contentShape(Capsule()) + } + .buttonStyle(.plain) + .accessibilityLabel(option.accessibilityName) + .accessibilityAddTraits(option == period ? .isSelected : []) + } + } + .padding(2) + .background(P.surfaceSunken, in: Capsule()) + + HStack { + StepButton(systemName: "chevron.left", label: NSLocalizedString("Previous period", comment: "VoiceOver label for the Trends button that moves back one period")) { step(-1) } + Spacer() + Text(LoopFormat.range(period.interval(endingOn: endDay, now: now).start, endDay)) + .font(LoopDesign.Typeface.headline.monospacedDigit()) + .foregroundStyle(P.ink) + Spacer() + StepButton(systemName: "chevron.right", label: NSLocalizedString("Next period", comment: "VoiceOver label for the Trends button that moves forward one period"), enabled: canStepForward) { step(1) } + } + } + } + + private func step(_ direction: Int) { + guard let next = calendar.date(byAdding: .day, value: direction * period.rawValue, to: endDay) else { return } + endDay = min(next, now) + } +} + +// MARK: - StatGrid + +struct StatTile: Identifiable { + let label: String + let value: String + let unit: String? + let detail: String? + var valueColor: Color = LoopDesign.Palette.ink + var id: String { label } +} + +struct StatGridView: View { + @Environment(\.dynamicTypeSize) private var typeSize + let tiles: [StatTile] + + /// Pairs of tiles, so both tiles in a row take the taller one's height; one per row at accessibility sizes. + private var rows: [[StatTile]] { + let shown = Array(tiles.prefix(6)) + let perRow = typeSize.isAccessibilitySize ? 1 : 2 + return stride(from: 0, to: shown.count, by: perRow).map { Array(shown[$0.. some View { + VStack(alignment: .leading, spacing: 2) { + Text(tile.label) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + HStack(alignment: .firstTextBaseline, spacing: 2) { + Text(tile.value) + .font(LoopDesign.Typeface.statValue) + .foregroundStyle(tile.valueColor) + .minimumScaleFactor(0.7) + .lineLimit(1) + if let unit = tile.unit { + Text(unit) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + } + } + if let detail = tile.detail { + Text(detail) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + .lineLimit(2) + } + } + .padding(LoopDesign.Space.s3) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background(P.surfaceSunken, in: RoundedRectangle(cornerRadius: LoopDesign.Radius.md, style: .continuous)) + .accessibilityElement(children: .combine) + } +} + +// MARK: - TimeInRange + +extension LoopHistoryMath.RangeBand { + /// The band's name in the user's language. + var localizedName: String { + switch self { + case .veryHigh: return NSLocalizedString("Very high", comment: "Time in range band: glucose above 250 mg/dL (13.9 mmol/L)") + case .high: return NSLocalizedString("High", comment: "Time in range band: glucose 181–250 mg/dL (10.1–13.9 mmol/L)") + case .inRange: return NSLocalizedString("In range", comment: "Time in range band: glucose 70–180 mg/dL (3.9–10.0 mmol/L)") + case .low: return NSLocalizedString("Low", comment: "Time in range band: glucose 54–69 mg/dL (3.0–3.8 mmol/L)") + case .veryLow: return NSLocalizedString("Very low", comment: "Time in range band: glucose below 54 mg/dL (3.0 mmol/L)") + } + } + + /// Consensus thresholds with the user's decimal separator, e.g. "3,9–10,0". + func localizedThresholds(_ unit: GlucoseUnitStyle) -> String { + let decimals = unit.isMmol ? 1 : 0 + let n: (Double) -> String = { LoopFormat.number($0, decimals: decimals) } + switch (self, unit.isMmol) { + case (.veryHigh, true): return "> " + n(13.9) + case (.high, true): return n(10.1) + "–" + n(13.9) + case (.inRange, true): return n(3.9) + "–" + n(10.0) + case (.low, true): return n(3.0) + "–" + n(3.8) + case (.veryLow, true): return "< " + n(3.0) + case (.veryHigh, false): return "> " + n(250) + case (.high, false): return n(181) + "–" + n(250) + case (.inRange, false): return n(70) + "–" + n(180) + case (.low, false): return n(54) + "–" + n(69) + case (.veryLow, false): return "< " + n(54) + } + } +} + +/// "12%" in the user's language. +private func percentText(_ value: Double, decimals: Int = 0) -> String { + String(format: NSLocalizedString("%1$@%%", comment: "A percentage. (1: number)"), LoopFormat.number(value, decimals: decimals)) +} + +struct TimeInRangeView: View { + @Environment(\.dynamicTypeSize) private var typeSize + + /// Fractions per band, 0–1. + let fractions: [LoopHistoryMath.RangeBand: Double] + let unit: GlucoseUnitStyle + + private var percentages: [LoopHistoryMath.RangeBand: Int] { LoopHistoryMath.roundedPercentages(fractions) } + + static func color(_ band: LoopHistoryMath.RangeBand) -> Color { + switch band { + case .veryHigh: return P.rangeVeryHigh + case .high: return P.rangeHigh + case .inRange: return P.rangeIn + case .low: return P.rangeLow + case .veryLow: return P.rangeVeryLow + } + } + + var body: some View { + HStack(alignment: .top, spacing: LoopDesign.Space.s4) { + bar + .frame(width: 14) + VStack(spacing: LoopDesign.Space.s2) { + ForEach(LoopHistoryMath.RangeBand.allCases) { band in + row(band) + } + } + } + .fixedSize(horizontal: false, vertical: true) + } + + private var bar: some View { + GeometryReader { geometry in + let minimum: CGFloat = 4 + let bands = LoopHistoryMath.RangeBand.allCases + let raw = bands.map { CGFloat(fractions[$0] ?? 0) } + let floors = raw.map { $0 > 0 ? max(minimum, $0 * geometry.size.height) : 0 } + let spare = geometry.size.height - floors.reduce(0, +) + // Trim the space the minimums borrowed from the largest band. + let largest = raw.indices.max { raw[$0] < raw[$1] } ?? 2 + VStack(spacing: 1) { + ForEach(Array(bands.enumerated()), id: \.offset) { index, band in + let height = index == largest ? max(0, floors[index] + spare) : floors[index] + if height > 0 { + Rectangle().fill(Self.color(band)).frame(height: max(0, height - 1)) + } + } + } + .clipShape(Capsule()) + } + .accessibilityHidden(true) + } + + private func row(_ band: LoopHistoryMath.RangeBand) -> some View { + let fraction = fractions[band] ?? 0 + let hours = fraction * 24 + let thresholds = Text(band.localizedThresholds(unit)) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + return VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { + Text(percentText(Double(percentages[band] ?? 0))) + .font(LoopDesign.Typeface.statValueSmall) + .foregroundStyle(P.ink) + .frame(minWidth: typeSize.isAccessibilitySize ? nil : 52, alignment: .trailing) + Text(band.localizedName) + .font(LoopDesign.Typeface.subhead) + .foregroundStyle(P.ink) + if !typeSize.isAccessibilitySize { + thresholds + } + Spacer(minLength: LoopDesign.Space.s1) + Text(String(format: NSLocalizedString("%1$@ h", comment: "Time in range row: average hours a day spent in the band, abbreviated. (1: hours with one decimal)"), + LoopFormat.number(hours, decimals: 1))) + .font(LoopDesign.Typeface.footnote.monospacedDigit()) + .foregroundStyle(P.inkMuted) + } + if typeSize.isAccessibilitySize { + thresholds + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(String(format: NSLocalizedString("%1$@, %2$@ %3$@: %4$@ percent, %5$@ hours a day", comment: "VoiceOver label for a Time in range row. (1: band name, e.g. In range, 2: glucose thresholds, e.g. 3.9–10.0, 3: glucose unit, 4: percentage number, 5: average hours a day with one decimal)"), + band.localizedName, band.localizedThresholds(unit), unit.unitName, + LoopFormat.number(Double(percentages[band] ?? 0), decimals: 0), LoopFormat.number(hours, decimals: 1))) + } +} + +// MARK: - AGP + +struct TrendsAGPChart: View { + let slots: [LoopHistoryMath.AGPSlot] + let readings: [GlucosePoint] + let days: Int + let unit: GlucoseUnitStyle + let targetRange: ClosedRange? + var height: CGFloat = 180 + + private func v(_ mgdl: Double) -> Double { + min(max(unit.value(mgdl), unit.agpDomain.lowerBound), unit.agpDomain.upperBound) + } + + private static let hourTicks = [0, 360, 720, 1080, 1440] + + private static func label(minute: Int) -> String { + let minute = minute % 1440 + let date = Calendar.current.date(bySettingHour: minute / 60, minute: minute % 60, second: 0, of: Date())! + return DayAxis.hourLabel(date) + } + + var enoughDays: Bool { days >= 5 } + + var body: some View { + VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { + if readings.isEmpty { + ChartPlaceholder(message: NSLocalizedString("No CGM data in this period", comment: "Trends chart placeholder when the period has no glucose readings"), height: height) + } else { + chart.frame(height: height) + if !enoughDays { + Text(String(format: NSLocalizedString("Not enough days for a pattern: %1$@ of 5", comment: "Trends Daily pattern footnote when fewer than 5 days have readings; the chart shows raw readings instead. (1: days with readings)"), + LoopFormat.number(Double(days), decimals: 0))) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + } + } + } + } + + private func minuteOfDay(_ date: Date) -> Int { + let parts = Calendar.current.dateComponents([.hour, .minute], from: date) + return (parts.hour ?? 0) * 60 + (parts.minute ?? 0) + } + + private var chart: some View { + Chart { + if let targetRange { + RectangleMark(xStart: .value("Start", 0), xEnd: .value("End", 1440), + yStart: .value("Low", v(targetRange.lowerBound)), yEnd: .value("High", v(targetRange.upperBound))) + .foregroundStyle(P.targetBand) + } + if enoughDays { + ForEach(slots, id: \.minute) { slot in + AreaMark(x: .value("Time", slot.minute), yStart: .value("5%", v(slot.p5)), yEnd: .value("95%", v(slot.p95)), series: .value("Band", "outer")) + .foregroundStyle(P.agpOuter) + .interpolationMethod(.monotone) + } + ForEach(slots, id: \.minute) { slot in + AreaMark(x: .value("Time", slot.minute), yStart: .value("25%", v(slot.p25)), yEnd: .value("75%", v(slot.p75)), series: .value("Band", "inner")) + .foregroundStyle(P.agpInner) + .interpolationMethod(.monotone) + } + ForEach(slots, id: \.minute) { slot in + LineMark(x: .value("Time", slot.minute), y: .value("Median", v(slot.p50)), series: .value("Band", "median")) + .foregroundStyle(P.glucose) + .lineStyle(StrokeStyle(lineWidth: 2.5)) + .interpolationMethod(.monotone) + } + } else { + ForEach(Array(readings.enumerated()), id: \.offset) { _, point in + PointMark(x: .value("Time", minuteOfDay(point.date)), y: .value("Glucose", v(point.mgdl))) + .symbolSize(8) + .foregroundStyle(P.glucose) + } + } + } + .chartXScale(domain: 0...1440) + .chartYScale(domain: unit.agpDomain) + .chartXAxis { + AxisMarks(values: Self.hourTicks) { value in + AxisGridLine().foregroundStyle(P.grid) + AxisValueLabel { + if let minute = value.as(Int.self) { Text(Self.label(minute: minute)) } + } + .font(LoopDesign.Typeface.caption) + .foregroundStyle(P.inkMuted) + } + } + .chartYAxis { + AxisMarks(position: .leading, values: unit.agpTicks) { _ in + AxisGridLine().foregroundStyle(P.grid) + AxisValueLabel().font(LoopDesign.Typeface.caption).foregroundStyle(P.inkMuted) + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilitySummary) + } + + private var accessibilitySummary: String { + guard enoughDays, let high = slots.max(by: { $0.p50 < $1.p50 }), let low = slots.min(by: { $0.p50 < $1.p50 }) else { + return NSLocalizedString("Daily pattern: not enough days", comment: "VoiceOver label for the Trends Daily pattern chart when fewer than 5 days have readings") + } + let span = String.localizedStringWithFormat(NSLocalizedString("%lld days", comment: "A number of days, e.g. a Trends period or how many days are averaged. (1: number of days)"), days) + return String(format: NSLocalizedString("Daily pattern over %1$@. Median highest %2$@ at %3$@, lowest %4$@ at %5$@", comment: "VoiceOver label for the Trends Daily pattern chart. (1: days with readings, e.g. 14 days; 2: highest median glucose; 3: time of day; 4: lowest median glucose; 5: time of day)"), + span, unit.format(high.p50), Self.label(minute: high.minute), unit.format(low.p50), Self.label(minute: low.minute)) + } +} + +// MARK: - InsulinSplit + +/// Basal against bolus in one capsule: basal, then manual bolus, then automatic bolus. +struct InsulinSplitBar: View { + let split: InsulinSplit + /// A 6-point line for list headers. It is still tall enough to show the automatic bolus stripes. + var hairline = false + @Environment(\.colorScheme) private var scheme + + private enum Part: CaseIterable { case basal, manual, automatic } + + private func units(_ part: Part) -> Double { + switch part { + case .basal: return split.basal + // Unclassified bolus draws as plain bolus. + case .manual: return split.manualBolus + split.unclassifiedBolus + case .automatic: return split.automaticBolus + } + } + + var body: some View { + GeometryReader { geometry in + let parts = Part.allCases.filter { units($0) > 0 } + let gap: CGFloat = hairline ? 1.5 : 2.5 + let width = geometry.size.width - gap * CGFloat(max(0, parts.count - 1)) + let minimum: CGFloat = hairline ? 2 : 4 + let widths = parts.map { max(minimum, width * units($0) / max(split.total, .ulpOfOne)) } + // Take what the minimums add from the widest part, so the bar keeps its length. + let excess = widths.reduce(0, +) - width + let widest = widths.indices.max { widths[$0] < widths[$1] } + HStack(spacing: gap) { + ForEach(Array(parts.enumerated()), id: \.offset) { index, part in + segment(part).frame(width: max(0, widths[index] - (index == widest ? excess : 0))) + } + } + .clipShape(Capsule()) + } + .frame(height: hairline ? 6 : 14) + .accessibilityHidden(true) + } + + @ViewBuilder + private func segment(_ part: Part) -> some View { + switch part { + case .basal: + Rectangle().fill(LoopDesign.Palette.basal) + .overlay(alignment: .trailing) { + if !hairline { Rectangle().fill(LoopDesign.Palette.basalEdge).frame(width: 2) } + } + case .manual: + Rectangle().fill(LoopDesign.Palette.bolus) + case .automatic: + Rectangle().fill(Hatch.paint(LoopDesign.Palette.bolus, scheme: scheme)) + } + } +} + +/// The basal and bolus card body: the bar, units and shares, how the bolus divides, and the total. +struct InsulinSplitView: View { + let split: InsulinSplit + /// Today so far, rather than a whole day or a per-day average. + var partial = false + + var body: some View { + VStack(alignment: .leading, spacing: LoopDesign.Space.s3) { + InsulinSplitBar(split: split) + VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { + row(.basal, NSLocalizedString("Basal", comment: "Basal and bolus row: insulin delivered as basal"), split.basal, split.basalPercent) + row(.solid(LoopDesign.Palette.bolus), NSLocalizedString("Bolus", comment: "Basal and bolus row: insulin delivered as bolus, manual and automatic"), split.bolus, split.bolusPercent) + if split.isClassified && split.bolus > 0 { + FlowLayout(spacing: LoopDesign.Space.s3, lineSpacing: 2) { + part(.solid(LoopDesign.Palette.bolus), String(format: NSLocalizedString("Manual %1$@", comment: "Basal and bolus: bolus the person gave. (1: units, e.g. 16.2 U)"), LoopFormat.totalUnits(split.manualBolus))) + part(.hatch(LoopDesign.Palette.bolus), String(format: NSLocalizedString("Automatic %1$@", comment: "Basal and bolus: bolus Loop gave automatically. (1: units, e.g. 5.4 U)"), LoopFormat.totalUnits(split.automaticBolus))) + } + .font(LoopDesign.Typeface.footnote.monospacedDigit()) + .foregroundStyle(LoopDesign.Palette.inkMuted) + .padding(.leading, 10 + LoopDesign.Space.s2) + } + Divider().padding(.vertical, 2) + HStack(alignment: .firstTextBaseline) { + Text(partial + ? NSLocalizedString("Total so far", comment: "Basal and bolus total row for today, which isn't over yet") + : NSLocalizedString("Total", comment: "Basal and bolus total row: basal plus bolus")) + .font(LoopDesign.Typeface.subhead.weight(.semibold)) + .foregroundStyle(LoopDesign.Palette.ink) + Spacer() + Text(LoopFormat.totalUnits(split.total)) + .font(LoopDesign.Typeface.statValueSmall) + .foregroundStyle(LoopDesign.Palette.insulinInk) + } + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(Self.accessibilityLabel(split, partial: partial)) + } + + private func row(_ swatch: LegendItem.Swatch, _ label: String, _ units: Double, _ percent: Int) -> some View { + HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { + LegendSwatch(swatch: swatch).alignmentGuide(.firstTextBaseline) { $0[.bottom] - 1 } + Text(label) + .font(LoopDesign.Typeface.subhead) + .foregroundStyle(LoopDesign.Palette.ink) + Spacer() + Text(LoopFormat.totalUnits(units)) + .font(LoopDesign.Typeface.body.weight(.semibold).monospacedDigit()) + .foregroundStyle(LoopDesign.Palette.ink) + Text(verbatim: "\(percent)%") + .font(LoopDesign.Typeface.footnote.monospacedDigit()) + .foregroundStyle(LoopDesign.Palette.inkMuted) + .frame(minWidth: 38, alignment: .trailing) + } + } + + private func part(_ swatch: LegendItem.Swatch, _ text: String) -> some View { + HStack(spacing: LoopDesign.Space.s1) { + LegendSwatch(swatch: swatch) + Text(text) + } + } + + static func accessibilityLabel(_ split: InsulinSplit, partial: Bool) -> String { + var parts = [ + String(format: NSLocalizedString("Basal %1$@, %2$@ percent", comment: "VoiceOver: basal units and share. (1: units, 2: percentage number)"), LoopFormat.totalUnits(split.basal), "\(split.basalPercent)"), + String(format: NSLocalizedString("Bolus %1$@, %2$@ percent", comment: "VoiceOver: bolus units and share. (1: units, 2: percentage number)"), LoopFormat.totalUnits(split.bolus), "\(split.bolusPercent)"), + ] + if split.isClassified && split.bolus > 0 { + parts.append(String(format: NSLocalizedString("%1$@ manual, %2$@ automatic", comment: "VoiceOver: how the bolus divides. (1: manual units, 2: automatic units)"), + LoopFormat.totalUnits(split.manualBolus), LoopFormat.totalUnits(split.automaticBolus))) + } + parts.append(String(format: partial + ? NSLocalizedString("Total so far %1$@", comment: "VoiceOver: today's total insulin so far. (1: units)") + : NSLocalizedString("Total %1$@", comment: "VoiceOver: total insulin. (1: units)"), + LoopFormat.totalUnits(split.total))) + return parts.joined(separator: ". ") + } +} + +// MARK: - InsulinDailyTotals + +struct InsulinDailyTotalsView: View { + let totals: [LoopHistoryMath.DayTotal] + /// Weekly averages instead of days, for periods past 30 days. + let weekly: Bool + /// Calculated from the original daily totals, even when the bars are weekly averages. + let average: Double + var height: CGFloat = 170 + var onSelectDay: ((Date) -> Void)? = nil + @Environment(\.colorScheme) private var scheme + + private var yMax: Double { + let top = max(totals.map(\.total).max() ?? 0, average) + return max(10, IOBCOBChartView.niceStep(top / 4) * 5) + } + + var body: some View { + if totals.allSatisfy({ $0.total == 0 }) { + ChartPlaceholder(message: NSLocalizedString("No insulin recorded in this period", comment: "Trends Insulin per day chart placeholder when no insulin was delivered in the period"), height: height) + } else { + chart.frame(height: height) + } + } + + private var unitWidth: Calendar.Component { weekly ? .weekOfYear : .day } + + private var chart: some View { + Chart { + ForEach(totals) { day in + BarMark(x: .value("Day", day.day, unit: unitWidth), yStart: .value("Base", 0), yEnd: .value("Basal", day.basal), width: .ratio(0.7)) + .foregroundStyle(P.basal) + BarMark(x: .value("Day", day.day, unit: unitWidth), yStart: .value("Basal", max(0, day.basal - yMax * 0.006)), yEnd: .value("Edge", day.basal), width: .ratio(0.7)) + .foregroundStyle(P.basalEdge) + // A thin gap under each bolus part, so it never touches the part below it + let gap = yMax * 0.01 + let manualTop = day.basal + day.split.manualBolus + day.split.unclassifiedBolus + BarMark(x: .value("Day", day.day, unit: unitWidth), yStart: .value("Basal", day.basal + (manualTop - day.basal > gap * 2 ? gap : 0)), yEnd: .value("Bolus", manualTop), width: .ratio(0.7)) + .foregroundStyle(P.bolus) + if day.split.automaticBolus > 0 { + BarMark(x: .value("Day", day.day, unit: unitWidth), yStart: .value("Bolus", manualTop + (day.total - manualTop > gap * 2 ? gap : 0)), yEnd: .value("Total", day.total), width: .ratio(0.7)) + .foregroundStyle(Hatch.paint(P.bolus, scheme: scheme)) + } + } + RuleMark(y: .value("Average", average)) + .foregroundStyle(P.ink) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [4, 3])) + .annotation(position: .top, alignment: .trailing, spacing: 2) { + Text(LoopFormat.number(average, decimals: 1)) + .font(LoopDesign.Typeface.caption.weight(.semibold)) + .foregroundStyle(P.ink) + } + } + .chartYScale(domain: 0...yMax) + .chartXAxis { + AxisMarks(values: axisDates) { value in + AxisValueLabel(centered: true) { + if let date = value.as(Date.self) { + Text(axisLabel(date)) + .multilineTextAlignment(.center) + } + } + .font(LoopDesign.Typeface.caption) + .foregroundStyle(P.inkMuted) + } + } + .chartYAxis { + AxisMarks(position: .leading) { _ in + AxisGridLine().foregroundStyle(P.grid) + AxisValueLabel().font(LoopDesign.Typeface.caption).foregroundStyle(P.inkMuted) + } + } + .chartOverlay { proxy in + GeometryReader { geometry in + Rectangle().fill(.clear).contentShape(Rectangle()) + .onTapGesture { location in + guard let onSelectDay, !weekly, let frame = proxy.plotFrame else { return } + let x = location.x - geometry[frame].origin.x + if let date: Date = proxy.value(atX: x), + let day = totals.min(by: { abs($0.day.addingTimeInterval(43200).timeIntervalSince(date)) < abs($1.day.addingTimeInterval(43200).timeIntervalSince(date)) }) { + onSelectDay(day.day) + } + } + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(String(format: weekly + ? NSLocalizedString("Insulin per day, weekly average, average %1$@", comment: "VoiceOver label for the Trends Insulin per day chart when bars are weekly averages. (1: average insulin per day, e.g. 32.1 U)") + : NSLocalizedString("Insulin per day, average %1$@", comment: "VoiceOver label for the Trends Insulin per day chart. (1: average insulin per day, e.g. 32.1 U)"), + LoopFormat.totalUnits(average))) + } + + private var axisDates: [Date] { + if weekly { + let stride = max(1, Int(ceil(Double(totals.count) / 6))) + return totals.enumerated().compactMap { index, total in index % stride == 0 ? total.day : nil } + } + if totals.count > 14 { + return totals.map(\.day).filter { Calendar.current.component(.weekday, from: $0) == 2 } + } + return totals.map(\.day) + } + + private func axisLabel(_ date: Date) -> String { + if weekly { + return date.formatted(.dateTime.day().month(.abbreviated)) + } + let weekday = String(date.formatted(.dateTime.weekday(.narrow))) + if totals.count <= 14 { + let first = totals.first?.day == date, last = totals.last?.day == date + let isMonday = Calendar.current.component(.weekday, from: date) == 2 + return first || last || isMonday ? "\(weekday)\n\(date.formatted(.dateTime.day()))" : weekday + } + // 30 days: label every Monday only. + return Calendar.current.component(.weekday, from: date) == 2 ? date.formatted(.dateTime.day().month(.abbreviated)) : "" + } +} + +// MARK: - LoopModeBar + +struct LoopModeBarView: View { + let spans: [LoopModeSpan] + + private var total: TimeInterval { spans.reduce(0) { $0 + $1.duration } } + + static func color(_ mode: LoopModeSpan.Mode) -> Color { + switch mode { + case .closed: return P.statusAutomated + case .limited: return P.statusWarning + case .open: return P.inkMuted + } + } + + static func name(_ mode: LoopModeSpan.Mode) -> String { + switch mode { + case .closed: return NSLocalizedString("Closed loop", comment: "Trends Loop status: time Loop was running in closed loop") + case .limited: return NSLocalizedString("Limited", comment: "Trends Loop status: time closed loop was on but Loop wasn't completing runs") + case .open: return NSLocalizedString("Open loop / manual", comment: "Trends Loop status: time closed loop was switched off") + } + } + + private let modes: [LoopModeSpan.Mode] = [.closed, .limited, .open] + + var body: some View { + if total <= 0 { + ChartPlaceholder(message: NSLocalizedString("No loop runs recorded in this period", comment: "Trends Loop status placeholder when there is no Loop run history for the period"), height: 60) + } else { + VStack(spacing: LoopDesign.Space.s3) { + bar.frame(height: 10) + ForEach(modes, id: \.self) { mode in + row(mode) + } + } + } + } + + private func fraction(_ mode: LoopModeSpan.Mode) -> Double { + LoopHistoryMath.duration(of: mode, in: spans) / total + } + + private var bar: some View { + GeometryReader { geometry in + let minimum: CGFloat = 4 + let widths = modes.map { fraction($0) > 0 ? max(minimum, fraction($0) * geometry.size.width) : 0 } + let excess = widths.reduce(0, +) - geometry.size.width + HStack(spacing: 1) { + ForEach(Array(modes.enumerated()), id: \.offset) { index, mode in + if widths[index] > 0 { + Rectangle() + .fill(Self.color(mode)) + .frame(width: max(0, index == 0 ? widths[index] - excess - 2 : widths[index])) + } + } + } + .clipShape(Capsule()) + } + .accessibilityHidden(true) + } + + private func row(_ mode: LoopModeSpan.Mode) -> some View { + let duration = LoopHistoryMath.duration(of: mode, in: spans) + return HStack(spacing: LoopDesign.Space.s2) { + Circle().fill(Self.color(mode)).frame(width: 8, height: 8) + Text(Self.name(mode)) + .font(LoopDesign.Typeface.subhead) + .foregroundStyle(P.ink) + Text("· \(LoopFormat.duration(duration))") + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + Spacer() + Text(percentText(fraction(mode) * 100, decimals: 1)) + .font(LoopDesign.Typeface.statValueSmall) + .foregroundStyle(P.ink) + } + .accessibilityElement(children: .combine) + } +} + +struct LoopGapsList: View { + let spans: [LoopModeSpan] + + private var gaps: [LoopModeSpan] { + spans.filter { $0.mode != .closed }.sorted { $0.start > $1.start } + } + + var body: some View { + List { + if gaps.isEmpty { + Text(NSLocalizedString("No gaps: Loop ran closed loop all through this period.", comment: "Trends Gaps list when Loop was in closed loop for the whole period")) + .foregroundStyle(P.inkMuted) + } + ForEach(Array(gaps.enumerated()), id: \.offset) { _, gap in + HStack(alignment: .top, spacing: LoopDesign.Space.s3) { + Circle().fill(LoopModeBarView.color(gap.mode)).frame(width: 8, height: 8).padding(.top, 6) + VStack(alignment: .leading, spacing: 2) { + Text(gap.cause ?? LoopModeBarView.name(gap.mode)) + .font(LoopDesign.Typeface.body) + Text("\(gap.start.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).hour().minute())) · \(LoopFormat.duration(gap.duration))") + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + } + } + .accessibilityElement(children: .combine) + } + } + .navigationTitle(NSLocalizedString("Gaps", comment: "Trends Loop status button and screen title: list of times Loop wasn't in closed loop")) + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift new file mode 100644 index 0000000000..efb520874d --- /dev/null +++ b/Loop/Redesign/TrendsScreen.swift @@ -0,0 +1,555 @@ +// +// TrendsScreen.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import UIKit + +private typealias P = LoopDesign.Palette + +/// The period review: everything on the screen follows one RangeSelector period. +@MainActor +final class TrendsModel: ObservableObject { + + struct Summary { + var interval: DateInterval + var unit: GlucoseUnitStyle + var glucose: [GlucosePoint] = [] + var stats: LoopHistoryMath.GlucoseStats? + var cgmActive: Double = 0 + var timeInRange: [LoopHistoryMath.RangeBand: Double] = [:] + var agp: [LoopHistoryMath.AGPSlot] = [] + var agpDays = 0 + var totals: [LoopHistoryMath.DayTotal] = [] + /// Carbs and bolus counts cover only the days per-day figures divide by (see `dataDays`). + var carbGrams = 0.0 + var carbEntries = 0 + var carbsByDay: [Date: Double] = [:] + var manualBoluses = 0 + var automaticBoluses = 0 + var loopSpans: [LoopModeSpan] = [] + var targetRange: ClosedRange? + /// The whole calendar day, for a one-day period: drawn as one timeline. + var day: DayHistory? + var dayTargets: [TargetBand] = [] + + /// The days per-day figures divide by: insulin's averaging days, or complete days with CGM data when there is + /// no insulin (today only when no complete day has any), never the period length. + var dataDays = 0 + var days: Double { Double(max(1, dataDays)) } + var basalUnits: Double { LoopHistoryMath.averagingDays(totals).reduce(0) { $0 + $1.basal } } + var bolusUnits: Double { LoopHistoryMath.averagingDays(totals).reduce(0) { $0 + $1.bolus } } + var insulinPerDay: Double { LoopHistoryMath.averageDailyInsulin(totals) } + var basalShare: Double { basalUnits + bolusUnits > 0 ? basalUnits / (basalUnits + bolusUnits) : 0 } + /// Per day over the averaged days; a single day in progress when that is all there is. + var split: InsulinSplit { InsulinSplit.average(LoopHistoryMath.averagingDays(totals).map(\.split)) } + /// The split is today's total so far rather than an average of complete days. + var splitIsPartial: Bool { LoopHistoryMath.averagingDays(totals).allSatisfy { !$0.isComplete } } + } + + @Published var period: TrendsPeriod = .twoWeeks + @Published var endDay = Date() + @Published private(set) var summary: Summary? + @Published private(set) var loading = false + @Published private(set) var loadError: String? + + let provider: LoopHistoryProvider + private var loadTask: Task? + + init(provider: LoopHistoryProvider) { + self.provider = provider + } + + var interval: DateInterval { period.interval(endingOn: endDay) } + + func reload() { + loadTask?.cancel() + let interval = self.interval + let oneDay = period == .day + loading = true + summary = nil + loadError = nil + loadTask = Task { + do { + let result = try await Self.load(provider: provider, interval: interval, oneDay: oneDay) + guard !Task.isCancelled else { return } + self.summary = result + } catch { + guard !Task.isCancelled else { return } + self.loadError = error.localizedDescription + } + self.loading = false + } + } + + private static func load(provider: LoopHistoryProvider, interval: DateInterval, oneDay: Bool) async throws -> Summary { + var summary = Summary(interval: interval, unit: provider.unitStyle) + summary.targetRange = provider.glucoseTargetRange + + // A single day is read to midnight, as Today so far is, so the scheduled basal still to come is drawn. + let fetchWindow = oneDay ? DateInterval(start: interval.start, end: Calendar.current.date(byAdding: .day, value: 1, to: interval.start)!) : interval + async let historyFetch = provider.dayHistory(fetchWindow, includeOnBoard: false) + async let runsFetch = provider.loopRuns() + var history = try await historyFetch + let (runs, coverageStart) = try await runsFetch + if oneDay { + summary.day = history + summary.dayTargets = try await provider.glucoseTargetBands(in: fetchWindow) + history = history.clipped(to: interval) + } + + summary.glucose = history.glucose + summary.stats = LoopHistoryMath.glucoseStats(history.glucose) + summary.cgmActive = LoopHistoryMath.cgmActiveFraction(history.glucose, over: interval) + summary.timeInRange = LoopHistoryMath.timeInRange(history.glucose) + summary.agp = LoopHistoryMath.agp(history.glucose) + summary.agpDays = LoopHistoryMath.daysWithReadings(history.glucose) + + let calendar = Calendar.current + summary.totals = LoopHistoryMath.days(in: interval).map { day in + let basal = history.basal.compactMap { $0.clamped(to: day) }.reduce(0) { $0 + $1.units } + let boluses = history.boluses.filter { $0.date >= day.start && $0.date < day.end } + return LoopHistoryMath.DayTotal(day: day.start, split: InsulinSplit(basal: basal, boluses: boluses), + isComplete: calendar.date(byAdding: .day, value: 1, to: day.start) == day.end) + } + let glucoseDays = Set(history.glucose.map { calendar.startOfDay(for: $0.date) }) + // Carbs and bolus counts share insulin's days, so a period with years of CGM data but a few days of Loop + // isn't spread thin. Without insulin, fall back to days with CGM data. + let withData = summary.totals.filter { $0.total > 0 || glucoseDays.contains($0.day) } + let insulinDays = LoopHistoryMath.averagingDays(summary.totals) + let averaged = !insulinDays.isEmpty ? insulinDays + : withData.contains(where: \.isComplete) ? withData.filter(\.isComplete) : withData + let averagedDays = Set(averaged.map(\.day)) + let inAveragedDays: (Date) -> Bool = { averagedDays.contains(calendar.startOfDay(for: $0)) } + summary.dataDays = averaged.count + summary.carbsByDay = Dictionary(grouping: history.carbs) { calendar.startOfDay(for: $0.date) } + .mapValues { $0.reduce(0) { $0 + $1.grams } } + let carbs = history.carbs.filter { inAveragedDays($0.date) } + summary.carbGrams = carbs.reduce(0) { $0 + $1.grams } + summary.carbEntries = carbs.count + let boluses = history.boluses.filter { inAveragedDays($0.date) } + summary.manualBoluses = boluses.filter { !$0.automatic }.count + summary.automaticBoluses = boluses.filter(\.automatic).count + + summary.loopSpans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: LoopModeHistory.shared.changes, + coverageStart: coverageStart) + return summary + } + + // MARK: Tiles + + func tiles(_ s: Summary) -> [StatTile] { + let unit = s.unit + var tiles: [StatTile] = [] + let averageLabel = NSLocalizedString("Average", comment: "Trends summary tile: mean glucose over the period") + let gmiLabel = NSLocalizedString("GMI", comment: "Trends summary tile: abbreviation for Glucose Management Indicator, an estimate of HbA1c from CGM data") + let loopActiveLabel = NSLocalizedString("Loop active", comment: "Trends summary tile: share of the period Loop spent in closed loop") + if let stats = s.stats { + tiles.append(StatTile(label: averageLabel, value: unit.format(stats.meanMgdl), unit: unit.unitName, + detail: String(format: NSLocalizedString("SD %1$@ · CV %2$@%%", comment: "Trends Average tile detail: standard deviation and coefficient of variation of glucose. (1: standard deviation in the glucose unit, 2: coefficient of variation as a percentage number)"), + unit.format(stats.sdMgdl), LoopFormat.number(stats.cv * 100, decimals: 0)))) + let enough = s.cgmActive * s.interval.duration / 86400 >= 10 + tiles.append(StatTile(label: gmiLabel, value: LoopFormat.number(stats.gmiPercent, decimals: 1), unit: "%", + detail: enough + ? String(format: NSLocalizedString("%1$@ mmol/mol", comment: "Trends GMI tile detail: the GMI in IFCC units. (1: value)"), + LoopFormat.number(stats.gmiMmolMol, decimals: 0)) + : NSLocalizedString("Needs 10 days of CGM", comment: "Trends GMI tile detail when fewer than 10 days of CGM data are available"))) + } else { + tiles.append(StatTile(label: averageLabel, value: "–", unit: unit.unitName, + detail: NSLocalizedString("No CGM data", comment: "Trends Average tile detail when the period has no glucose readings"))) + tiles.append(StatTile(label: gmiLabel, value: "–", unit: "%", detail: nil)) + } + let basalPercent = LoopFormat.number(s.basalShare * 100, decimals: 0) + let insulinDetail: String + if let note = daysNote(s) { + insulinDetail = String(format: NSLocalizedString("%1$@%% basal · %2$@", comment: "Trends Insulin per day tile detail when only some days are averaged. (1: basal share as a percentage number, 2: days averaged, e.g. 5 of 14 days)"), + basalPercent, note) + } else { + insulinDetail = String(format: NSLocalizedString("%1$@%% basal", comment: "Trends Insulin per day tile detail: share of insulin delivered as basal. (1: percentage number)"), + basalPercent) + } + // One day's figures are that day's own, so its tiles drop "per day" and show whole counts. + let oneDay = s.day != nil + let countDecimals = oneDay ? 0 : 1 + tiles.append(StatTile(label: oneDay + ? NSLocalizedString("Insulin", comment: "Trends summary tile for a one-day period: the day's total insulin") + : NSLocalizedString("Insulin per day", comment: "Trends tile and card title: average total insulin per day"), + value: LoopFormat.number(s.insulinPerDay, decimals: 1), + unit: NSLocalizedString("U", comment: "Abbreviation for insulin units"), + detail: insulinDetail, valueColor: P.insulinInk)) + tiles.append(StatTile(label: oneDay + ? NSLocalizedString("Carbs", comment: "Trends summary tile for a one-day period: the day's total carbohydrate") + : NSLocalizedString("Carbs per day", comment: "Trends summary tile: average grams of carbohydrate entered per day"), + value: LoopFormat.number(s.carbGrams / s.days, decimals: 0), + unit: NSLocalizedString("g", comment: "Abbreviation for grams"), + detail: oneDay + ? String(format: NSLocalizedString("%lld entries", comment: "Trends Carbs tile detail for a one-day period: how many carb entries were made that day. (1: number)"), s.carbEntries) + : String(format: NSLocalizedString("%1$@ entries", comment: "Trends Carbs per day tile detail: average number of carb entries per day, a fractional value such as 2.5, so not pluralised. (1: number with one decimal)"), + LoopFormat.number(Double(s.carbEntries) / s.days, decimals: 1)), + valueColor: P.carbsInk)) + tiles.append(StatTile(label: oneDay + ? NSLocalizedString("Boluses", comment: "Trends summary tile for a one-day period: how many manual boluses were given that day") + : NSLocalizedString("Boluses per day", comment: "Trends summary tile: average number of manual boluses per day"), + value: LoopFormat.number(Double(s.manualBoluses) / s.days, decimals: countDecimals), unit: nil, + detail: String(format: NSLocalizedString("+ %1$@ automatic", comment: "Trends Boluses per day tile detail: average number of automatic boluses per day on top of the manual ones, a fractional value such as 4.2. (1: number with one decimal)"), + LoopFormat.number(Double(s.automaticBoluses) / s.days, decimals: countDecimals)))) + let covered = s.loopSpans.reduce(0) { $0 + $1.duration } + if covered > 0 { + let closed = LoopHistoryMath.duration(of: .closed, in: s.loopSpans) + tiles.append(StatTile(label: loopActiveLabel, value: LoopFormat.number(closed / covered * 100, decimals: 0), unit: "%", + detail: String(format: NSLocalizedString("%1$@ of %2$@", comment: "Trends Loop active tile detail: time in closed loop out of the time Loop status is known. (1: duration in closed loop, e.g. 6d 4h, 2: duration covered, e.g. 7d)"), + LoopFormat.duration(closed), LoopFormat.duration(covered)))) + } else { + tiles.append(StatTile(label: loopActiveLabel, value: "–", unit: "%", + detail: NSLocalizedString("No loop runs kept", comment: "Trends Loop active tile detail when no Loop run history is available for the period"))) + } + return tiles + } + + /// "5 of 14 days" when only part of the period is averaged: days without data, or today while it runs. + private func daysNote(_ s: Summary) -> String? { + let insulinDays = LoopHistoryMath.averagingDays(s.totals).count + guard insulinDays < s.totals.count else { return nil } + let total = String.localizedStringWithFormat(NSLocalizedString("%lld days", comment: "A number of days, e.g. a Trends period or how many days are averaged. (1: number of days)"), s.totals.count) + return String(format: NSLocalizedString("%1$@ of %2$@", comment: "Trends: how many days of the period are averaged. (1: days averaged, a number; 2: days in the period, e.g. 14 days)"), LoopFormat.number(Double(insulinDays), decimals: 0), total) + } + + // MARK: Export + + static func exportCSV(_ s: Summary) throws -> URL { + // The CSV header and values are deliberately not localised: fixed English columns and '.' decimals for data interchange. + var lines = ["date,mean_glucose_\(s.unit.isMmol ? "mmol_l" : "mg_dl"),readings,time_in_range_pct,basal_u,bolus_u,total_u,carbs_g,manual_bolus_u,auto_bolus_u"] + let calendar = Calendar.current + let byDay = Dictionary(grouping: s.glucose) { calendar.startOfDay(for: $0.date) } + for total in s.totals { + let readings = byDay[total.day] ?? [] + let mean = readings.isEmpty ? "" : String(format: "%.*f", locale: Locale(identifier: "en_US_POSIX"), + s.unit.isMmol ? 1 : 0, s.unit.value(readings.reduce(0) { $0 + $1.mgdl } / Double(readings.count))) + let tir = readings.isEmpty ? "" : String(format: "%.0f", (LoopHistoryMath.timeInRange(readings)[.inRange] ?? 0) * 100) + let date = Date.ISO8601FormatStyle(timeZone: .current).year().month().day().format(total.day) + let carbs = String(format: "%.0f", s.carbsByDay[total.day] ?? 0) + // Left blank when some of the day's bolus doesn't say whether it was manual or automatic. + let bolusSplit = total.split.isClassified ? String(format: "%.2f,%.2f", total.split.manualBolus, total.split.automaticBolus) : "," + lines.append("\(date),\(mean),\(readings.count),\(tir),\(String(format: "%.2f,%.2f,%.2f", total.basal, total.bolus, total.total)),\(carbs),\(bolusSplit)") + } + let url = FileManager.default.temporaryDirectory.appendingPathComponent(Self.fileName(s.interval) + ".csv") + try lines.joined(separator: "\n").write(to: url, atomically: true, encoding: .utf8) + return url + } + + func exportPDF(_ s: Summary) throws -> URL { + let url = FileManager.default.temporaryDirectory.appendingPathComponent(Self.fileName(s.interval) + ".pdf") + let report = TrendsReport(summary: s, tiles: tiles(s)) + .frame(width: 560) + .environment(\.colorScheme, .light) + let renderer = ImageRenderer(content: report) + renderer.proposedSize = ProposedViewSize(width: 560, height: nil) + var thrown: Error? + renderer.render { size, draw in + var box = CGRect(x: 0, y: 0, width: 612, height: max(792, size.height + 52)) + guard let consumer = CGDataConsumer(url: url as CFURL), let context = CGContext(consumer: consumer, mediaBox: &box, nil) else { + thrown = CocoaError(.fileWriteUnknown) + return + } + context.beginPDFPage(nil) + context.translateBy(x: 26, y: box.height - size.height - 26) + draw(context) + context.endPDFPage() + context.closePDF() + } + if let thrown { throw thrown } + return url + } + + private static func fileName(_ interval: DateInterval) -> String { + String(format: NSLocalizedString("Loop trends %1$@", comment: "File name of an exported Trends report or CSV, without the extension. (1: date range of the period)"), + fileStamp(interval)) + } + + private static func fileStamp(_ interval: DateInterval) -> String { + LoopFormat.range(interval.start, interval.end.addingTimeInterval(-1)).replacingOccurrences(of: " – ", with: " to ") + } +} + +// MARK: - Screen + +struct TrendsView: View { + @StateObject private var model: TrendsModel + @State private var path: [TrendsRoute] = [] + @State private var exportURL: URL? + @State private var exportError: String? + + /// Closes Trends when it is presented on its own; nil where Trends has nowhere to return to. + private let done: (() -> Void)? + + enum TrendsRoute: Hashable { + case day(Date) + case gaps + } + + init(provider: LoopHistoryProvider, done: (() -> Void)? = nil) { + _model = StateObject(wrappedValue: TrendsModel(provider: provider)) + self.done = done + } + + var body: some View { + NavigationStack(path: $path) { + ScrollView { + LazyVStack(alignment: .leading, spacing: LoopDesign.Space.s6, pinnedViews: [.sectionHeaders]) { + Section { + content + } header: { + RangeSelectorView(period: $model.period, endDay: $model.endDay) + .padding(.vertical, LoopDesign.Space.s2) + .background(P.surface) + } + } + .padding(.horizontal, LoopDesign.Space.s4) + .padding(.bottom, LoopDesign.Space.s6) + } + .background(P.surface) + .navigationTitle(NSLocalizedString("Trends", comment: "Title of the Trends tab")) + .refreshable { model.reload() } + .toolbar { + if let done { + ToolbarItem(placement: .confirmationAction) { + Button(NSLocalizedString("Done", comment: "Button to close Trends"), action: done) + } + } + } + .navigationDestination(for: TrendsRoute.self) { route in + switch route { + case .day(let day): + DayDetailView(provider: model.provider, day: day) + case .gaps: + LoopGapsList(spans: model.summary?.loopSpans ?? []) + } + } + } + .onAppear { model.reload() } + .onChange(of: model.period) { _, _ in model.reload() } + .onChange(of: model.endDay) { _, _ in model.reload() } + .sheet(item: $exportURL) { url in + ShareSheet(items: [url]) + } + .alert(NSLocalizedString("Couldn't export", comment: "Alert title when exporting the Trends PDF or CSV fails"), + isPresented: Binding(get: { exportError != nil }, set: { if !$0 { exportError = nil } })) { + Button(NSLocalizedString("OK", comment: "Button to dismiss an alert"), role: .cancel) {} + } message: { + Text(exportError ?? "") + } + } + + @ViewBuilder + private var content: some View { + if let s = model.summary { + summaryCard(s) + // A single day leads with its timeline. + if let day = s.day { + dayCard(day, targets: s.dayTargets, unit: s.unit) + } + LoopCard { + CardHeader(title: NSLocalizedString("Time in range", comment: "Trends card title: share of glucose readings in each range")) { + Text(String(format: NSLocalizedString("%1$@%% CGM active", comment: "Trends Time in range card: share of the period with CGM readings. (1: percentage number)"), + LoopFormat.number(s.cgmActive * 100, decimals: 0))) + } + if s.glucose.isEmpty { + ChartPlaceholder(message: NSLocalizedString("No CGM data in this period", comment: "Trends chart placeholder when the period has no glucose readings")) + } else { + TimeInRangeView(fractions: s.timeInRange, unit: s.unit) + } + Text(String(format: NSLocalizedString("Aim: over 70%% in range, under 4%% low.", comment: "Trends Time in range card footnote: the consensus targets for time in range and time low"))) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + } + if s.day == nil { + patternCard(s) + } + LoopCard { + CardHeader(title: NSLocalizedString("Basal and bolus", comment: "Trends card title: insulin split into basal and bolus")) { + Text(s.splitIsPartial + ? NSLocalizedString("today so far", comment: "Trends Basal and bolus card subtitle when the figures are today's total so far") + : s.totals.count == 1 + ? NSLocalizedString("this day", comment: "Trends Basal and bolus card subtitle when the period is one complete day") + : NSLocalizedString("average per day", comment: "Trends Basal and bolus card subtitle when the figures are averaged per day over the period")) + } + if s.split.total > 0 { + InsulinSplitView(split: s.split, partial: s.splitIsPartial) + } else { + ChartPlaceholder(message: NSLocalizedString("No insulin recorded in this period", comment: "Trends Insulin per day chart placeholder when no insulin was delivered in the period"), height: 80) + } + } + // One day is one bar: the timeline and the Basal and bolus card already say it all. + if s.day == nil { + LoopCard { + CardHeader(title: NSLocalizedString("Insulin per day", comment: "Trends tile and card title: average total insulin per day")) { + Text(s.totals.count > 30 + ? NSLocalizedString("per day, weekly average", comment: "Trends Insulin per day card subtitle when each bar is a week's average daily total") + : NSLocalizedString("basal and bolus", comment: "Trends Insulin per day card subtitle when each bar is one day's basal and bolus")) + } + InsulinDailyTotalsView(totals: s.totals.count > 30 ? LoopHistoryMath.weekly(s.totals) : s.totals, + weekly: s.totals.count > 30, average: s.insulinPerDay) { day in + path.append(.day(day)) + } + ChartLegend(items: insulinLegend(s)) + } + } + LoopCard { + CardHeader(title: NSLocalizedString("Loop status", comment: "Trends card title: how long Loop spent in closed loop, limited or open loop")) { + Button(NSLocalizedString("Gaps", comment: "Trends Loop status button and screen title: list of times Loop wasn't in closed loop")) { path.append(.gaps) } + .foregroundStyle(P.accentInk) + .disabled(s.loopSpans.isEmpty) + } + LoopModeBarView(spans: s.loopSpans) + if let note = coverageNote(s) { + Text(note) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + } + } + } else if let error = model.loadError { + LoopCard { + ChartPlaceholder(message: String(format: NSLocalizedString("Couldn't load Trends: %1$@", comment: "Trends error message when the history can't be read. (1: error description)"), error)) + Button(NSLocalizedString("Retry", comment: "Button to try loading Trends again after an error")) { model.reload() }.foregroundStyle(P.accentInk) + } + } else { + ProgressView().frame(maxWidth: .infinity, minHeight: 300) + } + } + + /// The same timeline as Today so far: glucose, every bolus and basal change, and carbs, through the day. + private func dayCard(_ day: DayHistory, targets: [TargetBand], unit: GlucoseUnitStyle) -> some View { + let isToday = Calendar.current.isDateInToday(day.window.start) + return Button { + path.append(.day(day.window.start)) + } label: { + DayTimelineCard(title: isToday ? DayTimelineCard.todayTitle : LoopFormat.weekdayDay(day.window.start), + history: day, unit: unit, targetBands: targets, height: 260, showsTotals: false) + } + .buttonStyle(.plain) + .accessibilityHint(NSLocalizedString("Opens the full day", comment: "VoiceOver hint for the Today so far card on the Now screen")) + } + + private func patternCard(_ s: TrendsModel.Summary) -> some View { + LoopCard { + CardHeader(title: NSLocalizedString("Daily pattern", comment: "Trends card title: the ambulatory glucose profile, glucose by time of day across the period")) { Text(s.unit.unitName) } + TrendsAGPChart(slots: s.agp, readings: s.glucose, days: s.agpDays, unit: s.unit, targetRange: s.targetRange) + ChartLegend(items: [ + LegendItem(swatch: .solid(P.glucose), label: NSLocalizedString("Median", comment: "Trends Daily pattern legend: the median glucose line")), + LegendItem(swatch: .solid(P.agpInner), label: NSLocalizedString("25–75%", comment: "Trends Daily pattern legend: band between the 25th and 75th percentiles of glucose")), + LegendItem(swatch: .solid(P.agpOuter), label: NSLocalizedString("5–95%", comment: "Trends Daily pattern legend: band between the 5th and 95th percentiles of glucose")), + ]) + } + } + + private func summaryCard(_ s: TrendsModel.Summary) -> some View { + LoopCard { + CardHeader(title: NSLocalizedString("Summary", comment: "Trends card title: the summary statistics tiles")) { + Menu(NSLocalizedString("Export", comment: "Trends menu title: export the period as a PDF report or CSV file")) { + Button(NSLocalizedString("PDF report", comment: "Trends export menu item: share a PDF report of the period")) { export { try model.exportPDF(s) } } + Button(NSLocalizedString("CSV, one row per day", comment: "Trends export menu item: share a CSV spreadsheet file with one row for each day")) { export { try TrendsModel.exportCSV(s) } } + } + .foregroundStyle(P.accentInk) + } + StatGridView(tiles: model.tiles(s)) + .opacity(model.loading ? 0.5 : 1) + } + } + + private func export(_ make: () throws -> URL) { + do { + exportURL = try make() + } catch { + exportError = error.localizedDescription + } + } + + /// The figures are on the Basal and bolus card above, so the legend only names the marks. + private func insulinLegend(_ s: TrendsModel.Summary) -> [LegendItem] { + [ + LegendItem(swatch: .basal, label: NSLocalizedString("Basal", comment: "Chart legend: the basal part of each day's insulin")), + LegendItem(swatch: .solid(P.bolus), label: NSLocalizedString("Bolus", comment: "Chart legend: solid bars are manual boluses")), + LegendItem(swatch: .hatch(P.bolus), label: NSLocalizedString("Automatic", comment: "Chart legend: hatched bars are automatic boluses given by Loop")), + LegendItem(swatch: .dash(P.ink), + label: String(format: NSLocalizedString("Average %1$@ U", comment: "Trends Insulin per day legend: the dashed line showing average total insulin per day. (1: units)"), + LoopFormat.number(s.insulinPerDay, decimals: 1))), + ] + } + + private func coverageNote(_ s: TrendsModel.Summary) -> String? { + guard let start = s.loopSpans.first?.start else { + return NSLocalizedString("Loop status is recorded from when this version of Loop first ran.", comment: "Trends Loop status footnote when there is no Loop status history yet") + } + guard start > s.interval.start else { return nil } + return String(format: NSLocalizedString("Covers %1$@: Loop keeps 7 days of runs, and closed or open loop is recorded from when this version of Loop first ran.", comment: "Trends Loop status footnote when the status covers only part of the period. (1: date range covered)"), + LoopFormat.range(start, s.interval.end)) + } +} + +extension URL: @retroactive Identifiable { + public var id: String { absoluteString } +} + +struct ShareSheet: UIViewControllerRepresentable { + let items: [Any] + + func makeUIViewController(context: Context) -> UIActivityViewController { + UIActivityViewController(activityItems: items, applicationActivities: nil) + } + + func updateUIViewController(_ controller: UIActivityViewController, context: Context) {} +} + +// MARK: - PDF report + +/// The Trends cards laid out for paper. +private struct TrendsReport: View { + let summary: TrendsModel.Summary + let tiles: [StatTile] + + var body: some View { + VStack(alignment: .leading, spacing: LoopDesign.Space.s4) { + VStack(alignment: .leading, spacing: 2) { + Text(NSLocalizedString("Loop trends", comment: "Title of the exported Trends PDF report")).font(.title2.bold()) + Text(LoopFormat.range(summary.interval.start, summary.interval.end.addingTimeInterval(-1))) + .font(.subheadline) + .foregroundStyle(P.inkMuted) + } + StatGridView(tiles: tiles) + HStack(alignment: .top, spacing: LoopDesign.Space.s4) { + VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { + Text(NSLocalizedString("Time in range", comment: "Trends card title: share of glucose readings in each range")).font(.headline) + TimeInRangeView(fractions: summary.timeInRange, unit: summary.unit) + } + .frame(maxWidth: .infinity) + } + if let day = summary.day { + VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { + Text(LoopFormat.weekdayDay(day.window.start)).font(.headline) + InsulinTimelineView(history: day, unit: summary.unit, targetBands: summary.dayTargets, height: 220) + ChartLegend(items: DayTotalsTiles.legend) + } + } else { + VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { + Text(NSLocalizedString("Daily pattern", comment: "Trends card title: the ambulatory glucose profile, glucose by time of day across the period")).font(.headline) + TrendsAGPChart(slots: summary.agp, readings: summary.glucose, days: summary.agpDays, unit: summary.unit, + targetRange: summary.targetRange, height: 160) + } + VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { + Text(NSLocalizedString("Insulin per day", comment: "Trends tile and card title: average total insulin per day")).font(.headline) + InsulinDailyTotalsView(totals: summary.totals.count > 30 ? LoopHistoryMath.weekly(summary.totals) : summary.totals, + weekly: summary.totals.count > 30, average: summary.insulinPerDay, height: 140) + } + } + VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { + Text(NSLocalizedString("Loop status", comment: "Trends card title: how long Loop spent in closed loop, limited or open loop")).font(.headline) + LoopModeBarView(spans: summary.loopSpans) + } + } + .padding(LoopDesign.Space.s2) + .background(Color.white) + } +} diff --git a/Loop/View Models/SettingsViewModel.swift b/Loop/View Models/SettingsViewModel.swift index 626ae21e28..9762fe1efe 100644 --- a/Loop/View Models/SettingsViewModel.swift +++ b/Loop/View Models/SettingsViewModel.swift @@ -120,6 +120,8 @@ class SettingsViewModel { @ObservationIgnored var insulinDeliveryHistory: ((Int) async throws -> InsulinDeliveryHistory)? + @ObservationIgnored var historyProvider: LoopHistoryProvider? + @ObservationIgnored var setupHistory: (() async throws -> SetupHistory)? @ObservationIgnored var outcomesPackageBuilder: OutcomesPackageBuilder? diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 70f6d26b4e..babad179cc 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -55,6 +55,7 @@ struct SettingsView: View { case favoriteFoods case presets + case trends } } @@ -156,6 +157,11 @@ struct SettingsView: View { } case .favoriteFoods: FavoriteFoodsView(insightsDelegate: viewModel.favoriteFoodInsightsDelegate) + case .trends: + if let historyProvider = viewModel.historyProvider { + // Trends brings its own navigation stack, so it is a sheet rather than a push. Done closes only Trends. + TrendsView(provider: historyProvider, done: { self.sheet = nil }) + } } } .environmentObject(displayGlucosePreference) @@ -358,6 +364,18 @@ extension SettingsView { ) .accessibilityIdentifier("settingsViewStatistics") } + if viewModel.historyProvider != nil { + LargeButton( + action: { sheet = .trends }, + imageView: Image(systemName: "chart.bar.doc.horizontal") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30), + label: NSLocalizedString("Trends", comment: "Trends settings button text"), + descriptiveText: NSLocalizedString("Time in range, daily pattern, insulin per day and Loop status", comment: "Trends settings descriptive text") + ) + .accessibilityIdentifier("settingsViewTrends") + } NavigationLink(destination: CurrentSetupView(viewModel: CurrentSetupViewModel( selected: setupChange, loadHistory: { [viewModel] in try await viewModel.setupHistory?() ?? SetupHistory(settings: [], experiments: []) }, diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index cc81b3cff1..e28ebdb7af 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -695,6 +695,62 @@ class LoopDataManagerTests: XCTestCase { try await assertMissingCarbScheduleThrows(.insulinSensitivitySchedule, preview: true) } + func testHistoricalTargetsUseStoredProfilesAndEndedPresets() async throws { + func target(_ low: Double, _ high: Double) -> ClosedRange { + LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: low)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: high) + } + let window = DateInterval(start: d(-.hours(3)), end: d(-.minutes(30))) + settingsProvider.targetRangeHistory = [ + AbsoluteScheduleValue(startDate: window.start, endDate: d(-.hours(2)), value: target(100, 110)), + AbsoluteScheduleValue(startDate: d(-.hours(2)), endDate: window.end, value: target(120, 130)), + ] + var preset = TemporaryScheduleOverride(context: .custom, + settings: TemporaryPresetSettings(targetRange: target(150, 160), insulinNeedsScaleFactor: 1), + startDate: d(-.minutes(150)), duration: .finite(.hours(2)), + enactTrigger: .local, syncIdentifier: UUID()) + preset.actualEnd = .early(d(-.minutes(90))) + temporaryPresetsManager.presetHistory.recordOverride(preset, at: d(-.minutes(150))) + + let bands = try await loopDataManager.glucoseTargetBands(in: window) + XCTAssertEqual(bands, [ + TargetBand(start: window.start, end: d(-.minutes(150)), range: 100...110), + TargetBand(start: d(-.minutes(150)), end: d(-.minutes(90)), range: 150...160), + TargetBand(start: d(-.minutes(90)), end: window.end, range: 120...130), + ]) + } + + func testTargetBandsAreUnknownBeforeTherapyIsConfigured() async throws { + settingsProvider.settings.glucoseTargetRangeSchedule = nil + let bands = try await loopDataManager.glucoseTargetBands(in: DateInterval(start: d(-.hours(3)), end: now)) + XCTAssertTrue(bands.isEmpty) + } + + func testHistoricalIOBUsesPresetAdjustedBasal() async throws { + settingsProvider.basalHistory = [AbsoluteScheduleValue(startDate: d(-.hours(6)), endDate: now, value: 1.0)] + let window = DateInterval(start: d(-.minutes(30)), end: now) + + for multiplier in [0.5, 1.5] { + let preset = TemporaryScheduleOverride( + context: .custom, + settings: TemporaryPresetSettings(targetRange: nil, insulinNeedsScaleFactor: multiplier), + startDate: d(-.hours(1)), + duration: .finite(.hours(2)), + enactTrigger: .local, + syncIdentifier: UUID() + ) + temporaryPresetsManager.presetHistory.recordOverride(preset, at: now) + doseStore.doseHistory = [DoseEntry(type: .tempBasal, startDate: window.start, endDate: window.end, + value: multiplier, unit: .unitsPerHour, decisionId: nil)] + + let timeline = try await loopDataManager.insulinOnBoardTimeline(in: window) + XCTAssertFalse(timeline.isEmpty) + for value in timeline { + XCTAssertEqual(value.value, 0, accuracy: 1e-9, + "Delivery matching a \(multiplier)× preset's basal must add no net IOB") + } + } + } + func testFetchDataWithHighInsulinNeedsPresetMitigation() async throws { var input = try await loopDataManager.fetchData(for: now) XCTAssertEqual(input.targetMgdl.count, 1) diff --git a/LoopTests/Models/LoopHistoryMathTests.swift b/LoopTests/Models/LoopHistoryMathTests.swift new file mode 100644 index 0000000000..b468c9dc42 --- /dev/null +++ b/LoopTests/Models/LoopHistoryMathTests.swift @@ -0,0 +1,340 @@ +// +// LoopHistoryMathTests.swift +// LoopTests +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import XCTest +import HealthKit +import LoopKit +@testable import Loop + +class LoopHistoryMathTests: XCTestCase { + + private let start = Date(timeIntervalSinceReferenceDate: 800_000_000) + private func at(_ minutes: Double) -> Date { start.addingTimeInterval(minutes * 60) } + + // MARK: Basal + + func testUnrecordedBasalCountsAtTheProgrammedRateOnlyAfterRecordsBegin() { + let window = DateInterval(start: at(0), end: at(240)) + let programmed = [LoopHistoryMath.ScheduleValue(start: at(0), end: at(240), rate: 1.2)] + let temp = LoopHistoryMath.BasalDelivery(start: at(120), end: at(150), rate: 0.4, kind: .temp, automatic: true) + let segments = LoopHistoryMath.basalSegments(window: window, schedule: programmed, deliveries: [temp], + programmedRates: programmed, recordsStart: at(60)) + // Nothing before the first record; 60–120 and 150–240 at 1.2 U/h; 120–150 at the temp rate. + XCTAssertNil(LoopHistoryMath.basalRate(at: at(30), in: segments)) + XCTAssertEqual(LoopHistoryMath.basalRate(at: at(90), in: segments)?.kind, .scheduled) + XCTAssertEqual(LoopHistoryMath.basalRate(at: at(135), in: segments)?.rate, 0.4) + XCTAssertEqual(segments.reduce(0) { $0 + $1.units }, 1.2 * 2.5 + 0.4 * 0.5, accuracy: 1e-9) + } + + func testBasalGapsDoNotInventDelivery() { + let window = DateInterval(start: at(0), end: at(120)) + let schedule = [LoopHistoryMath.ScheduleValue(start: at(-60), end: at(240), rate: 1.0)] + let temp = LoopHistoryMath.BasalDelivery(start: at(60), end: at(90), rate: 2.0, kind: .temp, automatic: true) + let segments = LoopHistoryMath.basalSegments(window: window, schedule: schedule, deliveries: [temp]) + XCTAssertEqual(segments.count, 1) + XCTAssertEqual(segments.reduce(0) { $0 + $1.units }, 1, accuracy: 1e-9) + XCTAssertEqual(segments[0].netUnits, 0.5, accuracy: 1e-9) + } + + func testSuspendDeliversNothingAndLaterRecordWins() { + let window = DateInterval(start: at(0), end: at(60)) + let schedule = [LoopHistoryMath.ScheduleValue(start: at(0), end: at(60), rate: 1.2)] + let deliveries = [ + LoopHistoryMath.BasalDelivery(start: at(0), end: at(60), rate: 3.0, kind: .temp, automatic: true), + LoopHistoryMath.BasalDelivery(start: at(20), end: at(40), rate: 0, kind: .suspended, automatic: false), + ] + let segments = LoopHistoryMath.basalSegments(window: window, schedule: schedule, deliveries: deliveries) + XCTAssertEqual(segments.map(\.kind), [.temp, .suspended, .temp]) + XCTAssertEqual(segments[1].units, 0) + XCTAssertEqual(segments[1].netUnits, -0.4, accuracy: 1e-9) + } + + func testNormalisedBasalPreservesDeliveredQuantityInPartialWindow() throws { + // The pump reports 1.05 U delivered, although the programmed rate implies 1.0625 U. + let dose = DoseEntry(type: .tempBasal, startDate: at(0), endDate: at(30), value: 2.125, + unit: .unitsPerHour, decisionId: nil, deliveredUnits: 1.05, automatic: true) + let delivery = try XCTUnwrap(LoopHistoryMath.BasalDelivery(dose: dose)) + for (end, expected) in [(30.0, 1.05), (15.0, 0.525)] { + let segments = LoopHistoryMath.basalSegments(window: DateInterval(start: at(0), end: at(end)), + schedule: [], deliveries: [delivery]) + XCTAssertEqual(segments.reduce(0) { $0 + $1.units }, expected, accuracy: 1e-9) + } + } + + func testHealthAndLocalPartitionsConserveBasalDelivery() throws { + let sample = HKQuantitySample(type: HKQuantityType(.insulinDelivery), + quantity: HKQuantity(unit: .internationalUnit(), doubleValue: 1), + start: at(0), end: at(60), + metadata: [HKMetadataKeyInsulinDeliveryReason: HKInsulinDeliveryReason.basal.rawValue]) + let left = try XCTUnwrap(BasalSegment(healthSample: sample, in: DateInterval(start: at(-30), end: at(30)))) + let right = try XCTUnwrap(BasalSegment(healthSample: sample, in: DateInterval(start: at(30), end: at(90)))) + XCTAssertEqual(left.units, 0.5, accuracy: 1e-9) + XCTAssertEqual(right.units, 0.5, accuracy: 1e-9, "Include basal beginning before the selected interval") + let dose = DoseEntry(type: .basal, startDate: at(0), endDate: at(60), value: 1, unit: .units, decisionId: nil, deliveredUnits: 1) + let local = LoopHistoryMath.basalSegments(window: DateInterval(start: at(30), end: at(90)), schedule: [], + deliveries: [try XCTUnwrap(LoopHistoryMath.BasalDelivery(dose: dose))]) + XCTAssertEqual(left.units + local.reduce(0) { $0 + $1.units }, 1, accuracy: 1e-9) + XCTAssertNil(BasalSegment(healthSample: sample, in: DateInterval(start: at(60), end: at(90)))) + + } + + func testBolusWindowIsHalfOpenAndPreservesDeliveredUnits() { + let window = DateInterval(start: at(0), end: at(60)) + for (minute, expected) in [(-1.0, false), (0.0, true), (30.0, true), (60.0, false)] { + let dose = DoseEntry(type: .bolus, startDate: at(minute), value: 1, unit: .units, decisionId: nil, deliveredUnits: 0.75) + let event = BolusEvent(dose: dose, in: window) + XCTAssertEqual(event != nil, expected, "Bolus at minute \(minute)") + if expected { XCTAssertEqual(event?.units, 0.75) } + } + } + + // MARK: Glucose + + func testTimeInRangeBandsAndRoundingSumTo100() { + let values: [Double] = [50, 60, 100, 100, 100, 200, 300] + let points = values.enumerated().map { GlucosePoint(date: at(Double($0.offset * 5)), mgdl: $0.element) } + let fractions = LoopHistoryMath.timeInRange(points) + XCTAssertEqual(fractions[.veryLow]!, 1 / 7, accuracy: 1e-9) + XCTAssertEqual(fractions[.low]!, 1 / 7, accuracy: 1e-9) + XCTAssertEqual(fractions[.inRange]!, 3 / 7, accuracy: 1e-9) + XCTAssertEqual(fractions[.high]!, 1 / 7, accuracy: 1e-9) + XCTAssertEqual(fractions[.veryHigh]!, 1 / 7, accuracy: 1e-9) + XCTAssertEqual(LoopHistoryMath.roundedPercentages(fractions).values.reduce(0, +), 100) + } + + func testBandEdgesFollowConsensusThresholds() { + XCTAssertEqual(LoopHistoryMath.RangeBand.band(for: 53.9), .veryLow) + XCTAssertEqual(LoopHistoryMath.RangeBand.band(for: 54), .low) + XCTAssertEqual(LoopHistoryMath.RangeBand.band(for: 70), .inRange) + XCTAssertEqual(LoopHistoryMath.RangeBand.band(for: 180), .inRange) + XCTAssertEqual(LoopHistoryMath.RangeBand.band(for: 181), .high) + XCTAssertEqual(LoopHistoryMath.RangeBand.band(for: 251), .veryHigh) + } + + func testGMIFromMean() { + let points = [GlucosePoint(date: at(0), mgdl: 150), GlucosePoint(date: at(5), mgdl: 150)] + let stats = LoopHistoryMath.glucoseStats(points)! + XCTAssertEqual(stats.gmiPercent, 3.31 + 0.02392 * 150, accuracy: 1e-9) + XCTAssertEqual(stats.gmiMmolMol, (stats.gmiPercent - 2.152) * 10.929, accuracy: 1e-9) + XCTAssertNil(LoopHistoryMath.glucoseStats([points[0]])) + } + + func testPercentileInterpolates() { + XCTAssertEqual(LoopHistoryMath.percentile([1, 2, 3, 4, 5], 0.5), 3) + XCTAssertEqual(LoopHistoryMath.percentile([1, 2, 3, 4], 0.5), 2.5) + XCTAssertEqual(LoopHistoryMath.percentile([10, 20], 0.25), 12.5) + } + + // MARK: Loop modes + + func testLoopModesStartAtFirstModeRecord() { + let interval = DateInterval(start: at(0), end: at(60)) + let runs = stride(from: 0.0, through: 60, by: 5).map { LoopHistoryMath.LoopRun(date: at($0), succeeded: true, error: nil) } + let spans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, + modeChanges: [.init(date: at(30), closedLoop: true)], coverageStart: at(0)) + XCTAssertEqual(spans.first?.start, at(30), "Before the first record the mode is unknown and left out") + XCTAssertEqual(LoopHistoryMath.duration(of: .closed, in: spans), 30 * 60, accuracy: 1) + + XCTAssertTrue(LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: [], coverageStart: at(0)).isEmpty) + } + + func testLoopModesClassifyOpenAndLimited() { + let interval = DateInterval(start: at(0), end: at(90)) + // Runs succeed until 20 min, then fail with an error, then stop. + var runs = stride(from: 0.0, through: 20, by: 5).map { LoopHistoryMath.LoopRun(date: at($0), succeeded: true, error: nil) } + runs.append(.init(date: at(25), succeeded: false, error: "Glucose too old")) + let changes: [LoopHistoryMath.ModeChange] = [.init(date: at(0), closedLoop: true), .init(date: at(60), closedLoop: false)] + + let spans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: changes, coverageStart: at(0)) + XCTAssertEqual(spans.map(\.mode), [.closed, .limited, .open]) + XCTAssertEqual(spans[1].cause, "Glucose too old") + XCTAssertEqual(spans[1].start, at(35), "Limited starts once the last good run is over 15 minutes old") + XCTAssertEqual(spans[2].start, at(55), "The slot ending at the switch-off counts as open") + } + + // MARK: Calendar + + func testDaysAndQuarterMarksOnClockChangeDay() throws { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = try XCTUnwrap(TimeZone(identifier: "Europe/London")) + // 25 Oct 2026: clocks go back an hour, so the day is 25 hours long. + let day = try XCTUnwrap(calendar.date(from: DateComponents(year: 2026, month: 10, day: 25))) + let next = try XCTUnwrap(calendar.date(byAdding: .day, value: 1, to: day)) + XCTAssertEqual(next.timeIntervalSince(day), 25 * 3600) + + let marks = DayAxis.quarterDayMarks(in: DateInterval(start: day, end: next), calendar: calendar) + XCTAssertEqual(marks.map { calendar.component(.hour, from: $0) }, [0, 6, 12, 18, 0]) + + let days = LoopHistoryMath.days(in: DateInterval(start: day, end: next.addingTimeInterval(3600)), calendar: calendar) + XCTAssertEqual(days.count, 2) + XCTAssertEqual(days[0].duration, 25 * 3600) + } + + func testWeeklyAveragesDays() { + let monday = Date(timeIntervalSinceReferenceDate: 815_270_400) // 9436 days after Monday 1 Jan 2001, so a Monday at midnight UTC + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(identifier: "UTC")! + let totals = (0..<14).map { LoopHistoryMath.DayTotal(day: monday.addingTimeInterval(Double($0) * 86400), basal: 10, bolus: Double($0)) } + let weeks = LoopHistoryMath.weekly(totals, calendar: calendar) + XCTAssertEqual(weeks.count, 2) + XCTAssertEqual(weeks[0].bolus, 3, accuracy: 1e-9) + XCTAssertEqual(weeks[1].bolus, 10, accuracy: 1e-9) + } + + func testDailyInsulinAverageLeavesOutTheDayInProgress() { + let day0 = Date(timeIntervalSinceReferenceDate: 815_270_400) + let complete = (0..<3).map { LoopHistoryMath.DayTotal(day: day0.addingTimeInterval(Double($0) * 86400), basal: 10, bolus: 20) } + let today = LoopHistoryMath.DayTotal(day: day0.addingTimeInterval(3 * 86400), basal: 3, bolus: 2, isComplete: false) + XCTAssertEqual(LoopHistoryMath.averageDailyInsulin(complete + [today]), 30, accuracy: 1e-9) + XCTAssertEqual(LoopHistoryMath.averagingDays(complete + [today]).count, 3) + // With nothing complete, the day in progress is all there is to show. + XCTAssertEqual(LoopHistoryMath.averageDailyInsulin([today]), 5, accuracy: 1e-9) + } + + func testInsulinSplitSeparatesManualAutomaticAndUnclassifiedBolus() { + let boluses = [ + BolusEvent(id: "m", date: at(0), units: 4, automatic: false, manuallyEntered: false), + BolusEvent(id: "a", date: at(5), units: 0.5, automatic: true, manuallyEntered: false), + BolusEvent(id: "u", date: at(10), units: 1, automatic: false, manuallyEntered: false, automaticKnown: false), + ] + let split = InsulinSplit(basal: 14.5, boluses: Array(boluses.prefix(2))) + XCTAssertEqual(split.manualBolus, 4, accuracy: 1e-9) + XCTAssertEqual(split.automaticBolus, 0.5, accuracy: 1e-9) + XCTAssertEqual(split.total, 19, accuracy: 1e-9) + XCTAssertTrue(split.isClassified) + // Shares round independently but always add up to 100. + XCTAssertEqual(split.basalPercent + split.bolusPercent, 100) + XCTAssertEqual(split.bolusPercent, 24) + + // A bolus that doesn't say who gave it still counts, but hides the manual and automatic detail. + let health = InsulinSplit(basal: 14.5, boluses: boluses) + XCTAssertEqual(health.bolus, 5.5, accuracy: 1e-9) + XCTAssertFalse(health.isClassified) + + let average = InsulinSplit.average([split, InsulinSplit(basal: 10.5, manualBolus: 2, automaticBolus: 1.5)]) + XCTAssertEqual(average.basal, 12.5, accuracy: 1e-9) + XCTAssertEqual(average.manualBolus, 3, accuracy: 1e-9) + XCTAssertEqual(average.automaticBolus, 1, accuracy: 1e-9) + } + + func testWeeklyInsulinUsesTheSummaryAveragingDays() { + let monday = Date(timeIntervalSinceReferenceDate: 815_270_400) + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(identifier: "UTC")! + let totals = [ + LoopHistoryMath.DayTotal(day: monday, basal: 0, bolus: 0), + LoopHistoryMath.DayTotal(day: monday.addingTimeInterval(86400), basal: 10, bolus: 20), + LoopHistoryMath.DayTotal(day: monday.addingTimeInterval(2 * 86400), basal: 10, bolus: 20), + LoopHistoryMath.DayTotal(day: monday.addingTimeInterval(3 * 86400), basal: 1, bolus: 2, isComplete: false), + ] + let weeks = LoopHistoryMath.weekly(totals, calendar: calendar) + XCTAssertEqual(weeks.count, 1) + XCTAssertEqual(weeks[0].basal, 10, accuracy: 1e-9) + XCTAssertEqual(weeks[0].bolus, 20, accuracy: 1e-9) + + // A current week with no complete day must still show the data it has. + let currentWeek = LoopHistoryMath.weekly([totals[3]], calendar: calendar) + XCTAssertEqual(currentWeek.first?.total, 3) + } + + func testGapMessageForADayWithoutReadings() { + let start = Date(timeIntervalSinceReferenceDate: 815_270_400) + let day = DateInterval(start: start, duration: 86400) + let empty = DayAxis.gaps(in: [], window: day, until: day.end.addingTimeInterval(3600)) + XCTAssertEqual(DayAxis.gapMessage(empty, window: day, until: day.end.addingTimeInterval(3600)), "No CGM data on this day") + let morning = start.addingTimeInterval(9 * 3600) + XCTAssertEqual(DayAxis.gapMessage(DayAxis.gaps(in: [], window: day, until: morning), window: day, until: morning), "No CGM data yet today") + let reading = [GlucosePoint(date: start.addingTimeInterval(12 * 3600), mgdl: 120)] + let partial = DayAxis.gaps(in: reading, window: day, until: day.end) + XCTAssertTrue(DayAxis.gapMessage(partial, window: day, until: day.end)?.hasSuffix("and 1 shorter gap") ?? false) + } + + // MARK: Formatting + + func testRangeFormatting() throws { + let calendar = Calendar.current + let a = try XCTUnwrap(calendar.date(from: DateComponents(year: 2026, month: 10, day: 12, hour: 12))) + let b = try XCTUnwrap(calendar.date(from: DateComponents(year: 2026, month: 10, day: 25, hour: 12))) + let c = try XCTUnwrap(calendar.date(from: DateComponents(year: 2026, month: 9, day: 28, hour: 12))) + // Ranges and durations come from the system's formatters, so they follow the user's locale; + // the test host runs in en_US. + XCTAssertEqual(LoopFormat.range(b, b), LoopFormat.day(b)) + let sameMonth = LoopFormat.range(a, b) + XCTAssertTrue(sameMonth.contains("12") && sameMonth.contains("25")) + XCTAssertEqual(sameMonth.components(separatedBy: "2026").count, 2, "the year appears once") + let twoMonths = LoopFormat.range(c, b) + XCTAssertTrue(twoMonths.contains("Sep") && twoMonths.contains("Oct")) + XCTAssertEqual(LoopFormat.duration(13 * 86400 + 11 * 3600), "13d 11h") + XCTAssertEqual(LoopFormat.duration(9 * 3600 + 45 * 60), "9h 45m") + XCTAssertEqual(LoopFormat.number(7.25, decimals: 1), 7.25.formatted(.number.precision(.fractionLength(1)).grouping(.never))) + XCTAssertEqual(LoopFormat.signed(-0.4), "−0.40") + XCTAssertEqual(LoopFormat.signed(0.0001), "+0.00") + } + + @MainActor + func testCSVExportsTenColumnsWithASCIINumbers() throws { + let day = Calendar.current.startOfDay(for: start) + let nextDay = day.addingTimeInterval(86400) + for isMmol in [true, false] { + var summary = TrendsModel.Summary(interval: DateInterval(start: day, duration: 2 * 86400), + unit: GlucoseUnitStyle(isMmol: isMmol)) + summary.glucose = [GlucosePoint(date: day, mgdl: 131.53286)] + summary.totals = [ + .init(day: day, split: InsulinSplit(basal: 1.25, manualBolus: 2, automaticBolus: 0.5)), + // From another app in Health: the bolus doesn't say whether it was manual or automatic. + .init(day: nextDay, split: InsulinSplit(basal: 1, unclassifiedBolus: 3)), + ] + summary.carbsByDay = [day: 45] + let url = try TrendsModel.exportCSV(summary) + defer { try? FileManager.default.removeItem(at: url) } + let rows = try String(contentsOf: url, encoding: .utf8).components(separatedBy: "\n") + XCTAssertEqual(rows.count, 3) + XCTAssertEqual(rows[0].components(separatedBy: ",").count, 10) + XCTAssertTrue(rows[0].hasSuffix(",carbs_g,manual_bolus_u,auto_bolus_u")) + let columns = rows[1].components(separatedBy: ",") + XCTAssertEqual(columns.count, 10) + XCTAssertEqual(Array(columns.dropFirst()), [isMmol ? "7.3" : "132", "1", "100", "1.25", "2.50", "3.75", "45", "2.00", "0.50"]) + XCTAssertEqual(Array(rows[2].components(separatedBy: ",").suffix(5)), ["3.00", "4.00", "0", "", ""]) + } + } + + func testModeHistoryRetainsTheModeBeforeTheRetentionWindow() throws { + let suite = "LoopHistoryMathTests-\(UUID().uuidString)" + let defaults = try XCTUnwrap(UserDefaults(suiteName: suite)) + defer { defaults.removePersistentDomain(forName: suite) } + let history = LoopModeHistory(defaults: defaults) + history.record(closedLoop: false, at: at(0)) + history.record(closedLoop: true, at: at(60)) + let switchDate = at(200 * 1440) + history.record(closedLoop: false, at: switchDate) + XCTAssertEqual(history.changes, [.init(date: at(60), closedLoop: true), .init(date: switchDate, closedLoop: false)]) + let interval = DateInterval(start: switchDate.addingTimeInterval(-3600), end: switchDate) + let spans = LoopHistoryMath.loopModeSpans(interval: interval, runs: [], modeChanges: history.changes, + coverageStart: interval.start) + XCTAssertEqual(spans.first?.start, interval.start, "Retaining the old mode must preserve recent coverage") + } + + func testIssueDescriptions() { + XCTAssertEqual(LoopHistoryProvider.describe(issue: "glucoseTooOld"), "Glucose too old") + XCTAssertEqual(LoopHistoryProvider.describe(issue: "pumpDataTooOld"), "Pump data too old") + } + + // MARK: Day history + + func testDayHistoryClippedToNowDropsLaterEventsAndCutsBasal() { + var history = DayHistory(window: DateInterval(start: at(0), end: at(1440))) + history.glucose = [GlucosePoint(date: at(10), mgdl: 100), GlucosePoint(date: at(700), mgdl: 120)] + history.boluses = [BolusEvent(id: "a", date: at(30), units: 1, automatic: false, manuallyEntered: false), + BolusEvent(id: "b", date: at(650), units: 2, automatic: true, manuallyEntered: false)] + history.basal = [BasalSegment(start: at(0), end: at(720), rate: 1, scheduledRate: 1, kind: .scheduled, automatic: false)] + let clipped = history.clipped(to: DateInterval(start: at(0), end: at(600))) + XCTAssertEqual(clipped.window.end, at(600)) + XCTAssertEqual(clipped.glucose.count, 1) + XCTAssertEqual(clipped.boluses.map(\.id), ["a"]) + XCTAssertEqual(clipped.basalUnits, 10, accuracy: 1e-9) + } +} From e56ded8566817fc0fa021bc1eef8cd143aae1bec Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Sun, 1 Nov 2026 20:44:03 +0000 Subject: [PATCH 02/15] Bring what Statistics shows into Trends Trends now carries everything Statistics shows that it lacked, so it can take Statistics' place: - Consensus goals: average under 8.6 mmol/L (154 mg/dL), GMI under 7% and CV 36% or less under the summary tiles; time in range over 70%, under 25% above and 4% below, under 5% very high and 1% very low, and CGM active over 70% on the Time in range card. - Scheduled basal under Basal in Basal and bolus: the schedule that applied over the same days (the stored profile with presets), or today's schedule where the profile history doesn't reach. - "Usually X U by HH:MM": today's insulin against the median by this time over the last 14 days, from InsulinDeliveryHistory as on Statistics. Under Total so far on today's Day view, and under the Insulin per day chart on longer periods ending today. - "CGM data covers 5 of 14 days." under Summary when readings start partway through the period. - Typed-in (fingerstick) readings are left out of every figure, as on Statistics. The day timeline still draws them. Each new line reuses a pattern already on the screen: the Aim footnote, the indented line under Bolus, and the muted footnote. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QHnVP7ynB62LNuuYMy3yvH --- Loop/Localizable.xcstrings | 805 ++++++++++++++++++-- Loop/Redesign/LoopHistoryMath.swift | 22 + Loop/Redesign/LoopHistoryModels.swift | 2 + Loop/Redesign/LoopHistoryProvider.swift | 18 +- Loop/Redesign/TrendsComponents.swift | 26 +- Loop/Redesign/TrendsScreen.swift | 94 ++- LoopTests/Models/LoopHistoryMathTests.swift | 16 + 7 files changed, 921 insertions(+), 62 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 6cb9a9d92e..d67ac0036a 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -4180,145 +4180,288 @@ } } }, - "Aim: over 70%% in range, under 4%% low." : { - "comment" : "Trends Time in range card footnote: the consensus targets for time in range and time low", + "Aim: average under %1$@ %2$@, GMI under 7%%, CV 36%% or less." : { + "comment" : "Trends Summary footnote: the consensus targets for average glucose, GMI and glucose variability (CV). (1: glucose value, 2: glucose unit)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الهدف: أكثر من 70%% ضمن النطاق، وأقل من 4%% منخفض." + "value" : "الهدف: متوسط أقل من %1$@ %2$@، وGMI أقل من 7%%، وCV ‏36%% أو أقل." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Cíl: přes 70%% v rozsahu, pod 4%% nízko." + "value" : "Cíl: průměr pod %1$@ %2$@, GMI pod 7%%, CV nejvýše 36%%." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Mål: over 70 %% i området, under 4 %% lavt." + "value" : "Mål: gennemsnit under %1$@ %2$@, GMI under 7 %%, CV højst 36 %%." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ziel: über 70 %% im Zielbereich, unter 4 %% niedrig." + "value" : "Ziel: Durchschnitt unter %1$@ %2$@, GMI unter 7 %%, VK höchstens 36 %%." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Objetivo: más del 70%% en rango, menos del 4%% bajo." + "value" : "Objetivo: promedio por debajo de %1$@ %2$@, GMI por debajo del 7%%, CV del 36%% o menos." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Tavoite: yli 70 %% tavoitealueella, alle 4 %% matalalla." + "value" : "Tavoite: keskiarvo alle %1$@ %2$@, GMI alle 7 %%, CV enintään 36 %%." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Objectif : plus de 70 %% dans la cible, moins de 4 %% en hypo." + "value" : "Objectif : moyenne sous %1$@ %2$@, GMI sous 7 %%, CV de 36 %% au plus." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "יעד: מעל 70%% בטווח, מתחת ל-4%% נמוך." + "value" : "יעד: ממוצע מתחת ל-%1$@ %2$@, GMI מתחת ל-7%%, CV ‏36%% או פחות." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Cél: több mint 70%% tartományban, kevesebb mint 4%% alacsony." + "value" : "Cél: átlag %1$@ %2$@ alatt, GMI 7%% alatt, CV legfeljebb 36%%." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Obiettivo: oltre il 70%% nell'intervallo, meno del 4%% bassa." + "value" : "Obiettivo: media sotto %1$@ %2$@, GMI sotto il 7%%, CV al massimo 36%%." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "目標:範囲内70%%超、低値4%%未満。" + "value" : "目標:平均%1$@ %2$@未満、GMI 7%%未満、CV 36%%以下。" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Mål: over 70 %% i målområdet, under 4 %% lavt." + "value" : "Mål: gjennomsnitt under %1$@ %2$@, GMI under 7 %%, CV høyst 36 %%." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Doel: meer dan 70%% binnen bereik, minder dan 4%% laag." + "value" : "Doel: gemiddelde onder %1$@ %2$@, GMI onder 7%%, CV hoogstens 36%%." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Cel: ponad 70%% w zakresie, poniżej 4%% niskich." + "value" : "Cel: średnia poniżej %1$@ %2$@, GMI poniżej 7%%, CV najwyżej 36%%." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Meta: mais de 70%% no alvo, menos de 4%% baixo." + "value" : "Meta: média abaixo de %1$@ %2$@, GMI abaixo de 7%%, CV de no máximo 36%%." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Țintă: peste 70%% în interval, sub 4%% scăzut." + "value" : "Țintă: medie sub %1$@ %2$@, GMI sub 7%%, CV de cel mult 36%%." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Цель: в диапазоне более 70%%, низкая — менее 4%%." + "value" : "Цель: среднее ниже %1$@ %2$@, GMI ниже 7%%, CV не более 36%%." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Cieľ: nad 70%% v rozsahu, pod 4%% nízko." + "value" : "Cieľ: priemer pod %1$@ %2$@, GMI pod 7%%, CV najviac 36%%." } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Mål: över 70 %% inom målområdet, under 4 %% lågt." + "value" : "Mål: genomsnitt under %1$@ %2$@, GMI under 7 %%, CV högst 36 %%." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hedef: aralıkta %%70'in üzeri, düşükte %%4'ün altı." + "value" : "Hedef: ortalama %1$@ %2$@ altı, GMI %%7 altı, CV en fazla %%36." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Мета: у діапазоні понад 70%%, низька — менше 4%%." + "value" : "Мета: середнє нижче %1$@ %2$@, GMI нижче 7%%, CV не більше 36%%." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mục tiêu: trên 70%% trong khoảng, dưới 4%% thấp." + "value" : "Mục tiêu: trung bình dưới %1$@ %2$@, GMI dưới 7%%, CV từ 36%% trở xuống." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "目标:范围内超过70%%,低血糖低于4%%。" + "value" : "目标:平均值低于 %1$@ %2$@,GMI 低于 7%%,CV 不超过 36%%。" + } + } + } + }, + "Aim: over 70%% in range; under 25%% above and 4%% below; under 5%% very high and 1%% very low; CGM active over 70%%." : { + "comment" : "Trends Time in range card footnote: the consensus targets for time in range, above and below range, very high, very low and CGM wear", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الهدف: أكثر من 70%% ضمن النطاق؛ أقل من 25%% فوق و4%% تحت؛ أقل من 5%% مرتفع جدًا و1%% منخفض جدًا؛ وCGM نشط أكثر من 70%%." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cíl: přes 70%% v rozsahu; pod 25%% nad a 4%% pod; pod 5%% velmi vysoko a 1%% velmi nízko; CGM aktivní přes 70%%." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mål: over 70 %% i området; under 25 %% over og 4 %% under; under 5 %% meget højt og 1 %% meget lavt; CGM aktiv over 70 %%." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ziel: über 70 %% im Zielbereich; unter 25 %% darüber und 4 %% darunter; unter 5 %% sehr hoch und 1 %% sehr niedrig; CGM über 70 %% aktiv." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Objetivo: más del 70%% en rango; menos del 25%% por encima y del 4%% por debajo; menos del 5%% muy alto y del 1%% muy bajo; CGM activo más del 70%%." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tavoite: yli 70 %% tavoitealueella; alle 25 %% yläpuolella ja 4 %% alapuolella; alle 5 %% erittäin korkealla ja 1 %% erittäin matalalla; CGM aktiivinen yli 70 %%." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Objectif : plus de 70 %% dans la cible ; moins de 25 %% au-dessus et 4 %% en dessous ; moins de 5 %% très haut et 1 %% très bas ; CGM actif plus de 70 %%." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "יעד: מעל 70%% בטווח; מתחת ל-25%% מעל ו-4%% מתחת; מתחת ל-5%% גבוה מאוד ו-1%% נמוך מאוד; CGM פעיל מעל 70%%." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cél: több mint 70%% tartományban; kevesebb mint 25%% felette és 4%% alatta; kevesebb mint 5%% nagyon magas és 1%% nagyon alacsony; CGM aktív több mint 70%%." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obiettivo: oltre il 70%% nell'intervallo; meno del 25%% sopra e del 4%% sotto; meno del 5%% molto alta e dell'1%% molto bassa; CGM attivo oltre il 70%%." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "目標:範囲内70%%超、範囲上25%%未満・範囲下4%%未満、非常に高い5%%未満・非常に低い1%%未満、CGM稼働70%%超。" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mål: over 70 %% i målområdet; under 25 %% over og 4 %% under; under 5 %% svært høyt og 1 %% svært lavt; CGM aktiv over 70 %%." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doel: meer dan 70%% binnen bereik; minder dan 25%% erboven en 4%% eronder; minder dan 5%% zeer hoog en 1%% zeer laag; CGM meer dan 70%% actief." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cel: ponad 70%% w zakresie; poniżej 25%% powyżej i 4%% poniżej; poniżej 5%% bardzo wysokich i 1%% bardzo niskich; CGM aktywny ponad 70%%." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meta: mais de 70%% no alvo; menos de 25%% acima e 4%% abaixo; menos de 5%% muito alto e 1%% muito baixo; CGM ativo mais de 70%%." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Țintă: peste 70%% în interval; sub 25%% peste și 4%% sub; sub 5%% foarte ridicat și 1%% foarte scăzut; CGM activ peste 70%%." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Цель: в диапазоне более 70%%; выше — менее 25%%, ниже — менее 4%%; очень высокая — менее 5%%, очень низкая — менее 1%%; CGM активен более 70%%." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cieľ: nad 70%% v rozsahu; pod 25%% nad a 4%% pod; pod 5%% veľmi vysoko a 1%% veľmi nízko; CGM aktívny nad 70%%." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mål: över 70 %% inom målområdet; under 25 %% över och 4 %% under; under 5 %% mycket högt och 1 %% mycket lågt; CGM aktiv över 70 %%." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef: aralıkta %%70'in üzeri; üstünde %%25'in, altında %%4'ün altı; çok yüksekte %%5'in, çok düşükte %%1'in altı; CGM aktif %%70'in üzeri." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мета: у діапазоні понад 70%%; вище — менше 25%%, нижче — менше 4%%; дуже висока — менше 5%%, дуже низька — менше 1%%; CGM активний понад 70%%." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mục tiêu: trên 70%% trong khoảng; dưới 25%% trên khoảng và 4%% dưới khoảng; dưới 5%% rất cao và 1%% rất thấp; CGM hoạt động trên 70%%." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "目标:范围内超过70%%;高于范围低于25%%,低于范围低于4%%;极高低于5%%,极低低于1%%;CGM 在线超过70%%。" } } } @@ -6468,6 +6611,149 @@ } } }, + "CGM data covers %1$@." : { + "comment" : "Trends Summary note when glucose readings start partway through the period. (1: days covered, e.g. 5 of 14 days)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "بيانات CGM تغطي %1$@." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data CGM pokrývají %1$@." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM-data dækker %1$@." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM-Daten decken %1$@ ab." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los datos de CGM cubren %1$@." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM-data kattaa %1$@." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les données CGM couvrent %1$@." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "נתוני CGM מכסים %1$@." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM-adat: %1$@." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "I dati CGM coprono %1$@." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGMデータ:%1$@。" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM-data dekker %1$@." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM-gegevens beslaan %1$@." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dane CGM obejmują %1$@." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os dados de CGM cobrem %1$@." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datele CGM acoperă %1$@." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Данные CGM: %1$@." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Údaje CGM pokrývajú %1$@." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM-data täcker %1$@." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM verisi: %1$@." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дані CGM: %1$@." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dữ liệu CGM bao phủ %1$@." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM 数据覆盖 %1$@。" + } + } + } + }, "Closed loop" : { "comment" : "Trends Loop status: time Loop was running in closed loop", "localizations" : { @@ -16055,115 +16341,258 @@ } } }, - "SD %1$@ · CV %2$@%%" : { - "comment" : "Trends Average tile detail: standard deviation and coefficient of variation of glucose. (1: standard deviation in the glucose unit, 2: coefficient of variation as a percentage number)", + "Scheduled %1$@" : { + "comment" : "Basal and bolus: what the basal schedule delivers over the same time, under the basal row. (1: units, e.g. 18.0 U)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@%%" + "value" : "المجدول %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@%%" + "value" : "Plánovaný %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@ %%" + "value" : "Planlagt %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · VK %2$@ %%" + "value" : "Geplant %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "DE %1$@ · CV %2$@%%" + "value" : "Programado %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@ %%" + "value" : "Ohjelmoitu %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "ET %1$@ · CV %2$@ %%" + "value" : "Programmé %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@%%" + "value" : "מתוזמן %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@%%" + "value" : "Ütemezett %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "DS %1$@ · CV %2$@%%" + "value" : "Programmato %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@%%" + "value" : "スケジュール %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@ %%" + "value" : "Planlagt %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@%%" + "value" : "Gepland %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@%%" + "value" : "Zaplanowane %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "DP %1$@ · CV %2$@%%" + "value" : "Programada %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@%%" + "value" : "Programat %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@%%" + "value" : "Запланировано %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "SD %1$@ · CV %2$@%%" + "value" : "Plánovaný %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schemalagd %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Planlanan %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "За розкладом %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Theo lịch %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "预设 %1$@" + } + } + } + }, + "SD %1$@ · CV %2$@%%" : { + "comment" : "Trends Average tile detail: standard deviation and coefficient of variation of glucose. (1: standard deviation in the glucose unit, 2: coefficient of variation as a percentage number)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@ %%" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · VK %2$@ %%" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "DE %1$@ · CV %2$@%%" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@ %%" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ET %1$@ · CV %2$@ %%" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "DS %1$@ · CV %2$@%%" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@ %%" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "DP %1$@ · CV %2$@%%" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "SD %1$@ · CV %2$@%%" } }, "sv" : { @@ -17485,6 +17914,149 @@ } } }, + "Today so far %1$@, usually %2$@ by %3$@." : { + "comment" : "Trends Insulin per day footnote: today's insulin so far against the median by this time of day over the last 14 days. (1: today's units, 2: usual units, 3: time of day)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اليوم حتى الآن %1$@، وعادةً %2$@ بحلول %3$@." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dnes zatím %1$@, obvykle %2$@ do %3$@." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "I dag indtil nu %1$@, normalt %2$@ kl. %3$@." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Heute bisher %1$@, üblicherweise %2$@ bis %3$@." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoy hasta ahora %1$@, normalmente %2$@ a las %3$@." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tänään tähän mennessä %1$@, yleensä %2$@ klo %3$@ mennessä." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aujourd'hui jusqu'ici %1$@, d'habitude %2$@ à %3$@." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "היום עד כה %1$@, בדרך כלל %2$@ עד %3$@." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ma eddig %1$@, általában %2$@ ilyenkorra (%3$@)." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oggi finora %1$@, di solito %2$@ entro le %3$@." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日のこれまで%1$@、通常は%3$@までに%2$@。" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "I dag så langt %1$@, vanligvis %2$@ innen %3$@." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vandaag tot nu toe %1$@, meestal %2$@ om %3$@." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dziś do tej pory %1$@, zwykle %2$@ do %3$@." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoje até agora %1$@, normalmente %2$@ até %3$@." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Azi până acum %1$@, de obicei %2$@ până la %3$@." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сегодня на данный момент %1$@, обычно %2$@ к %3$@." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dnes zatiaľ %1$@, zvyčajne %2$@ do %3$@." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hittills i dag %1$@, vanligtvis %2$@ vid %3$@." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bugün şu ana kadar %1$@, genelde %3$@ itibarıyla %2$@." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сьогодні наразі %1$@, зазвичай %2$@ до %3$@." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hôm nay đến giờ %1$@, thường %2$@ trước %3$@." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "今日至今 %1$@,通常到 %3$@ 为 %2$@。" + } + } + } + }, "Total" : { "comment" : "Basal and bolus total row: basal plus bolus", "localizations" : { @@ -18629,6 +19201,149 @@ } } }, + "Usually %1$@ by %2$@" : { + "comment" : "Trends: the median insulin delivered by this time of day over the last 14 days. (1: units, e.g. 28.4 U, 2: time of day)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "عادةً %1$@ بحلول %2$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obvykle %1$@ do %2$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Normalt %1$@ kl. %2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üblicherweise %1$@ bis %2$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Normalmente %1$@ a las %2$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yleensä %1$@ klo %2$@ mennessä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "D'habitude %1$@ à %2$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "בדרך כלל %1$@ עד %2$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Általában %1$@ ilyenkorra (%2$@)" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Di solito %1$@ entro le %2$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "通常は%2$@までに%1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vanligvis %1$@ innen %2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meestal %1$@ om %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zwykle %1$@ do %2$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Normalmente %1$@ até %2$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "De obicei %1$@ până la %2$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Обычно %1$@ к %2$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zvyčajne %1$@ do %2$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vanligtvis %1$@ vid %2$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genelde %2$@ itibarıyla %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Зазвичай %1$@ до %2$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thường %1$@ trước %2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通常到 %2$@ 为 %1$@" + } + } + } + }, "Very high" : { "comment" : "Time in range band: glucose above 250 mg/dL (13.9 mmol/L) Time-in-range band name: glucose above 250 mg/dL (13.9 mmol/L)", "localizations" : { diff --git a/Loop/Redesign/LoopHistoryMath.swift b/Loop/Redesign/LoopHistoryMath.swift index 0bc1080785..cfdca173dc 100644 --- a/Loop/Redesign/LoopHistoryMath.swift +++ b/Loop/Redesign/LoopHistoryMath.swift @@ -126,6 +126,28 @@ enum LoopHistoryMath { Set(points.map { calendar.startOfDay(for: $0.date) }).count } + /// Whole days from the first reading to the end of `interval`, when that falls short of the period by more than a + /// day; nil when the readings span the period, or there are none. + static func cgmCoveredDays(_ points: [GlucosePoint], in interval: DateInterval, periodDays: Int) -> Int? { + guard periodDays > 1, let first = points.map(\.date).min() else { return nil } + let available = interval.end.timeIntervalSince(first) / 86400 + guard available + 1 < Double(periodDays) else { return nil } + return max(1, Int(available.rounded())) + } + + /// Units the schedule would deliver over `span`, or nil where the points don't cover all of it. + static func scheduledUnits(_ points: [ScheduledBasalPoint], over span: DateInterval) -> Double? { + var units = 0.0 + var covered: TimeInterval = 0 + for point in points { + let overlap = min(point.end, span.end).timeIntervalSince(max(point.start, span.start)) + guard overlap > 0 else { continue } + units += point.rate * overlap / 3600 + covered += overlap + } + return covered >= span.duration - 60 ? units : nil + } + // MARK: Days /// Midnight-to-midnight intervals covering `interval`, oldest first, the last one cut off at `interval.end`. diff --git a/Loop/Redesign/LoopHistoryModels.swift b/Loop/Redesign/LoopHistoryModels.swift index e1acc34c89..3fc71c5b47 100644 --- a/Loop/Redesign/LoopHistoryModels.swift +++ b/Loop/Redesign/LoopHistoryModels.swift @@ -16,6 +16,8 @@ import LoopAlgorithm struct GlucosePoint: Equatable { let date: Date let mgdl: Double + /// A fingerstick or other reading typed in: drawn on charts, but left out of CGM statistics. + var userEntered = false } struct BolusEvent: Equatable, Identifiable { diff --git a/Loop/Redesign/LoopHistoryProvider.swift b/Loop/Redesign/LoopHistoryProvider.swift index 5bdc47d9f6..0c5236808a 100644 --- a/Loop/Redesign/LoopHistoryProvider.swift +++ b/Loop/Redesign/LoopHistoryProvider.swift @@ -59,6 +59,19 @@ final class LoopHistoryProvider { return range.lowerBound.doubleValue(for: .milligramsPerDeciliter)...range.upperBound.doubleValue(for: .milligramsPerDeciliter) } + /// What the basal schedule delivers in a day, for days the stored profile history doesn't reach. + @MainActor + var scheduledBasalPerDay: Double? { + loopDataManager.settingsProvider.settings.basalRateSchedule?.total() + } + + /// The median insulin delivered from midnight to this time of day over the last 14 days, as on Statistics. + func typicalInsulinByNow() async -> (typical: TypicalInsulinDelivery, at: Date)? { + guard let history = try? await loopDataManager.insulinDeliveryHistory(days: 14), + let typical = history.typicalDelivery(by: history.end) else { return nil } + return (typical, history.end) + } + /// The targets that actually applied, including dated presets and changes to the stored profile. func glucoseTargetBands(in interval: DateInterval) async throws -> [TargetBand] { if let store = settingsManager.settingsStore { @@ -203,7 +216,7 @@ final class LoopHistoryProvider { let samples = try await glucoseStore.getGlucoseSamples(start: interval.start, end: interval.end) return samples .filter { !$0.isDisplayOnly } - .map { GlucosePoint(date: $0.startDate, mgdl: $0.quantity.doubleValue(for: .milligramsPerDeciliter)) } + .map { GlucosePoint(date: $0.startDate, mgdl: $0.quantity.doubleValue(for: .milligramsPerDeciliter), userEntered: $0.wasUserEntered) } } catch { os_log("Glucose fetch failed: %{public}@", log: log, type: .error, String(describing: error)) throw error @@ -320,7 +333,8 @@ final class LoopHistoryProvider { for sample in samples where !sample.isDisplayOnly && (FeatureFlags.observeHealthKitGlucoseSamplesFromOtherApps || sample.sourceRevision.source.bundleIdentifier == own) { if let last = points.last, sample.startDate.timeIntervalSince(last.date) < 150 { continue } - points.append(GlucosePoint(date: sample.startDate, mgdl: sample.quantity.doubleValue(for: .milligramsPerDeciliter))) + points.append(GlucosePoint(date: sample.startDate, mgdl: sample.quantity.doubleValue(for: .milligramsPerDeciliter), + userEntered: sample.wasUserEntered)) } return points } diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift index 5f93573c36..c98336a72c 100644 --- a/Loop/Redesign/TrendsComponents.swift +++ b/Loop/Redesign/TrendsComponents.swift @@ -460,12 +460,22 @@ struct InsulinSplitView: View { let split: InsulinSplit /// Today so far, rather than a whole day or a per-day average. var partial = false + /// What the basal schedule delivers over the same time, shown under the basal row. + var scheduledBasal: Double? + /// Today against a usual day, e.g. "Usually 28.4 U by 19:46", under the total. + var usual: String? var body: some View { VStack(alignment: .leading, spacing: LoopDesign.Space.s3) { InsulinSplitBar(split: split) VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { row(.basal, NSLocalizedString("Basal", comment: "Basal and bolus row: insulin delivered as basal"), split.basal, split.basalPercent) + if let scheduledBasal { + part(.dash(LoopDesign.Palette.basalScheduled), String(format: NSLocalizedString("Scheduled %1$@", comment: "Basal and bolus: what the basal schedule delivers over the same time, under the basal row. (1: units, e.g. 18.0 U)"), LoopFormat.totalUnits(scheduledBasal))) + .font(LoopDesign.Typeface.footnote.monospacedDigit()) + .foregroundStyle(LoopDesign.Palette.inkMuted) + .padding(.leading, 10 + LoopDesign.Space.s2) + } row(.solid(LoopDesign.Palette.bolus), NSLocalizedString("Bolus", comment: "Basal and bolus row: insulin delivered as bolus, manual and automatic"), split.bolus, split.bolusPercent) if split.isClassified && split.bolus > 0 { FlowLayout(spacing: LoopDesign.Space.s3, lineSpacing: 2) { @@ -488,10 +498,16 @@ struct InsulinSplitView: View { .font(LoopDesign.Typeface.statValueSmall) .foregroundStyle(LoopDesign.Palette.insulinInk) } + if let usual { + Text(usual) + .font(LoopDesign.Typeface.footnote.monospacedDigit()) + .foregroundStyle(LoopDesign.Palette.inkMuted) + .frame(maxWidth: .infinity, alignment: .trailing) + } } } .accessibilityElement(children: .ignore) - .accessibilityLabel(Self.accessibilityLabel(split, partial: partial)) + .accessibilityLabel(Self.accessibilityLabel(split, partial: partial, scheduledBasal: scheduledBasal, usual: usual)) } private func row(_ swatch: LegendItem.Swatch, _ label: String, _ units: Double, _ percent: Int) -> some View { @@ -518,7 +534,7 @@ struct InsulinSplitView: View { } } - static func accessibilityLabel(_ split: InsulinSplit, partial: Bool) -> String { + static func accessibilityLabel(_ split: InsulinSplit, partial: Bool, scheduledBasal: Double? = nil, usual: String? = nil) -> String { var parts = [ String(format: NSLocalizedString("Basal %1$@, %2$@ percent", comment: "VoiceOver: basal units and share. (1: units, 2: percentage number)"), LoopFormat.totalUnits(split.basal), "\(split.basalPercent)"), String(format: NSLocalizedString("Bolus %1$@, %2$@ percent", comment: "VoiceOver: bolus units and share. (1: units, 2: percentage number)"), LoopFormat.totalUnits(split.bolus), "\(split.bolusPercent)"), @@ -531,6 +547,12 @@ struct InsulinSplitView: View { ? NSLocalizedString("Total so far %1$@", comment: "VoiceOver: today's total insulin so far. (1: units)") : NSLocalizedString("Total %1$@", comment: "VoiceOver: total insulin. (1: units)"), LoopFormat.totalUnits(split.total))) + if let scheduledBasal { + parts.insert(String(format: NSLocalizedString("Scheduled %1$@", comment: "Basal and bolus: what the basal schedule delivers over the same time, under the basal row. (1: units, e.g. 18.0 U)"), LoopFormat.totalUnits(scheduledBasal)), at: 1) + } + if let usual { + parts.append(usual) + } return parts.joined(separator: ". ") } } diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift index efb520874d..9f8c5960a6 100644 --- a/Loop/Redesign/TrendsScreen.swift +++ b/Loop/Redesign/TrendsScreen.swift @@ -17,7 +17,10 @@ final class TrendsModel: ObservableObject { struct Summary { var interval: DateInterval var unit: GlucoseUnitStyle + /// CGM readings only: typed-in fingersticks are left out of every figure, as on Statistics. var glucose: [GlucosePoint] = [] + /// Days from the first reading, when that is short of the period; nil when the readings span it. + var cgmCoveredDays: Int? var stats: LoopHistoryMath.GlucoseStats? var cgmActive: Double = 0 var timeInRange: [LoopHistoryMath.RangeBand: Double] = [:] @@ -35,6 +38,10 @@ final class TrendsModel: ObservableObject { /// The whole calendar day, for a one-day period: drawn as one timeline. var day: DayHistory? var dayTargets: [TargetBand] = [] + /// What the basal schedule delivers over the same days the split averages. + var scheduledBasal: Double? + /// The median insulin by this time of day over the last 14 days, when the period ends now. + var typicalByNow: (typical: TypicalInsulinDelivery, at: Date)? /// The days per-day figures divide by: insulin's averaging days, or complete days with CGM data when there is /// no insulin (today only when no complete day has any), never the period length. @@ -101,12 +108,15 @@ final class TrendsModel: ObservableObject { history = history.clipped(to: interval) } - summary.glucose = history.glucose - summary.stats = LoopHistoryMath.glucoseStats(history.glucose) - summary.cgmActive = LoopHistoryMath.cgmActiveFraction(history.glucose, over: interval) - summary.timeInRange = LoopHistoryMath.timeInRange(history.glucose) - summary.agp = LoopHistoryMath.agp(history.glucose) - summary.agpDays = LoopHistoryMath.daysWithReadings(history.glucose) + let cgm = history.glucose.filter { !$0.userEntered } + summary.glucose = cgm + summary.stats = LoopHistoryMath.glucoseStats(cgm) + summary.cgmActive = LoopHistoryMath.cgmActiveFraction(cgm, over: interval) + summary.timeInRange = LoopHistoryMath.timeInRange(cgm) + summary.agp = LoopHistoryMath.agp(cgm) + summary.agpDays = LoopHistoryMath.daysWithReadings(cgm) + summary.cgmCoveredDays = LoopHistoryMath.cgmCoveredDays(cgm, in: interval, + periodDays: LoopHistoryMath.days(in: interval).count) let calendar = Calendar.current summary.totals = LoopHistoryMath.days(in: interval).map { day in @@ -134,6 +144,20 @@ final class TrendsModel: ObservableObject { summary.manualBoluses = boluses.filter { !$0.automatic }.count summary.automaticBoluses = boluses.filter(\.automatic).count + // Days the stored profile history doesn't reach (older ones, read from Health) use today's schedule. + let spans = Dictionary(uniqueKeysWithValues: LoopHistoryMath.days(in: interval).map { ($0.start, $0) }) + let perDay = provider.scheduledBasalPerDay + let scheduled = insulinDays.compactMap { day -> Double? in + guard let span = spans[day.day] else { return nil } + return LoopHistoryMath.scheduledUnits(history.scheduledBasal, over: span) ?? perDay.map { $0 * span.duration / 86400 } + } + if !scheduled.isEmpty, scheduled.count == insulinDays.count { + summary.scheduledBasal = scheduled.reduce(0, +) / Double(scheduled.count) + } + if interval.end.timeIntervalSinceNow > -300 { + summary.typicalByNow = await provider.typicalInsulinByNow() + } + summary.loopSpans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: LoopModeHistory.shared.changes, coverageStart: coverageStart) return summary @@ -361,9 +385,7 @@ struct TrendsView: View { } else { TimeInRangeView(fractions: s.timeInRange, unit: s.unit) } - Text(String(format: NSLocalizedString("Aim: over 70%% in range, under 4%% low.", comment: "Trends Time in range card footnote: the consensus targets for time in range and time low"))) - .font(LoopDesign.Typeface.footnote) - .foregroundStyle(P.inkMuted) + Footnote(String(format: NSLocalizedString("Aim: over 70%% in range; under 25%% above and 4%% below; under 5%% very high and 1%% very low; CGM active over 70%%.", comment: "Trends Time in range card footnote: the consensus targets for time in range, above and below range, very high, very low and CGM wear"))) } if s.day == nil { patternCard(s) @@ -377,7 +399,8 @@ struct TrendsView: View { : NSLocalizedString("average per day", comment: "Trends Basal and bolus card subtitle when the figures are averaged per day over the period")) } if s.split.total > 0 { - InsulinSplitView(split: s.split, partial: s.splitIsPartial) + InsulinSplitView(split: s.split, partial: s.splitIsPartial, scheduledBasal: s.scheduledBasal, + usual: s.splitIsPartial ? usualByNow(s) : nil) } else { ChartPlaceholder(message: NSLocalizedString("No insulin recorded in this period", comment: "Trends Insulin per day chart placeholder when no insulin was delivered in the period"), height: 80) } @@ -395,6 +418,9 @@ struct TrendsView: View { path.append(.day(day)) } ChartLegend(items: insulinLegend(s)) + if let today = todaySoFar(s) { + Footnote(today) + } } } LoopCard { @@ -405,9 +431,7 @@ struct TrendsView: View { } LoopModeBarView(spans: s.loopSpans) if let note = coverageNote(s) { - Text(note) - .font(LoopDesign.Typeface.footnote) - .foregroundStyle(P.inkMuted) + Footnote(note) } } } else if let error = model.loadError { @@ -454,11 +478,39 @@ struct TrendsView: View { } .foregroundStyle(P.accentInk) } + if let covered = s.cgmCoveredDays { + Footnote(String(format: NSLocalizedString("CGM data covers %1$@.", comment: "Trends Summary note when glucose readings start partway through the period. (1: days covered, e.g. 5 of 14 days)"), + daysOf(covered, s.totals.count))) + } StatGridView(tiles: model.tiles(s)) .opacity(model.loading ? 0.5 : 1) + if s.stats != nil { + Footnote(String(format: NSLocalizedString("Aim: average under %1$@ %2$@, GMI under 7%%, CV 36%% or less.", comment: "Trends Summary footnote: the consensus targets for average glucose, GMI and glucose variability (CV). (1: glucose value, 2: glucose unit)"), + s.unit.isMmol ? LoopFormat.number(8.6, decimals: 1) : "154", s.unit.unitName)) + } } } + /// "5 of 14 days". + private func daysOf(_ count: Int, _ total: Int) -> String { + let days = String.localizedStringWithFormat(NSLocalizedString("%lld days", comment: "A number of days, e.g. a Trends period or how many days are averaged. (1: number of days)"), total) + return String(format: NSLocalizedString("%1$@ of %2$@", comment: "Trends: how many days of the period are averaged. (1: days averaged, a number; 2: days in the period, e.g. 14 days)"), LoopFormat.number(Double(count), decimals: 0), days) + } + + /// "Usually 28.4 U by 19:46": today's insulin against the last 14 days at the same time. + private func usualByNow(_ s: TrendsModel.Summary) -> String? { + guard let usual = s.typicalByNow else { return nil } + return String(format: NSLocalizedString("Usually %1$@ by %2$@", comment: "Trends: the median insulin delivered by this time of day over the last 14 days. (1: units, e.g. 28.4 U, 2: time of day)"), + LoopFormat.totalUnits(usual.typical.median), LoopFormat.time(usual.at)) + } + + /// Today's bar is still growing, so say where it stands against a usual day. + private func todaySoFar(_ s: TrendsModel.Summary) -> String? { + guard let today = s.totals.last, !today.isComplete, today.total > 0, let usual = s.typicalByNow else { return nil } + return String(format: NSLocalizedString("Today so far %1$@, usually %2$@ by %3$@.", comment: "Trends Insulin per day footnote: today's insulin so far against the median by this time of day over the last 14 days. (1: today's units, 2: usual units, 3: time of day)"), + LoopFormat.totalUnits(today.total), LoopFormat.totalUnits(usual.typical.median), LoopFormat.time(usual.at)) + } + private func export(_ make: () throws -> URL) { do { exportURL = try make() @@ -503,6 +555,22 @@ struct ShareSheet: UIViewControllerRepresentable { func updateUIViewController(_ controller: UIActivityViewController, context: Context) {} } +/// A card's quiet closing line: a target, a caveat or a comparison. +private struct Footnote: View { + let text: String + + init(_ text: String) { + self.text = text + } + + var body: some View { + Text(text) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + .fixedSize(horizontal: false, vertical: true) + } +} + // MARK: - PDF report /// The Trends cards laid out for paper. diff --git a/LoopTests/Models/LoopHistoryMathTests.swift b/LoopTests/Models/LoopHistoryMathTests.swift index b468c9dc42..baa7b68960 100644 --- a/LoopTests/Models/LoopHistoryMathTests.swift +++ b/LoopTests/Models/LoopHistoryMathTests.swift @@ -123,6 +123,22 @@ class LoopHistoryMathTests: XCTestCase { XCTAssertNil(LoopHistoryMath.glucoseStats([points[0]])) } + func testCGMCoverageNotedOnlyWhenReadingsStartLate() { + let interval = DateInterval(start: at(0), end: at(14 * 1440)) + let late = [GlucosePoint(date: at(9 * 1440), mgdl: 120), GlucosePoint(date: at(10 * 1440), mgdl: 130)] + XCTAssertEqual(LoopHistoryMath.cgmCoveredDays(late, in: interval, periodDays: 14), 5) + // Within a day of the full period, or a one-day period, says nothing. + XCTAssertNil(LoopHistoryMath.cgmCoveredDays([GlucosePoint(date: at(600), mgdl: 120)], in: interval, periodDays: 14)) + XCTAssertNil(LoopHistoryMath.cgmCoveredDays(late, in: DateInterval(start: at(0), end: at(1440)), periodDays: 1)) + XCTAssertNil(LoopHistoryMath.cgmCoveredDays([], in: interval, periodDays: 14)) + } + + func testScheduledUnitsNeedTheWholeSpanCovered() { + let points = [ScheduledBasalPoint(start: at(0), end: at(120), rate: 1.0), ScheduledBasalPoint(start: at(120), end: at(240), rate: 0.5)] + XCTAssertEqual(try XCTUnwrap(LoopHistoryMath.scheduledUnits(points, over: DateInterval(start: at(60), end: at(180)))), 1.5, accuracy: 1e-9) + XCTAssertNil(LoopHistoryMath.scheduledUnits(points, over: DateInterval(start: at(60), end: at(360)))) + } + func testPercentileInterpolates() { XCTAssertEqual(LoopHistoryMath.percentile([1, 2, 3, 4, 5], 0.5), 3) XCTAssertEqual(LoopHistoryMath.percentile([1, 2, 3, 4], 0.5), 2.5) From c99b616d22dd205a880ea6e0ad711782a4eaf004 Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:36 +0000 Subject: [PATCH 03/15] Replace Statistics with Trends Removes the Statistics screen and its Settings entry, and puts Trends where it was. GlucoseBand's display extension, SetupComparisonCharts, GlucoseStatistics and InsulinDeliveryHistory stay, because Current Setup uses them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- .../Screenshots/Phone Daily Insulin.png | Bin 215695 -> 0 bytes .../Screenshots/Phone Statistics.png | Bin 143342 -> 0 bytes Loop.xcodeproj/project.pbxproj | 24 +- Loop/Localizable.xcstrings | 60 ----- Loop/Redesign/LoopHistoryProvider.swift | 2 +- Loop/Redesign/TrendsScreen.swift | 2 +- Loop/View Models/StatisticsViewModel.swift | 136 ---------- Loop/Views/SettingsView.swift | 17 -- Loop/Views/Statistics/AGPChartView.swift | 157 ------------ .../Statistics/DailyInsulinChartView.swift | 83 ------ .../Statistics/GlucoseBand+Display.swift | 53 ++++ .../Statistics/SetupComparisonCharts.swift | 19 +- Loop/Views/Statistics/StatisticsView.swift | 239 ------------------ Loop/Views/Statistics/TimeInRangeBar.swift | 163 ------------ README.md | 4 +- 15 files changed, 77 insertions(+), 882 deletions(-) delete mode 100644 Documentation/Screenshots/Phone Daily Insulin.png delete mode 100644 Documentation/Screenshots/Phone Statistics.png delete mode 100644 Loop/View Models/StatisticsViewModel.swift delete mode 100644 Loop/Views/Statistics/AGPChartView.swift delete mode 100644 Loop/Views/Statistics/DailyInsulinChartView.swift create mode 100644 Loop/Views/Statistics/GlucoseBand+Display.swift delete mode 100644 Loop/Views/Statistics/StatisticsView.swift delete mode 100644 Loop/Views/Statistics/TimeInRangeBar.swift diff --git a/Documentation/Screenshots/Phone Daily Insulin.png b/Documentation/Screenshots/Phone Daily Insulin.png deleted file mode 100644 index c29ab07df2d00ffde5096d71cf7df9aa99f7ef50..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 215695 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
uHcb)^_4Z4{r26 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
j6f|h5gu(AM8~-y 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
uK$AZG{Jj<4FLVAgm4J@6OlU4 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 diff --git a/Documentation/Screenshots/Phone Statistics.png b/Documentation/Screenshots/Phone Statistics.png deleted file mode 100644 index c04d0769743c49c0fce7ae21d0b31c712685506f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143342 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 Focus." : { "comment" : "Focus modes step 1" }, - "Goal: <%@" : { - "comment" : "Average glucose goal" - }, - "Goal: <1%" : { - "comment" : "TIR goal for very low" - }, - "Goal: <5%" : { - "comment" : "TIR goal for very high" - }, - "Goal: <7%" : { - "comment" : "GMI goal" - }, - "Goal: >70%" : { - "comment" : "CGM active goal\nTIR goal for target range" - }, - "Goal: ≤36%" : { - "comment" : "Glucose variability goal" - }, "grams" : { }, @@ -42074,9 +42038,6 @@ } } }, - "In Range" : { - "comment" : "Time-in-range band label (70–180 mg/dL)" - }, "Includes basal and automated boluses" : { }, @@ -46684,9 +46645,6 @@ }, "Meal Timing" : { - }, - "Median (line) with 25–75% and 5–95% bands, by time of day." : { - "comment" : "AGP chart explanation" }, "Medium Intensity (Aerobic)" : { @@ -48098,9 +48056,6 @@ }, "No Delivery" : { - }, - "No glucose data for this period." : { - "comment" : "Statistics empty state" }, "No Maximum Bolus Configured" : { "comment" : "Alert title for a missing maximum bolus setting error", @@ -48797,9 +48752,6 @@ }, "None in last 24 hours" : { - }, - "Not enough data to plot a profile." : { - "comment" : "AGP empty state" }, "Not Set" : { "comment" : "Health sharing status: not determined" @@ -54519,9 +54471,6 @@ }, "She can do this by raising her **Overall Insulin** setting. This tells %@ to deliver more than her usual amount, making her insulin settings stronger." : { - }, - "Showing %1$d days of data — not enough for the full %2$d-day period yet." : { - "comment" : "Notice when the selected statistics range has incomplete data" }, "Shows last loop error" : { "comment" : "Loop Completion HUD accessibility hint", @@ -55570,9 +55519,6 @@ }, "Starting your exercise with high active insulin" : { - }, - "Statistics" : { - "comment" : "Statistics screen title\nStatistics settings button text" }, "Stay hydrated" : { @@ -57784,9 +57730,6 @@ }, "These recommendations should be used as a starting point. Checking your glucose during exercise will help you find the settings that work best for you." : { - }, - "This device keeps %d days of glucose. Increase the cache duration and rebuild to review the full period." : { - "comment" : "Notice when the local cache limits the statistics history" }, "This dose still has active insulin. Deleting it may cause Loop to make up for the reduced active insulin by dosing more." : { "comment" : "Warning when deleting a Loop-recorded dose that still has active insulin" @@ -60451,9 +60394,6 @@ } } }, - "Variability" : { - "comment" : "Coefficient of variation metric title" - }, "Version" : { "comment" : "Label for the app version row in the Support section" }, diff --git a/Loop/Redesign/LoopHistoryProvider.swift b/Loop/Redesign/LoopHistoryProvider.swift index 0c5236808a..84cc3da600 100644 --- a/Loop/Redesign/LoopHistoryProvider.swift +++ b/Loop/Redesign/LoopHistoryProvider.swift @@ -65,7 +65,7 @@ final class LoopHistoryProvider { loopDataManager.settingsProvider.settings.basalRateSchedule?.total() } - /// The median insulin delivered from midnight to this time of day over the last 14 days, as on Statistics. + /// The median insulin delivered from midnight to this time of day over the last 14 days. func typicalInsulinByNow() async -> (typical: TypicalInsulinDelivery, at: Date)? { guard let history = try? await loopDataManager.insulinDeliveryHistory(days: 14), let typical = history.typicalDelivery(by: history.end) else { return nil } diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift index 9f8c5960a6..05e56b9881 100644 --- a/Loop/Redesign/TrendsScreen.swift +++ b/Loop/Redesign/TrendsScreen.swift @@ -17,7 +17,7 @@ final class TrendsModel: ObservableObject { struct Summary { var interval: DateInterval var unit: GlucoseUnitStyle - /// CGM readings only: typed-in fingersticks are left out of every figure, as on Statistics. + /// CGM readings only: typed-in fingersticks are left out of every figure. var glucose: [GlucosePoint] = [] /// Days from the first reading, when that is short of the period; nil when the readings span it. var cgmCoveredDays: Int? diff --git a/Loop/View Models/StatisticsViewModel.swift b/Loop/View Models/StatisticsViewModel.swift deleted file mode 100644 index d2078f2017..0000000000 --- a/Loop/View Models/StatisticsViewModel.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// StatisticsViewModel.swift -// Loop -// -// Copyright © 2026 LoopKit Authors. All rights reserved. -// - -import Foundation -import LoopKit -import LoopCore -import LoopAlgorithm - -@MainActor -final class StatisticsViewModel: ObservableObject { - - /// Selectable look-back windows. Bounded by the on-device glucose cache. - enum DateRange: Int, CaseIterable, Identifiable { - case week = 7 - case twoWeeks = 14 - case month = 30 - case quarter = 90 - - var id: Int { rawValue } - var days: Int { rawValue } - } - - @Published var selectedRange: DateRange = .twoWeeks { - didSet { - guard selectedRange != oldValue else { return } - recompute() - } - } - - @Published private(set) var statistics: GlucoseStatistics? - @Published private(set) var isLoading = false - /// Days of glucose actually available (bounded by data accumulated and the cache). - @Published private(set) var availableDays: Double = 0 - - /// Complete days of insulin delivery within the selected range, oldest first. - @Published private(set) var insulinDays: [DailyInsulinDelivery] = [] - @Published private(set) var insulinToday: DailyInsulinDelivery? - @Published private(set) var typicalInsulinByNow: TypicalInsulinDelivery? - - private var allSamples: [StoredGlucoseSample] = [] - private var allInsulinDays: [DailyInsulinDelivery] = [] - private let glucoseStore: GlucoseStoreProtocol - private let insulinDeliveryHistory: ((Int) async throws -> InsulinDeliveryHistory)? - private let calendar: Calendar - private let now: () -> Date - - /// Configured local cache duration (how far back glucose is retained on device). - let cacheDuration: TimeInterval - let scheduledBasalTotal: Double? - - init(glucoseStore: GlucoseStoreProtocol, - insulinDeliveryHistory: ((Int) async throws -> InsulinDeliveryHistory)? = nil, - scheduledBasalTotal: Double? = nil, - cacheDuration: TimeInterval = Bundle.main.localCacheDuration, - calendar: Calendar = .current, - now: @escaping () -> Date = { Date() }) { - self.glucoseStore = glucoseStore - self.insulinDeliveryHistory = insulinDeliveryHistory - self.scheduledBasalTotal = scheduledBasalTotal - self.cacheDuration = cacheDuration - self.calendar = calendar - self.now = now - } - - var showsInsulin: Bool { insulinDeliveryHistory != nil } - - /// Mean of the complete days in the selected range. - var averageInsulinDay: InsulinDeliveryTotals? { - guard !insulinDays.isEmpty else { return nil } - let count = Double(insulinDays.count) - let sum = insulinDays.reduce(into: InsulinDeliveryTotals()) { sum, day in - sum.basal += day.totals.basal - sum.automaticBolus += day.totals.automaticBolus - sum.manualBolus += day.totals.manualBolus - } - return InsulinDeliveryTotals(basal: sum.basal / count, automaticBolus: sum.automaticBolus / count, manualBolus: sum.manualBolus / count) - } - - var cacheDurationDays: Int { Int((cacheDuration / 86400).rounded()) } - - /// Info notices for the selected range: shown when the window is incomplete, - /// with an extra line when the local cache is what's limiting the history. - /// Empty when there's enough data to fill the selected window. - var dataNotices: [String] { - guard availableDays + 1 < Double(selectedRange.days) else { return [] } - let have = max(0, Int(availableDays.rounded())) - var notices = [String(format: NSLocalizedString( - "Showing %1$d days of data — not enough for the full %2$d-day period yet.", - comment: "Notice when the selected statistics range has incomplete data"), have, selectedRange.days)] - if cacheDurationDays < selectedRange.days { - notices.append(String(format: NSLocalizedString( - "This device keeps %d days of glucose. Increase the cache duration and rebuild to review the full period.", - comment: "Notice when the local cache limits the statistics history"), cacheDurationDays)) - } - return notices - } - - func load() async { - isLoading = true - defer { isLoading = false } - - let end = now() - let start = end.addingTimeInterval(-cacheDuration) - do { - allSamples = try await glucoseStore.getGlucoseSamples(start: start, end: end) - .filter { !$0.isDisplayOnly && !$0.wasUserEntered } - .sorted { $0.startDate < $1.startDate } - } catch { - allSamples = [] - } - availableDays = allSamples.first.map { end.timeIntervalSince($0.startDate) / 86400 } ?? 0 - - if let insulinDeliveryHistory, let history = try? await insulinDeliveryHistory(cacheDurationDays) { - let days = history.dailyTotals(calendar: calendar) - allInsulinDays = days.filter(\.isComplete) - insulinToday = days.last(where: { !$0.isComplete }) - typicalInsulinByNow = history.typicalDelivery(by: history.end, calendar: calendar) - } - recompute() - } - - /// Recompute statistics for the selected window from the already-loaded samples - /// (no store round-trip), so switching ranges is instant. - private func recompute() { - let end = now() - let start = calendar.date(byAdding: .day, value: -selectedRange.days, to: end) - ?? end.addingTimeInterval(-Double(selectedRange.days) * 24 * 60 * 60) - statistics = GlucoseStatistics(samples: allSamples, start: start, end: end, calendar: calendar) - let firstDay = calendar.date(byAdding: .day, value: -selectedRange.days, to: calendar.startOfDay(for: end)) ?? start - insulinDays = allInsulinDays.filter { $0.start >= firstDay } - } -} diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index babad179cc..656a2099b2 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -347,23 +347,6 @@ extension SettingsView { private var statisticsSection: some View { if let glucoseStore = viewModel.deviceManager?.glucoseStore { Section { - NavigationLink(destination: StatisticsView( - glucoseStore: glucoseStore, - insulinDeliveryHistory: viewModel.insulinDeliveryHistory, - scheduledBasalTotal: viewModel.therapySettings().basalRateSchedule?.total() - )) { - LargeButton( - action: {}, - includeArrow: false, - imageView: Image(systemName: "chart.xyaxis.line") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 30), - label: NSLocalizedString("Statistics", comment: "Statistics settings button text"), - descriptiveText: NSLocalizedString("Glucose overview, ambulatory glucose profile and daily insulin", comment: "Statistics settings descriptive text") - ) - .accessibilityIdentifier("settingsViewStatistics") - } if viewModel.historyProvider != nil { LargeButton( action: { sheet = .trends }, diff --git a/Loop/Views/Statistics/AGPChartView.swift b/Loop/Views/Statistics/AGPChartView.swift deleted file mode 100644 index 55bac2b930..0000000000 --- a/Loop/Views/Statistics/AGPChartView.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// AGPChartView.swift -// Loop -// -// Copyright © 2026 LoopKit Authors. All rights reserved. -// - -import SwiftUI -import LoopKit -import LoopUI - -/// The ambulatory glucose profile, styled after the IDC / captūrAGP report. -/// -/// The percentile bands (5–95% and 25–75%) are each built once as a single -/// continuous shape, then filled with the glucose-zone colors (green target, -/// gold high, orange very-high, red lows) by clipping each color to that zone's -/// horizontal stripe. Because every zone clips the *same* band path, the colored -/// segments always line up at the zone boundaries. Drawn with a `Canvas` for -/// full control over the clipping. Glucose is plotted in the user's display unit. -struct AGPChartView: View { - let profile: [AGPBand] - let unit: LoopUnit - var targetLowMgDL: Double = 70 - var targetHighMgDL: Double = 180 - - private struct Zone { let lo: Double, hi: Double, band: GlucoseBand } - - private var topMgDL: Double { max(350, (profile.map { $0.p95 }.max() ?? 350).rounded(.up)) } - private var zones: [Zone] { - [Zone(lo: 0, hi: 54, band: .veryLow), - Zone(lo: 54, hi: 70, band: .low), - Zone(lo: 70, hi: 180, band: .target), - Zone(lo: 180, hi: 250, band: .high), - Zone(lo: 250, hi: topMgDL, band: .veryHigh)] - } - - private func display(_ mgdl: Double) -> Double { - LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl).doubleValue(for: unit) - } - private func yLabel(_ mgdl: Double) -> String { - let v = display(mgdl) - return unit == .milligramsPerDeciliter ? "\(Int(v.rounded()))" : String(format: "%.1f", v) - } - - private let leftInset: CGFloat = 34 - private let bottomInset: CGFloat = 18 - private let topInset: CGFloat = 6 - private let rightInset: CGFloat = 6 - private let medianColor = Color(red: 0.16, green: 0.40, blue: 0.20) - - /// Append a smooth (Catmull-Rom) curve through `points` to a path already - /// positioned at `points[0]`. - static func addSmoothCurve(through points: [CGPoint], to path: inout Path) { - guard points.count > 1 else { return } - for i in 0..<(points.count - 1) { - let p0 = points[max(0, i - 1)] - let p1 = points[i] - let p2 = points[i + 1] - let p3 = points[min(points.count - 1, i + 2)] - let c1 = CGPoint(x: p1.x + (p2.x - p0.x) / 6, y: p1.y + (p2.y - p0.y) / 6) - let c2 = CGPoint(x: p2.x - (p3.x - p1.x) / 6, y: p2.y - (p3.y - p1.y) / 6) - path.addCurve(to: p2, control1: c1, control2: c2) - } - } - - var body: some View { - Canvas { ctx, size in - let plot = CGRect(x: leftInset, y: topInset, - width: size.width - leftInset - rightInset, - height: size.height - topInset - bottomInset) - guard plot.width > 0, plot.height > 0 else { return } - let pts = profile.sorted { $0.timeOfDay < $1.timeOfDay } - guard !pts.isEmpty else { return } - let top = topMgDL - - func x(_ hour: Double) -> CGFloat { plot.minX + CGFloat(hour / 24) * plot.width } - func y(_ mgdl: Double) -> CGFloat { plot.minY + CGFloat(1 - mgdl / top) * plot.height } - - func bandPath(_ lo: (AGPBand) -> Double, _ hi: (AGPBand) -> Double) -> Path { - let topPts = pts.map { CGPoint(x: x($0.timeOfDay / 3600), y: y(hi($0))) } - let botPts = pts.reversed().map { CGPoint(x: x($0.timeOfDay / 3600), y: y(lo($0))) } - var p = Path() - p.move(to: topPts[0]) - Self.addSmoothCurve(through: topPts, to: &p) - p.addLine(to: botPts[0]) - Self.addSmoothCurve(through: botPts, to: &p) - p.closeSubpath() - return p - } - let outer = bandPath({ $0.p05 }, { $0.p95 }) - let inner = bandPath({ $0.p25 }, { $0.p75 }) - - // Y gridlines. - for v in [54.0, 70, 180, 250, top] { - var grid = Path() - grid.move(to: CGPoint(x: plot.minX, y: y(v))) - grid.addLine(to: CGPoint(x: plot.maxX, y: y(v))) - ctx.stroke(grid, with: .color(.gray.opacity(0.25)), lineWidth: 0.5) - } - - // Bands: fill each glucose zone's color, clipped to that zone's stripe. - for z in zones { - let stripe = CGRect(x: plot.minX, y: y(z.hi), width: plot.width, height: y(z.lo) - y(z.hi)) - ctx.drawLayer { layer in - layer.clip(to: Path(stripe)) - layer.fill(outer, with: .color(z.band.agpColor.opacity(0.35))) - layer.fill(inner, with: .color(z.band.agpColor.opacity(0.75))) - } - } - - // Target-range boundaries. - for t in [targetLowMgDL, targetHighMgDL] { - var line = Path() - line.move(to: CGPoint(x: plot.minX, y: y(t))) - line.addLine(to: CGPoint(x: plot.maxX, y: y(t))) - ctx.stroke(line, with: .color(GlucoseBand.target.agpColor), lineWidth: 1.5) - } - - // Median. - let medPts = pts.map { CGPoint(x: x($0.timeOfDay / 3600), y: y($0.p50)) } - var med = Path() - med.move(to: medPts[0]) - Self.addSmoothCurve(through: medPts, to: &med) - ctx.stroke(med, with: .color(medianColor), lineWidth: 2.5) - - // Y-axis labels. - for v in [54.0, 70, 180, 250, top] { - ctx.draw(Text(yLabel(v)).font(.caption2).foregroundColor(.secondary), - at: CGPoint(x: plot.minX - 4, y: y(v)), anchor: .trailing) - } - // X-axis labels. - for (hour, label) in [(0, "12a"), (6, "6a"), (12, "12p"), (18, "6p"), (24, "12a")] { - ctx.draw(Text(label).font(.caption2).foregroundColor(.secondary), - at: CGPoint(x: x(Double(hour)), y: plot.maxY + 9), anchor: .center) - } - } - .accessibilityLabel(Text(NSLocalizedString("Daily glucose pattern", comment: "Accessibility label for the daily glucose pattern (AGP) chart"))) - } -} - -#if DEBUG -#Preview { - let bands: [AGPBand] = (0..<48).map { i in - let hour = Double(i) * 0.5 - let base = 150.0 + 55 * sin((hour - 4) / 24 * 2 * .pi) - let spread = 25 + 18 * (1 + sin((hour - 8) / 24 * 2 * .pi)) - return AGPBand( - timeOfDay: (Double(i) + 0.5) * 1800, - p05: max(45, base - spread * 1.8), p25: base - spread * 0.7, - p50: base, p75: base + spread * 0.8, p95: base + spread * 1.9 - ) - } - return AGPChartView(profile: bands, unit: .milligramsPerDeciliter) - .frame(height: 260) - .padding() -} -#endif diff --git a/Loop/Views/Statistics/DailyInsulinChartView.swift b/Loop/Views/Statistics/DailyInsulinChartView.swift deleted file mode 100644 index e372747da3..0000000000 --- a/Loop/Views/Statistics/DailyInsulinChartView.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// DailyInsulinChartView.swift -// Loop -// -// Copyright © 2026 LoopKit Authors. All rights reserved. -// - -import SwiftUI -import Charts -import LoopKitUI - -/// Insulin delivered per calendar day, stacked by basal, automatic bolus and manual bolus, -/// with the average as a dashed line. -struct DailyInsulinChartView: View { - let days: [DailyInsulinDelivery] - let average: Double? - - @Environment(\.insulinTintColor) private var insulinTintColor - - private enum Part: CaseIterable { - case basal, automaticBolus, manualBolus - - var name: String { - switch self { - case .basal: - return NSLocalizedString("Basal", comment: "Daily insulin chart legend for basal delivery") - case .automaticBolus: - return NSLocalizedString("Automatic Bolus", comment: "Daily insulin chart legend for automatic boluses") - case .manualBolus: - return NSLocalizedString("Manual Bolus", comment: "Daily insulin chart legend for manual boluses") - } - } - - func units(_ totals: InsulinDeliveryTotals) -> Double { - switch self { - case .basal: return totals.basal - case .automaticBolus: return totals.automaticBolus - case .manualBolus: return totals.manualBolus - } - } - } - - private var labelStride: Int { - switch days.count { - case ...8: return 1 - case ...15: return 2 - case ...31: return 7 - default: return 14 - } - } - - var body: some View { - Chart { - ForEach(days) { day in - ForEach(Part.allCases, id: \.self) { part in - BarMark( - x: .value("Day", day.start, unit: .day), - y: .value("Units", part.units(day.totals)) - ) - .foregroundStyle(by: .value("Delivery", part.name)) - } - } - if let average { - RuleMark(y: .value("Average", average)) - .lineStyle(StrokeStyle(lineWidth: 1, dash: [4, 3])) - .foregroundStyle(Color.primary.opacity(0.6)) - } - } - .chartForegroundStyleScale([ - Part.basal.name: insulinTintColor.opacity(0.35), - Part.automaticBolus.name: insulinTintColor.opacity(0.65), - Part.manualBolus.name: insulinTintColor, - ]) - .chartXAxis { - AxisMarks(values: .stride(by: .day, count: labelStride)) { - AxisGridLine() - AxisValueLabel(format: .dateTime.month(.defaultDigits).day(), centered: true) - } - } - .chartLegend(position: .bottom, alignment: .leading) - .accessibilityLabel(Text(NSLocalizedString("Insulin delivered per day", comment: "Accessibility label for the daily insulin chart"))) - } -} diff --git a/Loop/Views/Statistics/GlucoseBand+Display.swift b/Loop/Views/Statistics/GlucoseBand+Display.swift new file mode 100644 index 0000000000..5c4beec4ba --- /dev/null +++ b/Loop/Views/Statistics/GlucoseBand+Display.swift @@ -0,0 +1,53 @@ +// +// GlucoseBand+Display.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import LoopKitUI +import LoopUI + +/// Writes one band edge in the unit the user reads glucose in. +/// +/// The consensus thresholds are defined in mg/dL, but the AGP report publishes +/// its own rounded mmol/L edges rather than conversions of them: converting +/// 181 mg/dL yields 10.046, which prints as "10.0" and collides with the top of +/// the target band. So each edge carries both spellings, and only the number +/// formatting — decimal separator included — is left to the preference. +private func bandEdge(mgdl: Double, mmol: Double, _ preference: DisplayGlucosePreference) -> String { + let quantity = preference.unit == .millimolesPerLiter + ? LoopQuantity(unit: .millimolesPerLiter, doubleValue: mmol) + : LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl) + return preference.format(quantity, includeUnit: false) +} + +/// captūrAGP / IDC report palette for the glucose bands, matched to the standard +/// Ambulatory Glucose Profile report colors. +extension GlucoseBand { + var agpColor: Color { + switch self { + case .veryLow: return Color(red: 0.60, green: 0.13, blue: 0.15) // dark red + case .low: return Color(red: 0.85, green: 0.26, blue: 0.24) // red + case .target: return Color(red: 0.36, green: 0.65, blue: 0.37) // green + case .high: return Color(red: 0.91, green: 0.77, blue: 0.26) // gold / yellow + case .veryHigh: return Color(red: 0.85, green: 0.55, blue: 0.24) // orange + } + } + + /// The band's range, written in the unit the user reads glucose in. + func rangeDescription(_ preference: DisplayGlucosePreference) -> String { + func edge(_ mgdl: Double, _ mmol: Double) -> String { + bandEdge(mgdl: mgdl, mmol: mmol, preference) + } + switch self { + case .veryLow: return "<\(edge(54, 3.0))" + case .low: return "\(edge(54, 3.0))–\(edge(69, 3.8))" + case .target: return "\(edge(70, 3.9))–\(edge(180, 10.0))" + case .high: return "\(edge(181, 10.1))–\(edge(250, 13.9))" + case .veryHigh: return ">\(edge(250, 13.9))" + } + } +} diff --git a/Loop/Views/Statistics/SetupComparisonCharts.swift b/Loop/Views/Statistics/SetupComparisonCharts.swift index d6e104f0f6..0341036f4d 100644 --- a/Loop/Views/Statistics/SetupComparisonCharts.swift +++ b/Loop/Views/Statistics/SetupComparisonCharts.swift @@ -82,7 +82,7 @@ struct MedianComparisonChartView: View { var path = Path() if let first = points.first { path.move(to: first) - AGPChartView.addSmoothCurve(through: points, to: &path) + addSmoothCurve(through: points, to: &path) } return path } @@ -99,7 +99,7 @@ struct MedianComparisonChartView: View { var band = curve(afterSorted, \.p75) let lower = afterSorted.reversed().map { CGPoint(x: x($0), y: y($0.p25)) } band.addLine(to: lower[0]) - AGPChartView.addSmoothCurve(through: lower, to: &band) + addSmoothCurve(through: lower, to: &band) band.closeSubpath() ctx.fill(band, with: .color(Color.accentColor.opacity(0.15))) } @@ -118,3 +118,18 @@ struct MedianComparisonChartView: View { .accessibilityLabel(Text(NSLocalizedString("Median glucose by time of day, before and since the change", comment: "Accessibility label for the setup comparison chart"))) } } + +/// Append a smooth (Catmull-Rom) curve through `points` to a path already +/// positioned at `points[0]`. +private func addSmoothCurve(through points: [CGPoint], to path: inout Path) { + guard points.count > 1 else { return } + for i in 0..<(points.count - 1) { + let p0 = points[max(0, i - 1)] + let p1 = points[i] + let p2 = points[i + 1] + let p3 = points[min(points.count - 1, i + 2)] + let c1 = CGPoint(x: p1.x + (p2.x - p0.x) / 6, y: p1.y + (p2.y - p0.y) / 6) + let c2 = CGPoint(x: p2.x - (p3.x - p1.x) / 6, y: p2.y - (p3.y - p1.y) / 6) + path.addCurve(to: p2, control1: c1, control2: c2) + } +} diff --git a/Loop/Views/Statistics/StatisticsView.swift b/Loop/Views/Statistics/StatisticsView.swift deleted file mode 100644 index 752ba229eb..0000000000 --- a/Loop/Views/Statistics/StatisticsView.swift +++ /dev/null @@ -1,239 +0,0 @@ -// -// StatisticsView.swift -// Loop -// -// Copyright © 2026 LoopKit Authors. All rights reserved. -// - -import SwiftUI -import LoopKit -import LoopCore -import LoopKitUI -import LoopUI - -/// "How am I doing?" overview — an Ambulatory Glucose Profile report: summary -/// metrics, a time-in-range breakdown, and the 24-hour percentile AGP chart, -/// over a selectable look-back window. -struct StatisticsView: View { - @StateObject private var viewModel: StatisticsViewModel - @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference - - init(glucoseStore: GlucoseStoreProtocol, insulinDeliveryHistory: ((Int) async throws -> InsulinDeliveryHistory)? = nil, scheduledBasalTotal: Double? = nil) { - _viewModel = StateObject(wrappedValue: StatisticsViewModel(glucoseStore: glucoseStore, insulinDeliveryHistory: insulinDeliveryHistory, scheduledBasalTotal: scheduledBasalTotal)) - } - - var body: some View { - List { - Section { - Picker(NSLocalizedString("Range", comment: "Statistics date-range picker label"), - selection: $viewModel.selectedRange) { - ForEach(StatisticsViewModel.DateRange.allCases) { range in - Text("\(range.days)d").tag(range) - } - } - .pickerStyle(.segmented) - } - - if !viewModel.dataNotices.isEmpty { - Section { - ForEach(viewModel.dataNotices, id: \.self) { line in - Label { - Text(line) - } icon: { - Image(systemName: "info.circle") - } - .font(.footnote) - .foregroundColor(.secondary) - } - } - } - - if let stats = viewModel.statistics, stats.sampleCount > 0 { - metricsSection(stats) - timeInRangeSection(stats) - agpSection(stats) - } else if viewModel.isLoading { - Section { - HStack { Spacer(); ProgressView(); Spacer() } - } - } else { - Section { - Text(NSLocalizedString("No glucose data for this period.", comment: "Statistics empty state")) - .foregroundColor(.secondary) - } - } - - if viewModel.showsInsulin { - insulinSection - } - } - .navigationTitle(Text(NSLocalizedString("Statistics", comment: "Statistics screen title"))) - .navigationBarTitleDisplayMode(.inline) - .task { await viewModel.load() } - } - - // MARK: - Sections - - private func metricsSection(_ stats: GlucoseStatistics) -> some View { - let averageGoal = displayGlucosePreference.format(LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: 154)) - return Section(header: Text(NSLocalizedString("Glucose Metrics", comment: "Glucose metrics section header"))) { - LazyVGrid(columns: [GridItem(.flexible(), spacing: 12), GridItem(.flexible(), spacing: 12)], spacing: 12) { - metric(NSLocalizedString("GMI", comment: "Glucose Management Indicator metric title"), - value: stats.gmi.map { String(format: "%.1f%%", $0) }, - goal: NSLocalizedString("Goal: <7%", comment: "GMI goal")) - metric(NSLocalizedString("Average", comment: "Average glucose metric title"), - value: stats.averageGlucose.map { - displayGlucosePreference.format(LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: $0)) - }, - goal: String(format: NSLocalizedString("Goal: <%@", comment: "Average glucose goal"), averageGoal)) - metric(NSLocalizedString("Variability", comment: "Coefficient of variation metric title"), - value: stats.coefficientOfVariation.map { String(format: "%.0f%%", $0) }, - goal: NSLocalizedString("Goal: ≤36%", comment: "Glucose variability goal")) - metric(NSLocalizedString("CGM Active", comment: "Percent of time CGM data present metric title"), - value: String(format: "%.0f%%", stats.percentActive * 100), - goal: NSLocalizedString("Goal: >70%", comment: "CGM active goal")) - } - .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) - .listRowBackground(Color.clear) - } - } - - private func metric(_ title: String, value: String?, goal: String) -> some View { - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.subheadline.weight(.semibold)) - Text(value ?? "–") - .font(.system(.title, design: .rounded).weight(.bold)) - .monospacedDigit() - .minimumScaleFactor(0.6) - .lineLimit(1) - Text(goal) - .font(.caption2) - .foregroundColor(.secondary) - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(12) - .background( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(Color(.secondarySystemGroupedBackground)) - ) - } - - private func timeInRangeSection(_ stats: GlucoseStatistics) -> some View { - Section(header: Text(NSLocalizedString("Time in Range", comment: "Time-in-range section header"))) { - TimeInRangeBar(timeInRange: stats.timeInRange) - .padding(.vertical, 8) - } - } - - private func units(_ value: Double) -> String { - String(format: NSLocalizedString("%@ U", comment: "Insulin amount in units (1: number)"), value.formatted(.number.precision(.fractionLength(1)))) - } - - private func percent(_ part: Double, of total: Double) -> String { - total > 0 ? (part / total).formatted(.percent.precision(.fractionLength(0))) : "–" - } - - private var todayCaption: String { - guard let typical = viewModel.typicalInsulinByNow, let today = viewModel.insulinToday else { - return NSLocalizedString("Since midnight", comment: "Caption for insulin delivered today when there is no usual amount to compare") - } - return String(format: NSLocalizedString("Usually %1$@ by %2$@", comment: "Caption comparing today's insulin with the usual amount by this time (1: units, 2: time of day)"), - units(typical.median), today.end.formatted(date: .omitted, time: .shortened)) - } - - private var insulinSection: some View { - let average = viewModel.averageInsulinDay - return Section(header: Text(NSLocalizedString("Daily Insulin", comment: "Daily insulin section header")), - footer: Text(NSLocalizedString("Each bar is one calendar day; the dashed line is the average. Today isn't included until the day is over. Usual amounts are the median of the last 14 days at the same time of day.", comment: "Daily insulin section explanation"))) { - LazyVGrid(columns: [GridItem(.flexible(), spacing: 12), GridItem(.flexible(), spacing: 12)], spacing: 12) { - metric(NSLocalizedString("Average", comment: "Average daily insulin metric title"), - value: average.map { units($0.total) }, - goal: String(format: NSLocalizedString("Per day, over %d days", comment: "Caption for average daily insulin (1: number of days)"), viewModel.insulinDays.count)) - metric(NSLocalizedString("Today", comment: "Insulin delivered today metric title"), - value: viewModel.insulinToday.map { units($0.totals.total) }, - goal: todayCaption) - metric(NSLocalizedString("Basal", comment: "Average daily basal metric title"), - value: average.map { units($0.basal) }, - goal: average.map { average in - if let scheduled = viewModel.scheduledBasalTotal { - return String(format: NSLocalizedString("%1$@ · scheduled %2$@", comment: "Caption for average daily basal (1: percent of total, 2: scheduled basal units)"), percent(average.basal, of: average.total), units(scheduled)) - } - return percent(average.basal, of: average.total) - } ?? "–") - metric(NSLocalizedString("Bolus", comment: "Average daily bolus metric title"), - value: average.map { units($0.bolus) }, - goal: average.map { average in - String(format: NSLocalizedString("%1$@ · %2$@ automatic", comment: "Caption for average daily bolus (1: percent of total, 2: automatic bolus units)"), percent(average.bolus, of: average.total), units(average.automaticBolus)) - } ?? "–") - } - .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) - .listRowBackground(Color.clear) - - if !viewModel.insulinDays.isEmpty { - DailyInsulinChartView(days: viewModel.insulinDays, average: average?.total) - .frame(height: 220) - .padding(.vertical, 8) - } - } - } - - @ViewBuilder - private func agpSection(_ stats: GlucoseStatistics) -> some View { - Section(header: Text(NSLocalizedString("Daily Glucose Pattern", comment: "Daily glucose pattern (AGP) section header")), - footer: Text(NSLocalizedString("Median (line) with 25–75% and 5–95% bands, by time of day.", comment: "AGP chart explanation"))) { - if stats.agpProfile.isEmpty { - Text(NSLocalizedString("Not enough data to plot a profile.", comment: "AGP empty state")) - .foregroundColor(.secondary) - } else { - AGPChartView(profile: stats.agpProfile, unit: displayGlucosePreference.unit) - .frame(height: 240) - .padding(.vertical, 8) - } - } - } -} - -#if DEBUG -/// Generates a daily glucose pattern with day-to-day spread so the AGP bands and -/// metrics populate in the canvas. Deterministic (seeded) so previews are stable. -private final class PreviewGlucoseStore: GlucoseStoreProtocol { - private let samples: [StoredGlucoseSample] - - init() { - var seed: UInt64 = 0x9E3779B97F4A7C15 - func unit01() -> Double { - seed = seed &* 6364136223846793005 &+ 1442695040888963407 - return Double(seed >> 11) / Double(UInt64(1) << 53) - } - let cal = Calendar.current - let now = Date() - let cadence: TimeInterval = 15 * 60 - var t = now.addingTimeInterval(-14 * 24 * 60 * 60) - var out: [StoredGlucoseSample] = [] - while t < now { - let hour = t.timeIntervalSince(cal.startOfDay(for: t)) / 3600 - let base = 140.0 + 40 * sin((hour - 4) / 24 * 2 * .pi) - let value = max(45, base + (unit01() - 0.5) * 70) - out.append(StoredGlucoseSample(startDate: t, quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: value))) - t = t.addingTimeInterval(cadence) - } - samples = out - } - - var latestGlucose: GlucoseSampleValue? { samples.last } - - func getGlucoseSamples(start: Date?, end: Date?) async throws -> [StoredGlucoseSample] { - samples.filter { (start == nil || $0.startDate >= start!) && (end == nil || $0.startDate < end!) } - } - - func addGlucoseSamples(_ samples: [NewGlucoseSample]) async throws -> [StoredGlucoseSample] { [] } -} - -#Preview { - NavigationView { - StatisticsView(glucoseStore: PreviewGlucoseStore()) - .environmentObject(DisplayGlucosePreference(displayGlucoseUnit: .milligramsPerDeciliter)) - } -} -#endif diff --git a/Loop/Views/Statistics/TimeInRangeBar.swift b/Loop/Views/Statistics/TimeInRangeBar.swift deleted file mode 100644 index 171c28b041..0000000000 --- a/Loop/Views/Statistics/TimeInRangeBar.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// TimeInRangeBar.swift -// Loop -// -// Copyright © 2026 LoopKit Authors. All rights reserved. -// - -import SwiftUI -import LoopKit -import LoopKitUI -import LoopUI - -/// Writes one band edge in the unit the user reads glucose in. -/// -/// The consensus thresholds are defined in mg/dL, but the AGP report publishes -/// its own rounded mmol/L edges rather than conversions of them: converting -/// 181 mg/dL yields 10.046, which prints as "10.0" and collides with the top of -/// the target band. So each edge carries both spellings, and only the number -/// formatting — decimal separator included — is left to the preference. -private func bandEdge(mgdl: Double, mmol: Double, _ preference: DisplayGlucosePreference) -> String { - let quantity = preference.unit == .millimolesPerLiter - ? LoopQuantity(unit: .millimolesPerLiter, doubleValue: mmol) - : LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: mgdl) - return preference.format(quantity, includeUnit: false) -} - -/// captūrAGP / IDC report palette for the glucose bands, matched to the standard -/// Ambulatory Glucose Profile report colors. -extension GlucoseBand { - var agpColor: Color { - switch self { - case .veryLow: return Color(red: 0.60, green: 0.13, blue: 0.15) // dark red - case .low: return Color(red: 0.85, green: 0.26, blue: 0.24) // red - case .target: return Color(red: 0.36, green: 0.65, blue: 0.37) // green - case .high: return Color(red: 0.91, green: 0.77, blue: 0.26) // gold / yellow - case .veryHigh: return Color(red: 0.85, green: 0.55, blue: 0.24) // orange - } - } - - var localizedTitle: String { - switch self { - case .veryLow: return NSLocalizedString("Very Low", comment: "Time-in-range band label (<54 mg/dL)") - case .low: return NSLocalizedString("Low", comment: "Time-in-range band label (54–69 mg/dL)") - case .target: return NSLocalizedString("In Range", comment: "Time-in-range band label (70–180 mg/dL)") - case .high: return NSLocalizedString("High", comment: "Time-in-range band label (181–250 mg/dL)") - case .veryHigh: return NSLocalizedString("Very High", comment: "Time-in-range band label (>250 mg/dL)") - } - } - - /// The band's range, written in the unit the user reads glucose in. - func rangeDescription(_ preference: DisplayGlucosePreference) -> String { - func edge(_ mgdl: Double, _ mmol: Double) -> String { - bandEdge(mgdl: mgdl, mmol: mmol, preference) - } - switch self { - case .veryLow: return "<\(edge(54, 3.0))" - case .low: return "\(edge(54, 3.0))–\(edge(69, 3.8))" - case .target: return "\(edge(70, 3.9))–\(edge(180, 10.0))" - case .high: return "\(edge(181, 10.1))–\(edge(250, 13.9))" - case .veryHigh: return ">\(edge(250, 13.9))" - } - } - - /// The consensus per-band goal, where the standard report lists one. - var goalText: String? { - switch self { - case .veryHigh: return NSLocalizedString("Goal: <5%", comment: "TIR goal for very high") - case .target: return NSLocalizedString("Goal: >70%", comment: "TIR goal for target range") - case .veryLow: return NSLocalizedString("Goal: <1%", comment: "TIR goal for very low") - default: return nil - } - } -} - -/// A vertical stacked time-in-range bar (Very High at top → Very Low at bottom) -/// with per-band percentages, consensus goals, and the standard clinical helper -/// notes, styled after the IDC / captūrAGP report. -struct TimeInRangeBar: View { - let timeInRange: [GlucoseBand: Double] - - @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference - - // Top-to-bottom display order. - private static let order: [GlucoseBand] = [.veryHigh, .high, .target, .low, .veryLow] - - private func fraction(_ band: GlucoseBand) -> Double { timeInRange[band] ?? 0 } - private func percent(_ band: GlucoseBand) -> Int { Int(fraction(band) * 100 + 0.5) } - private var aboveRange: Int { Int((fraction(.veryHigh) + fraction(.high)) * 100 + 0.5) } - private var belowRange: Int { Int((fraction(.low) + fraction(.veryLow)) * 100 + 0.5) } - - /// A target-range edge, written in the user's unit. - private func edge(_ mgdl: Double, _ mmol: Double) -> String { - bandEdge(mgdl: mgdl, mmol: mmol, displayGlucosePreference) - } - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - HStack(alignment: .top, spacing: 16) { - // Vertical stacked bar. - GeometryReader { geo in - VStack(spacing: 0) { - ForEach(Self.order, id: \.self) { band in - band.agpColor - .frame(height: max(0, geo.size.height * fraction(band))) - } - Spacer(minLength: 0) - } - .clipShape(RoundedRectangle(cornerRadius: 5)) - } - .frame(width: 26) - - // Per-band rows: name + range on the left, percentage with its - // goal directly beneath it on the right. - VStack(alignment: .leading, spacing: 10) { - ForEach(Self.order, id: \.self) { band in - HStack(alignment: .top, spacing: 8) { - RoundedRectangle(cornerRadius: 2) - .fill(band.agpColor) - .frame(width: 10, height: 10) - .padding(.top, 3) - VStack(alignment: .leading, spacing: 1) { - Text(band.localizedTitle).font(.subheadline.weight(.medium)) - Text(band.rangeDescription(displayGlucosePreference)).font(.caption2).foregroundColor(.secondary) - } - Spacer() - VStack(alignment: .trailing, spacing: 1) { - Text("\(percent(band))%").font(.subheadline.monospacedDigit()) - if let goal = band.goalText { - Text(goal).font(.caption2).foregroundColor(.secondary) - } - } - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } - .frame(height: 220) - - // Clinical helper / suggestion text, as on the standard report. - VStack(alignment: .leading, spacing: 4) { - Text(String(format: NSLocalizedString("Above range (>%1$@): %2$d%% · goal <25%%", - comment: "TIR above-range summary (1: upper target edge, 2: percent of time)"), - edge(180, 10.0), aboveRange)) - Text(String(format: NSLocalizedString("Below range (<%1$@): %2$d%% · goal <4%%", - comment: "TIR below-range summary (1: lower target edge, 2: percent of time)"), - edge(70, 3.9), belowRange)) - Text(NSLocalizedString("Each 5% increase in range is clinically beneficial.", comment: "TIR suggestion")) - Text(NSLocalizedString("Each 1% time in range ≈ 15 minutes.", comment: "TIR helper")) - } - .font(.caption) - .foregroundColor(.secondary) - .fixedSize(horizontal: false, vertical: true) - } - } -} - -#if DEBUG -#Preview { - TimeInRangeBar(timeInRange: [.veryLow: 0.04, .low: 0.04, .target: 0.36, .high: 0.25, .veryHigh: 0.31]) - .padding() - .environmentObject(DisplayGlucosePreference(displayGlucoseUnit: .milligramsPerDeciliter)) -} -#endif diff --git a/README.md b/README.md index 40506a4c42..099d1d64dc 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,6 @@ Please understand that this project: Presets screen Live Activity on the Lock Screen -Statistics with glucose metrics and time in range -Daily insulin totals on the Statistics screen Choosing a CGM Apple Watch app @@ -34,7 +32,7 @@ Please understand that this project: - **Automated insulin delivery.** Loop forecasts glucose several hours ahead and adjusts delivery with each new glucose reading, using either temporary basal rates or automatic boluses. - **Meal and correction boluses** with a recommended amount and a forecast of where glucose is heading. Carb entries can include food type, absorption time and saved favorite foods. - **Presets** that temporarily adjust insulin needs and correction range: pre-meal, your own custom presets, and built-in activity presets for walking, jogging, biking and strength training (which can be hidden). Presets can be scheduled, and in-app training explains how to use them. -- **Statistics**: GMI, average glucose, variability and time in range, an ambulatory glucose profile, and daily insulin totals split into basal, automatic bolus and manual bolus. +- **Trends**: time in range, GMI, average glucose and variability, a daily glucose pattern, insulin per day split into basal, manual bolus and automatic bolus, and time in closed loop, for one day or for 7 to 90 days. - **Live Activity** on the Lock Screen, in the Dynamic Island and on CarPlay. - **Apple Watch app** with glucose and forecast, carb entry, bolus and presets. - **Alerts** for glucose and device conditions, with urgent alerts that sound even when the phone is silenced or in a Focus. From c471a2c37d68694eba08fd311fb4ffa698cbcb2b Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:36 +0000 Subject: [PATCH 04/15] Move the day page, Gaps and Export to follow-up parts A one-day period still leads with its timeline but no longer opens a day page. The removed code, strings and tests come back in their own parts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- Loop.xcodeproj/project.pbxproj | 4 - Loop/Localizable.xcstrings | 5260 ++++------------- Loop/Redesign/DayCharts.swift | 168 +- Loop/Redesign/DayDetailView.swift | 198 - Loop/Redesign/LoopHistoryModels.swift | 9 - Loop/Redesign/LoopHistoryProvider.swift | 39 +- Loop/Redesign/TrendsComponents.swift | 57 +- Loop/Redesign/TrendsScreen.swift | 186 +- LoopTests/Managers/LoopDataManagerTests.swift | 26 - LoopTests/Models/LoopHistoryMathTests.swift | 27 - 10 files changed, 1114 insertions(+), 4860 deletions(-) delete mode 100644 Loop/Redesign/DayDetailView.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index adef0e74a0..24f9661b75 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -751,7 +751,6 @@ 0C70F4D862F6D3FDB3D91D87 /* LoopHistoryMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21622B2355AED8F6A12FE17C /* LoopHistoryMath.swift */; }; 2ADBB92A8ABB74360D18EEFA /* LoopHistoryModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471FFD525A4EB897745608BE /* LoopHistoryModels.swift */; }; A743DC26D066EE5AC88C2F45 /* LoopHistoryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09448CF97996C40BEDDE6588 /* LoopHistoryProvider.swift */; }; - 6A6C1E1C7A26EC88AFB4FAB1 /* DayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3A83D311A9ECAE563F74A51 /* DayDetailView.swift */; }; 591F13AF435B37B4818E2F2B /* TrendsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFDE56977CC73CBDA1170C5 /* TrendsComponents.swift */; }; 5CD0888FF33D6F844A81E638 /* TrendsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF0FD64B2B3E9C9AA8764684 /* TrendsScreen.swift */; }; 97CF13197936D2794766C84A /* LoopHistoryMathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B08644C3031D9E815F9892 /* LoopHistoryMathTests.swift */; }; @@ -1630,7 +1629,6 @@ 21622B2355AED8F6A12FE17C /* LoopHistoryMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopHistoryMath.swift; sourceTree = ""; }; 471FFD525A4EB897745608BE /* LoopHistoryModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopHistoryModels.swift; sourceTree = ""; }; 09448CF97996C40BEDDE6588 /* LoopHistoryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopHistoryProvider.swift; sourceTree = ""; }; - E3A83D311A9ECAE563F74A51 /* DayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayDetailView.swift; sourceTree = ""; }; 4BFDE56977CC73CBDA1170C5 /* TrendsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsComponents.swift; sourceTree = ""; }; AF0FD64B2B3E9C9AA8764684 /* TrendsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsScreen.swift; sourceTree = ""; }; 42B08644C3031D9E815F9892 /* LoopHistoryMathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopHistoryMathTests.swift; sourceTree = ""; }; @@ -3065,7 +3063,6 @@ children = ( AF0FD64B2B3E9C9AA8764684 /* TrendsScreen.swift */, 4BFDE56977CC73CBDA1170C5 /* TrendsComponents.swift */, - E3A83D311A9ECAE563F74A51 /* DayDetailView.swift */, 09448CF97996C40BEDDE6588 /* LoopHistoryProvider.swift */, 471FFD525A4EB897745608BE /* LoopHistoryModels.swift */, 21622B2355AED8F6A12FE17C /* LoopHistoryMath.swift */, @@ -3738,7 +3735,6 @@ files = ( 5CD0888FF33D6F844A81E638 /* TrendsScreen.swift in Sources */, 591F13AF435B37B4818E2F2B /* TrendsComponents.swift in Sources */, - 6A6C1E1C7A26EC88AFB4FAB1 /* DayDetailView.swift in Sources */, A743DC26D066EE5AC88C2F45 /* LoopHistoryProvider.swift in Sources */, 2ADBB92A8ABB74360D18EEFA /* LoopHistoryModels.swift in Sources */, 0C70F4D862F6D3FDB3D91D87 /* LoopHistoryMath.swift in Sources */, diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 395087fdbb..b20a06f299 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -3894,292 +3894,6 @@ } } }, - "Active insulin %1$@, active carbs %2$@" : { - "comment" : "VoiceOver summary of the active insulin and carbs chart. (1: insulin on board, e.g. 1.25 U, or unknown; 2: carbs on board, e.g. 20 g, or unknown)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الأنسولين النشط %1$@، الكارب النشط %2$@" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktivní inzulín %1$@, aktivní sacharidy %2$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktivt insulin %1$@, aktive kulhydrater %2$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktives Insulin %1$@, aktive KH %2$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina activa %1$@, carbohidratos activos %2$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akt. insuliini %1$@, akt. hiilari %2$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuline active %1$@, glucides actifs %2$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אינסולין פעיל %1$@, פחמימות פעילות %2$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktív inzulin %1$@, aktív szénhidrát %2$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina attiva %1$@, carboidrati attivi %2$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "残存インスリン %1$@、残存糖質 %2$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktivt insulin %1$@, aktive karbohydrater %2$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actieve insuline %1$@, actieve koolhydraten %2$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktywna insulina %1$@, aktywne węglowodany %2$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina ativa %1$@, carboidratos ativos %2$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulină activă %1$@, carbohidrați activi %2$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Активный инсулин %1$@, активные углеводы %2$@" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktívny inzulín %1$@, aktívne sacharidy %2$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktivt insulin %1$@, aktiva kolhydrater %2$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktif insülin %1$@, aktif karb %2$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Активний інсулін %1$@, активні вуглеводи %2$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin còn hoạt động %1$@, Carbs còn hoạt động %2$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "活性胰岛素 %1$@,活性碳水 %2$@" - } - } - } - }, - "Active insulin and carbs" : { - "comment" : "Title of the day page card charting insulin and carbs on board", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الأنسولين والكارب النشطان" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktivní inzulín a sacharidy" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktivt insulin og kulhydrater" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktives Insulin und KH" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina y carbohidratos activos" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktiivinen insuliini ja hiilihydraatit" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuline et glucides actifs" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אינסולין ופחמימות פעילים" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktív inzulin és szénhidrát" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina e carboidrati attivi" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "残存インスリンと残存糖質" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktivt insulin og karbohydrater" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Actieve insuline en koolhydraten" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktywna insulina i węglowodany" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina e carboidratos ativos" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulină activă și carbohidrați activi" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Активный инсулин и углеводы" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktívny inzulín a sacharidy" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktivt insulin och kolhydrater" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktif insülin ve karb" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Активний інсулін і вуглеводи" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin và Carbs còn hoạt động" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "活性胰岛素和碳水" - } - } - } - }, "Aim: average under %1$@ %2$@, GMI under 7%%, CV 36%% or less." : { "comment" : "Trends Summary footnote: the consensus targets for average glucose, GMI and glucose variability (CV). (1: glucose value, 2: glucose unit)", "localizations" : { @@ -7183,8588 +6897,6014 @@ } } }, - "COB (g, right)" : { - "comment" : "Chart legend: carbs on board in grams, on the right axis", + "Configuration error" : { + "comment" : "Reason Loop could not run: its settings are incomplete", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g، يمين)" + "value" : "خطأ في الإعدادات" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, vpravo)" + "value" : "Chyba konfigurace" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, højre)" + "value" : "Konfigurationsfejl" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, rechts)" + "value" : "Konfigurationsfehler" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, derecha)" + "value" : "Error de configuración" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, oikea)" + "value" : "Määritysvirhe" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "GA (g, droite)" + "value" : "Erreur de configuration" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, ימין)" + "value" : "שגיאת תצורה" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, jobb)" + "value" : "Konfigurációs hiba" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, destra)" + "value" : "Errore di configurazione" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "COB(g、右)" + "value" : "設定エラー" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, høyre)" + "value" : "Konfigurasjonsfeil" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, rechts)" + "value" : "Configuratiefout" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, prawa oś)" + "value" : "Błąd konfiguracji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, direita)" + "value" : "Erro de configuração" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, dreapta)" + "value" : "Eroare de configurare" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "COB (г, справа)" + "value" : "Ошибка настроек" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, vpravo)" + "value" : "Chyba konfigurácie" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, höger)" + "value" : "Konfigurationsfel" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "COB (gr, sağ)" + "value" : "Yapılandırma hatası" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "COB (г, праворуч)" + "value" : "Помилка налаштувань" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "COB (g, bên phải)" + "value" : "Lỗi cấu hình" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "COB(克,右轴)" + "value" : "配置错误" } } } }, - "Configuration error" : { - "comment" : "Reason Loop could not run: its settings are incomplete", + "Connection error" : { + "comment" : "Reason Loop could not run: no connected devices, or a device connection failed", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "خطأ في الإعدادات" + "value" : "خطأ في الاتصال" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba konfigurace" + "value" : "Chyba připojení" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurationsfejl" + "value" : "Forbindelsesfejl" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurationsfehler" + "value" : "Verbindungsfehler" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Error de configuración" + "value" : "Error de conexión" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Määritysvirhe" + "value" : "Yhteysvirhe" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Erreur de configuration" + "value" : "Erreur de connexion" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שגיאת תצורה" + "value" : "שגיאת חיבור" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurációs hiba" + "value" : "Kapcsolódási hiba" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Errore di configurazione" + "value" : "Errore di connessione" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "設定エラー" + "value" : "接続エラー" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurasjonsfeil" + "value" : "Tilkoblingsfeil" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Configuratiefout" + "value" : "Verbindingsfout" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Błąd konfiguracji" + "value" : "Błąd połączenia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Erro de configuração" + "value" : "Erro de conexão" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Eroare de configurare" + "value" : "Eroare de conexiune" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ошибка настроек" + "value" : "Ошибка подключения" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba konfigurácie" + "value" : "Chyba pripojenia" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurationsfel" + "value" : "Anslutningsfel" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yapılandırma hatası" + "value" : "Bağlantı hatası" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Помилка налаштувань" + "value" : "Помилка підключення" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Lỗi cấu hình" + "value" : "Lỗi kết nối" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "配置错误" + "value" : "连接错误" } } } }, - "Connection error" : { - "comment" : "Reason Loop could not run: no connected devices, or a device connection failed", + "Couldn't load Trends: %1$@" : { + "comment" : "Trends error message when the history can't be read. (1: error description)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "خطأ في الاتصال" + "value" : "تعذّر تحميل الاتجاهات: %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba připojení" + "value" : "Nelze načíst trendy: %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Forbindelsesfejl" + "value" : "Kunne ikke indlæse Tendenser: %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Verbindungsfehler" + "value" : "Trends konnten nicht geladen werden: %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Error de conexión" + "value" : "No se pudieron cargar las tendencias: %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Yhteysvirhe" + "value" : "Trendejä ei voitu ladata: %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Erreur de connexion" + "value" : "Impossible de charger les tendances : %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שגיאת חיבור" + "value" : "לא ניתן היה לטעון מגמות: %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kapcsolódási hiba" + "value" : "Nem sikerült betölteni a trendeket: %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Errore di connessione" + "value" : "Impossibile caricare le tendenze: %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "接続エラー" + "value" : "トレンドを読み込めませんでした:%1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Tilkoblingsfeil" + "value" : "Kunne ikke laste inn Trender: %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verbindingsfout" + "value" : "Trends kunnen niet worden geladen: %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Błąd połączenia" + "value" : "Nie udało się wczytać trendów: %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Erro de conexão" + "value" : "Não foi possível carregar as tendências: %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Eroare de conexiune" + "value" : "Nu s-au putut încărca tendințele: %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ошибка подключения" + "value" : "Не удалось загрузить тренды: %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba pripojenia" + "value" : "Trendy sa nedajú načítať: %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Anslutningsfel" + "value" : "Kunde inte läsa in Trender: %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bağlantı hatası" + "value" : "Eğilimler yüklenemedi: %1$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Помилка підключення" + "value" : "Не вдалося завантажити тренди: %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Lỗi kết nối" + "value" : "Không thể tải Xu hướng: %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "连接错误" + "value" : "无法载入趋势:%1$@" } } } }, - "Couldn't export" : { - "comment" : "Alert title when exporting the Trends PDF or CSV fails", + "Covers %1$@: Loop keeps 7 days of runs, and closed or open loop is recorded from when this version of Loop first ran." : { + "comment" : "Trends Loop status footnote when the status covers only part of the period. (1: date range covered)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تعذّر التصدير" + "value" : "يغطي %1$@: يحتفظ Loop بسجل تشغيلات 7 أيام، وتُسجَّل الحلقة المغلقة أو المفتوحة منذ أول تشغيل لهذا الإصدار من Loop." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nelze exportovat" + "value" : "Pokrývá %1$@: Loop uchovává běhy za 7 dní a uzavřená či otevřená smyčka se zaznamenává od prvního spuštění této verze Loop." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke eksportere" + "value" : "Dækker %1$@: Loop gemmer kørsler i 7 dage, og lukket eller åbent Loop registreres fra første gang denne version af Loop kørte." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Export fehlgeschlagen" + "value" : "Umfasst %1$@: Loop speichert Durchläufe 7 Tage lang, und Closed oder Open Loop wird erst ab dem ersten Start dieser Loop-Version aufgezeichnet." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "No se pudo exportar" + "value" : "Cubre %1$@: Loop guarda 7 días de ejecuciones, y el circuito cerrado o abierto se registra desde la primera vez que se ejecutó esta versión de Loop." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vienti epäonnistui" + "value" : "Kattaa %1$@: Loop säilyttää 7 päivän ajot, ja suljettu tai avoin säätö tallennetaan siitä lähtien, kun tämä Loopin versio käynnistettiin ensimmäisen kerran." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Exportation impossible" + "value" : "Couvre %1$@ : Loop conserve 7 jours d'exécutions, et la boucle fermée ou ouverte est enregistrée depuis le premier lancement de cette version de Loop." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "לא ניתן היה לייצא" + "value" : "מכסה %1$@: Loop שומר 7 ימים של הרצות, ו-Closed Loop או Open Loop נרשמים מאז ההפעלה הראשונה של גרסה זו של Loop." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nem sikerült exportálni" + "value" : "Lefedett időszak: %1$@. A Loop 7 napnyi futást őriz meg, a zárt vagy nyitott hurok pedig a Loop ezen verziójának első futtatásától kerül rögzítésre." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Impossibile esportare" + "value" : "Copre %1$@: Loop conserva 7 giorni di esecuzioni e il loop chiuso o aperto è registrato dal primo avvio di questa versione di Loop." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "書き出せませんでした" + "value" : "対象期間 %1$@:Loopは実行記録を7日間保持し、クローズド/オープンループはこのバージョンのLoopを初めて起動した時点から記録されます。" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke eksportere" + "value" : "Dekker %1$@: Loop lagrer kjøringer i 7 dager, og lukket eller åpen Loop registreres fra første gang denne versjonen av Loop kjørte." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Exporteren mislukt" + "value" : "Omvat %1$@: Loop bewaart 7 dagen aan loop-runs, en actieve of open loop wordt vastgelegd vanaf de eerste keer dat deze versie van Loop draaide." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nie udało się wyeksportować" + "value" : "Obejmuje %1$@: Loop przechowuje cykle z 7 dni, a pętla zamknięta lub otwarta jest zapisywana od pierwszego uruchomienia tej wersji Loop." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Não foi possível exportar" + "value" : "Cobre %1$@: o Loop guarda 7 dias de execuções, e o ciclo fechado ou aberto é registrado desde a primeira vez que esta versão do Loop foi executada." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nu s-a putut exporta" + "value" : "Acoperă %1$@: Loop păstrează 7 zile de rulări, iar bucla închisă sau deschisă este înregistrată de la prima rulare a acestei versiuni de Loop." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Не удалось экспортировать" + "value" : "Охвачено: %1$@. Loop хранит запуски за 7 дней, а замкнутый или открытый цикл записывается с первого запуска этой версии Loop." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Nedá sa exportovať" + "value" : "Pokrýva %1$@: Loop uchováva behy za 7 dní a uzavretá či otvorená slučka sa zaznamenáva od prvého spustenia tejto verzie Loop." } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Kunde inte exportera" + "value" : "Omfattar %1$@: Loop sparar 7 dagars körningar, och sluten eller öppen loop registreras från när den här versionen av Loop först kördes." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Dışa aktarılamadı" + "value" : "Kapsanan dönem %1$@: Loop 7 günlük çalışma kaydı tutar; kapalı veya açık döngü, Loop'un bu sürümünün ilk çalıştığı andan itibaren kaydedilir." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Не вдалося експортувати" + "value" : "Охоплено: %1$@. Loop зберігає запуски за 7 днів, а замкнутий чи відкритий цикл записується з першого запуску цієї версії Loop." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không thể xuất" + "value" : "Bao gồm %1$@: Loop lưu các lần chạy trong 7 ngày, và trạng thái vòng lặp kín hoặc mở được ghi lại từ khi phiên bản Loop này chạy lần đầu." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无法导出" + "value" : "覆盖 %1$@:Loop 保留 7 天的运行记录,闭环或开环状态从此版本 Loop 首次运行时开始记录。" } } } }, - "Couldn't load this day: %1$@" : { - "comment" : "Day page error when the day's history can't be read. (1: system error description)", + "Daily pattern" : { + "comment" : "Trends card title: the ambulatory glucose profile, glucose by time of day across the period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تعذّر تحميل هذا اليوم: %1$@" + "value" : "النمط اليومي" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nelze načíst tento den: %1$@" + "value" : "Denní profil" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke indlæse denne dag: %1$@" + "value" : "Døgnmønster" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Dieser Tag konnte nicht geladen werden: %1$@" + "value" : "Tagesprofil" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "No se pudo cargar este día: %1$@" + "value" : "Patrón diario" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Päivää ei voitu ladata: %1$@" + "value" : "Vuorokausiprofiili" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Impossible de charger cette journée : %1$@" + "value" : "Profil journalier" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "לא ניתן היה לטעון יום זה: %1$@" + "value" : "דפוס יומי" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nem sikerült betölteni ezt a napot: %1$@" + "value" : "Napi mintázat" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Impossibile caricare questo giorno: %1$@" + "value" : "Andamento giornaliero" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "この日を読み込めませんでした:%1$@" + "value" : "日内パターン" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke laste inn denne dagen: %1$@" + "value" : "Døgnmønster" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Deze dag kan niet worden geladen: %1$@" + "value" : "Dagpatroon" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nie udało się wczytać tego dnia: %1$@" + "value" : "Profil dobowy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Não foi possível carregar este dia: %1$@" + "value" : "Padrão diário" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nu s-a putut încărca această zi: %1$@" + "value" : "Tipar zilnic" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Не удалось загрузить этот день: %1$@" + "value" : "Суточный профиль" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Tento deň sa nedá načítať: %1$@" + "value" : "Denný profil" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Kunde inte läsa in dagen: %1$@" + "value" : "Dygnsmönster" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu gün yüklenemedi: %1$@" + "value" : "Günlük örüntü" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Не вдалося завантажити цей день: %1$@" + "value" : "Добовий профіль" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không thể tải ngày này: %1$@" + "value" : "Mẫu hằng ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无法载入这一天:%1$@" + "value" : "每日模式" } } } }, - "Couldn't load Trends: %1$@" : { - "comment" : "Trends error message when the history can't be read. (1: error description)", + "Daily pattern over %1$@. Median highest %2$@ at %3$@, lowest %4$@ at %5$@" : { + "comment" : "VoiceOver label for the Trends Daily pattern chart. (1: days with readings, e.g. 14 days; 2: highest median glucose; 3: time of day; 4: lowest median glucose; 5: time of day)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تعذّر تحميل الاتجاهات: %1$@" + "value" : "النمط اليومي خلال %1$@. أعلى وسيط %2$@ عند %3$@، وأدنى وسيط %4$@ عند %5$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nelze načíst trendy: %1$@" + "value" : "Denní profil za %1$@. Nejvyšší medián %2$@ v %3$@, nejnižší %4$@ v %5$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke indlæse Tendenser: %1$@" + "value" : "Døgnmønster over %1$@. Median højest %2$@ kl. %3$@, lavest %4$@ kl. %5$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Trends konnten nicht geladen werden: %1$@" + "value" : "Tagesprofil über %1$@. Median am höchsten %2$@ um %3$@, am niedrigsten %4$@ um %5$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "No se pudieron cargar las tendencias: %1$@" + "value" : "Patrón diario de %1$@. Mediana más alta %2$@ a las %3$@, más baja %4$@ a las %5$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Trendejä ei voitu ladata: %1$@" + "value" : "Vuorokausiprofiili %1$@ ajalta. Korkein mediaani %2$@ klo %3$@, matalin %4$@ klo %5$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Impossible de charger les tendances : %1$@" + "value" : "Profil journalier sur %1$@. Médiane la plus haute %2$@ à %3$@, la plus basse %4$@ à %5$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "לא ניתן היה לטעון מגמות: %1$@" + "value" : "דפוס יומי במשך %1$@. חציון גבוה ביותר %2$@ ב-%3$@, נמוך ביותר %4$@ ב-%5$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nem sikerült betölteni a trendeket: %1$@" + "value" : "Napi mintázat, %1$@. Legmagasabb medián %2$@ ekkor: %3$@, legalacsonyabb %4$@ ekkor: %5$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Impossibile caricare le tendenze: %1$@" + "value" : "Andamento giornaliero su %1$@. Mediana più alta %2$@ alle %3$@, più bassa %4$@ alle %5$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "トレンドを読み込めませんでした:%1$@" + "value" : "%1$@の日内パターン。中央値の最高は%3$@の%2$@、最低は%5$@の%4$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke laste inn Trender: %1$@" + "value" : "Døgnmønster over %1$@. Median høyest %2$@ kl. %3$@, lavest %4$@ kl. %5$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Trends kunnen niet worden geladen: %1$@" + "value" : "Dagpatroon over %1$@. Mediaan hoogst %2$@ om %3$@, laagst %4$@ om %5$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nie udało się wczytać trendów: %1$@" + "value" : "Profil dobowy z %1$@. Najwyższa mediana %2$@ o %3$@, najniższa %4$@ o %5$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Não foi possível carregar as tendências: %1$@" + "value" : "Padrão diário em %1$@. Mediana mais alta %2$@ às %3$@, mais baixa %4$@ às %5$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nu s-au putut încărca tendințele: %1$@" + "value" : "Tipar zilnic pe %1$@. Mediana cea mai mare %2$@ la %3$@, cea mai mică %4$@ la %5$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Не удалось загрузить тренды: %1$@" + "value" : "Суточный профиль за %1$@. Наибольшая медиана %2$@ в %3$@, наименьшая %4$@ в %5$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Trendy sa nedajú načítať: %1$@" + "value" : "Denný profil za %1$@. Najvyšší medián %2$@ o %3$@, najnižší %4$@ o %5$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Kunde inte läsa in Trender: %1$@" + "value" : "Dygnsmönster över %1$@. Högsta median %2$@ kl. %3$@, lägsta %4$@ kl. %5$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Eğilimler yüklenemedi: %1$@" + "value" : "%1$@ boyunca günlük örüntü. En yüksek medyan %3$@ saatinde %2$@, en düşük %5$@ saatinde %4$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Не вдалося завантажити тренди: %1$@" + "value" : "Добовий профіль за %1$@. Найвища медіана %2$@ о %3$@, найнижча %4$@ о %5$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không thể tải Xu hướng: %1$@" + "value" : "Mẫu hằng ngày trong %1$@. Trung vị cao nhất %2$@ lúc %3$@, thấp nhất %4$@ lúc %5$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无法载入趋势:%1$@" + "value" : "%1$@的每日模式。中位数最高 %2$@(%3$@),最低 %4$@(%5$@)" } } } }, - "Covers %1$@: Loop keeps 7 days of runs, and closed or open loop is recorded from when this version of Loop first ran." : { - "comment" : "Trends Loop status footnote when the status covers only part of the period. (1: date range covered)", + "Daily pattern: not enough days" : { + "comment" : "VoiceOver label for the Trends Daily pattern chart when fewer than 5 days have readings", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يغطي %1$@: يحتفظ Loop بسجل تشغيلات 7 أيام، وتُسجَّل الحلقة المغلقة أو المفتوحة منذ أول تشغيل لهذا الإصدار من Loop." + "value" : "النمط اليومي: لا توجد أيام كافية" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Pokrývá %1$@: Loop uchovává běhy za 7 dní a uzavřená či otevřená smyčka se zaznamenává od prvního spuštění této verze Loop." + "value" : "Denní profil: málo dní" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Dækker %1$@: Loop gemmer kørsler i 7 dage, og lukket eller åbent Loop registreres fra første gang denne version af Loop kørte." + "value" : "Døgnmønster: ikke nok dage" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Umfasst %1$@: Loop speichert Durchläufe 7 Tage lang, und Closed oder Open Loop wird erst ab dem ersten Start dieser Loop-Version aufgezeichnet." + "value" : "Tagesprofil: nicht genug Tage" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Cubre %1$@: Loop guarda 7 días de ejecuciones, y el circuito cerrado o abierto se registra desde la primera vez que se ejecutó esta versión de Loop." + "value" : "Patrón diario: no hay suficientes días" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kattaa %1$@: Loop säilyttää 7 päivän ajot, ja suljettu tai avoin säätö tallennetaan siitä lähtien, kun tämä Loopin versio käynnistettiin ensimmäisen kerran." + "value" : "Vuorokausiprofiili: liian vähän päiviä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Couvre %1$@ : Loop conserve 7 jours d'exécutions, et la boucle fermée ou ouverte est enregistrée depuis le premier lancement de cette version de Loop." + "value" : "Profil journalier : pas assez de jours" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מכסה %1$@: Loop שומר 7 ימים של הרצות, ו-Closed Loop או Open Loop נרשמים מאז ההפעלה הראשונה של גרסה זו של Loop." + "value" : "דפוס יומי: אין מספיק ימים" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Lefedett időszak: %1$@. A Loop 7 napnyi futást őriz meg, a zárt vagy nyitott hurok pedig a Loop ezen verziójának első futtatásától kerül rögzítésre." + "value" : "Napi mintázat: nincs elég nap" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Copre %1$@: Loop conserva 7 giorni di esecuzioni e il loop chiuso o aperto è registrato dal primo avvio di questa versione di Loop." + "value" : "Andamento giornaliero: giorni insufficienti" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "対象期間 %1$@:Loopは実行記録を7日間保持し、クローズド/オープンループはこのバージョンのLoopを初めて起動した時点から記録されます。" + "value" : "日内パターン:日数が不足しています" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Dekker %1$@: Loop lagrer kjøringer i 7 dager, og lukket eller åpen Loop registreres fra første gang denne versjonen av Loop kjørte." + "value" : "Døgnmønster: ikke nok dager" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Omvat %1$@: Loop bewaart 7 dagen aan loop-runs, en actieve of open loop wordt vastgelegd vanaf de eerste keer dat deze versie van Loop draaide." + "value" : "Dagpatroon: niet genoeg dagen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Obejmuje %1$@: Loop przechowuje cykle z 7 dni, a pętla zamknięta lub otwarta jest zapisywana od pierwszego uruchomienia tej wersji Loop." + "value" : "Profil dobowy: za mało dni" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Cobre %1$@: o Loop guarda 7 dias de execuções, e o ciclo fechado ou aberto é registrado desde a primeira vez que esta versão do Loop foi executada." + "value" : "Padrão diário: dias insuficientes" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Acoperă %1$@: Loop păstrează 7 zile de rulări, iar bucla închisă sau deschisă este înregistrată de la prima rulare a acestei versiuni de Loop." + "value" : "Tipar zilnic: prea puține zile" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Охвачено: %1$@. Loop хранит запуски за 7 дней, а замкнутый или открытый цикл записывается с первого запуска этой версии Loop." + "value" : "Суточный профиль: недостаточно дней" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Pokrýva %1$@: Loop uchováva behy za 7 dní a uzavretá či otvorená slučka sa zaznamenáva od prvého spustenia tejto verzie Loop." + "value" : "Denný profil: málo dní" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Omfattar %1$@: Loop sparar 7 dagars körningar, och sluten eller öppen loop registreras från när den här versionen av Loop först kördes." + "value" : "Dygnsmönster: för få dagar" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kapsanan dönem %1$@: Loop 7 günlük çalışma kaydı tutar; kapalı veya açık döngü, Loop'un bu sürümünün ilk çalıştığı andan itibaren kaydedilir." + "value" : "Günlük örüntü: yeterli gün yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Охоплено: %1$@. Loop зберігає запуски за 7 днів, а замкнутий чи відкритий цикл записується з першого запуску цієї версії Loop." + "value" : "Добовий профіль: замало днів" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Bao gồm %1$@: Loop lưu các lần chạy trong 7 ngày, và trạng thái vòng lặp kín hoặc mở được ghi lại từ khi phiên bản Loop này chạy lần đầu." + "value" : "Mẫu hằng ngày: không đủ số ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "覆盖 %1$@:Loop 保留 7 天的运行记录,闭环或开环状态从此版本 Loop 首次运行时开始记录。" + "value" : "每日模式:天数不足" } } } }, - "CSV, one row per day" : { - "comment" : "Trends export menu item: share a CSV spreadsheet file with one row for each day", - "localizations" : { + "Day" : { + "comment" : "Appearance option and app icon name: the light (daytime) version", + "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "CSV، صف واحد لكل يوم" + "value" : "نهار" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, jeden řádek na den" + "value" : "Den" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, én række pr. dag" + "value" : "Dag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, eine Zeile pro Tag" + "value" : "Tag" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, una fila por día" + "value" : "Día" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, yksi rivi päivää kohden" + "value" : "Päivä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, une ligne par jour" + "value" : "Jour" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, שורה אחת לכל יום" + "value" : "יום" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, naponta egy sor" + "value" : "Nap" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, una riga al giorno" + "value" : "Giorno" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "CSV(1日1行)" + "value" : "日" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, én rad per dag" + "value" : "Dag" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, één rij per dag" + "value" : "Dag" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, jeden wiersz na dzień" + "value" : "Dzień" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, uma linha por dia" + "value" : "Dia" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, un rând pe zi" + "value" : "Zi" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, одна строка на день" + "value" : "День" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, jeden riadok na deň" + "value" : "Deň" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, en rad per dag" + "value" : "Dag" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, gün başına bir satır" + "value" : "Gündüz" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, один рядок на день" + "value" : "День" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "CSV, mỗi ngày một dòng" + "value" : "Ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "CSV,每天一行" + "value" : "日" } } } }, - "Daily pattern" : { - "comment" : "Trends card title: the ambulatory glucose profile, glucose by time of day across the period", + "Day (Trends period)" : { + "comment" : "Trends period picker: a period of one calendar day (not daytime)", "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Day" + } + }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النمط اليومي" + "value" : "يوم" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Denní profil" + "value" : "Den" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster" + "value" : "Dag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tagesprofil" + "value" : "Tag" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Patrón diario" + "value" : "Día" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vuorokausiprofiili" + "value" : "Päivä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Profil journalier" + "value" : "Jour" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דפוס יומי" + "value" : "יום" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Napi mintázat" + "value" : "Nap" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Andamento giornaliero" + "value" : "Giorno" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "日内パターン" + "value" : "日" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster" + "value" : "Dag" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dagpatroon" + "value" : "Dag" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Profil dobowy" + "value" : "Dzień" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Padrão diário" + "value" : "Dia" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Tipar zilnic" + "value" : "Zi" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Суточный профиль" + "value" : "День" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Denný profil" + "value" : "Deň" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Dygnsmönster" + "value" : "Dag" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Günlük örüntü" + "value" : "Gün" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Добовий профіль" + "value" : "День" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mẫu hằng ngày" + "value" : "Ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "每日模式" + "value" : "日" } } } }, - "Daily pattern over %1$@. Median highest %2$@ at %3$@, lowest %4$@ at %5$@" : { - "comment" : "VoiceOver label for the Trends Daily pattern chart. (1: days with readings, e.g. 14 days; 2: highest median glucose; 3: time of day; 4: lowest median glucose; 5: time of day)", + "Glucose too old" : { + "comment" : "Reason Loop could not run: the latest glucose reading was too old", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النمط اليومي خلال %1$@. أعلى وسيط %2$@ عند %3$@، وأدنى وسيط %4$@ عند %5$@" + "value" : "قراءة السكر قديمة جدًا" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Denní profil za %1$@. Nejvyšší medián %2$@ v %3$@, nejnižší %4$@ v %5$@" + "value" : "Glukóza je příliš stará" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster over %1$@. Median højest %2$@ kl. %3$@, lavest %4$@ kl. %5$@" + "value" : "Glukosemåling for gammel" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tagesprofil über %1$@. Median am höchsten %2$@ um %3$@, am niedrigsten %4$@ um %5$@" + "value" : "Blutzuckerwert zu alt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Patrón diario de %1$@. Mediana más alta %2$@ a las %3$@, más baja %4$@ a las %5$@" + "value" : "Glucosa demasiado antigua" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vuorokausiprofiili %1$@ ajalta. Korkein mediaani %2$@ klo %3$@, matalin %4$@ klo %5$@" + "value" : "Glukoosi liian vanha" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Profil journalier sur %1$@. Médiane la plus haute %2$@ à %3$@, la plus basse %4$@ à %5$@" + "value" : "Glycémie trop ancienne" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דפוס יומי במשך %1$@. חציון גבוה ביותר %2$@ ב-%3$@, נמוך ביותר %4$@ ב-%5$@" + "value" : "קריאת הסוכר ישנה מדי" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Napi mintázat, %1$@. Legmagasabb medián %2$@ ekkor: %3$@, legalacsonyabb %4$@ ekkor: %5$@" + "value" : "Túl régi glükózérték" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Andamento giornaliero su %1$@. Mediana più alta %2$@ alle %3$@, più bassa %4$@ alle %5$@" + "value" : "Glicemia troppo vecchia" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@の日内パターン。中央値の最高は%3$@の%2$@、最低は%5$@の%4$@" + "value" : "血糖値が古すぎます" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster over %1$@. Median høyest %2$@ kl. %3$@, lavest %4$@ kl. %5$@" + "value" : "Blodsukkermåling for gammel" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dagpatroon over %1$@. Mediaan hoogst %2$@ om %3$@, laagst %4$@ om %5$@" + "value" : "Glucosewaarde te oud" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Profil dobowy z %1$@. Najwyższa mediana %2$@ o %3$@, najniższa %4$@ o %5$@" + "value" : "Glukoza zbyt stara" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Padrão diário em %1$@. Mediana mais alta %2$@ às %3$@, mais baixa %4$@ às %5$@" + "value" : "Glicose muito antiga" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Tipar zilnic pe %1$@. Mediana cea mai mare %2$@ la %3$@, cea mai mică %4$@ la %5$@" + "value" : "Glicemie prea veche" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Суточный профиль за %1$@. Наибольшая медиана %2$@ в %3$@, наименьшая %4$@ в %5$@" + "value" : "Данные глюкозы устарели" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Denný profil za %1$@. Najvyšší medián %2$@ o %3$@, najnižší %4$@ o %5$@" + "value" : "Glykémia je príliš stará" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Dygnsmönster över %1$@. Högsta median %2$@ kl. %3$@, lägsta %4$@ kl. %5$@" + "value" : "Glukosvärdet är för gammalt" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ boyunca günlük örüntü. En yüksek medyan %3$@ saatinde %2$@, en düşük %5$@ saatinde %4$@" + "value" : "Kan şekeri verisi çok eski" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Добовий профіль за %1$@. Найвища медіана %2$@ о %3$@, найнижча %4$@ о %5$@" + "value" : "Дані глюкози застаріли" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mẫu hằng ngày trong %1$@. Trung vị cao nhất %2$@ lúc %3$@, thấp nhất %4$@ lúc %5$@" + "value" : "Dữ liệu đường huyết quá cũ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@的每日模式。中位数最高 %2$@(%3$@),最低 %4$@(%5$@)" + "value" : "血糖数据过旧" } } } }, - "Daily pattern: not enough days" : { - "comment" : "VoiceOver label for the Trends Daily pattern chart when fewer than 5 days have readings", + "In range" : { + "comment" : "Time in range band: glucose 70–180 mg/dL (3.9–10.0 mmol/L) Time-in-range band name: glucose 70–180 mg/dL (3.9–10.0 mmol/L)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النمط اليومي: لا توجد أيام كافية" + "value" : "ضمن النطاق" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Denní profil: málo dní" + "value" : "V rozsahu" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster: ikke nok dage" + "value" : "I området" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tagesprofil: nicht genug Tage" + "value" : "Im Zielbereich" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Patrón diario: no hay suficientes días" + "value" : "En rango" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vuorokausiprofiili: liian vähän päiviä" + "value" : "Tavoitealueella" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Profil journalier : pas assez de jours" + "value" : "Dans la cible" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דפוס יומי: אין מספיק ימים" + "value" : "בטווח" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Napi mintázat: nincs elég nap" + "value" : "Tartományban" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Andamento giornaliero: giorni insufficienti" + "value" : "Nell'intervallo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "日内パターン:日数が不足しています" + "value" : "範囲内" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster: ikke nok dager" + "value" : "I målområdet" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dagpatroon: niet genoeg dagen" + "value" : "Binnen bereik" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Profil dobowy: za mało dni" + "value" : "W zakresie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Padrão diário: dias insuficientes" + "value" : "No alvo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Tipar zilnic: prea puține zile" + "value" : "În interval" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Суточный профиль: недостаточно дней" + "value" : "В диапазоне" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Denný profil: málo dní" + "value" : "V rozsahu" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Dygnsmönster: för få dagar" + "value" : "Inom målområdet" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Günlük örüntü: yeterli gün yok" + "value" : "Aralıkta" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Добовий профіль: замало днів" + "value" : "У діапазоні" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mẫu hằng ngày: không đủ số ngày" + "value" : "Trong khoảng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "每日模式:天数不足" + "value" : "范围内" } } } }, - "Day" : { - "comment" : "Appearance option and app icon name: the light (daytime) version", + "Insulin per day" : { + "comment" : "Trends tile and card title: average total insulin per day", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "نهار" + "value" : "الأنسولين يوميًا" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Den" + "value" : "Inzulín za den" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin pr. dag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tag" + "value" : "Insulin pro Tag" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Día" + "value" : "Insulina por día" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Päivä" + "value" : "Insuliini päivässä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Jour" + "value" : "Insuline par jour" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "יום" + "value" : "אינסולין ליום" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nap" + "value" : "Inzulin naponta" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Giorno" + "value" : "Insulina al giorno" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "日" + "value" : "1日のインスリン量" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin per dag" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insuline per dag" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dzień" + "value" : "Insulina dziennie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Dia" + "value" : "Insulina por dia" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Zi" + "value" : "Insulină pe zi" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "День" + "value" : "Инсулин в день" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Deň" + "value" : "Inzulín za deň" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin per dag" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gündüz" + "value" : "Günlük insülin" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "День" + "value" : "Інсулін на день" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Ngày" + "value" : "Insulin mỗi ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "日" + "value" : "每日胰岛素" } } } }, - "Day (Trends period)" : { - "comment" : "Trends period picker: a period of one calendar day (not daytime)", + "Insulin per day, average %1$@" : { + "comment" : "VoiceOver label for the Trends Insulin per day chart. (1: average insulin per day, e.g. 32.1 U)", "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Day" - } - }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يوم" + "value" : "الأنسولين يوميًا، المتوسط %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Den" + "value" : "Inzulín za den, průměr %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin pr. dag, gennemsnit %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tag" + "value" : "Insulin pro Tag, Durchschnitt %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Día" + "value" : "Insulina por día, promedio %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Päivä" + "value" : "Insuliini päivässä, keskiarvo %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Jour" + "value" : "Insuline par jour, moyenne %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "יום" + "value" : "אינסולין ליום, ממוצע %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nap" + "value" : "Inzulin naponta, átlag %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Giorno" + "value" : "Insulina al giorno, media %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "日" + "value" : "1日のインスリン量、平均 %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin per dag, gjennomsnitt %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insuline per dag, gemiddeld %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dzień" + "value" : "Insulina dziennie, średnio %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Dia" + "value" : "Insulina por dia, média %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Zi" + "value" : "Insulină pe zi, medie %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "День" + "value" : "Инсулин в день, в среднем %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Deň" + "value" : "Inzulín za deň, priemer %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin per dag, medel %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gün" + "value" : "Günlük insülin, ortalama %1$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "День" + "value" : "Інсулін на день, у середньому %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Ngày" + "value" : "Insulin mỗi ngày, trung bình %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "日" + "value" : "每日胰岛素,平均 %1$@" } } } }, - "Export" : { - "comment" : "Trends menu title: export the period as a PDF report or CSV file", + "Insulin per day, weekly average, average %1$@" : { + "comment" : "VoiceOver label for the Trends Insulin per day chart when bars are weekly averages. (1: average insulin per day, e.g. 32.1 U)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تصدير" + "value" : "الأنسولين يوميًا، متوسط أسبوعي، المتوسط %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Exportovat" + "value" : "Inzulín za den, týdenní průměr, průměr %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Eksporter" + "value" : "Insulin pr. dag, ugegennemsnit, gennemsnit %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Exportieren" + "value" : "Insulin pro Tag, Wochendurchschnitt, Durchschnitt %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Exportar" + "value" : "Insulina por día, promedio semanal, promedio %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vie" + "value" : "Insuliini päivässä, viikkokeskiarvo, keskiarvo %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Exporter" + "value" : "Insuline par jour, moyenne hebdomadaire, moyenne %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "ייצוא" + "value" : "אינסולין ליום, ממוצע שבועי, ממוצע %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Exportálás" + "value" : "Inzulin naponta, heti átlag, átlag %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Esporta" + "value" : "Insulina al giorno, media settimanale, media %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "書き出し" + "value" : "1日のインスリン量、週平均、平均 %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Eksporter" + "value" : "Insulin per dag, ukesgjennomsnitt, gjennomsnitt %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Exporteer" + "value" : "Insuline per dag, weekgemiddelde, gemiddeld %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Eksportuj" + "value" : "Insulina dziennie, średnia tygodniowa, średnio %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Exportar" + "value" : "Insulina por dia, média semanal, média %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Exportați" + "value" : "Insulină pe zi, medie săptămânală, medie %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Экспорт" + "value" : "Инсулин в день, среднее за неделю, в среднем %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Exportovať" + "value" : "Inzulín za deň, týždenný priemer, priemer %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Exportera" + "value" : "Insulin per dag, veckomedel, medel %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Dışa Aktar" + "value" : "Günlük insülin, haftalık ortalama, ortalama %1$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Експорт" + "value" : "Інсулін на день, середнє за тиждень, у середньому %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Xuất" + "value" : "Insulin mỗi ngày, trung bình theo tuần, trung bình %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "导出" + "value" : "每日胰岛素,按周平均,平均 %1$@" } } } }, - "From Apple Health: Loop keeps 7 days, so this day has no temp basal or schedule detail." : { - "comment" : "Day page note when the day is older than Loop's 7-day history and was read from Apple Health", + "Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@" : { + "comment" : "VoiceOver summary of the day's insulin timeline chart with no glucose readings. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "من Apple Health: يحتفظ Loop بـ 7 أيام فقط، لذا لا توجد تفاصيل Temp Basal أو الجدول لهذا اليوم." + "value" : "مخطط الأنسولين، %1$@، Bolus %2$@، القاعدية %3$@، الكارب %4$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Z aplikace Zdraví: Loop uchovává 7 dní, takže pro tento den chybí podrobnosti o dočasném bazálu a plánu." + "value" : "Časová osa inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Fra Apple Sundhed: Loop gemmer 7 dage, så denne dag har ingen detaljer om midlertidig basal eller tidsplan." + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kulhydrater %4$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Aus Apple Health: Loop speichert 7 Tage, daher gibt es für diesen Tag keine Details zu temporären Basalraten oder zum Zeitplan." + "value" : "Insulin-Zeitleiste, %1$@, Bolus %2$@, Basal %3$@, KH %4$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "De Apple Salud: Loop guarda 7 días, así que este día no tiene detalle de basal temporal ni de programación." + "value" : "Cronología de insulina, %1$@, bolo %2$@, basal %3$@, carbohidratos %4$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Apple Terveydestä: Loop säilyttää 7 päivää, joten tältä päivältä ei ole tilapäisen basaalin tai ohjelman tietoja." + "value" : "Insuliinin aikajana, %1$@, bolus %2$@, basaali %3$@, hiilihydraatit %4$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Depuis Apple Santé : Loop conserve 7 jours, donc ce jour n'a pas de détail de basal temp. ni de programmation." + "value" : "Chronologie de l'insuline, %1$@, bolus %2$@, basal %3$@, glucides %4$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מ-Apple Health: Loop שומר 7 ימים, ולכן אין ליום זה פרטי Temp Basal או תוכנית." + "value" : "ציר זמן אינסולין, %1$@, Bolus %2$@, Basal %3$@, פחמימות %4$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Az Apple Egészség appból: a Loop 7 napot őriz meg, így ehhez a naphoz nincsenek ideiglenes basal- és ütemezési részletek." + "value" : "Inzulin idővonal, %1$@, bólus %2$@, basal %3$@, szénhidrát %4$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Da Apple Salute: Loop conserva 7 giorni, quindi questo giorno non ha dettagli su basale temporanea o programmazione." + "value" : "Cronologia insulina, %1$@, bolo %2$@, basale %3$@, carboidrati %4$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Appleヘルスケアから取得:Loopは7日間のみ保持するため、この日には一時ベーサルやスケジュールの詳細がありません。" + "value" : "インスリンのタイムライン、%1$@、ボーラス %2$@、ベーサル %3$@、カーボ %4$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Fra Apple Helse: Loop lagrer 7 dager, så denne dagen har ingen detaljer om midlertidig basal eller tidsplan." + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, karbohydrater %4$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Uit Apple Health: Loop bewaart 7 dagen, dus deze dag heeft geen details over tijdelijk basaal of het schema." + "value" : "Insulinetijdlijn, %1$@, bolus %2$@, basaal %3$@, koolhydraten %4$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Z aplikacji Zdrowie: Loop przechowuje 7 dni, więc dla tego dnia brak szczegółów bazy tymczasowej i harmonogramu." + "value" : "Oś czasu insuliny, %1$@, bolus %2$@, baza %3$@, węglowodany %4$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Do app Saúde: o Loop guarda 7 dias, então este dia não tem detalhes de basal temporária nem da programação." + "value" : "Linha do tempo de insulina, %1$@, bolus %2$@, basal %3$@, carbs %4$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Din Apple Sănătate: Loop păstrează 7 zile, așa că această zi nu are detalii despre bazala temporară sau program." + "value" : "Cronologie insulină, %1$@, bolus %2$@, bazală %3$@, carbohidrați %4$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Из приложения «Здоровье»: Loop хранит 7 дней, поэтому для этого дня нет данных о временном базале и расписании." + "value" : "Хронология инсулина, %1$@, болюс %2$@, базал %3$@, углеводы %4$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Z aplikácie Zdravie: Loop uchováva 7 dní, preto pre tento deň chýbajú podrobnosti o dočasnom bazáli a pláne." + "value" : "Časová os inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Från Apple Hälsa: Loop sparar 7 dagar, så den här dagen saknar detaljer om temporär basal och schema." + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kolhydrater %4$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Apple Health'ten: Loop 7 gün saklar, bu nedenle bu gün için geçici bazal veya program ayrıntısı yok." + "value" : "İnsülin zaman çizelgesi, %1$@, bolus %2$@, bazal %3$@, karb %4$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "З програми «Здоровʼя»: Loop зберігає 7 днів, тому для цього дня немає даних про тимчасовий базал і розклад." + "value" : "Хронологія інсуліну, %1$@, болюс %2$@, базал %3$@, вуглеводи %4$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Từ Apple Health: Loop chỉ lưu 7 ngày, nên ngày này không có chi tiết Temp Basal hoặc lịch biểu." + "value" : "Dòng thời gian insulin, %1$@, Bolus %2$@, Basal %3$@, Carbs %4$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "来自 Apple 健康:Loop 只保留 7 天,因此这一天没有临时基础率或预设详情。" + "value" : "胰岛素时间线,%1$@,大剂量 %2$@,基础 %3$@,碳水 %4$@" } } } }, - "Gaps" : { - "comment" : "Trends Loop status button and screen title: list of times Loop wasn't in closed loop", + "Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@, latest glucose %5$@ %6$@" : { + "comment" : "VoiceOver summary of the day's insulin timeline chart. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g; 5: latest glucose value; 6: glucose unit, mmol/L or mg/dL)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الفجوات" + "value" : "مخطط الأنسولين، %1$@، Bolus %2$@، القاعدية %3$@، الكارب %4$@، آخر قراءة سكر %5$@ %6$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Výpadky" + "value" : "Časová osa inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@, poslední glukóza %5$@ %6$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Huller" + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kulhydrater %4$@, seneste glukose %5$@ %6$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Lücken" + "value" : "Insulin-Zeitleiste, %1$@, Bolus %2$@, Basal %3$@, KH %4$@, letzter Blutzucker %5$@ %6$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Interrupciones" + "value" : "Cronología de insulina, %1$@, bolo %2$@, basal %3$@, carbohidratos %4$@, última glucosa %5$@ %6$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Katkot" + "value" : "Insuliinin aikajana, %1$@, bolus %2$@, basaali %3$@, hiilihydraatit %4$@, viimeisin glukoosi %5$@ %6$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Interruptions" + "value" : "Chronologie de l'insuline, %1$@, bolus %2$@, basal %3$@, glucides %4$@, dernière glycémie %5$@ %6$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "פערים" + "value" : "ציר זמן אינסולין, %1$@, Bolus %2$@, Basal %3$@, פחמימות %4$@, סוכר אחרון %5$@ %6$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kimaradások" + "value" : "Inzulin idővonal, %1$@, bólus %2$@, basal %3$@, szénhidrát %4$@, legutóbbi glükóz %5$@ %6$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Interruzioni" + "value" : "Cronologia insulina, %1$@, bolo %2$@, basale %3$@, carboidrati %4$@, ultima glicemia %5$@ %6$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "中断" + "value" : "インスリンのタイムライン、%1$@、ボーラス %2$@、ベーサル %3$@、カーボ %4$@、最新の血糖値 %5$@ %6$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Hull" + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, karbohydrater %4$@, siste blodsukker %5$@ %6$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Onderbrekingen" + "value" : "Insulinetijdlijn, %1$@, bolus %2$@, basaal %3$@, koolhydraten %4$@, laatste glucose %5$@ %6$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Przerwy" + "value" : "Oś czasu insuliny, %1$@, bolus %2$@, baza %3$@, węglowodany %4$@, ostatnia glukoza %5$@ %6$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Interrupções" + "value" : "Linha do tempo de insulina, %1$@, bolus %2$@, basal %3$@, carbs %4$@, última glicose %5$@ %6$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Întreruperi" + "value" : "Cronologie insulină, %1$@, bolus %2$@, bazală %3$@, carbohidrați %4$@, ultima glicemie %5$@ %6$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Пропуски" + "value" : "Хронология инсулина, %1$@, болюс %2$@, базал %3$@, углеводы %4$@, последняя глюкоза %5$@ %6$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Výpadky" + "value" : "Časová os inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@, posledná glykémia %5$@ %6$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Luckor" + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kolhydrater %4$@, senaste glukos %5$@ %6$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Boşluklar" + "value" : "İnsülin zaman çizelgesi, %1$@, bolus %2$@, bazal %3$@, karb %4$@, son KŞ %5$@ %6$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Пропуски" + "value" : "Хронологія інсуліну, %1$@, болюс %2$@, базал %3$@, вуглеводи %4$@, остання глюкоза %5$@ %6$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Gián đoạn" + "value" : "Dòng thời gian insulin, %1$@, Bolus %2$@, Basal %3$@, Carbs %4$@, đường huyết mới nhất %5$@ %6$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "中断" + "value" : "胰岛素时间线,%1$@,大剂量 %2$@,基础 %3$@,碳水 %4$@,最新血糖 %5$@ %6$@" } } } }, - "Glucose too old" : { - "comment" : "Reason Loop could not run: the latest glucose reading was too old", + "Invalid future glucose" : { + "comment" : "Reason Loop could not run: a glucose reading had a timestamp in the future", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "قراءة السكر قديمة جدًا" + "value" : "قراءة سكر مستقبلية غير صالحة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Glukóza je příliš stará" + "value" : "Glukóza s budoucím časem" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Glukosemåling for gammel" + "value" : "Ugyldig glukosemåling i fremtiden" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Blutzuckerwert zu alt" + "value" : "Ungültiger Blutzuckerwert in der Zukunft" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Glucosa demasiado antigua" + "value" : "Glucosa futura no válida" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoosi liian vanha" + "value" : "Virheellinen tulevaisuuden glukoosiarvo" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Glycémie trop ancienne" + "value" : "Glycémie future non valide" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "קריאת הסוכר ישנה מדי" + "value" : "קריאת סוכר עתידית לא תקינה" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Túl régi glükózérték" + "value" : "Érvénytelen, jövőbeli glükózérték" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemia troppo vecchia" + "value" : "Glicemia futura non valida" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "血糖値が古すぎます" + "value" : "未来時刻の無効な血糖値" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Blodsukkermåling for gammel" + "value" : "Ugyldig blodsukkermåling i fremtiden" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Glucosewaarde te oud" + "value" : "Ongeldige glucosewaarde in de toekomst" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoza zbyt stara" + "value" : "Glukoza z przyszłą datą" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Glicose muito antiga" + "value" : "Glicose futura inválida" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemie prea veche" + "value" : "Glicemie invalidă, din viitor" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Данные глюкозы устарели" + "value" : "Глюкоза с временем в будущем" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Glykémia je príliš stará" + "value" : "Glykémia s budúcim časom" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Glukosvärdet är för gammalt" + "value" : "Ogiltigt glukosvärde i framtiden" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kan şekeri verisi çok eski" + "value" : "Geçersiz, ileri tarihli KŞ" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Дані глюкози застаріли" + "value" : "Глюкоза з часом у майбутньому" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Dữ liệu đường huyết quá cũ" + "value" : "Đường huyết có thời gian trong tương lai không hợp lệ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "血糖数据过旧" + "value" : "无效的未来血糖" } } } }, - "In range" : { - "comment" : "Time in range band: glucose 70–180 mg/dL (3.9–10.0 mmol/L) Time-in-range band name: glucose 70–180 mg/dL (3.9–10.0 mmol/L)", + "IOB" : { + "comment" : "Abbreviation for insulin on board (active insulin) Abbreviation for insulin on board; label of a tile on the Now screen", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ضمن النطاق" + "value" : "IOB" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "V rozsahu" + "value" : "IOB" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "I området" + "value" : "IOB" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Im Zielbereich" + "value" : "IOB" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "En rango" + "value" : "IOB" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Tavoitealueella" + "value" : "IOB" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Dans la cible" + "value" : "IA" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "בטווח" + "value" : "IOB" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Tartományban" + "value" : "IOB" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nell'intervallo" + "value" : "IOB" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "範囲内" + "value" : "IOB" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "I målområdet" + "value" : "IOB" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Binnen bereik" + "value" : "IOB" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "W zakresie" + "value" : "IOB" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "No alvo" + "value" : "IOB" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "În interval" + "value" : "IOB" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "В диапазоне" + "value" : "IOB" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "V rozsahu" + "value" : "IOB" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inom målområdet" + "value" : "IOB" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Aralıkta" + "value" : "IOB" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "У діапазоні" + "value" : "IOB" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trong khoảng" + "value" : "IOB" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "范围内" + "value" : "IOB" } } } }, - "Insulin per day" : { - "comment" : "Trends tile and card title: average total insulin per day", + "Limited" : { + "comment" : "Trends Loop status: time closed loop was on but Loop wasn't completing runs", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الأنسولين يوميًا" + "value" : "محدود" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulín za den" + "value" : "Omezeno" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin pr. dag" + "value" : "Begrænset" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin pro Tag" + "value" : "Eingeschränkt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina por día" + "value" : "Limitado" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Insuliini päivässä" + "value" : "Rajoitettu" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Insuline par jour" + "value" : "Limitée" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אינסולין ליום" + "value" : "מוגבל" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulin naponta" + "value" : "Korlátozott" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina al giorno" + "value" : "Limitato" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "1日のインスリン量" + "value" : "制限あり" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin per dag" + "value" : "Begrenset" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Insuline per dag" + "value" : "Beperkt" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina dziennie" + "value" : "Ograniczona" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina por dia" + "value" : "Limitado" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Insulină pe zi" + "value" : "Limitat" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Инсулин в день" + "value" : "Ограничен" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulín za deň" + "value" : "Obmedzená" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin per dag" + "value" : "Begränsad" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Günlük insülin" + "value" : "Sınırlı" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Інсулін на день" + "value" : "Обмежено" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin mỗi ngày" + "value" : "Hạn chế" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "每日胰岛素" - } - } - } - }, - "Insulin per day, average %1$@" : { - "comment" : "VoiceOver label for the Trends Insulin per day chart. (1: average insulin per day, e.g. 32.1 U)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الأنسولين يوميًا، المتوسط %1$@" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inzulín za den, průměr %1$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin pr. dag, gennemsnit %1$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin pro Tag, Durchschnitt %1$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina por día, promedio %1$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuliini päivässä, keskiarvo %1$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuline par jour, moyenne %1$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אינסולין ליום, ממוצע %1$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inzulin naponta, átlag %1$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina al giorno, media %1$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "1日のインスリン量、平均 %1$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin per dag, gjennomsnitt %1$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuline per dag, gemiddeld %1$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina dziennie, średnio %1$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina por dia, média %1$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulină pe zi, medie %1$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Инсулин в день, в среднем %1$@" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inzulín za deň, priemer %1$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin per dag, medel %1$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Günlük insülin, ortalama %1$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Інсулін на день, у середньому %1$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin mỗi ngày, trung bình %1$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "每日胰岛素,平均 %1$@" - } - } - } - }, - "Insulin per day, weekly average, average %1$@" : { - "comment" : "VoiceOver label for the Trends Insulin per day chart when bars are weekly averages. (1: average insulin per day, e.g. 32.1 U)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الأنسولين يوميًا، متوسط أسبوعي، المتوسط %1$@" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inzulín za den, týdenní průměr, průměr %1$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin pr. dag, ugegennemsnit, gennemsnit %1$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin pro Tag, Wochendurchschnitt, Durchschnitt %1$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina por día, promedio semanal, promedio %1$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuliini päivässä, viikkokeskiarvo, keskiarvo %1$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuline par jour, moyenne hebdomadaire, moyenne %1$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אינסולין ליום, ממוצע שבועי, ממוצע %1$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inzulin naponta, heti átlag, átlag %1$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina al giorno, media settimanale, media %1$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "1日のインスリン量、週平均、平均 %1$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin per dag, ukesgjennomsnitt, gjennomsnitt %1$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuline per dag, weekgemiddelde, gemiddeld %1$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina dziennie, średnia tygodniowa, średnio %1$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina por dia, média semanal, média %1$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulină pe zi, medie săptămânală, medie %1$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Инсулин в день, среднее за неделю, в среднем %1$@" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inzulín za deň, týždenný priemer, priemer %1$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin per dag, veckomedel, medel %1$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Günlük insülin, haftalık ortalama, ortalama %1$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Інсулін на день, середнє за тиждень, у середньому %1$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin mỗi ngày, trung bình theo tuần, trung bình %1$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "每日胰岛素,按周平均,平均 %1$@" - } - } - } - }, - "Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@" : { - "comment" : "VoiceOver summary of the day's insulin timeline chart with no glucose readings. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "مخطط الأنسولين، %1$@، Bolus %2$@، القاعدية %3$@، الكارب %4$@" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Časová osa inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kulhydrater %4$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin-Zeitleiste, %1$@, Bolus %2$@, Basal %3$@, KH %4$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cronología de insulina, %1$@, bolo %2$@, basal %3$@, carbohidratos %4$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuliinin aikajana, %1$@, bolus %2$@, basaali %3$@, hiilihydraatit %4$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chronologie de l'insuline, %1$@, bolus %2$@, basal %3$@, glucides %4$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "ציר זמן אינסולין, %1$@, Bolus %2$@, Basal %3$@, פחמימות %4$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inzulin idővonal, %1$@, bólus %2$@, basal %3$@, szénhidrát %4$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cronologia insulina, %1$@, bolo %2$@, basale %3$@, carboidrati %4$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "インスリンのタイムライン、%1$@、ボーラス %2$@、ベーサル %3$@、カーボ %4$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, karbohydrater %4$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulinetijdlijn, %1$@, bolus %2$@, basaal %3$@, koolhydraten %4$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oś czasu insuliny, %1$@, bolus %2$@, baza %3$@, węglowodany %4$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Linha do tempo de insulina, %1$@, bolus %2$@, basal %3$@, carbs %4$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cronologie insulină, %1$@, bolus %2$@, bazală %3$@, carbohidrați %4$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Хронология инсулина, %1$@, болюс %2$@, базал %3$@, углеводы %4$@" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Časová os inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kolhydrater %4$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "İnsülin zaman çizelgesi, %1$@, bolus %2$@, bazal %3$@, karb %4$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Хронологія інсуліну, %1$@, болюс %2$@, базал %3$@, вуглеводи %4$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dòng thời gian insulin, %1$@, Bolus %2$@, Basal %3$@, Carbs %4$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "胰岛素时间线,%1$@,大剂量 %2$@,基础 %3$@,碳水 %4$@" - } - } - } - }, - "Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@, latest glucose %5$@ %6$@" : { - "comment" : "VoiceOver summary of the day's insulin timeline chart. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g; 5: latest glucose value; 6: glucose unit, mmol/L or mg/dL)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "مخطط الأنسولين، %1$@، Bolus %2$@، القاعدية %3$@، الكارب %4$@، آخر قراءة سكر %5$@ %6$@" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Časová osa inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@, poslední glukóza %5$@ %6$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kulhydrater %4$@, seneste glukose %5$@ %6$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulin-Zeitleiste, %1$@, Bolus %2$@, Basal %3$@, KH %4$@, letzter Blutzucker %5$@ %6$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cronología de insulina, %1$@, bolo %2$@, basal %3$@, carbohidratos %4$@, última glucosa %5$@ %6$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insuliinin aikajana, %1$@, bolus %2$@, basaali %3$@, hiilihydraatit %4$@, viimeisin glukoosi %5$@ %6$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chronologie de l'insuline, %1$@, bolus %2$@, basal %3$@, glucides %4$@, dernière glycémie %5$@ %6$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "ציר זמן אינסולין, %1$@, Bolus %2$@, Basal %3$@, פחמימות %4$@, סוכר אחרון %5$@ %6$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inzulin idővonal, %1$@, bólus %2$@, basal %3$@, szénhidrát %4$@, legutóbbi glükóz %5$@ %6$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cronologia insulina, %1$@, bolo %2$@, basale %3$@, carboidrati %4$@, ultima glicemia %5$@ %6$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "インスリンのタイムライン、%1$@、ボーラス %2$@、ベーサル %3$@、カーボ %4$@、最新の血糖値 %5$@ %6$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, karbohydrater %4$@, siste blodsukker %5$@ %6$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulinetijdlijn, %1$@, bolus %2$@, basaal %3$@, koolhydraten %4$@, laatste glucose %5$@ %6$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oś czasu insuliny, %1$@, bolus %2$@, baza %3$@, węglowodany %4$@, ostatnia glukoza %5$@ %6$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Linha do tempo de insulina, %1$@, bolus %2$@, basal %3$@, carbs %4$@, última glicose %5$@ %6$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cronologie insulină, %1$@, bolus %2$@, bazală %3$@, carbohidrați %4$@, ultima glicemie %5$@ %6$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Хронология инсулина, %1$@, болюс %2$@, базал %3$@, углеводы %4$@, последняя глюкоза %5$@ %6$@" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Časová os inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@, posledná glykémia %5$@ %6$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kolhydrater %4$@, senaste glukos %5$@ %6$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "İnsülin zaman çizelgesi, %1$@, bolus %2$@, bazal %3$@, karb %4$@, son KŞ %5$@ %6$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Хронологія інсуліну, %1$@, болюс %2$@, базал %3$@, вуглеводи %4$@, остання глюкоза %5$@ %6$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dòng thời gian insulin, %1$@, Bolus %2$@, Basal %3$@, Carbs %4$@, đường huyết mới nhất %5$@ %6$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "胰岛素时间线,%1$@,大剂量 %2$@,基础 %3$@,碳水 %4$@,最新血糖 %5$@ %6$@" - } - } - } - }, - "Invalid future glucose" : { - "comment" : "Reason Loop could not run: a glucose reading had a timestamp in the future", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "قراءة سكر مستقبلية غير صالحة" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glukóza s budoucím časem" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ugyldig glukosemåling i fremtiden" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ungültiger Blutzuckerwert in der Zukunft" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glucosa futura no válida" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Virheellinen tulevaisuuden glukoosiarvo" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glycémie future non valide" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "קריאת סוכר עתידית לא תקינה" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Érvénytelen, jövőbeli glükózérték" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glicemia futura non valida" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "未来時刻の無効な血糖値" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ugyldig blodsukkermåling i fremtiden" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ongeldige glucosewaarde in de toekomst" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glukoza z przyszłą datą" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glicose futura inválida" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glicemie invalidă, din viitor" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Глюкоза с временем в будущем" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Glykémia s budúcim časom" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ogiltigt glukosvärde i framtiden" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geçersiz, ileri tarihli KŞ" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Глюкоза з часом у майбутньому" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Đường huyết có thời gian trong tương lai không hợp lệ" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无效的未来血糖" - } - } - } - }, - "IOB" : { - "comment" : "Abbreviation for insulin on board (active insulin) Abbreviation for insulin on board; label of a tile on the Now screen", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "IA" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB" - } - } - } - }, - "IOB (U, left)" : { - "comment" : "Chart legend: insulin on board in units, on the left axis", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (وحدة، يسار)" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (U, vlevo)" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (E, venstre)" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (IE, links)" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (U, izquierda)" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (U, vasen)" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "IA (U, gauche)" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (U, שמאל)" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (U, bal)" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (U, sinistra)" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB(U、左)" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (E, venstre)" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (E, links)" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (J, lewa oś)" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (U, esquerda)" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (U, stânga)" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (Ед, слева)" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (j, vľavo)" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (E, vänster)" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (Ü, sol)" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (U, ліворуч)" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB (U, bên trái)" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "IOB(U,左轴)" - } - } - } - }, - "Limited" : { - "comment" : "Trends Loop status: time closed loop was on but Loop wasn't completing runs", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "محدود" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Omezeno" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Begrænset" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eingeschränkt" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Limitado" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Rajoitettu" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Limitée" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מוגבל" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Korlátozott" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Limitato" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "制限あり" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Begrenset" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beperkt" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ograniczona" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Limitado" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Limitat" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ограничен" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obmedzená" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Begränsad" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sınırlı" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Обмежено" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hạn chế" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "受限" - } - } - } - }, - "Loop active" : { - "comment" : "Trends summary tile: share of the period Loop spent in closed loop", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop نشط" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktivní" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktiv" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktiv" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop activo" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktiivinen" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop actif" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop פעיל" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktív" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop attivo" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop稼働" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktiv" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop actief" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktywny" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop ativo" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop activ" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop активен" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktívny" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktiv" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktif" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop активний" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop hoạt động" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop 运行" - } - } - } - }, - "Loop didn't run" : { - "comment" : "Reason shown for a stretch of time when Loop did not complete a run and recorded no error", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "لم يعمل Loop" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop neproběhl" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop kørte ikke" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop ist nicht gelaufen" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop no se ejecutó" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop ei ajanut" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop ne s'est pas exécuté" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop לא רץ" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A Loop nem futott" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop non eseguito" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopが実行されませんでした" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop kjørte ikke" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop heeft niet gedraaid" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop nie działał" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "O Loop não executou" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop nu a rulat" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop не запускался" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop neprebehol" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop kördes inte" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop çalışmadı" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop не запускався" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop không chạy" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop 未运行" - } - } - } - }, - "Loop only works out active insulin and carbs for the days it keeps" : { - "comment" : "Day page placeholder for active insulin and carbs on a day older than Loop's 7-day history", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "يحسب Loop الأنسولين والكارب النشطين فقط للأيام التي يحتفظ بها" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop počítá aktivní inzulín a sacharidy jen pro dny, které uchovává" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop beregner kun aktivt insulin og kulhydrater for de dage, den gemmer" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop berechnet aktives Insulin und aktive KH nur für die gespeicherten Tage" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop solo calcula la insulina y los carbohidratos activos de los días que guarda" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop laskee aktiivisen insuliinin ja hiilihydraatit vain säilyttämilleen päiville" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop calcule l'insuline et les glucides actifs uniquement pour les jours qu'il conserve" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop מחשב אינסולין ופחמימות פעילים רק עבור הימים שהוא שומר" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A Loop csak a megőrzött napokra számolja ki az aktív inzulint és szénhidrátot" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop calcola insulina e carboidrati attivi solo per i giorni che conserva" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopが残存インスリンと残存糖質を計算するのは、保持している日のみです" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop beregner bare aktivt insulin og karbohydrater for dagene den lagrer" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop berekent actieve insuline en koolhydraten alleen voor de dagen die het bewaart" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop oblicza aktywną insulinę i węglowodany tylko dla przechowywanych dni" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "O Loop só calcula a insulina e os carboidratos ativos dos dias que guarda" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop calculează insulina și carbohidrații activi doar pentru zilele pe care le păstrează" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop рассчитывает активный инсулин и углеводы только за хранимые дни" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop počíta aktívny inzulín a sacharidy len pre dni, ktoré uchováva" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop beräknar bara aktivt insulin och kolhydrater för de dagar som sparas" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktif insülini ve karbı yalnızca sakladığı günler için hesaplar" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop розраховує активний інсулін і вуглеводи лише за дні, які зберігає" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop chỉ tính insulin và Carbs còn hoạt động cho những ngày nó lưu" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop 只为其保留的日期计算活性胰岛素和碳水" - } - } - } - }, - "Loop status" : { - "comment" : "Trends card title: how long Loop spent in closed loop, limited or open loop", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "حالة Loop" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stav Loop" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-status" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-Status" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Estado de Loop" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopin tila" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "État de Loop" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מצב Loop" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop állapota" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stato di Loop" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopの状態" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-status" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopstatus" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stan Loop" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Status do Loop" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Starea Loop" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Статус Loop" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stav Loop" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopstatus" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop durumu" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стан Loop" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trạng thái Loop" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop 状态" - } - } - } - }, - "Loop status is recorded from when this version of Loop first ran." : { - "comment" : "Trends Loop status footnote when there is no Loop status history yet", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "تُسجَّل حالة Loop منذ أول تشغيل لهذا الإصدار من Loop." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stav Loop se zaznamenává od prvního spuštění této verze Loop." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-status registreres fra første gang denne version af Loop kørte." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Loop-Status wird ab dem ersten Start dieser Loop-Version aufgezeichnet." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El estado de Loop se registra desde la primera vez que se ejecutó esta versión de Loop." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopin tila tallennetaan siitä lähtien, kun tämä Loopin versio käynnistettiin ensimmäisen kerran." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "L'état de Loop est enregistré depuis le premier lancement de cette version de Loop." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מצב Loop נרשם מאז ההפעלה הראשונה של גרסה זו של Loop." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "A Loop állapota a Loop ezen verziójának első futtatásától kerül rögzítésre." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lo stato di Loop è registrato dal primo avvio di questa versione di Loop." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopの状態は、このバージョンのLoopを初めて起動した時点から記録されます。" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-status registreres fra første gang denne versjonen av Loop kjørte." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "De loopstatus wordt vastgelegd vanaf de eerste keer dat deze versie van Loop draaide." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stan Loop jest zapisywany od pierwszego uruchomienia tej wersji Loop." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "O status do Loop é registrado desde a primeira vez que esta versão do Loop foi executada." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Starea Loop este înregistrată de la prima rulare a acestei versiuni de Loop." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Статус Loop записывается с первого запуска этой версии Loop." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Stav Loop sa zaznamenáva od prvého spustenia tejto verzie Loop." - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopstatus registreras från när den här versionen av Loop först kördes." - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop durumu, Loop'un bu sürümünün ilk çalıştığı andan itibaren kaydedilir." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Стан Loop записується з першого запуску цієї версії Loop." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trạng thái Loop được ghi lại từ khi phiên bản Loop này chạy lần đầu." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop 状态从此版本 Loop 首次运行时开始记录。" - } - } - } - }, - "Loop trends" : { - "comment" : "Title of the exported Trends PDF report", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "اتجاهات Loop" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trendy Loop" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-tendenser" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-Trends" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tendencias de Loop" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-trendit" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tendances Loop" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מגמות Loop" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-trendek" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tendenze Loop" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopトレンド" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-trender" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-trends" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trendy Loop" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tendências do Loop" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tendințe Loop" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Тренды Loop" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trendy Loop" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-trender" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop eğilimleri" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Тренди Loop" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xu hướng Loop" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop 趋势" - } - } - } - }, - "Loop trends %1$@" : { - "comment" : "File name of an exported Trends report or CSV, without the extension. (1: date range of the period)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "اتجاهات Loop %1$@" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trendy Loop %1$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-tendenser %1$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-Trends %1$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tendencias de Loop %1$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-trendit %1$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tendances Loop %1$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "מגמות Loop %1$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-trendek %1$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tendenze Loop %1$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loopトレンド %1$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-trender %1$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-trends %1$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trendy Loop %1$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tendências do Loop %1$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tendințe Loop %1$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Тренды Loop %1$@" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trendy Loop %1$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop-trender %1$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop eğilimleri %1$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Тренди Loop %1$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Xu hướng Loop %1$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop 趋势 %1$@" - } - } - } - }, - "Manual %1$@" : { - "comment" : "Basal and bolus: bolus the person gave. (1: units, e.g. 16.2 U)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "يدوي %1$@" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ručně %1$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manuel %1$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manuell %1$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manual %1$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manuaalinen %1$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manuel %1$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "ידני %1$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kézi %1$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manuale %1$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "手動 %1$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manuell %1$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Handmatig %1$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ręcznie %1$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manual %1$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manual %1$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вручную %1$@" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ručne %1$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manuell %1$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manuel %1$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Вручну %1$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thủ công %1$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "手动 %1$@" - } - } - } - }, - "Median" : { - "comment" : "Trends Daily pattern legend: the median glucose line", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الوسيط" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Medián" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Median" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Median" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediana" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediaani" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Médiane" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "חציון" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Medián" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediana" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "中央値" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Median" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediaan" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediana" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediana" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mediană" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Медиана" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Medián" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Median" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Medyan" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Медіана" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trung vị" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "中位数" - } - } - } - }, - "Missing data error" : { - "comment" : "Reason Loop could not run: data it needs was missing", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "خطأ بيانات مفقودة" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chyba chybějících dat" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fejl: manglende data" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fehler: fehlende Daten" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Error de datos faltantes" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tietoja puuttuu" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erreur de données manquantes" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "שגיאת נתונים חסרים" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hiányzó adat" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Errore dati mancanti" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "データ欠落エラー" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Feil: manglende data" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fout: ontbrekende gegevens" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Błąd brakujących danych" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erro de dados ausentes" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eroare: date lipsă" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ошибка: нет данных" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chyba chýbajúcich údajov" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Data saknas" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eksik veri hatası" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Помилка: немає даних" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lỗi thiếu dữ liệu" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "数据缺失错误" + "value" : "受限" } } } }, - "Needs 10 days of CGM" : { - "comment" : "Trends GMI tile detail when fewer than 10 days of CGM data are available", + "Loop active" : { + "comment" : "Trends summary tile: share of the period Loop spent in closed loop", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يحتاج إلى 10 أيام من CGM" + "value" : "Loop نشط" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vyžaduje 10 dní CGM" + "value" : "Loop aktivní" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Kræver 10 dages CGM" + "value" : "Loop aktiv" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Benötigt 10 Tage CGM" + "value" : "Loop aktiv" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Requiere 10 días de CGM" + "value" : "Loop activo" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vaatii 10 päivää CGM-dataa" + "value" : "Loop aktiivinen" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nécessite 10 jours de CGM" + "value" : "Loop actif" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "נדרשים 10 ימי CGM" + "value" : "Loop פעיל" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "10 nap CGM-adat szükséges" + "value" : "Loop aktív" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Servono 10 giorni di CGM" + "value" : "Loop attivo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "CGMデータが10日分必要" + "value" : "Loop稼働" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Krever 10 dager med CGM" + "value" : "Loop aktiv" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vereist 10 dagen CGM" + "value" : "Loop actief" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wymaga 10 dni CGM" + "value" : "Loop aktywny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Requer 10 dias de CGM" + "value" : "Loop ativo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Necesită 10 zile de CGM" + "value" : "Loop activ" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нужно 10 дней CGM" + "value" : "Loop активен" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Vyžaduje 10 dní CGM" + "value" : "Loop aktívny" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Kräver 10 dagars CGM" + "value" : "Loop aktiv" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "10 günlük CGM gerekir" + "value" : "Loop aktif" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Потрібно 10 днів CGM" + "value" : "Loop активний" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cần 10 ngày dữ liệu CGM" + "value" : "Loop hoạt động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "需要 10 天 CGM 数据" + "value" : "Loop 运行" } } } }, - "Next day" : { - "comment" : "Accessibility label for the day page button that steps forward a day", + "Loop didn't run" : { + "comment" : "Reason shown for a stretch of time when Loop did not complete a run and recorded no error", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اليوم التالي" + "value" : "لم يعمل Loop" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Další den" + "value" : "Loop neproběhl" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Næste dag" + "value" : "Loop kørte ikke" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nächster Tag" + "value" : "Loop ist nicht gelaufen" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Día siguiente" + "value" : "Loop no se ejecutó" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Seuraava päivä" + "value" : "Loop ei ajanut" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Jour suivant" + "value" : "Loop ne s'est pas exécuté" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "היום הבא" + "value" : "Loop לא רץ" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Következő nap" + "value" : "A Loop nem futott" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Giorno successivo" + "value" : "Loop non eseguito" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "翌日" + "value" : "Loopが実行されませんでした" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Neste dag" + "value" : "Loop kjørte ikke" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Volgende dag" + "value" : "Loop heeft niet gedraaid" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Następny dzień" + "value" : "Loop nie działał" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Próximo dia" + "value" : "O Loop não executou" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ziua următoare" + "value" : "Loop nu a rulat" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Следующий день" + "value" : "Loop не запускался" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Ďalší deň" + "value" : "Loop neprebehol" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Nästa dag" + "value" : "Loop kördes inte" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sonraki gün" + "value" : "Loop çalışmadı" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Наступний день" + "value" : "Loop не запускався" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Ngày sau" + "value" : "Loop không chạy" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "后一天" + "value" : "Loop 未运行" } } } }, - "Next period" : { - "comment" : "VoiceOver label for the Trends button that moves forward one period", + "Loop status" : { + "comment" : "Trends card title: how long Loop spent in closed loop, limited or open loop", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الفترة التالية" + "value" : "حالة Loop" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Další období" + "value" : "Stav Loop" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Næste periode" + "value" : "Loop-status" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nächster Zeitraum" + "value" : "Loop-Status" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Período siguiente" + "value" : "Estado de Loop" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Seuraava jakso" + "value" : "Loopin tila" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Période suivante" + "value" : "État de Loop" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "התקופה הבאה" + "value" : "מצב Loop" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Következő időszak" + "value" : "Loop állapota" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Periodo successivo" + "value" : "Stato di Loop" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "次の期間" + "value" : "Loopの状態" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Neste periode" + "value" : "Loop-status" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Volgende periode" + "value" : "Loopstatus" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Następny okres" + "value" : "Stan Loop" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Próximo período" + "value" : "Status do Loop" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Perioada următoare" + "value" : "Starea Loop" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Следующий период" + "value" : "Статус Loop" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Ďalšie obdobie" + "value" : "Stav Loop" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Nästa period" + "value" : "Loopstatus" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sonraki dönem" + "value" : "Loop durumu" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Наступний період" + "value" : "Стан Loop" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Khoảng thời gian sau" + "value" : "Trạng thái Loop" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "下一时段" + "value" : "Loop 状态" } } } }, - "No active insulin or carbs on this day" : { - "comment" : "Day page placeholder when there was no insulin or carbs on board all day", + "Loop status is recorded from when this version of Loop first ran." : { + "comment" : "Trends Loop status footnote when there is no Loop status history yet", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا يوجد أنسولين أو كارب نشط في هذا اليوم" + "value" : "تُسجَّل حالة Loop منذ أول تشغيل لهذا الإصدار من Loop." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "V tento den nebyl žádný aktivní inzulín ani sacharidy" + "value" : "Stav Loop se zaznamenává od prvního spuštění této verze Loop." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Intet aktivt insulin eller kulhydrater denne dag" + "value" : "Loop-status registreres fra første gang denne version af Loop kørte." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kein aktives Insulin und keine aktiven KH an diesem Tag" + "value" : "Der Loop-Status wird ab dem ersten Start dieser Loop-Version aufgezeichnet." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin insulina ni carbohidratos activos este día" + "value" : "El estado de Loop se registra desde la primera vez que se ejecutó esta versión de Loop." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei aktiivista insuliinia tai hiilihydraatteja tänä päivänä" + "value" : "Loopin tila tallennetaan siitä lähtien, kun tämä Loopin versio käynnistettiin ensimmäisen kerran." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune insuline ni glucides actifs ce jour-là" + "value" : "L'état de Loop est enregistré depuis le premier lancement de cette version de Loop." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין אינסולין או פחמימות פעילים ביום זה" + "value" : "מצב Loop נרשם מאז ההפעלה הראשונה של גרסה זו של Loop." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Ezen a napon nem volt aktív inzulin vagy szénhidrát" + "value" : "A Loop állapota a Loop ezen verziójának első futtatásától kerül rögzítésre." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessuna insulina o carboidrati attivi in questo giorno" + "value" : "Lo stato di Loop è registrato dal primo avvio di questa versione di Loop." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "この日は残存インスリンも残存糖質もありません" + "value" : "Loopの状態は、このバージョンのLoopを初めて起動した時点から記録されます。" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen aktivt insulin eller karbohydrater denne dagen" + "value" : "Loop-status registreres fra første gang denne versjonen av Loop kjørte." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen actieve insuline of koolhydraten op deze dag" + "value" : "De loopstatus wordt vastgelegd vanaf de eerste keer dat deze versie van Loop draaide." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak aktywnej insuliny i węglowodanów tego dnia" + "value" : "Stan Loop jest zapisywany od pierwszego uruchomienia tej wersji Loop." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Sem insulina ou carboidratos ativos neste dia" + "value" : "O status do Loop é registrado desde a primeira vez que esta versão do Loop foi executada." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Fără insulină sau carbohidrați activi în această zi" + "value" : "Starea Loop este înregistrată de la prima rulare a acestei versiuni de Loop." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "В этот день не было активного инсулина или углеводов" + "value" : "Статус Loop записывается с первого запуска этой версии Loop." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "V tento deň nebol žiadny aktívny inzulín ani sacharidy" + "value" : "Stav Loop sa zaznamenáva od prvého spustenia tejto verzie Loop." } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inget aktivt insulin eller kolhydrater denna dag" + "value" : "Loopstatus registreras från när den här versionen av Loop först kördes." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu gün aktif insülin veya karb yok" + "value" : "Loop durumu, Loop'un bu sürümünün ilk çalıştığı andan itibaren kaydedilir." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Цього дня не було активного інсуліну чи вуглеводів" + "value" : "Стан Loop записується з першого запуску цієї версії Loop." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không có insulin hoặc Carbs còn hoạt động trong ngày này" + "value" : "Trạng thái Loop được ghi lại từ khi phiên bản Loop này chạy lần đầu." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "这一天没有活性胰岛素或碳水" + "value" : "Loop 状态从此版本 Loop 首次运行时开始记录。" } } } }, - "No CGM data" : { - "comment" : "Trends Average tile detail when the period has no glucose readings", + "Manual %1$@" : { + "comment" : "Basal and bolus: bolus the person gave. (1: units, e.g. 16.2 U)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد بيانات CGM" + "value" : "يدوي %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Žádná data CGM" + "value" : "Ručně %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data" + "value" : "Manuel %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine CGM-Daten" + "value" : "Manuell %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin datos del CGM" + "value" : "Manual %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei CGM-dataa" + "value" : "Manuaalinen %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune donnée CGM" + "value" : "Manuel %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין נתוני CGM" + "value" : "ידני %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs CGM-adat" + "value" : "Kézi %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessun dato CGM" + "value" : "Manuale %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "CGMデータなし" + "value" : "手動 %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data" + "value" : "Manuell %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen CGM-gegevens" + "value" : "Handmatig %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak danych CGM" + "value" : "Ręcznie %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Sem dados do CGM" + "value" : "Manual %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Fără date CGM" + "value" : "Manual %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нет данных CGM" + "value" : "Вручную %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Žiadne údaje CGM" + "value" : "Ručne %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inga CGM-data" + "value" : "Manuell %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "CGM verisi yok" + "value" : "Manuel %1$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Немає даних CGM" + "value" : "Вручну %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không có dữ liệu CGM" + "value" : "Thủ công %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无 CGM 数据" + "value" : "手动 %1$@" } } } }, - "No CGM data %1$@ – %2$@" : { - "comment" : "Chart note: a gap in glucose readings. (1: start time; 2: end time)", + "Median" : { + "comment" : "Trends Daily pattern legend: the median glucose line", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد بيانات CGM %1$@ – %2$@" + "value" : "الوسيط" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Žádná data CGM %1$@ – %2$@" + "value" : "Medián" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data %1$@ – %2$@" + "value" : "Median" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine CGM-Daten %1$@ – %2$@" + "value" : "Median" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin datos del CGM %1$@ – %2$@" + "value" : "Mediana" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei CGM-dataa %1$@ – %2$@" + "value" : "Mediaani" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune donnée CGM %1$@ – %2$@" + "value" : "Médiane" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין נתוני CGM %1$@ – %2$@" + "value" : "חציון" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs CGM-adat %1$@ – %2$@" + "value" : "Medián" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessun dato CGM %1$@ – %2$@" + "value" : "Mediana" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "CGMデータなし %1$@ – %2$@" + "value" : "中央値" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data %1$@ – %2$@" + "value" : "Median" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen CGM-gegevens %1$@ – %2$@" + "value" : "Mediaan" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak danych CGM %1$@ – %2$@" + "value" : "Mediana" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Sem dados do CGM %1$@ – %2$@" + "value" : "Mediana" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Fără date CGM %1$@ – %2$@" + "value" : "Mediană" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нет данных CGM %1$@ – %2$@" + "value" : "Медиана" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Žiadne údaje CGM %1$@ – %2$@" + "value" : "Medián" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inga CGM-data %1$@ – %2$@" + "value" : "Median" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "CGM verisi yok %1$@ – %2$@" + "value" : "Medyan" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Немає даних CGM %1$@ – %2$@" + "value" : "Медіана" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không có dữ liệu CGM %1$@ – %2$@" + "value" : "Trung vị" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无 CGM 数据 %1$@ – %2$@" + "value" : "中位数" } } } }, - "No CGM data %1$@ – %2$@ and %3$@" : { - "comment" : "Chart note: the longest gap in glucose readings, plus the shorter ones. (1: start time; 2: end time; 3: number of shorter gaps, e.g. 2 shorter gaps)", + "Missing data error" : { + "comment" : "Reason Loop could not run: data it needs was missing", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد بيانات CGM %1$@ – %2$@ و %3$@" + "value" : "خطأ بيانات مفقودة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Žádná data CGM %1$@ – %2$@ a %3$@" + "value" : "Chyba chybějících dat" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data %1$@ – %2$@ og %3$@" + "value" : "Fejl: manglende data" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine CGM-Daten %1$@ – %2$@ und %3$@" + "value" : "Fehler: fehlende Daten" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin datos del CGM %1$@ – %2$@ y %3$@" + "value" : "Error de datos faltantes" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei CGM-dataa %1$@ – %2$@ ja %3$@" + "value" : "Tietoja puuttuu" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune donnée CGM %1$@ – %2$@ et %3$@" + "value" : "Erreur de données manquantes" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין נתוני CGM %1$@ – %2$@ ו-%3$@" + "value" : "שגיאת נתונים חסרים" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs CGM-adat %1$@ – %2$@, és %3$@" + "value" : "Hiányzó adat" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessun dato CGM %1$@ – %2$@ e %3$@" + "value" : "Errore dati mancanti" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "CGMデータなし %1$@ – %2$@(%3$@)" + "value" : "データ欠落エラー" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data %1$@ – %2$@ og %3$@" + "value" : "Feil: manglende data" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen CGM-gegevens %1$@ – %2$@ en %3$@" + "value" : "Fout: ontbrekende gegevens" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak danych CGM %1$@ – %2$@ oraz %3$@" + "value" : "Błąd brakujących danych" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Sem dados do CGM %1$@ – %2$@ e %3$@" + "value" : "Erro de dados ausentes" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Fără date CGM %1$@ – %2$@ și %3$@" + "value" : "Eroare: date lipsă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нет данных CGM %1$@ – %2$@ и ещё %3$@" + "value" : "Ошибка: нет данных" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Žiadne údaje CGM %1$@ – %2$@ a %3$@" + "value" : "Chyba chýbajúcich údajov" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inga CGM-data %1$@ – %2$@ och %3$@" + "value" : "Data saknas" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "CGM verisi yok %1$@ – %2$@ ve %3$@" + "value" : "Eksik veri hatası" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Немає даних CGM %1$@ – %2$@ і ще %3$@" + "value" : "Помилка: немає даних" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không có dữ liệu CGM %1$@ – %2$@ và %3$@" + "value" : "Lỗi thiếu dữ liệu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无 CGM 数据 %1$@ – %2$@,%3$@" + "value" : "数据缺失错误" } } } }, - "No CGM data in this period" : { - "comment" : "Trends chart placeholder when the period has no glucose readings", + "Needs 10 days of CGM" : { + "comment" : "Trends GMI tile detail when fewer than 10 days of CGM data are available", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد بيانات CGM في هذه الفترة" + "value" : "يحتاج إلى 10 أيام من CGM" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Žádná data CGM v tomto období" + "value" : "Vyžaduje 10 dní CGM" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data i denne periode" + "value" : "Kræver 10 dages CGM" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine CGM-Daten in diesem Zeitraum" + "value" : "Benötigt 10 Tage CGM" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin datos del CGM en este período" + "value" : "Requiere 10 días de CGM" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei CGM-dataa tällä jaksolla" + "value" : "Vaatii 10 päivää CGM-dataa" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune donnée CGM sur cette période" + "value" : "Nécessite 10 jours de CGM" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין נתוני CGM בתקופה זו" + "value" : "נדרשים 10 ימי CGM" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs CGM-adat ebben az időszakban" + "value" : "10 nap CGM-adat szükséges" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessun dato CGM in questo periodo" + "value" : "Servono 10 giorni di CGM" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "この期間のCGMデータなし" + "value" : "CGMデータが10日分必要" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data i denne perioden" + "value" : "Krever 10 dager med CGM" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen CGM-gegevens in deze periode" + "value" : "Vereist 10 dagen CGM" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak danych CGM w tym okresie" + "value" : "Wymaga 10 dni CGM" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Sem dados do CGM neste período" + "value" : "Requer 10 dias de CGM" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Fără date CGM în această perioadă" + "value" : "Necesită 10 zile de CGM" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нет данных CGM за этот период" + "value" : "Нужно 10 дней CGM" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Žiadne údaje CGM v tomto období" + "value" : "Vyžaduje 10 dní CGM" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inga CGM-data under denna period" + "value" : "Kräver 10 dagars CGM" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu dönemde CGM verisi yok" + "value" : "10 günlük CGM gerekir" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Немає даних CGM за цей період" + "value" : "Потрібно 10 днів CGM" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không có dữ liệu CGM trong khoảng thời gian này" + "value" : "Cần 10 ngày dữ liệu CGM" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "此时段无 CGM 数据" + "value" : "需要 10 天 CGM 数据" } } } }, - "No CGM data on this day" : { - "comment" : "Chart note: there are no glucose readings for the whole day", + "Next period" : { + "comment" : "VoiceOver label for the Trends button that moves forward one period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد بيانات CGM في هذا اليوم" + "value" : "الفترة التالية" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Žádná data CGM v tento den" + "value" : "Další období" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data denne dag" + "value" : "Næste periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine CGM-Daten an diesem Tag" + "value" : "Nächster Zeitraum" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin datos del CGM este día" + "value" : "Período siguiente" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei CGM-dataa tänä päivänä" + "value" : "Seuraava jakso" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune donnée CGM ce jour-là" + "value" : "Période suivante" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין נתוני CGM ביום זה" + "value" : "התקופה הבאה" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs CGM-adat ezen a napon" + "value" : "Következő időszak" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessun dato CGM in questo giorno" + "value" : "Periodo successivo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "この日のCGMデータなし" + "value" : "次の期間" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data denne dagen" + "value" : "Neste periode" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen CGM-gegevens op deze dag" + "value" : "Volgende periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak danych CGM tego dnia" + "value" : "Następny okres" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Sem dados do CGM neste dia" + "value" : "Próximo período" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Fără date CGM în această zi" + "value" : "Perioada următoare" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нет данных CGM за этот день" + "value" : "Следующий период" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Žiadne údaje CGM v tento deň" + "value" : "Ďalšie obdobie" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inga CGM-data denna dag" + "value" : "Nästa period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu günde CGM verisi yok" + "value" : "Sonraki dönem" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Немає даних CGM за цей день" + "value" : "Наступний період" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không có dữ liệu CGM trong ngày này" + "value" : "Khoảng thời gian sau" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "这一天无 CGM 数据" + "value" : "下一时段" } } } }, - "No CGM data yet today" : { - "comment" : "Chart note: there are no glucose readings so far today", + "No CGM data" : { + "comment" : "Trends Average tile detail when the period has no glucose readings", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد بيانات CGM اليوم حتى الآن" + "value" : "لا توجد بيانات CGM" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Dnes zatím žádná data CGM" + "value" : "Žádná data CGM" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data endnu i dag" + "value" : "Ingen CGM-data" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Heute noch keine CGM-Daten" + "value" : "Keine CGM-Daten" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Aún no hay datos del CGM hoy" + "value" : "Sin datos del CGM" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei vielä CGM-dataa tänään" + "value" : "Ei CGM-dataa" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune donnée CGM aujourd'hui pour l'instant" + "value" : "Aucune donnée CGM" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין עדיין נתוני CGM היום" + "value" : "אין נתוני CGM" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Ma még nincs CGM-adat" + "value" : "Nincs CGM-adat" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Ancora nessun dato CGM oggi" + "value" : "Nessun dato CGM" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "今日はまだCGMデータなし" + "value" : "CGMデータなし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data ennå i dag" + "value" : "Ingen CGM-data" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vandaag nog geen CGM-gegevens" + "value" : "Geen CGM-gegevens" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dziś jeszcze brak danych CGM" + "value" : "Brak danych CGM" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Ainda sem dados do CGM hoje" + "value" : "Sem dados do CGM" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Încă nu există date CGM azi" + "value" : "Fără date CGM" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сегодня пока нет данных CGM" + "value" : "Нет данных CGM" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Dnes zatiaľ žiadne údaje CGM" + "value" : "Žiadne údaje CGM" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inga CGM-data än i dag" + "value" : "Inga CGM-data" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bugün henüz CGM verisi yok" + "value" : "CGM verisi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Сьогодні ще немає даних CGM" + "value" : "Немає даних CGM" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Hôm nay chưa có dữ liệu CGM" + "value" : "Không có dữ liệu CGM" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "今天尚无 CGM 数据" + "value" : "无 CGM 数据" } } } }, - "No gaps: Loop ran closed loop all through this period." : { - "comment" : "Trends Gaps list when Loop was in closed loop for the whole period", + "No CGM data %1$@ – %2$@" : { + "comment" : "Chart note: a gap in glucose readings. (1: start time; 2: end time)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا فجوات: عمل Loop بالحلقة المغلقة طوال هذه الفترة." + "value" : "لا توجد بيانات CGM %1$@ – %2$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Žádné výpadky: Loop běžel v uzavřené smyčce celé období." + "value" : "Žádná data CGM %1$@ – %2$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen huller: Loop kørte lukket Loop i hele perioden." + "value" : "Ingen CGM-data %1$@ – %2$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine Lücken: Loop lief im gesamten Zeitraum im Closed Loop." + "value" : "Keine CGM-Daten %1$@ – %2$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin interrupciones: Loop funcionó en circuito cerrado durante todo el período." + "value" : "Sin datos del CGM %1$@ – %2$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei katkoja: Loop toimi suljetulla säädöllä koko jakson ajan." + "value" : "Ei CGM-dataa %1$@ – %2$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune interruption : Loop est resté en boucle fermée toute la période." + "value" : "Aucune donnée CGM %1$@ – %2$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין פערים: Loop פעל ב-Closed Loop לאורך כל התקופה." + "value" : "אין נתוני CGM %1$@ – %2$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs kimaradás: a Loop az egész időszakban zárt hurokban futott." + "value" : "Nincs CGM-adat %1$@ – %2$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessuna interruzione: Loop è rimasto in loop chiuso per tutto il periodo." + "value" : "Nessun dato CGM %1$@ – %2$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "中断なし:この期間中、Loopは常にクローズドループで動作しました。" + "value" : "CGMデータなし %1$@ – %2$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen hull: Loop kjørte lukket Loop gjennom hele perioden." + "value" : "Ingen CGM-data %1$@ – %2$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen onderbrekingen: Loop draaide de hele periode in actieve loop." + "value" : "Geen CGM-gegevens %1$@ – %2$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak przerw: Loop działał w pętli zamkniętej przez cały okres." + "value" : "Brak danych CGM %1$@ – %2$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Sem interrupções: o Loop ficou em ciclo fechado durante todo o período." + "value" : "Sem dados do CGM %1$@ – %2$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Fără întreruperi: Loop a rulat în buclă închisă toată perioada." + "value" : "Fără date CGM %1$@ – %2$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Пропусков нет: Loop работал в замкнутом цикле весь период." + "value" : "Нет данных CGM %1$@ – %2$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Žiadne výpadky: Loop bežal v uzavretej slučke celé obdobie." + "value" : "Žiadne údaje CGM %1$@ – %2$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inga luckor: Loop körde sluten loop under hela perioden." + "value" : "Inga CGM-data %1$@ – %2$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Boşluk yok: Loop bu dönem boyunca kapalı döngüde çalıştı." + "value" : "CGM verisi yok %1$@ – %2$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Пропусків немає: Loop працював у замкнутому циклі весь період." + "value" : "Немає даних CGM %1$@ – %2$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không gián đoạn: Loop chạy vòng lặp kín suốt khoảng thời gian này." + "value" : "Không có dữ liệu CGM %1$@ – %2$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无中断:此时段内 Loop 一直以闭环运行。" + "value" : "无 CGM 数据 %1$@ – %2$@" } } } }, - "No insulin recorded in this period" : { - "comment" : "Trends Insulin per day chart placeholder when no insulin was delivered in the period", + "No CGM data %1$@ – %2$@ and %3$@" : { + "comment" : "Chart note: the longest gap in glucose readings, plus the shorter ones. (1: start time; 2: end time; 3: number of shorter gaps, e.g. 2 shorter gaps)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لم يُسجَّل أنسولين في هذه الفترة" + "value" : "لا توجد بيانات CGM %1$@ – %2$@ و %3$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "V tomto období nebyl zaznamenán žádný inzulín" + "value" : "Žádná data CGM %1$@ – %2$@ a %3$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Intet insulin registreret i denne periode" + "value" : "Ingen CGM-data %1$@ – %2$@ og %3$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kein Insulin in diesem Zeitraum erfasst" + "value" : "Keine CGM-Daten %1$@ – %2$@ und %3$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "No se registró insulina en este período" + "value" : "Sin datos del CGM %1$@ – %2$@ y %3$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei kirjattua insuliinia tällä jaksolla" + "value" : "Ei CGM-dataa %1$@ – %2$@ ja %3$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune insuline enregistrée sur cette période" + "value" : "Aucune donnée CGM %1$@ – %2$@ et %3$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "לא נרשם אינסולין בתקופה זו" + "value" : "אין נתוני CGM %1$@ – %2$@ ו-%3$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs rögzített inzulin ebben az időszakban" + "value" : "Nincs CGM-adat %1$@ – %2$@, és %3$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessuna insulina registrata in questo periodo" + "value" : "Nessun dato CGM %1$@ – %2$@ e %3$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "この期間のインスリン記録なし" + "value" : "CGMデータなし %1$@ – %2$@(%3$@)" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen insulin registrert i denne perioden" + "value" : "Ingen CGM-data %1$@ – %2$@ og %3$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen insuline vastgelegd in deze periode" + "value" : "Geen CGM-gegevens %1$@ – %2$@ en %3$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak zapisanej insuliny w tym okresie" + "value" : "Brak danych CGM %1$@ – %2$@ oraz %3$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Nenhuma insulina registrada neste período" + "value" : "Sem dados do CGM %1$@ – %2$@ e %3$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nicio insulină înregistrată în această perioadă" + "value" : "Fără date CGM %1$@ – %2$@ și %3$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "За этот период инсулин не записан" + "value" : "Нет данных CGM %1$@ – %2$@ и ещё %3$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "V tomto období nebol zaznamenaný žiadny inzulín" + "value" : "Žiadne údaje CGM %1$@ – %2$@ a %3$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inget insulin registrerat under denna period" + "value" : "Inga CGM-data %1$@ – %2$@ och %3$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu dönemde kaydedilmiş insülin yok" + "value" : "CGM verisi yok %1$@ – %2$@ ve %3$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "За цей період інсулін не записано" + "value" : "Немає даних CGM %1$@ – %2$@ і ще %3$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không có insulin được ghi lại trong khoảng thời gian này" + "value" : "Không có dữ liệu CGM %1$@ – %2$@ và %3$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "此时段内无胰岛素记录" + "value" : "无 CGM 数据 %1$@ – %2$@,%3$@" } } } }, - "No loop runs kept" : { - "comment" : "Trends Loop active tile detail when no Loop run history is available for the period", + "No CGM data in this period" : { + "comment" : "Trends chart placeholder when the period has no glucose readings", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد تشغيلات Loop محفوظة" + "value" : "لا توجد بيانات CGM في هذه الفترة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Žádné uložené běhy" + "value" : "Žádná data CGM v tomto období" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen Loop-kørsler gemt" + "value" : "Ingen CGM-data i denne periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine Loop-Durchläufe gespeichert" + "value" : "Keine CGM-Daten in diesem Zeitraum" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "No se guardaron ejecuciones de Loop" + "value" : "Sin datos del CGM en este período" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei tallennettuja Loop-ajoja" + "value" : "Ei CGM-dataa tällä jaksolla" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune exécution de Loop conservée" + "value" : "Aucune donnée CGM sur cette période" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "לא נשמרו הרצות Loop" + "value" : "אין נתוני CGM בתקופה זו" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs megőrzött Loop-futás" + "value" : "Nincs CGM-adat ebben az időszakban" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessuna esecuzione di Loop conservata" + "value" : "Nessun dato CGM in questo periodo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Loop実行記録なし" + "value" : "この期間のCGMデータなし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen Loop-kjøringer lagret" + "value" : "Ingen CGM-data i denne perioden" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen loop-runs bewaard" + "value" : "Geen CGM-gegevens in deze periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak zapisanych cykli" + "value" : "Brak danych CGM w tym okresie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Nenhuma execução do Loop guardada" + "value" : "Sem dados do CGM neste período" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nu sunt păstrate rulări Loop" + "value" : "Fără date CGM în această perioadă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нет сохранённых запусков" + "value" : "Нет данных CGM за этот период" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Žiadne uložené behy" + "value" : "Žiadne údaje CGM v tomto období" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inga loopkörningar sparade" + "value" : "Inga CGM-data under denna period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Saklanan Loop çalışması yok" + "value" : "Bu dönemde CGM verisi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Немає збережених запусків" + "value" : "Немає даних CGM за цей період" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không lưu lần chạy Loop nào" + "value" : "Không có dữ liệu CGM trong khoảng thời gian này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无 Loop 运行记录" + "value" : "此时段无 CGM 数据" } } } }, - "No loop runs recorded in this period" : { - "comment" : "Trends Loop status placeholder when there is no Loop run history for the period", + "No CGM data on this day" : { + "comment" : "Chart note: there are no glucose readings for the whole day", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لم تُسجَّل تشغيلات Loop في هذه الفترة" + "value" : "لا توجد بيانات CGM في هذا اليوم" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "V tomto období nebyly zaznamenány žádné běhy Loop" + "value" : "Žádná data CGM v tento den" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen Loop-kørsler registreret i denne periode" + "value" : "Ingen CGM-data denne dag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine Loop-Durchläufe in diesem Zeitraum erfasst" + "value" : "Keine CGM-Daten an diesem Tag" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "No se registraron ejecuciones de Loop en este período" + "value" : "Sin datos del CGM este día" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei Loop-ajoja tällä jaksolla" + "value" : "Ei CGM-dataa tänä päivänä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune exécution de Loop enregistrée sur cette période" + "value" : "Aucune donnée CGM ce jour-là" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "לא נרשמו הרצות Loop בתקופה זו" + "value" : "אין נתוני CGM ביום זה" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs rögzített Loop-futás ebben az időszakban" + "value" : "Nincs CGM-adat ezen a napon" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessuna esecuzione di Loop registrata in questo periodo" + "value" : "Nessun dato CGM in questo giorno" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "この期間のLoop実行記録なし" + "value" : "この日のCGMデータなし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen Loop-kjøringer registrert i denne perioden" + "value" : "Ingen CGM-data denne dagen" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen loop-runs vastgelegd in deze periode" + "value" : "Geen CGM-gegevens op deze dag" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak zapisanych cykli Loop w tym okresie" + "value" : "Brak danych CGM tego dnia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Nenhuma execução do Loop registrada neste período" + "value" : "Sem dados do CGM neste dia" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nicio rulare Loop înregistrată în această perioadă" + "value" : "Fără date CGM în această zi" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "За этот период запуски Loop не записаны" + "value" : "Нет данных CGM за этот день" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "V tomto období neboli zaznamenané žiadne behy Loop" + "value" : "Žiadne údaje CGM v tento deň" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inga loopkörningar registrerade under denna period" + "value" : "Inga CGM-data denna dag" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu dönemde kaydedilmiş Loop çalışması yok" + "value" : "Bu günde CGM verisi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "За цей період запуски Loop не записано" + "value" : "Немає даних CGM за цей день" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không có lần chạy Loop nào được ghi lại trong khoảng thời gian này" + "value" : "Không có dữ liệu CGM trong ngày này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "此时段内无 Loop 运行记录" + "value" : "这一天无 CGM 数据" } } } }, - "Not enough days for a pattern: %1$@ of 5" : { - "comment" : "Trends Daily pattern footnote when fewer than 5 days have readings; the chart shows raw readings instead. (1: days with readings)", + "No CGM data yet today" : { + "comment" : "Chart note: there are no glucose readings so far today", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "أيام غير كافية لنمط: %1$@ من 5" + "value" : "لا توجد بيانات CGM اليوم حتى الآن" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Málo dní pro profil: %1$@ z 5" + "value" : "Dnes zatím žádná data CGM" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ikke nok dage til et mønster: %1$@ af 5" + "value" : "Ingen CGM-data endnu i dag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nicht genug Tage für ein Profil: %1$@ von 5" + "value" : "Heute noch keine CGM-Daten" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "No hay suficientes días para un patrón: %1$@ de 5" + "value" : "Aún no hay datos del CGM hoy" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Liian vähän päiviä profiiliin: %1$@ / 5" + "value" : "Ei vielä CGM-dataa tänään" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Pas assez de jours pour un profil : %1$@ sur 5" + "value" : "Aucune donnée CGM aujourd'hui pour l'instant" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין מספיק ימים לדפוס: %1$@ מתוך 5" + "value" : "אין עדיין נתוני CGM היום" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs elég nap a mintázathoz: %1$@ / 5" + "value" : "Ma még nincs CGM-adat" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Giorni insufficienti per un andamento: %1$@ su 5" + "value" : "Ancora nessun dato CGM oggi" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "パターン作成には日数不足:%1$@ / 5日" + "value" : "今日はまだCGMデータなし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ikke nok dager for et mønster: %1$@ av 5" + "value" : "Ingen CGM-data ennå i dag" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Niet genoeg dagen voor een patroon: %1$@ van 5" + "value" : "Vandaag nog geen CGM-gegevens" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Za mało dni na profil: %1$@ z 5" + "value" : "Dziś jeszcze brak danych CGM" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Dias insuficientes para um padrão: %1$@ de 5" + "value" : "Ainda sem dados do CGM hoje" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Prea puține zile pentru un tipar: %1$@ din 5" + "value" : "Încă nu există date CGM azi" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Мало дней для профиля: %1$@ из 5" + "value" : "Сегодня пока нет данных CGM" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Málo dní pre profil: %1$@ z 5" + "value" : "Dnes zatiaľ žiadne údaje CGM" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "För få dagar för ett mönster: %1$@ av 5" + "value" : "Inga CGM-data än i dag" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Örüntü için yeterli gün yok: %1$@ / 5" + "value" : "Bugün henüz CGM verisi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Замало днів для профілю: %1$@ з 5" + "value" : "Сьогодні ще немає даних CGM" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không đủ ngày để tạo mẫu: %1$@ / 5" + "value" : "Hôm nay chưa có dữ liệu CGM" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "天数不足以生成模式:%1$@ / 5" + "value" : "今天尚无 CGM 数据" } } } }, - "Now" : { - "comment" : "Title at the top of the Now screen, showing current glucose and Loop status Title of the Now tab in the redesigned interface, showing current glucose and Loop status", + "No insulin recorded in this period" : { + "comment" : "Trends Insulin per day chart placeholder when no insulin was delivered in the period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الآن" + "value" : "لم يُسجَّل أنسولين في هذه الفترة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nyní" + "value" : "V tomto období nebyl zaznamenán žádný inzulín" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Nu" + "value" : "Intet insulin registreret i denne periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Jetzt" + "value" : "Kein Insulin in diesem Zeitraum erfasst" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Ahora" + "value" : "No se registró insulina en este período" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Nyt" + "value" : "Ei kirjattua insuliinia tällä jaksolla" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Maintenant" + "value" : "Aucune insuline enregistrée sur cette période" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "עכשיו" + "value" : "לא נרשם אינסולין בתקופה זו" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Most" + "value" : "Nincs rögzített inzulin ebben az időszakban" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Ora" + "value" : "Nessuna insulina registrata in questo periodo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "現在" + "value" : "この期間のインスリン記録なし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Nå" + "value" : "Ingen insulin registrert i denne perioden" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Nu" + "value" : "Geen insuline vastgelegd in deze periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Teraz" + "value" : "Brak zapisanej insuliny w tym okresie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Agora" + "value" : "Nenhuma insulina registrada neste período" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Acum" + "value" : "Nicio insulină înregistrată în această perioadă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сейчас" + "value" : "За этот период инсулин не записан" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Teraz" + "value" : "V tomto období nebol zaznamenaný žiadny inzulín" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Nu" + "value" : "Inget insulin registrerat under denna period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Şimdi" + "value" : "Bu dönemde kaydedilmiş insülin yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Зараз" + "value" : "За цей період інсулін не записано" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Hiện tại" + "value" : "Không có insulin được ghi lại trong khoảng thời gian này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "当前" + "value" : "此时段内无胰岛素记录" } } } }, - "Open loop / manual" : { - "comment" : "Trends Loop status: time closed loop was switched off", + "No loop runs kept" : { + "comment" : "Trends Loop active tile detail when no Loop run history is available for the period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حلقة مفتوحة / يدوي" + "value" : "لا توجد تشغيلات Loop محفوظة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Otevřená smyčka / ručně" + "value" : "Žádné uložené běhy" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Åbent Loop / manuel" + "value" : "Ingen Loop-kørsler gemt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Open Loop / manuell" + "value" : "Keine Loop-Durchläufe gespeichert" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Circuito abierto / manual" + "value" : "No se guardaron ejecuciones de Loop" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Avoin säätö / manuaalinen" + "value" : "Ei tallennettuja Loop-ajoja" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Boucle ouverte / manuel" + "value" : "Aucune exécution de Loop conservée" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "Open Loop / ידני" + "value" : "לא נשמרו הרצות Loop" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nyitott hurok / kézi" + "value" : "Nincs megőrzött Loop-futás" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Loop aperto / manuale" + "value" : "Nessuna esecuzione di Loop conservata" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "オープンループ/手動" + "value" : "Loop実行記録なし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Åpen Loop / manuell" + "value" : "Ingen Loop-kjøringer lagret" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Open loop / handmatig" + "value" : "Geen loop-runs bewaard" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pętla otwarta / ręcznie" + "value" : "Brak zapisanych cykli" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Ciclo aberto / manual" + "value" : "Nenhuma execução do Loop guardada" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Buclă deschisă / manual" + "value" : "Nu sunt păstrate rulări Loop" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Открытый цикл / вручную" + "value" : "Нет сохранённых запусков" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Otvorená slučka / ručne" + "value" : "Žiadne uložené behy" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Öppen loop / manuell" + "value" : "Inga loopkörningar sparade" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Açık döngü / manuel" + "value" : "Saklanan Loop çalışması yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Відкритий цикл / вручну" + "value" : "Немає збережених запусків" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Vòng lặp mở / thủ công" + "value" : "Không lưu lần chạy Loop nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "开环 / 手动" + "value" : "无 Loop 运行记录" } } } }, - "Opens the full day" : { - "comment" : "VoiceOver hint for the Today so far card on the Now screen", + "No loop runs recorded in this period" : { + "comment" : "Trends Loop status placeholder when there is no Loop run history for the period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يفتح اليوم كاملًا" + "value" : "لم تُسجَّل تشغيلات Loop في هذه الفترة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Otevře celý den" + "value" : "V tomto období nebyly zaznamenány žádné běhy Loop" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Åbner hele dagen" + "value" : "Ingen Loop-kørsler registreret i denne periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Öffnet den ganzen Tag" + "value" : "Keine Loop-Durchläufe in diesem Zeitraum erfasst" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Abre el día completo" + "value" : "No se registraron ejecuciones de Loop en este período" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Avaa koko päivän" + "value" : "Ei Loop-ajoja tällä jaksolla" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Ouvre la journée complète" + "value" : "Aucune exécution de Loop enregistrée sur cette période" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "פותח את היום המלא" + "value" : "לא נרשמו הרצות Loop בתקופה זו" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Megnyitja a teljes napot" + "value" : "Nincs rögzített Loop-futás ebben az időszakban" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Apre il giorno completo" + "value" : "Nessuna esecuzione di Loop registrata in questo periodo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "1日全体を開きます" + "value" : "この期間のLoop実行記録なし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Åpner hele dagen" + "value" : "Ingen Loop-kjøringer registrert i denne perioden" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Opent de hele dag" + "value" : "Geen loop-runs vastgelegd in deze periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Otwiera cały dzień" + "value" : "Brak zapisanych cykli Loop w tym okresie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Abre o dia completo" + "value" : "Nenhuma execução do Loop registrada neste período" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Deschide ziua completă" + "value" : "Nicio rulare Loop înregistrată în această perioadă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Открывает весь день" + "value" : "За этот период запуски Loop не записаны" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Otvorí celý deň" + "value" : "V tomto období neboli zaznamenané žiadne behy Loop" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Öppnar hela dagen" + "value" : "Inga loopkörningar registrerade under denna period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tüm günü açar" + "value" : "Bu dönemde kaydedilmiş Loop çalışması yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Відкриває весь день" + "value" : "За цей період запуски Loop не записано" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mở toàn bộ ngày" + "value" : "Không có lần chạy Loop nào được ghi lại trong khoảng thời gian này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "打开全天视图" + "value" : "此时段内无 Loop 运行记录" } } } }, - "PDF report" : { - "comment" : "Trends export menu item: share a PDF report of the period", + "Not enough days for a pattern: %1$@ of 5" : { + "comment" : "Trends Daily pattern footnote when fewer than 5 days have readings; the chart shows raw readings instead. (1: days with readings)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تقرير PDF" + "value" : "أيام غير كافية لنمط: %1$@ من 5" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zpráva PDF" + "value" : "Málo dní pro profil: %1$@ z 5" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "PDF-rapport" + "value" : "Ikke nok dage til et mønster: %1$@ af 5" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "PDF-Bericht" + "value" : "Nicht genug Tage für ein Profil: %1$@ von 5" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Informe PDF" + "value" : "No hay suficientes días para un patrón: %1$@ de 5" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "PDF-raportti" + "value" : "Liian vähän päiviä profiiliin: %1$@ / 5" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Rapport PDF" + "value" : "Pas assez de jours pour un profil : %1$@ sur 5" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דוח PDF" + "value" : "אין מספיק ימים לדפוס: %1$@ מתוך 5" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "PDF-jelentés" + "value" : "Nincs elég nap a mintázathoz: %1$@ / 5" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Report PDF" + "value" : "Giorni insufficienti per un andamento: %1$@ su 5" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "PDFレポート" + "value" : "パターン作成には日数不足:%1$@ / 5日" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "PDF-rapport" + "value" : "Ikke nok dager for et mønster: %1$@ av 5" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "PDF-rapport" + "value" : "Niet genoeg dagen voor een patroon: %1$@ van 5" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Raport PDF" + "value" : "Za mało dni na profil: %1$@ z 5" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Relatório PDF" + "value" : "Dias insuficientes para um padrão: %1$@ de 5" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Raport PDF" + "value" : "Prea puține zile pentru un tipar: %1$@ din 5" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Отчёт PDF" + "value" : "Мало дней для профиля: %1$@ из 5" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Správa PDF" + "value" : "Málo dní pre profil: %1$@ z 5" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "PDF-rapport" + "value" : "För få dagar för ett mönster: %1$@ av 5" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "PDF raporu" + "value" : "Örüntü için yeterli gün yok: %1$@ / 5" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Звіт PDF" + "value" : "Замало днів для профілю: %1$@ з 5" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Báo cáo PDF" + "value" : "Không đủ ngày để tạo mẫu: %1$@ / 5" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "PDF 报告" + "value" : "天数不足以生成模式:%1$@ / 5" } } } }, - "per day, weekly average" : { - "comment" : "Trends Insulin per day card subtitle when each bar is a week's average daily total", + "Now" : { + "comment" : "Title at the top of the Now screen, showing current glucose and Loop status Title of the Now tab in the redesigned interface, showing current glucose and Loop status", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يوميًا، متوسط أسبوعي" + "value" : "الآن" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "za den, týdenní průměr" + "value" : "Nyní" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "pr. dag, ugegennemsnit" + "value" : "Nu" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "pro Tag, Wochendurchschnitt" + "value" : "Jetzt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "por día, promedio semanal" + "value" : "Ahora" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "päivässä, viikkokeskiarvo" + "value" : "Nyt" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "par jour, moyenne hebdomadaire" + "value" : "Maintenant" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "ליום, ממוצע שבועי" + "value" : "עכשיו" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "naponta, heti átlag" + "value" : "Most" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "al giorno, media settimanale" + "value" : "Ora" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "1日あたり、週平均" + "value" : "現在" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "per dag, ukesgjennomsnitt" + "value" : "Nå" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "per dag, weekgemiddelde" + "value" : "Nu" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "dziennie, średnia tygodniowa" + "value" : "Teraz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "por dia, média semanal" + "value" : "Agora" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "pe zi, medie săptămânală" + "value" : "Acum" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "в день, среднее за неделю" + "value" : "Сейчас" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "za deň, týždenný priemer" + "value" : "Teraz" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "per dag, veckomedel" + "value" : "Nu" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "günlük, haftalık ortalama" + "value" : "Şimdi" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "на день, середнє за тиждень" + "value" : "Зараз" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "mỗi ngày, trung bình theo tuần" + "value" : "Hiện tại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "每日,按周平均" + "value" : "当前" } } } }, - "Previous day" : { - "comment" : "Accessibility label for the day page button that steps back a day", + "Open loop / manual" : { + "comment" : "Trends Loop status: time closed loop was switched off", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اليوم السابق" + "value" : "حلقة مفتوحة / يدوي" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Předchozí den" + "value" : "Otevřená smyčka / ručně" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Forrige dag" + "value" : "Åbent Loop / manuel" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Vorheriger Tag" + "value" : "Open Loop / manuell" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Día anterior" + "value" : "Circuito abierto / manual" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Edellinen päivä" + "value" : "Avoin säätö / manuaalinen" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Jour précédent" + "value" : "Boucle ouverte / manuel" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "היום הקודם" + "value" : "Open Loop / ידני" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Előző nap" + "value" : "Nyitott hurok / kézi" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Giorno precedente" + "value" : "Loop aperto / manuale" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "前日" + "value" : "オープンループ/手動" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Forrige dag" + "value" : "Åpen Loop / manuell" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vorige dag" + "value" : "Open loop / handmatig" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Poprzedni dzień" + "value" : "Pętla otwarta / ręcznie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Dia anterior" + "value" : "Ciclo aberto / manual" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ziua anterioară" + "value" : "Buclă deschisă / manual" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Предыдущий день" + "value" : "Открытый цикл / вручную" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Predchádzajúci deň" + "value" : "Otvorená slučka / ručne" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Föregående dag" + "value" : "Öppen loop / manuell" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Önceki gün" + "value" : "Açık döngü / manuel" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Попередній день" + "value" : "Відкритий цикл / вручну" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Ngày trước" + "value" : "Vòng lặp mở / thủ công" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "前一天" + "value" : "开环 / 手动" } } } }, - "Previous period" : { - "comment" : "VoiceOver label for the Trends button that moves back one period", + "per day, weekly average" : { + "comment" : "Trends Insulin per day card subtitle when each bar is a week's average daily total", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الفترة السابقة" + "value" : "يوميًا، متوسط أسبوعي" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Předchozí období" + "value" : "za den, týdenní průměr" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Forrige periode" + "value" : "pr. dag, ugegennemsnit" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Vorheriger Zeitraum" + "value" : "pro Tag, Wochendurchschnitt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Período anterior" + "value" : "por día, promedio semanal" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Edellinen jakso" + "value" : "päivässä, viikkokeskiarvo" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Période précédente" + "value" : "par jour, moyenne hebdomadaire" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "התקופה הקודמת" + "value" : "ליום, ממוצע שבועי" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Előző időszak" + "value" : "naponta, heti átlag" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Periodo precedente" + "value" : "al giorno, media settimanale" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "前の期間" + "value" : "1日あたり、週平均" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Forrige periode" + "value" : "per dag, ukesgjennomsnitt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vorige periode" + "value" : "per dag, weekgemiddelde" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Poprzedni okres" + "value" : "dziennie, średnia tygodniowa" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Período anterior" + "value" : "por dia, média semanal" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Perioada anterioară" + "value" : "pe zi, medie săptămânală" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Предыдущий период" + "value" : "в день, среднее за неделю" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Predchádzajúce obdobie" + "value" : "za deň, týždenný priemer" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Föregående period" + "value" : "per dag, veckomedel" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Önceki dönem" + "value" : "günlük, haftalık ortalama" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Попередній період" + "value" : "на день, середнє за тиждень" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Khoảng thời gian trước" + "value" : "mỗi ngày, trung bình theo tuần" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "上一时段" + "value" : "每日,按周平均" } } } }, - "Projected" : { - "comment" : "Chart legend: dashed lines show Loop's projection of insulin and carbs on board", + "Previous period" : { + "comment" : "VoiceOver label for the Trends button that moves back one period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "المتوقع" + "value" : "الفترة السابقة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Předpověď" + "value" : "Předchozí období" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Forventet" + "value" : "Forrige periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Prognose" + "value" : "Vorheriger Zeitraum" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Proyectado" + "value" : "Período anterior" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ennuste" + "value" : "Edellinen jakso" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Projeté" + "value" : "Période précédente" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "צפי" + "value" : "התקופה הקודמת" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Előrevetített" + "value" : "Előző időszak" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Previsto" + "value" : "Periodo precedente" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "予測" + "value" : "前の期間" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Beregnet" + "value" : "Forrige periode" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verwacht" + "value" : "Vorige periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Prognozowane" + "value" : "Poprzedni okres" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Projetado" + "value" : "Período anterior" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Proiectat" + "value" : "Perioada anterioară" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Прогнозируемые" + "value" : "Предыдущий период" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Predpoveď" + "value" : "Predchádzajúce obdobie" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Prognos" + "value" : "Föregående period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Öngörülen" + "value" : "Önceki dönem" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Прогнозовані" + "value" : "Попередній період" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Dự kiến" + "value" : "Khoảng thời gian trước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "预计" + "value" : "上一时段" } } } @@ -17485,149 +14625,6 @@ } } }, - "Today" : { - "comment" : "Day page title when showing today", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "اليوم" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dnes" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "I dag" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Heute" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hoy" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tänään" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aujourd'hui" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "היום" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ma" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Oggi" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "今日" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "I dag" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vandaag" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dziś" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hoje" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Azi" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сегодня" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dnes" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "I dag" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bugün" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сьогодні" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hôm nay" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "今天" - } - } - } - }, "today so far" : { "comment" : "Trends Basal and bolus card subtitle when the figures are today's total so far", "localizations" : { @@ -18629,149 +15626,6 @@ } } }, - "Touch and hold a chart to see exact values." : { - "comment" : "Hint under the day page charts explaining how to scrub them", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "المس مخططًا مع الاستمرار لرؤية القيم الدقيقة." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Podržte prst na grafu a uvidíte přesné hodnoty." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tryk og hold på et diagram for at se præcise værdier." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Halte ein Diagramm gedrückt, um genaue Werte zu sehen." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mantén pulsado un gráfico para ver los valores exactos." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pidä kaaviota painettuna nähdäksesi tarkat arvot." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Touchez un graphique de manière prolongée pour voir les valeurs exactes." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "גע והחזק תרשים כדי לראות ערכים מדויקים." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Érints meg és tarts lenyomva egy diagramot a pontos értékekért." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tieni premuto su un grafico per vedere i valori esatti." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "グラフを長押しすると正確な値が表示されます。" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Trykk og hold på et diagram for å se nøyaktige verdier." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Houd een grafiek ingedrukt om exacte waarden te zien." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dotknij i przytrzymaj wykres, aby zobaczyć dokładne wartości." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Toque e segure um gráfico para ver os valores exatos." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Țineți apăsat pe un grafic pentru a vedea valorile exacte." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нажмите и удерживайте график, чтобы увидеть точные значения." - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Podržte prst na grafe a uvidíte presné hodnoty." - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tryck och håll på ett diagram för att se exakta värden." - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tam değerleri görmek için bir grafiğe dokunup basılı tutun." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Торкніться й утримуйте графік, щоб побачити точні значення." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chạm và giữ biểu đồ để xem giá trị chính xác." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "按住图表可查看精确数值。" - } - } - } - }, "Trends" : { "comment" : "Title of the Trends tab Title of the Trends tab in the redesigned interface", "localizations" : { diff --git a/Loop/Redesign/DayCharts.swift b/Loop/Redesign/DayCharts.swift index 83e2032d74..3f24f63466 100644 --- a/Loop/Redesign/DayCharts.swift +++ b/Loop/Redesign/DayCharts.swift @@ -70,6 +70,43 @@ enum DayAxis { } } +// MARK: - DayTimelineCard + +/// A day on one timeline, for a one-day period on Trends. +struct DayTimelineCard: View { + let title: String + let history: DayHistory + let unit: GlucoseUnitStyle + let targetBands: [TargetBand] + var now: Date = Date() + var height: CGFloat = 230 + + static var todayTitle: String { + NSLocalizedString("Today so far", comment: "Title of the card on the Now screen showing today's insulin and carbs") + } + + static let legend: [LegendItem] = [ + LegendItem(swatch: .solid(P.bolus), label: NSLocalizedString("Bolus", comment: "Chart legend: solid bars are manual boluses")), + LegendItem(swatch: .hatch(P.bolus), label: NSLocalizedString("Automatic", comment: "Chart legend: hatched bars are automatic boluses given by Loop")), + LegendItem(swatch: .basal, label: NSLocalizedString("Basal given", comment: "Chart legend: filled area is basal insulin actually delivered")), + LegendItem(swatch: .dash(P.basalScheduled), label: NSLocalizedString("Scheduled", comment: "Chart legend: dashed line is the scheduled basal rate")), + ] + + var body: some View { + LoopCard { + CardHeader(title: title) { + HStack(spacing: 4) { + Text(LoopFormat.totalUnits(history.totalUnits)).foregroundStyle(P.insulinInk) + Text(verbatim: "·") + Text(LoopFormat.grams(history.carbGrams)).foregroundStyle(P.carbsInk) + } + } + InsulinTimelineView(history: history, unit: unit, targetBands: targetBands, now: now, height: height) + ChartLegend(items: Self.legend) + } + } +} + // MARK: - InsulinTimeline /// Carbs, glucose and insulin for one day on one shared time axis. @@ -292,12 +329,6 @@ struct InsulinTimelineView: View { if let glucose = nearest(history.glucose.map { ($0.date, $0.mgdl) }, to: date, within: 10 * 60) { rows.append(.init(label: NSLocalizedString("Glucose", comment: "Chart callout label for the glucose reading at the touched time"), value: "\(unit.format(glucose)) \(unit.unitName)", color: P.glucoseInk)) } - if let iob = nearest(history.iob.map { ($0.date, $0.value) }, to: date, within: 10 * 60) { - rows.append(.init(label: NSLocalizedString("IOB", comment: "Abbreviation for insulin on board (active insulin)"), value: LoopFormat.doseUnits(iob), color: P.insulinInk)) - } - if let cob = nearest(history.cob.map { ($0.date, $0.value) }, to: date, within: 10 * 60) { - rows.append(.init(label: NSLocalizedString("COB", comment: "Abbreviation for carbs on board (active carbohydrate)"), value: LoopFormat.grams(cob), color: P.carbsInk)) - } if let segment = LoopHistoryMath.basalRate(at: date, in: history.basal) { let value = segment.kind == .suspended ? NSLocalizedString("Suspended", comment: "Chart callout value: insulin delivery was suspended at the touched time") @@ -421,128 +452,3 @@ struct ScrubCallout: View { .overlay(RoundedRectangle(cornerRadius: LoopDesign.Radius.sm).strokeBorder(P.separator)) } } - -/// Insulin and carbs on board over the same window, each against its own coloured axis. -struct IOBCOBChartView: View { - let iob: [TimedValue] - let cob: [TimedValue] - let window: DateInterval - var now: Date = Date() - var height: CGFloat = 140 - - @State private var selection: Date? - - private var iobMax: Double { max(1, (iob.map(\.value).max() ?? 0) * 1.1) } - private var iobMin: Double { min(0, (iob.map(\.value).min() ?? 0) * 1.1) } - private var cobMax: Double { max(10, (cob.map(\.value).max() ?? 0) * 1.1) } - - /// COB drawn on the IOB scale. - private func scaled(cob value: Double) -> Double { value / cobMax * iobMax } - - private var iobTicks: [Double] { - let step = Self.niceStep(iobMax / 2) - return stride(from: (iobMin / step).rounded(.down) * step, through: iobMax, by: step).map { $0 } - } - - private var cobTicks: [Double] { - let step = Self.niceStep(cobMax / 2) - return stride(from: 0, through: cobMax, by: step).map { $0 } - } - - static func niceStep(_ raw: Double) -> Double { - guard raw > 0 else { return 1 } - let magnitude = pow(10, floor(log10(raw))) - let normalized = raw / magnitude - let nice: Double = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10 - return nice * magnitude - } - - var body: some View { - Chart { - RuleMark(y: .value("Zero", 0)) - .foregroundStyle(P.inkMuted) - .lineStyle(StrokeStyle(lineWidth: 1)) - - ForEach(Array(cob.enumerated()), id: \.offset) { _, value in - AreaMark(x: .value("Time", value.date), yStart: .value("Base", 0), yEnd: .value("COB", scaled(cob: value.value)), series: .value("Series", "cob-fill")) - .foregroundStyle(P.carbs.opacity(0.14)) - LineMark(x: .value("Time", value.date), y: .value("COB", scaled(cob: value.value)), series: .value("Series", value.date > now ? "cob-projected" : "cob")) - .foregroundStyle(P.carbs) - .lineStyle(StrokeStyle(lineWidth: 1.5, dash: value.date > now ? [4, 3] : [])) - } - ForEach(Array(iob.enumerated()), id: \.offset) { _, value in - AreaMark(x: .value("Time", value.date), yStart: .value("Base", 0), yEnd: .value("IOB", value.value), series: .value("Series", "iob-fill")) - .foregroundStyle(P.insulin.opacity(0.14)) - LineMark(x: .value("Time", value.date), y: .value("IOB", value.value), series: .value("Series", value.date > now ? "iob-projected" : "iob")) - .foregroundStyle(P.insulin) - .lineStyle(StrokeStyle(lineWidth: 1.5, dash: value.date > now ? [4, 3] : [])) - } - if window.contains(now) { - RuleMark(x: .value("Now", now)) - .foregroundStyle(P.inkMuted) - .lineStyle(StrokeStyle(lineWidth: 1, dash: [1, 3])) - } - if let selection { - RuleMark(x: .value("Selected", selection)).foregroundStyle(P.ink.opacity(0.6)) - } - } - .frame(height: height) - .chartXScale(domain: window.start...window.end) - .chartYScale(domain: iobMin...iobMax) - .chartXAxis { - AxisMarks(values: DayAxis.quarterDayMarks(in: window)) { value in - AxisGridLine().foregroundStyle(P.grid) - AxisValueLabel { - if let date = value.as(Date.self) { Text(DayAxis.hourLabel(date)) } - } - .font(LoopDesign.Typeface.caption) - .foregroundStyle(P.inkMuted) - } - } - .chartYAxis { - AxisMarks(position: .leading, values: iobTicks) { value in - AxisGridLine().foregroundStyle(P.grid) - AxisValueLabel { - if let v = value.as(Double.self) { Text(v.formatted(.number.precision(.fractionLength(0...1)))) } - } - .font(LoopDesign.Typeface.caption) - .foregroundStyle(P.insulinInk) - } - AxisMarks(position: .trailing, values: cobTicks.map { scaled(cob: $0) }) { value in - AxisValueLabel { - if let v = value.as(Double.self) { Text((v / iobMax * cobMax).formatted(.number.precision(.fractionLength(0)))) } - } - .font(LoopDesign.Typeface.caption) - .foregroundStyle(P.carbsInk) - } - } - .chartOverlay { proxy in - ScrubOverlay(proxy: proxy, selection: $selection, clampTo: window) - } - .overlay(alignment: .top) { - if let selection { - ScrubCallout(rows: scrubRows(at: selection)).allowsHitTesting(false) - } - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(accessibilitySummary) - } - - private func scrubRows(at date: Date) -> [ScrubCallout.Row] { - var rows: [ScrubCallout.Row] = [.init(label: nil, value: LoopFormat.time(date), color: P.ink)] - if let value = nearest(iob.map { ($0.date, $0.value) }, to: date, within: 10 * 60) { - rows.append(.init(label: NSLocalizedString("IOB", comment: "Abbreviation for insulin on board (active insulin)"), value: LoopFormat.doseUnits(value), color: P.insulinInk)) - } - if let value = nearest(cob.map { ($0.date, $0.value) }, to: date, within: 10 * 60) { - rows.append(.init(label: NSLocalizedString("COB", comment: "Abbreviation for carbs on board (active carbohydrate)"), value: LoopFormat.grams(value), color: P.carbsInk)) - } - return rows - } - - private var accessibilitySummary: String { - let iobNow = nearest(iob.map { ($0.date, $0.value) }, to: now, within: 10 * 60) - let cobNow = nearest(cob.map { ($0.date, $0.value) }, to: now, within: 10 * 60) - let unknown = NSLocalizedString("unknown", comment: "VoiceOver value when active insulin or carbs at the current time isn't known") - return String(format: NSLocalizedString("Active insulin %1$@, active carbs %2$@", comment: "VoiceOver summary of the active insulin and carbs chart. (1: insulin on board, e.g. 1.25 U, or unknown; 2: carbs on board, e.g. 20 g, or unknown)"), iobNow.map(LoopFormat.doseUnits) ?? unknown, cobNow.map(LoopFormat.grams) ?? unknown) - } -} diff --git a/Loop/Redesign/DayDetailView.swift b/Loop/Redesign/DayDetailView.swift deleted file mode 100644 index 0f8186b71b..0000000000 --- a/Loop/Redesign/DayDetailView.swift +++ /dev/null @@ -1,198 +0,0 @@ -// -// DayDetailView.swift -// Loop -// -// Copyright © 2026 LoopKit Authors. All rights reserved. -// - -import SwiftUI - -private typealias P = LoopDesign.Palette - -/// One full day on InsulinTimeline with IOB and COB under it, stepping a day at a time. -struct DayDetailView: View { - let provider: LoopHistoryProvider - - @State private var day: Date - @State private var history: DayHistory? - @State private var targetBands: [TargetBand] = [] - @State private var now = Date() - @State private var loadError: String? - - init(provider: LoopHistoryProvider, day: Date) { - self.provider = provider - _day = State(initialValue: Calendar.current.startOfDay(for: day)) - } - - private var window: DateInterval { - DateInterval(start: day, end: Calendar.current.date(byAdding: .day, value: 1, to: day)!) - } - - private var isToday: Bool { Calendar.current.isDate(day, inSameDayAs: now) } - - var body: some View { - ScrollView { - VStack(alignment: .leading, spacing: LoopDesign.Space.s6) { - dayStepper - if let history { - LoopCard { - CardHeader(title: NSLocalizedString("Insulin", comment: "Title of the day page card showing insulin, carbs and glucose on one timeline")) { - HStack(spacing: 4) { - Text(LoopFormat.totalUnits(history.totalUnits)).foregroundStyle(P.insulinInk) - Text("·") - Text(LoopFormat.grams(history.carbGrams)).foregroundStyle(P.carbsInk) - } - } - InsulinTimelineView(history: history, unit: provider.unitStyle, targetBands: targetBands, - now: now, height: 300) - ChartLegend(items: DayTotalsTiles.legend) - if history.source == .health { - Text(NSLocalizedString("From Apple Health: Loop keeps 7 days, so this day has no temp basal or schedule detail.", comment: "Day page note when the day is older than Loop's 7-day history and was read from Apple Health")) - .font(LoopDesign.Typeface.footnote) - .foregroundStyle(P.inkMuted) - } - if #available(iOS 18.0, *) { - Text(NSLocalizedString("Touch and hold a chart to see exact values.", comment: "Hint under the day page charts explaining how to scrub them")) - .font(LoopDesign.Typeface.footnote) - .foregroundStyle(P.inkMuted) - } - } - LoopCard { - CardHeader(title: NSLocalizedString("Basal and bolus", comment: "Trends card title: insulin split into basal and bolus")) - InsulinSplitView(split: history.insulinSplit, partial: isToday) - ValueTile(label: NSLocalizedString("Carbs", comment: "Label of a tile showing the day's total carbohydrate")) { - Text(LoopFormat.grams(history.carbGrams)) - .font(LoopDesign.Typeface.statValueSmall) - .foregroundStyle(P.carbsInk) - } - .fixedSize(horizontal: false, vertical: true) - } - LoopCard { - CardHeader(title: NSLocalizedString("Active insulin and carbs", comment: "Title of the day page card charting insulin and carbs on board")) - if history.iob.isEmpty && history.cob.isEmpty { - ChartPlaceholder(message: history.source == .health - ? NSLocalizedString("Loop only works out active insulin and carbs for the days it keeps", comment: "Day page placeholder for active insulin and carbs on a day older than Loop's 7-day history") - : NSLocalizedString("No active insulin or carbs on this day", comment: "Day page placeholder when there was no insulin or carbs on board all day")) - } else { - IOBCOBChartView(iob: history.iob, cob: history.cob, window: window, now: now) - } - ChartLegend(items: [ - LegendItem(swatch: .solid(P.insulin), label: NSLocalizedString("IOB (U, left)", comment: "Chart legend: insulin on board in units, on the left axis")), - LegendItem(swatch: .solid(P.carbs), label: NSLocalizedString("COB (g, right)", comment: "Chart legend: carbs on board in grams, on the right axis")), - LegendItem(swatch: .dash(P.inkMuted), label: NSLocalizedString("Projected", comment: "Chart legend: dashed lines show Loop's projection of insulin and carbs on board")), - ]) - } - } else if let loadError { - ChartPlaceholder(message: String(format: NSLocalizedString("Couldn't load this day: %1$@", comment: "Day page error when the day's history can't be read. (1: system error description)"), loadError)) - } else { - ProgressView() - .frame(maxWidth: .infinity, minHeight: 300) - } - } - .padding(LoopDesign.Space.s4) - } - .background(P.surface) - .navigationTitle(isToday ? NSLocalizedString("Today", comment: "Day page title when showing today") : LoopFormat.weekdayDay(day)) - .navigationBarTitleDisplayMode(.inline) - .task(id: day) { - now = Date() - let requestedWindow = window - history = nil - loadError = nil - do { - async let historyFetch = provider.dayHistory(requestedWindow) - async let targetsFetch = provider.glucoseTargetBands(in: requestedWindow) - let (result, targets) = try await (historyFetch, targetsFetch) - guard !Task.isCancelled, window == requestedWindow else { return } - history = result - targetBands = targets - } catch { - guard !Task.isCancelled, window == requestedWindow else { return } - loadError = error.localizedDescription - } - } - } - - private var dayStepper: some View { - HStack { - StepButton(systemName: "chevron.left", label: NSLocalizedString("Previous day", comment: "Accessibility label for the day page button that steps back a day")) { - day = Calendar.current.date(byAdding: .day, value: -1, to: day)! - } - Spacer() - Text(LoopFormat.day(day)) - .font(LoopDesign.Typeface.headline) - .foregroundStyle(P.ink) - Spacer() - StepButton(systemName: "chevron.right", label: NSLocalizedString("Next day", comment: "Accessibility label for the day page button that steps forward a day"), enabled: !isToday) { - day = Calendar.current.date(byAdding: .day, value: 1, to: day)! - } - } - } -} - -/// A day on one timeline with its totals, for a one-day period on Trends. -struct DayTimelineCard: View { - let title: String - let history: DayHistory - let unit: GlucoseUnitStyle - let targetBands: [TargetBand] - var now: Date = Date() - var height: CGFloat = 230 - /// The bolus, basal and carb tiles; off where a Basal and bolus card follows. - var showsTotals = true - - static var todayTitle: String { - NSLocalizedString("Today so far", comment: "Title of the card on the Now screen showing today's insulin and carbs") - } - - var body: some View { - LoopCard { - CardHeader(title: title) { - HStack(spacing: 4) { - Text(LoopFormat.totalUnits(history.totalUnits)).foregroundStyle(P.insulinInk) - Text(verbatim: "·") - Text(LoopFormat.grams(history.carbGrams)).foregroundStyle(P.carbsInk) - } - } - InsulinTimelineView(history: history, unit: unit, targetBands: targetBands, now: now, height: height) - if showsTotals { - DayTotalsTiles(history: history) - } - ChartLegend(items: DayTotalsTiles.legend) - } - } -} - -/// Bolus, basal and carb totals under a timeline. -struct DayTotalsTiles: View { - let history: DayHistory - - static let legend: [LegendItem] = [ - LegendItem(swatch: .solid(P.bolus), label: NSLocalizedString("Bolus", comment: "Chart legend: solid bars are manual boluses")), - LegendItem(swatch: .hatch(P.bolus), label: NSLocalizedString("Automatic", comment: "Chart legend: hatched bars are automatic boluses given by Loop")), - LegendItem(swatch: .basal, label: NSLocalizedString("Basal given", comment: "Chart legend: filled area is basal insulin actually delivered")), - LegendItem(swatch: .dash(P.basalScheduled), label: NSLocalizedString("Scheduled", comment: "Chart legend: dashed line is the scheduled basal rate")), - ] - - var body: some View { - AdaptiveRow { - ValueTile(label: NSLocalizedString("Bolus", comment: "Label of a tile showing the day's total bolus insulin")) { - Text(LoopFormat.totalUnits(history.bolusUnits)) - .font(LoopDesign.Typeface.statValueSmall) - .foregroundStyle(P.insulinInk) - } - ValueTile(label: NSLocalizedString("Basal", comment: "Label of a tile showing the current basal rate (Now screen) or the day's basal insulin total (Today so far totals)")) { - // Plain ink: basal is the pale fill on the chart, and orange here read as bolus. - Text(LoopFormat.totalUnits(history.basalUnits)) - .font(LoopDesign.Typeface.statValueSmall) - .foregroundStyle(P.ink) - } - ValueTile(label: NSLocalizedString("Carbs", comment: "Label of a tile showing the day's total carbohydrate")) { - Text(LoopFormat.grams(history.carbGrams)) - .font(LoopDesign.Typeface.statValueSmall) - .foregroundStyle(P.carbsInk) - } - } - .fixedSize(horizontal: false, vertical: true) - } -} diff --git a/Loop/Redesign/LoopHistoryModels.swift b/Loop/Redesign/LoopHistoryModels.swift index 3fc71c5b47..f443ecbbdf 100644 --- a/Loop/Redesign/LoopHistoryModels.swift +++ b/Loop/Redesign/LoopHistoryModels.swift @@ -99,11 +99,6 @@ struct CarbEvent: Equatable, Identifiable { } } -struct TimedValue: Equatable { - let date: Date - let value: Double -} - /// Everything known about one day or reporting period, for InsulinTimeline. struct DayHistory { var window: DateInterval @@ -112,8 +107,6 @@ struct DayHistory { var basal: [BasalSegment] = [] var scheduledBasal: [ScheduledBasalPoint] = [] var carbs: [CarbEvent] = [] - var iob: [TimedValue] = [] - var cob: [TimedValue] = [] /// Where the data came from, so older days can say they are missing Loop's own detail. var source: HistorySource = .loop @@ -132,8 +125,6 @@ struct DayHistory { history.boluses = boluses.filter { inside($0.date) } history.basal = basal.compactMap { $0.clamped(to: interval) } history.carbs = carbs.filter { inside($0.date) } - history.iob = iob.filter { inside($0.date) } - history.cob = cob.filter { inside($0.date) } return history } } diff --git a/Loop/Redesign/LoopHistoryProvider.swift b/Loop/Redesign/LoopHistoryProvider.swift index 84cc3da600..c0c771bc21 100644 --- a/Loop/Redesign/LoopHistoryProvider.swift +++ b/Loop/Redesign/LoopHistoryProvider.swift @@ -87,8 +87,8 @@ final class LoopHistoryProvider { // MARK: - Days - /// Everything in `window`; IOB and COB only when asked, as they are the slowest part. - func dayHistory(_ window: DateInterval, includeOnBoard: Bool = true) async throws -> DayHistory { + /// Glucose, insulin and carbs in `window`. + func dayHistory(_ window: DateInterval) async throws -> DayHistory { var history = DayHistory(window: window) let boundary = localHistoryStart @@ -100,12 +100,6 @@ final class LoopHistoryProvider { history.glucose = try await glucose (history.boluses, history.basal, history.scheduledBasal) = try await insulin history.carbs = try await carbs - if includeOnBoard { - async let iob = localIOB(local) - async let cob = localCOB(local) - history.iob = try await iob - history.cob = try await cob - } } if window.start < boundary { @@ -297,14 +291,6 @@ final class LoopHistoryProvider { } } - private func localIOB(_ interval: DateInterval) async throws -> [TimedValue] { - try await loopDataManager.insulinOnBoardTimeline(in: interval).map { TimedValue(date: $0.startDate, value: $0.value) } - } - - private func localCOB(_ interval: DateInterval) async throws -> [TimedValue] { - try await loopDataManager.carbsOnBoardTimeline(in: interval).map { TimedValue(date: $0.startDate, value: $0.quantity.doubleValue(for: .gram)) } - } - // MARK: - Apple Health private func healthSamples(_ type: HKQuantityType, _ interval: DateInterval, options: HKQueryOptions = .strictStartDate) async throws -> [HKQuantitySample] { @@ -412,7 +398,7 @@ final class LoopModeHistory { } } -// MARK: - Loop's on-board history +// MARK: - Historical targets extension LoopDataManager { /// A historical chart must use the profile and presets from that time, rather than repeat today's targets. @@ -442,23 +428,4 @@ extension LoopDataManager { return bands.filter { $0.end > $0.start }.sorted { $0.start < $1.start } } - /// Insulin on board across `interval`, from the doses and basal history Loop keeps, as `iobValues` is for the display window. - func insulinOnBoardTimeline(in interval: DateInterval) async throws -> [InsulinValue] { - let dosesStart = interval.start.addingTimeInterval(-InsulinMath.defaultInsulinActivityDuration) - let doses = try await doseStore.getNormalizedDoseEntries(start: dosesStart, end: interval.end) - .map { $0.simpleDose(with: insulinModel(for: $0.insulinType)) } - let basalStart = min(dosesStart, doses.map(\.startDate).min() ?? dosesStart) - let basal = try await settingsProvider.getBasalHistory(startDate: basalStart, endDate: interval.end) - let overrides = temporaryPresetsManager.presetHistory.getOverrideHistory(startDate: basalStart, endDate: interval.end) - return doses.annotated(with: overrides.applyBasal(over: basal).algorithmSchedule()) - .insulinOnBoardTimeline(from: interval.start, to: interval.end) - .map(\.loopKit) - } - - /// Carbs on board across `interval`, using the same absorption review as Loop's carb screen. - func carbsOnBoardTimeline(in interval: DateInterval) async throws -> [CarbValue] { - let review = try await fetchCarbAbsorptionReview(start: interval.start.addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval), end: interval.end) - return review.carbStatuses.dynamicCarbsOnBoard(from: interval.start, to: interval.end, absorptionModel: carbAbsorptionModel.model) - .map(\.loopKit) - } } diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift index c98336a72c..37c4b49d97 100644 --- a/Loop/Redesign/TrendsComponents.swift +++ b/Loop/Redesign/TrendsComponents.swift @@ -566,12 +566,20 @@ struct InsulinDailyTotalsView: View { /// Calculated from the original daily totals, even when the bars are weekly averages. let average: Double var height: CGFloat = 170 - var onSelectDay: ((Date) -> Void)? = nil @Environment(\.colorScheme) private var scheme private var yMax: Double { let top = max(totals.map(\.total).max() ?? 0, average) - return max(10, IOBCOBChartView.niceStep(top / 4) * 5) + return max(10, Self.niceStep(top / 4) * 5) + } + + /// 1, 2 or 5 times a power of ten, at or above `raw`. + static func niceStep(_ raw: Double) -> Double { + guard raw > 0 else { return 1 } + let magnitude = pow(10, floor(log10(raw))) + let normalized = raw / magnitude + let nice: Double = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10 + return nice * magnitude } var body: some View { @@ -629,19 +637,6 @@ struct InsulinDailyTotalsView: View { AxisValueLabel().font(LoopDesign.Typeface.caption).foregroundStyle(P.inkMuted) } } - .chartOverlay { proxy in - GeometryReader { geometry in - Rectangle().fill(.clear).contentShape(Rectangle()) - .onTapGesture { location in - guard let onSelectDay, !weekly, let frame = proxy.plotFrame else { return } - let x = location.x - geometry[frame].origin.x - if let date: Date = proxy.value(atX: x), - let day = totals.min(by: { abs($0.day.addingTimeInterval(43200).timeIntervalSince(date)) < abs($1.day.addingTimeInterval(43200).timeIntervalSince(date)) }) { - onSelectDay(day.day) - } - } - } - } .accessibilityElement(children: .ignore) .accessibilityLabel(String(format: weekly ? NSLocalizedString("Insulin per day, weekly average, average %1$@", comment: "VoiceOver label for the Trends Insulin per day chart when bars are weekly averages. (1: average insulin per day, e.g. 32.1 U)") @@ -754,35 +749,3 @@ struct LoopModeBarView: View { .accessibilityElement(children: .combine) } } - -struct LoopGapsList: View { - let spans: [LoopModeSpan] - - private var gaps: [LoopModeSpan] { - spans.filter { $0.mode != .closed }.sorted { $0.start > $1.start } - } - - var body: some View { - List { - if gaps.isEmpty { - Text(NSLocalizedString("No gaps: Loop ran closed loop all through this period.", comment: "Trends Gaps list when Loop was in closed loop for the whole period")) - .foregroundStyle(P.inkMuted) - } - ForEach(Array(gaps.enumerated()), id: \.offset) { _, gap in - HStack(alignment: .top, spacing: LoopDesign.Space.s3) { - Circle().fill(LoopModeBarView.color(gap.mode)).frame(width: 8, height: 8).padding(.top, 6) - VStack(alignment: .leading, spacing: 2) { - Text(gap.cause ?? LoopModeBarView.name(gap.mode)) - .font(LoopDesign.Typeface.body) - Text("\(gap.start.formatted(.dateTime.weekday(.abbreviated).day().month(.abbreviated).hour().minute())) · \(LoopFormat.duration(gap.duration))") - .font(LoopDesign.Typeface.footnote) - .foregroundStyle(P.inkMuted) - } - } - .accessibilityElement(children: .combine) - } - } - .navigationTitle(NSLocalizedString("Gaps", comment: "Trends Loop status button and screen title: list of times Loop wasn't in closed loop")) - .navigationBarTitleDisplayMode(.inline) - } -} diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift index 05e56b9881..39af08a282 100644 --- a/Loop/Redesign/TrendsScreen.swift +++ b/Loop/Redesign/TrendsScreen.swift @@ -98,7 +98,7 @@ final class TrendsModel: ObservableObject { // A single day is read to midnight, as Today so far is, so the scheduled basal still to come is drawn. let fetchWindow = oneDay ? DateInterval(start: interval.start, end: Calendar.current.date(byAdding: .day, value: 1, to: interval.start)!) : interval - async let historyFetch = provider.dayHistory(fetchWindow, includeOnBoard: false) + async let historyFetch = provider.dayHistory(fetchWindow) async let runsFetch = provider.loopRuns() var history = try await historyFetch let (runs, coverageStart) = try await runsFetch @@ -240,87 +240,23 @@ final class TrendsModel: ObservableObject { let total = String.localizedStringWithFormat(NSLocalizedString("%lld days", comment: "A number of days, e.g. a Trends period or how many days are averaged. (1: number of days)"), s.totals.count) return String(format: NSLocalizedString("%1$@ of %2$@", comment: "Trends: how many days of the period are averaged. (1: days averaged, a number; 2: days in the period, e.g. 14 days)"), LoopFormat.number(Double(insulinDays), decimals: 0), total) } - - // MARK: Export - - static func exportCSV(_ s: Summary) throws -> URL { - // The CSV header and values are deliberately not localised: fixed English columns and '.' decimals for data interchange. - var lines = ["date,mean_glucose_\(s.unit.isMmol ? "mmol_l" : "mg_dl"),readings,time_in_range_pct,basal_u,bolus_u,total_u,carbs_g,manual_bolus_u,auto_bolus_u"] - let calendar = Calendar.current - let byDay = Dictionary(grouping: s.glucose) { calendar.startOfDay(for: $0.date) } - for total in s.totals { - let readings = byDay[total.day] ?? [] - let mean = readings.isEmpty ? "" : String(format: "%.*f", locale: Locale(identifier: "en_US_POSIX"), - s.unit.isMmol ? 1 : 0, s.unit.value(readings.reduce(0) { $0 + $1.mgdl } / Double(readings.count))) - let tir = readings.isEmpty ? "" : String(format: "%.0f", (LoopHistoryMath.timeInRange(readings)[.inRange] ?? 0) * 100) - let date = Date.ISO8601FormatStyle(timeZone: .current).year().month().day().format(total.day) - let carbs = String(format: "%.0f", s.carbsByDay[total.day] ?? 0) - // Left blank when some of the day's bolus doesn't say whether it was manual or automatic. - let bolusSplit = total.split.isClassified ? String(format: "%.2f,%.2f", total.split.manualBolus, total.split.automaticBolus) : "," - lines.append("\(date),\(mean),\(readings.count),\(tir),\(String(format: "%.2f,%.2f,%.2f", total.basal, total.bolus, total.total)),\(carbs),\(bolusSplit)") - } - let url = FileManager.default.temporaryDirectory.appendingPathComponent(Self.fileName(s.interval) + ".csv") - try lines.joined(separator: "\n").write(to: url, atomically: true, encoding: .utf8) - return url - } - - func exportPDF(_ s: Summary) throws -> URL { - let url = FileManager.default.temporaryDirectory.appendingPathComponent(Self.fileName(s.interval) + ".pdf") - let report = TrendsReport(summary: s, tiles: tiles(s)) - .frame(width: 560) - .environment(\.colorScheme, .light) - let renderer = ImageRenderer(content: report) - renderer.proposedSize = ProposedViewSize(width: 560, height: nil) - var thrown: Error? - renderer.render { size, draw in - var box = CGRect(x: 0, y: 0, width: 612, height: max(792, size.height + 52)) - guard let consumer = CGDataConsumer(url: url as CFURL), let context = CGContext(consumer: consumer, mediaBox: &box, nil) else { - thrown = CocoaError(.fileWriteUnknown) - return - } - context.beginPDFPage(nil) - context.translateBy(x: 26, y: box.height - size.height - 26) - draw(context) - context.endPDFPage() - context.closePDF() - } - if let thrown { throw thrown } - return url - } - - private static func fileName(_ interval: DateInterval) -> String { - String(format: NSLocalizedString("Loop trends %1$@", comment: "File name of an exported Trends report or CSV, without the extension. (1: date range of the period)"), - fileStamp(interval)) - } - - private static func fileStamp(_ interval: DateInterval) -> String { - LoopFormat.range(interval.start, interval.end.addingTimeInterval(-1)).replacingOccurrences(of: " – ", with: " to ") - } } // MARK: - Screen struct TrendsView: View { @StateObject private var model: TrendsModel - @State private var path: [TrendsRoute] = [] - @State private var exportURL: URL? - @State private var exportError: String? /// Closes Trends when it is presented on its own; nil where Trends has nowhere to return to. private let done: (() -> Void)? - enum TrendsRoute: Hashable { - case day(Date) - case gaps - } - init(provider: LoopHistoryProvider, done: (() -> Void)? = nil) { _model = StateObject(wrappedValue: TrendsModel(provider: provider)) self.done = done } var body: some View { - NavigationStack(path: $path) { + NavigationStack { ScrollView { LazyVStack(alignment: .leading, spacing: LoopDesign.Space.s6, pinnedViews: [.sectionHeaders]) { Section { @@ -344,27 +280,10 @@ struct TrendsView: View { } } } - .navigationDestination(for: TrendsRoute.self) { route in - switch route { - case .day(let day): - DayDetailView(provider: model.provider, day: day) - case .gaps: - LoopGapsList(spans: model.summary?.loopSpans ?? []) - } - } } .onAppear { model.reload() } .onChange(of: model.period) { _, _ in model.reload() } .onChange(of: model.endDay) { _, _ in model.reload() } - .sheet(item: $exportURL) { url in - ShareSheet(items: [url]) - } - .alert(NSLocalizedString("Couldn't export", comment: "Alert title when exporting the Trends PDF or CSV fails"), - isPresented: Binding(get: { exportError != nil }, set: { if !$0 { exportError = nil } })) { - Button(NSLocalizedString("OK", comment: "Button to dismiss an alert"), role: .cancel) {} - } message: { - Text(exportError ?? "") - } } @ViewBuilder @@ -414,9 +333,7 @@ struct TrendsView: View { : NSLocalizedString("basal and bolus", comment: "Trends Insulin per day card subtitle when each bar is one day's basal and bolus")) } InsulinDailyTotalsView(totals: s.totals.count > 30 ? LoopHistoryMath.weekly(s.totals) : s.totals, - weekly: s.totals.count > 30, average: s.insulinPerDay) { day in - path.append(.day(day)) - } + weekly: s.totals.count > 30, average: s.insulinPerDay) ChartLegend(items: insulinLegend(s)) if let today = todaySoFar(s) { Footnote(today) @@ -424,11 +341,7 @@ struct TrendsView: View { } } LoopCard { - CardHeader(title: NSLocalizedString("Loop status", comment: "Trends card title: how long Loop spent in closed loop, limited or open loop")) { - Button(NSLocalizedString("Gaps", comment: "Trends Loop status button and screen title: list of times Loop wasn't in closed loop")) { path.append(.gaps) } - .foregroundStyle(P.accentInk) - .disabled(s.loopSpans.isEmpty) - } + CardHeader(title: NSLocalizedString("Loop status", comment: "Trends card title: how long Loop spent in closed loop, limited or open loop")) LoopModeBarView(spans: s.loopSpans) if let note = coverageNote(s) { Footnote(note) @@ -447,14 +360,8 @@ struct TrendsView: View { /// The same timeline as Today so far: glucose, every bolus and basal change, and carbs, through the day. private func dayCard(_ day: DayHistory, targets: [TargetBand], unit: GlucoseUnitStyle) -> some View { let isToday = Calendar.current.isDateInToday(day.window.start) - return Button { - path.append(.day(day.window.start)) - } label: { - DayTimelineCard(title: isToday ? DayTimelineCard.todayTitle : LoopFormat.weekdayDay(day.window.start), - history: day, unit: unit, targetBands: targets, height: 260, showsTotals: false) - } - .buttonStyle(.plain) - .accessibilityHint(NSLocalizedString("Opens the full day", comment: "VoiceOver hint for the Today so far card on the Now screen")) + return DayTimelineCard(title: isToday ? DayTimelineCard.todayTitle : LoopFormat.weekdayDay(day.window.start), + history: day, unit: unit, targetBands: targets, height: 260) } private func patternCard(_ s: TrendsModel.Summary) -> some View { @@ -471,13 +378,7 @@ struct TrendsView: View { private func summaryCard(_ s: TrendsModel.Summary) -> some View { LoopCard { - CardHeader(title: NSLocalizedString("Summary", comment: "Trends card title: the summary statistics tiles")) { - Menu(NSLocalizedString("Export", comment: "Trends menu title: export the period as a PDF report or CSV file")) { - Button(NSLocalizedString("PDF report", comment: "Trends export menu item: share a PDF report of the period")) { export { try model.exportPDF(s) } } - Button(NSLocalizedString("CSV, one row per day", comment: "Trends export menu item: share a CSV spreadsheet file with one row for each day")) { export { try TrendsModel.exportCSV(s) } } - } - .foregroundStyle(P.accentInk) - } + CardHeader(title: NSLocalizedString("Summary", comment: "Trends card title: the summary statistics tiles")) if let covered = s.cgmCoveredDays { Footnote(String(format: NSLocalizedString("CGM data covers %1$@.", comment: "Trends Summary note when glucose readings start partway through the period. (1: days covered, e.g. 5 of 14 days)"), daysOf(covered, s.totals.count))) @@ -511,14 +412,6 @@ struct TrendsView: View { LoopFormat.totalUnits(today.total), LoopFormat.totalUnits(usual.typical.median), LoopFormat.time(usual.at)) } - private func export(_ make: () throws -> URL) { - do { - exportURL = try make() - } catch { - exportError = error.localizedDescription - } - } - /// The figures are on the Basal and bolus card above, so the legend only names the marks. private func insulinLegend(_ s: TrendsModel.Summary) -> [LegendItem] { [ @@ -541,20 +434,6 @@ struct TrendsView: View { } } -extension URL: @retroactive Identifiable { - public var id: String { absoluteString } -} - -struct ShareSheet: UIViewControllerRepresentable { - let items: [Any] - - func makeUIViewController(context: Context) -> UIActivityViewController { - UIActivityViewController(activityItems: items, applicationActivities: nil) - } - - func updateUIViewController(_ controller: UIActivityViewController, context: Context) {} -} - /// A card's quiet closing line: a target, a caveat or a comparison. private struct Footnote: View { let text: String @@ -570,54 +449,3 @@ private struct Footnote: View { .fixedSize(horizontal: false, vertical: true) } } - -// MARK: - PDF report - -/// The Trends cards laid out for paper. -private struct TrendsReport: View { - let summary: TrendsModel.Summary - let tiles: [StatTile] - - var body: some View { - VStack(alignment: .leading, spacing: LoopDesign.Space.s4) { - VStack(alignment: .leading, spacing: 2) { - Text(NSLocalizedString("Loop trends", comment: "Title of the exported Trends PDF report")).font(.title2.bold()) - Text(LoopFormat.range(summary.interval.start, summary.interval.end.addingTimeInterval(-1))) - .font(.subheadline) - .foregroundStyle(P.inkMuted) - } - StatGridView(tiles: tiles) - HStack(alignment: .top, spacing: LoopDesign.Space.s4) { - VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { - Text(NSLocalizedString("Time in range", comment: "Trends card title: share of glucose readings in each range")).font(.headline) - TimeInRangeView(fractions: summary.timeInRange, unit: summary.unit) - } - .frame(maxWidth: .infinity) - } - if let day = summary.day { - VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { - Text(LoopFormat.weekdayDay(day.window.start)).font(.headline) - InsulinTimelineView(history: day, unit: summary.unit, targetBands: summary.dayTargets, height: 220) - ChartLegend(items: DayTotalsTiles.legend) - } - } else { - VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { - Text(NSLocalizedString("Daily pattern", comment: "Trends card title: the ambulatory glucose profile, glucose by time of day across the period")).font(.headline) - TrendsAGPChart(slots: summary.agp, readings: summary.glucose, days: summary.agpDays, unit: summary.unit, - targetRange: summary.targetRange, height: 160) - } - VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { - Text(NSLocalizedString("Insulin per day", comment: "Trends tile and card title: average total insulin per day")).font(.headline) - InsulinDailyTotalsView(totals: summary.totals.count > 30 ? LoopHistoryMath.weekly(summary.totals) : summary.totals, - weekly: summary.totals.count > 30, average: summary.insulinPerDay, height: 140) - } - } - VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { - Text(NSLocalizedString("Loop status", comment: "Trends card title: how long Loop spent in closed loop, limited or open loop")).font(.headline) - LoopModeBarView(spans: summary.loopSpans) - } - } - .padding(LoopDesign.Space.s2) - .background(Color.white) - } -} diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index e28ebdb7af..f6e9fa4949 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -725,32 +725,6 @@ class LoopDataManagerTests: XCTestCase { XCTAssertTrue(bands.isEmpty) } - func testHistoricalIOBUsesPresetAdjustedBasal() async throws { - settingsProvider.basalHistory = [AbsoluteScheduleValue(startDate: d(-.hours(6)), endDate: now, value: 1.0)] - let window = DateInterval(start: d(-.minutes(30)), end: now) - - for multiplier in [0.5, 1.5] { - let preset = TemporaryScheduleOverride( - context: .custom, - settings: TemporaryPresetSettings(targetRange: nil, insulinNeedsScaleFactor: multiplier), - startDate: d(-.hours(1)), - duration: .finite(.hours(2)), - enactTrigger: .local, - syncIdentifier: UUID() - ) - temporaryPresetsManager.presetHistory.recordOverride(preset, at: now) - doseStore.doseHistory = [DoseEntry(type: .tempBasal, startDate: window.start, endDate: window.end, - value: multiplier, unit: .unitsPerHour, decisionId: nil)] - - let timeline = try await loopDataManager.insulinOnBoardTimeline(in: window) - XCTAssertFalse(timeline.isEmpty) - for value in timeline { - XCTAssertEqual(value.value, 0, accuracy: 1e-9, - "Delivery matching a \(multiplier)× preset's basal must add no net IOB") - } - } - } - func testFetchDataWithHighInsulinNeedsPresetMitigation() async throws { var input = try await loopDataManager.fetchData(for: now) XCTAssertEqual(input.targetMgdl.count, 1) diff --git a/LoopTests/Models/LoopHistoryMathTests.swift b/LoopTests/Models/LoopHistoryMathTests.swift index baa7b68960..a5519a513a 100644 --- a/LoopTests/Models/LoopHistoryMathTests.swift +++ b/LoopTests/Models/LoopHistoryMathTests.swift @@ -291,33 +291,6 @@ class LoopHistoryMathTests: XCTestCase { XCTAssertEqual(LoopFormat.signed(0.0001), "+0.00") } - @MainActor - func testCSVExportsTenColumnsWithASCIINumbers() throws { - let day = Calendar.current.startOfDay(for: start) - let nextDay = day.addingTimeInterval(86400) - for isMmol in [true, false] { - var summary = TrendsModel.Summary(interval: DateInterval(start: day, duration: 2 * 86400), - unit: GlucoseUnitStyle(isMmol: isMmol)) - summary.glucose = [GlucosePoint(date: day, mgdl: 131.53286)] - summary.totals = [ - .init(day: day, split: InsulinSplit(basal: 1.25, manualBolus: 2, automaticBolus: 0.5)), - // From another app in Health: the bolus doesn't say whether it was manual or automatic. - .init(day: nextDay, split: InsulinSplit(basal: 1, unclassifiedBolus: 3)), - ] - summary.carbsByDay = [day: 45] - let url = try TrendsModel.exportCSV(summary) - defer { try? FileManager.default.removeItem(at: url) } - let rows = try String(contentsOf: url, encoding: .utf8).components(separatedBy: "\n") - XCTAssertEqual(rows.count, 3) - XCTAssertEqual(rows[0].components(separatedBy: ",").count, 10) - XCTAssertTrue(rows[0].hasSuffix(",carbs_g,manual_bolus_u,auto_bolus_u")) - let columns = rows[1].components(separatedBy: ",") - XCTAssertEqual(columns.count, 10) - XCTAssertEqual(Array(columns.dropFirst()), [isMmol ? "7.3" : "132", "1", "100", "1.25", "2.50", "3.75", "45", "2.00", "0.50"]) - XCTAssertEqual(Array(rows[2].components(separatedBy: ",").suffix(5)), ["3.00", "4.00", "0", "", ""]) - } - } - func testModeHistoryRetainsTheModeBeforeTheRetentionWindow() throws { let suite = "LoopHistoryMathTests-\(UUID().uuidString)" let defaults = try XCTUnwrap(UserDefaults(suiteName: suite)) From 1a52bcb5a6ce7890b23394322540f97bd24bacf3 Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:36 +0000 Subject: [PATCH 05/15] Show each Trends figure once Summary keeps glucose, carbs and boluses. Basal and bolus fold into the Insulin per day card, Loop active is only in Loop status, and "usually by now" appears once. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- Loop/Localizable.xcstrings | 948 ++------------------------- Loop/Redesign/TrendsComponents.swift | 21 +- Loop/Redesign/TrendsScreen.swift | 114 ++-- 3 files changed, 99 insertions(+), 984 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index b20a06f299..35c4f86002 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -1300,292 +1300,6 @@ } } }, - "%1$@%% basal" : { - "comment" : "Trends Insulin per day tile detail: share of insulin delivered as basal. (1: percentage number)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% القاعدية" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "bazál %1$@%%" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% basal" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% Basal" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% basal" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% basaalia" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% basal" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% Basal" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% basal" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% basale" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ベーサル %1$@%%" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% basal" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% basaal" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "baza %1$@%%" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% basal" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% bazală" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "базал %1$@%%" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "bazál %1$@%%" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% basal" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%%%1$@ bazal" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "базал %1$@%%" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% Basal" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "基础 %1$@%%" - } - } - } - }, - "%1$@%% basal · %2$@" : { - "comment" : "Trends Insulin per day tile detail when only some days are averaged. (1: basal share as a percentage number, 2: days averaged, e.g. 5 of 14 days)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% القاعدية · %2$@" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "bazál %1$@%% · %2$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% basal · %2$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% Basal · %2$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% basal · %2$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% basaalia · %2$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% basal · %2$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% Basal · %2$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% basal · %2$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% basale · %2$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ベーサル %1$@%% · %2$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% basal · %2$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% basaal · %2$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "baza %1$@%% · %2$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% basal · %2$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% bazală · %2$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "базал %1$@%% · %2$@" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "bazál %1$@%% · %2$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@ %% basal · %2$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%%%1$@ bazal · %2$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "базал %1$@%% · %2$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@%% Basal · %2$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "基础 %1$@%% · %2$@" - } - } - } - }, "%1$@%% CGM active" : { "comment" : "Trends Time in range card: share of the period with CGM readings. (1: percentage number)", "localizations" : { @@ -4609,145 +4323,145 @@ } } }, - "Average %1$@ U" : { - "comment" : "Trends Insulin per day legend: the dashed line showing average total insulin per day. (1: units)", + "average of %1$@" : { + "comment" : "Trends Insulin per day card subtitle when only some days of the period are averaged, such as a day still in progress. (1: days averaged, e.g. 13 of 14 days)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "المتوسط %1$@ وحدة" + "value" : "متوسط %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Průměr %1$@ U" + "value" : "průměr za %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Gennemsnit %1$@ E" + "value" : "gennemsnit af %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Durchschnitt %1$@ IE" + "value" : "Durchschnitt aus %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Promedio %1$@ U" + "value" : "promedio de %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Keskiarvo %1$@ U" + "value" : "keskiarvo, %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Moyenne %1$@ U" + "value" : "moyenne sur %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "ממוצע U %1$@" + "value" : "ממוצע של %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Átlag %1$@ U" + "value" : "átlag, %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Media %1$@ U" + "value" : "media su %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "平均 %1$@ 単位" + "value" : "%1$@の平均" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Gjennomsnitt %1$@ E" + "value" : "gjennomsnitt av %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gemiddeld %1$@ E" + "value" : "gemiddelde over %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Średnio %1$@ J" + "value" : "średnia z %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Média %1$@ U" + "value" : "média de %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Medie %1$@ U" + "value" : "medie pe %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Среднее %1$@ Ед." + "value" : "среднее за %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Priemer %1$@ j" + "value" : "priemer za %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Medel %1$@ E" + "value" : "genomsnitt av %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ortalama %1$@ Ü" + "value" : "%1$@ ortalaması" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Середнє %1$@ U" + "value" : "середнє за %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trung bình %1$@ U" + "value" : "trung bình của %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "平均 %1$@ U" + "value" : "%1$@的平均值" } } } @@ -5062,407 +4776,121 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Basal %1$@, %2$@ Prozent" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal %1$@, %2$@ por ciento" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basaali %1$@, %2$@ prosenttia" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal %1$@, %2$@ pour cent" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal %1$@, %2$@ אחוז" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal %1$@, %2$@ százalék" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basale %1$@, %2$@ percento" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ベーサル %1$@、%2$@ パーセント" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal %1$@, %2$@ prosent" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basaal %1$@, %2$@ procent" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Podstawowy %1$@, %2$@ procent" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal %1$@, %2$@ por cento" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bazală %1$@, %2$@ la sută" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Базал %1$@, %2$@ процентов" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bazál %1$@, %2$@ percent" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal %1$@, %2$@ procent" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bazal %1$@, yüzde %2$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Базал %1$@, %2$@ відсотків" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal %1$@, %2$@ phần trăm" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "基础 %1$@,百分之 %2$@" - } - } - } - }, - "Basal and bolus" : { - "comment" : "Trends and day page card title: insulin split into basal and bolus", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "القاعدية والبلعة" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bazál a bolus" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal og bolus" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal und Bolus" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal y bolo" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basaali ja bolus" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal et bolus" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal ו-Bolus" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal és bólus" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basale e bolo" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ベーサルとボーラス" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal og bolus" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basaal en bolus" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Insulina podstawowa i bolus" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal e bolus" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bazală și bolus" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Базал и болюс" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bazál a bolus" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal och bolus" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bazal ve bolus" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Базал і болюс" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal và bolus" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "基础与大剂量" - } - } - } - }, - "basal and bolus" : { - "comment" : "Trends Insulin per day card subtitle when each bar is one day's basal and bolus", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "القاعدية و Bolus" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "bazál a bolus" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "basal og bolus" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Basal und Bolus" + "value" : "Basal %1$@, %2$@ Prozent" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "basal y bolo" + "value" : "Basal %1$@, %2$@ por ciento" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "basaali ja bolus" + "value" : "Basaali %1$@, %2$@ prosenttia" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "basal et bolus" + "value" : "Basal %1$@, %2$@ pour cent" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "Basal ו-Bolus" + "value" : "Basal %1$@, %2$@ אחוז" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "basal és bólus" + "value" : "Basal %1$@, %2$@ százalék" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "basale e bolo" + "value" : "Basale %1$@, %2$@ percento" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ベーサルとボーラス" + "value" : "ベーサル %1$@、%2$@ パーセント" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "basal og bolus" + "value" : "Basal %1$@, %2$@ prosent" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "basaal en bolus" + "value" : "Basaal %1$@, %2$@ procent" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "baza i bolus" + "value" : "Podstawowy %1$@, %2$@ procent" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "basal e bolus" + "value" : "Basal %1$@, %2$@ por cento" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "bazală și bolus" + "value" : "Bazală %1$@, %2$@ la sută" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "базал и болюс" + "value" : "Базал %1$@, %2$@ процентов" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "bazál a bolus" + "value" : "Bazál %1$@, %2$@ percent" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "basal och bolus" + "value" : "Basal %1$@, %2$@ procent" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "bazal ve bolus" + "value" : "Bazal %1$@, yüzde %2$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "базал і болюс" + "value" : "Базал %1$@, %2$@ відсотків" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Basal và Bolus" + "value" : "Basal %1$@, %2$@ phần trăm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "基础和大剂量" + "value" : "基础 %1$@,百分之 %2$@" } } } @@ -9620,149 +9048,6 @@ } } }, - "Loop active" : { - "comment" : "Trends summary tile: share of the period Loop spent in closed loop", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop نشط" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktivní" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktiv" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktiv" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop activo" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktiivinen" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop actif" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop פעיל" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktív" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop attivo" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop稼働" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktiv" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop actief" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktywny" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop ativo" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop activ" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop активен" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktívny" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktiv" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aktif" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop активний" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop hoạt động" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop 运行" - } - } - } - }, "Loop didn't run" : { "comment" : "Reason shown for a stretch of time when Loop did not complete a run and recorded no error", "localizations" : { @@ -11908,149 +11193,6 @@ } } }, - "No loop runs kept" : { - "comment" : "Trends Loop active tile detail when no Loop run history is available for the period", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "لا توجد تشغيلات Loop محفوظة" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žádné uložené běhy" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen Loop-kørsler gemt" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Loop-Durchläufe gespeichert" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No se guardaron ejecuciones de Loop" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ei tallennettuja Loop-ajoja" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucune exécution de Loop conservée" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "לא נשמרו הרצות Loop" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs megőrzött Loop-futás" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nessuna esecuzione di Loop conservata" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop実行記録なし" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen Loop-kjøringer lagret" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen loop-runs bewaard" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brak zapisanych cykli" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nenhuma execução do Loop guardada" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nu sunt păstrate rulări Loop" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нет сохранённых запусков" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žiadne uložené behy" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inga loopkörningar sparade" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saklanan Loop çalışması yok" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Немає збережених запусків" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không lưu lần chạy Loop nào" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无 Loop 运行记录" - } - } - } - }, "No loop runs recorded in this period" : { "comment" : "Trends Loop status placeholder when there is no Loop run history for the period", "localizations" : { diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift index 37c4b49d97..c490d87e95 100644 --- a/Loop/Redesign/TrendsComponents.swift +++ b/Loop/Redesign/TrendsComponents.swift @@ -455,7 +455,7 @@ struct InsulinSplitBar: View { } } -/// The basal and bolus card body: the bar, units and shares, how the bolus divides, and the total. +/// Basal and bolus in units and shares, how the bolus divides, and the total. struct InsulinSplitView: View { let split: InsulinSplit /// Today so far, rather than a whole day or a per-day average. @@ -464,10 +464,16 @@ struct InsulinSplitView: View { var scheduledBasal: Double? /// Today against a usual day, e.g. "Usually 28.4 U by 19:46", under the total. var usual: String? + /// The split as one capsule, where no chart above already draws it. + var showsBar = true + /// The total is the dashed average line of the chart above, so it carries that line's swatch. + var averageLine = false var body: some View { VStack(alignment: .leading, spacing: LoopDesign.Space.s3) { - InsulinSplitBar(split: split) + if showsBar { + InsulinSplitBar(split: split) + } VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { row(.basal, NSLocalizedString("Basal", comment: "Basal and bolus row: insulin delivered as basal"), split.basal, split.basalPercent) if let scheduledBasal { @@ -487,7 +493,10 @@ struct InsulinSplitView: View { .padding(.leading, 10 + LoopDesign.Space.s2) } Divider().padding(.vertical, 2) - HStack(alignment: .firstTextBaseline) { + HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { + if averageLine { + LegendSwatch(swatch: .dash(LoopDesign.Palette.ink)).alignmentGuide(.firstTextBaseline) { $0[.bottom] + 3 } + } Text(partial ? NSLocalizedString("Total so far", comment: "Basal and bolus total row for today, which isn't over yet") : NSLocalizedString("Total", comment: "Basal and bolus total row: basal plus bolus")) @@ -609,14 +618,10 @@ struct InsulinDailyTotalsView: View { .foregroundStyle(Hatch.paint(P.bolus, scheme: scheme)) } } + // The average's value is the Total row under the chart. RuleMark(y: .value("Average", average)) .foregroundStyle(P.ink) .lineStyle(StrokeStyle(lineWidth: 1, dash: [4, 3])) - .annotation(position: .top, alignment: .trailing, spacing: 2) { - Text(LoopFormat.number(average, decimals: 1)) - .font(LoopDesign.Typeface.caption.weight(.semibold)) - .foregroundStyle(P.ink) - } } .chartYScale(domain: 0...yMax) .chartXAxis { diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift index 39af08a282..377573ad32 100644 --- a/Loop/Redesign/TrendsScreen.swift +++ b/Loop/Redesign/TrendsScreen.swift @@ -47,10 +47,7 @@ final class TrendsModel: ObservableObject { /// no insulin (today only when no complete day has any), never the period length. var dataDays = 0 var days: Double { Double(max(1, dataDays)) } - var basalUnits: Double { LoopHistoryMath.averagingDays(totals).reduce(0) { $0 + $1.basal } } - var bolusUnits: Double { LoopHistoryMath.averagingDays(totals).reduce(0) { $0 + $1.bolus } } var insulinPerDay: Double { LoopHistoryMath.averageDailyInsulin(totals) } - var basalShare: Double { basalUnits + bolusUnits > 0 ? basalUnits / (basalUnits + bolusUnits) : 0 } /// Per day over the averaged days; a single day in progress when that is all there is. var split: InsulinSplit { InsulinSplit.average(LoopHistoryMath.averagingDays(totals).map(\.split)) } /// The split is today's total so far rather than an average of complete days. @@ -170,7 +167,6 @@ final class TrendsModel: ObservableObject { var tiles: [StatTile] = [] let averageLabel = NSLocalizedString("Average", comment: "Trends summary tile: mean glucose over the period") let gmiLabel = NSLocalizedString("GMI", comment: "Trends summary tile: abbreviation for Glucose Management Indicator, an estimate of HbA1c from CGM data") - let loopActiveLabel = NSLocalizedString("Loop active", comment: "Trends summary tile: share of the period Loop spent in closed loop") if let stats = s.stats { tiles.append(StatTile(label: averageLabel, value: unit.format(stats.meanMgdl), unit: unit.unitName, detail: String(format: NSLocalizedString("SD %1$@ · CV %2$@%%", comment: "Trends Average tile detail: standard deviation and coefficient of variation of glucose. (1: standard deviation in the glucose unit, 2: coefficient of variation as a percentage number)"), @@ -186,24 +182,9 @@ final class TrendsModel: ObservableObject { detail: NSLocalizedString("No CGM data", comment: "Trends Average tile detail when the period has no glucose readings"))) tiles.append(StatTile(label: gmiLabel, value: "–", unit: "%", detail: nil)) } - let basalPercent = LoopFormat.number(s.basalShare * 100, decimals: 0) - let insulinDetail: String - if let note = daysNote(s) { - insulinDetail = String(format: NSLocalizedString("%1$@%% basal · %2$@", comment: "Trends Insulin per day tile detail when only some days are averaged. (1: basal share as a percentage number, 2: days averaged, e.g. 5 of 14 days)"), - basalPercent, note) - } else { - insulinDetail = String(format: NSLocalizedString("%1$@%% basal", comment: "Trends Insulin per day tile detail: share of insulin delivered as basal. (1: percentage number)"), - basalPercent) - } // One day's figures are that day's own, so its tiles drop "per day" and show whole counts. let oneDay = s.day != nil let countDecimals = oneDay ? 0 : 1 - tiles.append(StatTile(label: oneDay - ? NSLocalizedString("Insulin", comment: "Trends summary tile for a one-day period: the day's total insulin") - : NSLocalizedString("Insulin per day", comment: "Trends tile and card title: average total insulin per day"), - value: LoopFormat.number(s.insulinPerDay, decimals: 1), - unit: NSLocalizedString("U", comment: "Abbreviation for insulin units"), - detail: insulinDetail, valueColor: P.insulinInk)) tiles.append(StatTile(label: oneDay ? NSLocalizedString("Carbs", comment: "Trends summary tile for a one-day period: the day's total carbohydrate") : NSLocalizedString("Carbs per day", comment: "Trends summary tile: average grams of carbohydrate entered per day"), @@ -220,21 +201,11 @@ final class TrendsModel: ObservableObject { value: LoopFormat.number(Double(s.manualBoluses) / s.days, decimals: countDecimals), unit: nil, detail: String(format: NSLocalizedString("+ %1$@ automatic", comment: "Trends Boluses per day tile detail: average number of automatic boluses per day on top of the manual ones, a fractional value such as 4.2. (1: number with one decimal)"), LoopFormat.number(Double(s.automaticBoluses) / s.days, decimals: countDecimals)))) - let covered = s.loopSpans.reduce(0) { $0 + $1.duration } - if covered > 0 { - let closed = LoopHistoryMath.duration(of: .closed, in: s.loopSpans) - tiles.append(StatTile(label: loopActiveLabel, value: LoopFormat.number(closed / covered * 100, decimals: 0), unit: "%", - detail: String(format: NSLocalizedString("%1$@ of %2$@", comment: "Trends Loop active tile detail: time in closed loop out of the time Loop status is known. (1: duration in closed loop, e.g. 6d 4h, 2: duration covered, e.g. 7d)"), - LoopFormat.duration(closed), LoopFormat.duration(covered)))) - } else { - tiles.append(StatTile(label: loopActiveLabel, value: "–", unit: "%", - detail: NSLocalizedString("No loop runs kept", comment: "Trends Loop active tile detail when no Loop run history is available for the period"))) - } return tiles } /// "5 of 14 days" when only part of the period is averaged: days without data, or today while it runs. - private func daysNote(_ s: Summary) -> String? { + func daysNote(_ s: Summary) -> String? { let insulinDays = LoopHistoryMath.averagingDays(s.totals).count guard insulinDays < s.totals.count else { return nil } let total = String.localizedStringWithFormat(NSLocalizedString("%lld days", comment: "A number of days, e.g. a Trends period or how many days are averaged. (1: number of days)"), s.totals.count) @@ -309,37 +280,7 @@ struct TrendsView: View { if s.day == nil { patternCard(s) } - LoopCard { - CardHeader(title: NSLocalizedString("Basal and bolus", comment: "Trends card title: insulin split into basal and bolus")) { - Text(s.splitIsPartial - ? NSLocalizedString("today so far", comment: "Trends Basal and bolus card subtitle when the figures are today's total so far") - : s.totals.count == 1 - ? NSLocalizedString("this day", comment: "Trends Basal and bolus card subtitle when the period is one complete day") - : NSLocalizedString("average per day", comment: "Trends Basal and bolus card subtitle when the figures are averaged per day over the period")) - } - if s.split.total > 0 { - InsulinSplitView(split: s.split, partial: s.splitIsPartial, scheduledBasal: s.scheduledBasal, - usual: s.splitIsPartial ? usualByNow(s) : nil) - } else { - ChartPlaceholder(message: NSLocalizedString("No insulin recorded in this period", comment: "Trends Insulin per day chart placeholder when no insulin was delivered in the period"), height: 80) - } - } - // One day is one bar: the timeline and the Basal and bolus card already say it all. - if s.day == nil { - LoopCard { - CardHeader(title: NSLocalizedString("Insulin per day", comment: "Trends tile and card title: average total insulin per day")) { - Text(s.totals.count > 30 - ? NSLocalizedString("per day, weekly average", comment: "Trends Insulin per day card subtitle when each bar is a week's average daily total") - : NSLocalizedString("basal and bolus", comment: "Trends Insulin per day card subtitle when each bar is one day's basal and bolus")) - } - InsulinDailyTotalsView(totals: s.totals.count > 30 ? LoopHistoryMath.weekly(s.totals) : s.totals, - weekly: s.totals.count > 30, average: s.insulinPerDay) - ChartLegend(items: insulinLegend(s)) - if let today = todaySoFar(s) { - Footnote(today) - } - } - } + insulinCard(s) LoopCard { CardHeader(title: NSLocalizedString("Loop status", comment: "Trends card title: how long Loop spent in closed loop, limited or open loop")) LoopModeBarView(spans: s.loopSpans) @@ -357,6 +298,45 @@ struct TrendsView: View { } } + /// Insulin per day with its basal and bolus split, or one day's insulin under its timeline. + private func insulinCard(_ s: TrendsModel.Summary) -> some View { + let weekly = s.totals.count > 30 + return LoopCard { + if s.day != nil { + // One day is one bar, so the timeline above stands in for the chart. + CardHeader(title: NSLocalizedString("Insulin", comment: "Trends card title for a one-day period: the day's insulin, split into basal and bolus")) { + Text(s.splitIsPartial + ? NSLocalizedString("today so far", comment: "Trends Basal and bolus card subtitle when the figures are today's total so far") + : NSLocalizedString("this day", comment: "Trends Basal and bolus card subtitle when the period is one complete day")) + } + } else { + CardHeader(title: NSLocalizedString("Insulin per day", comment: "Trends tile and card title: average total insulin per day")) { + if weekly { + Text(NSLocalizedString("per day, weekly average", comment: "Trends Insulin per day card subtitle when each bar is a week's average daily total")) + } else if let note = model.daysNote(s) { + Text(String(format: NSLocalizedString("average of %1$@", comment: "Trends Insulin per day card subtitle when only some days of the period are averaged, such as a day still in progress. (1: days averaged, e.g. 13 of 14 days)"), note)) + } else { + Text(NSLocalizedString("average per day", comment: "Trends Basal and bolus card subtitle when the figures are averaged per day over the period")) + } + } + } + if s.split.total > 0 { + if s.day == nil { + InsulinDailyTotalsView(totals: weekly ? LoopHistoryMath.weekly(s.totals) : s.totals, weekly: weekly, average: s.insulinPerDay) + } + // Under the chart, the rows name its marks, so there is no separate legend. + InsulinSplitView(split: s.split, partial: s.splitIsPartial, scheduledBasal: s.scheduledBasal, + usual: s.day != nil && s.splitIsPartial ? usualByNow(s) : nil, showsBar: s.day != nil, + averageLine: s.day == nil) + if s.day == nil, let today = todaySoFar(s) { + Footnote(today) + } + } else { + ChartPlaceholder(message: NSLocalizedString("No insulin recorded in this period", comment: "Trends Insulin per day chart placeholder when no insulin was delivered in the period"), height: 80) + } + } + } + /// The same timeline as Today so far: glucose, every bolus and basal change, and carbs, through the day. private func dayCard(_ day: DayHistory, targets: [TargetBand], unit: GlucoseUnitStyle) -> some View { let isToday = Calendar.current.isDateInToday(day.window.start) @@ -412,18 +392,6 @@ struct TrendsView: View { LoopFormat.totalUnits(today.total), LoopFormat.totalUnits(usual.typical.median), LoopFormat.time(usual.at)) } - /// The figures are on the Basal and bolus card above, so the legend only names the marks. - private func insulinLegend(_ s: TrendsModel.Summary) -> [LegendItem] { - [ - LegendItem(swatch: .basal, label: NSLocalizedString("Basal", comment: "Chart legend: the basal part of each day's insulin")), - LegendItem(swatch: .solid(P.bolus), label: NSLocalizedString("Bolus", comment: "Chart legend: solid bars are manual boluses")), - LegendItem(swatch: .hatch(P.bolus), label: NSLocalizedString("Automatic", comment: "Chart legend: hatched bars are automatic boluses given by Loop")), - LegendItem(swatch: .dash(P.ink), - label: String(format: NSLocalizedString("Average %1$@ U", comment: "Trends Insulin per day legend: the dashed line showing average total insulin per day. (1: units)"), - LoopFormat.number(s.insulinPerDay, decimals: 1))), - ] - } - private func coverageNote(_ s: TrendsModel.Summary) -> String? { guard let start = s.loopSpans.first?.start else { return NSLocalizedString("Loop status is recorded from when this version of Loop first ran.", comment: "Trends Loop status footnote when there is no Loop status history yet") From 967106b931f38088a280fd194e1b74ed2d94e2ba Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:36 +0000 Subject: [PATCH 06/15] Show targets beside their figures instead of Aim lines Each summary tile carries its aim and each time-in-range band shows its target in its row. Misses are not coloured. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- Loop/Localizable.xcstrings | 386 +++++++++++++++++++++++---- Loop/Redesign/TrendsComponents.swift | 46 +++- Loop/Redesign/TrendsScreen.swift | 20 +- 3 files changed, 389 insertions(+), 63 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 35c4f86002..0d8566cd09 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -12,6 +12,292 @@ }, " -" : { + }, + "%1$@ above range" : { + "comment" : "Time in range High row: the consensus target for all time above range, high and very high together. (1: target, e.g. < 25%)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ فوق النطاق" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ nad rozsahem" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ over området" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ über dem Bereich" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ por encima" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ alueen yläpuolella" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ au-dessus" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ מעל הטווח" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ tartomány felett" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ sopra" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "範囲上 %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ over området" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ boven bereik" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ powyżej zakresu" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ acima do alvo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ peste interval" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ выше диапазона" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ nad rozsahom" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ över målområdet" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "aralık üstü %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ вище діапазону" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ trên khoảng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "高于范围 %1$@" + } + } + } + }, + "%1$@ below range" : { + "comment" : "Time in range Low row: the consensus target for all time below range, low and very low together. (1: target, e.g. < 4%)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ تحت النطاق" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ pod rozsahem" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ under området" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ unter dem Bereich" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ por debajo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ alueen alapuolella" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ en dessous" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ מתחת לטווח" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ tartomány alatt" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ sotto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "範囲下 %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ under området" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ onder bereik" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ poniżej zakresu" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ abaixo do alvo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ sub interval" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ниже диапазона" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ pod rozsahom" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ under målområdet" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "aralık altı %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ нижче діапазону" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ dưới khoảng" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "低于范围 %1$@" + } + } + } }, "%1$@ d" : { "comment" : "Trends period picker, short label for a period of several days, e.g. 7 d. (1: number of days)", @@ -3608,288 +3894,288 @@ } } }, - "Aim: average under %1$@ %2$@, GMI under 7%%, CV 36%% or less." : { - "comment" : "Trends Summary footnote: the consensus targets for average glucose, GMI and glucose variability (CV). (1: glucose value, 2: glucose unit)", + "aim %1$@" : { + "comment" : "Trends: the consensus target under a figure, in lower case. (1: target, e.g. < 7%)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الهدف: متوسط أقل من %1$@ %2$@، وGMI أقل من 7%%، وCV ‏36%% أو أقل." + "value" : "الهدف %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Cíl: průměr pod %1$@ %2$@, GMI pod 7%%, CV nejvýše 36%%." + "value" : "cíl %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Mål: gennemsnit under %1$@ %2$@, GMI under 7 %%, CV højst 36 %%." + "value" : "mål %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ziel: Durchschnitt unter %1$@ %2$@, GMI unter 7 %%, VK höchstens 36 %%." + "value" : "Ziel %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Objetivo: promedio por debajo de %1$@ %2$@, GMI por debajo del 7%%, CV del 36%% o menos." + "value" : "objetivo %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Tavoite: keskiarvo alle %1$@ %2$@, GMI alle 7 %%, CV enintään 36 %%." + "value" : "tavoite %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Objectif : moyenne sous %1$@ %2$@, GMI sous 7 %%, CV de 36 %% au plus." + "value" : "objectif %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "יעד: ממוצע מתחת ל-%1$@ %2$@, GMI מתחת ל-7%%, CV ‏36%% או פחות." + "value" : "יעד %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Cél: átlag %1$@ %2$@ alatt, GMI 7%% alatt, CV legfeljebb 36%%." + "value" : "cél: %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Obiettivo: media sotto %1$@ %2$@, GMI sotto il 7%%, CV al massimo 36%%." + "value" : "obiettivo %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "目標:平均%1$@ %2$@未満、GMI 7%%未満、CV 36%%以下。" + "value" : "目標 %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Mål: gjennomsnitt under %1$@ %2$@, GMI under 7 %%, CV høyst 36 %%." + "value" : "mål %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Doel: gemiddelde onder %1$@ %2$@, GMI onder 7%%, CV hoogstens 36%%." + "value" : "doel %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Cel: średnia poniżej %1$@ %2$@, GMI poniżej 7%%, CV najwyżej 36%%." + "value" : "cel %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Meta: média abaixo de %1$@ %2$@, GMI abaixo de 7%%, CV de no máximo 36%%." + "value" : "meta %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Țintă: medie sub %1$@ %2$@, GMI sub 7%%, CV de cel mult 36%%." + "value" : "țintă %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Цель: среднее ниже %1$@ %2$@, GMI ниже 7%%, CV не более 36%%." + "value" : "цель %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Cieľ: priemer pod %1$@ %2$@, GMI pod 7%%, CV najviac 36%%." + "value" : "cieľ %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Mål: genomsnitt under %1$@ %2$@, GMI under 7 %%, CV högst 36 %%." + "value" : "mål %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hedef: ortalama %1$@ %2$@ altı, GMI %%7 altı, CV en fazla %%36." + "value" : "hedef %1$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Мета: середнє нижче %1$@ %2$@, GMI нижче 7%%, CV не більше 36%%." + "value" : "мета %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mục tiêu: trung bình dưới %1$@ %2$@, GMI dưới 7%%, CV từ 36%% trở xuống." + "value" : "mục tiêu %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "目标:平均值低于 %1$@ %2$@,GMI 低于 7%%,CV 不超过 36%%。" + "value" : "目标 %1$@" } } } }, - "Aim: over 70%% in range; under 25%% above and 4%% below; under 5%% very high and 1%% very low; CGM active over 70%%." : { - "comment" : "Trends Time in range card footnote: the consensus targets for time in range, above and below range, very high, very low and CGM wear", + "aim %1$@, CV %2$@" : { + "comment" : "Trends Average tile: the consensus targets for average glucose and its coefficient of variation, in lower case. (1: glucose target, e.g. < 8.6; 2: CV target, e.g. ≤ 36%)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الهدف: أكثر من 70%% ضمن النطاق؛ أقل من 25%% فوق و4%% تحت؛ أقل من 5%% مرتفع جدًا و1%% منخفض جدًا؛ وCGM نشط أكثر من 70%%." + "value" : "الهدف %1$@، CV %2$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Cíl: přes 70%% v rozsahu; pod 25%% nad a 4%% pod; pod 5%% velmi vysoko a 1%% velmi nízko; CGM aktivní přes 70%%." + "value" : "cíl %1$@, CV %2$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Mål: over 70 %% i området; under 25 %% over og 4 %% under; under 5 %% meget højt og 1 %% meget lavt; CGM aktiv over 70 %%." + "value" : "mål %1$@, CV %2$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ziel: über 70 %% im Zielbereich; unter 25 %% darüber und 4 %% darunter; unter 5 %% sehr hoch und 1 %% sehr niedrig; CGM über 70 %% aktiv." + "value" : "Ziel %1$@, VK %2$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Objetivo: más del 70%% en rango; menos del 25%% por encima y del 4%% por debajo; menos del 5%% muy alto y del 1%% muy bajo; CGM activo más del 70%%." + "value" : "objetivo %1$@, CV %2$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Tavoite: yli 70 %% tavoitealueella; alle 25 %% yläpuolella ja 4 %% alapuolella; alle 5 %% erittäin korkealla ja 1 %% erittäin matalalla; CGM aktiivinen yli 70 %%." + "value" : "tavoite %1$@, CV %2$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Objectif : plus de 70 %% dans la cible ; moins de 25 %% au-dessus et 4 %% en dessous ; moins de 5 %% très haut et 1 %% très bas ; CGM actif plus de 70 %%." + "value" : "objectif %1$@, CV %2$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "יעד: מעל 70%% בטווח; מתחת ל-25%% מעל ו-4%% מתחת; מתחת ל-5%% גבוה מאוד ו-1%% נמוך מאוד; CGM פעיל מעל 70%%." + "value" : "יעד %1$@, CV %2$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Cél: több mint 70%% tartományban; kevesebb mint 25%% felette és 4%% alatta; kevesebb mint 5%% nagyon magas és 1%% nagyon alacsony; CGM aktív több mint 70%%." + "value" : "cél: %1$@, CV %2$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Obiettivo: oltre il 70%% nell'intervallo; meno del 25%% sopra e del 4%% sotto; meno del 5%% molto alta e dell'1%% molto bassa; CGM attivo oltre il 70%%." + "value" : "obiettivo %1$@, CV %2$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "目標:範囲内70%%超、範囲上25%%未満・範囲下4%%未満、非常に高い5%%未満・非常に低い1%%未満、CGM稼働70%%超。" + "value" : "目標 %1$@、CV %2$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Mål: over 70 %% i målområdet; under 25 %% over og 4 %% under; under 5 %% svært høyt og 1 %% svært lavt; CGM aktiv over 70 %%." + "value" : "mål %1$@, CV %2$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Doel: meer dan 70%% binnen bereik; minder dan 25%% erboven en 4%% eronder; minder dan 5%% zeer hoog en 1%% zeer laag; CGM meer dan 70%% actief." + "value" : "doel %1$@, CV %2$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Cel: ponad 70%% w zakresie; poniżej 25%% powyżej i 4%% poniżej; poniżej 5%% bardzo wysokich i 1%% bardzo niskich; CGM aktywny ponad 70%%." + "value" : "cel %1$@, CV %2$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Meta: mais de 70%% no alvo; menos de 25%% acima e 4%% abaixo; menos de 5%% muito alto e 1%% muito baixo; CGM ativo mais de 70%%." + "value" : "meta %1$@, CV %2$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Țintă: peste 70%% în interval; sub 25%% peste și 4%% sub; sub 5%% foarte ridicat și 1%% foarte scăzut; CGM activ peste 70%%." + "value" : "țintă %1$@, CV %2$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Цель: в диапазоне более 70%%; выше — менее 25%%, ниже — менее 4%%; очень высокая — менее 5%%, очень низкая — менее 1%%; CGM активен более 70%%." + "value" : "цель %1$@, CV %2$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Cieľ: nad 70%% v rozsahu; pod 25%% nad a 4%% pod; pod 5%% veľmi vysoko a 1%% veľmi nízko; CGM aktívny nad 70%%." + "value" : "cieľ %1$@, CV %2$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Mål: över 70 %% inom målområdet; under 25 %% över och 4 %% under; under 5 %% mycket högt och 1 %% mycket lågt; CGM aktiv över 70 %%." + "value" : "mål %1$@, CV %2$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hedef: aralıkta %%70'in üzeri; üstünde %%25'in, altında %%4'ün altı; çok yüksekte %%5'in, çok düşükte %%1'in altı; CGM aktif %%70'in üzeri." + "value" : "hedef %1$@, CV %2$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Мета: у діапазоні понад 70%%; вище — менше 25%%, нижче — менше 4%%; дуже висока — менше 5%%, дуже низька — менше 1%%; CGM активний понад 70%%." + "value" : "мета %1$@, CV %2$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mục tiêu: trên 70%% trong khoảng; dưới 25%% trên khoảng và 4%% dưới khoảng; dưới 5%% rất cao và 1%% rất thấp; CGM hoạt động trên 70%%." + "value" : "mục tiêu %1$@, CV %2$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "目标:范围内超过70%%;高于范围低于25%%,低于范围低于4%%;极高低于5%%,极低低于1%%;CGM 在线超过70%%。" + "value" : "目标 %1$@,CV %2$@" } } } diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift index c490d87e95..d31bc56e85 100644 --- a/Loop/Redesign/TrendsComponents.swift +++ b/Loop/Redesign/TrendsComponents.swift @@ -105,6 +105,8 @@ struct StatTile: Identifiable { let unit: String? let detail: String? var valueColor: Color = LoopDesign.Palette.ink + /// The consensus target for the figure, e.g. "aim < 7%". + var aim: String? = nil var id: String { label } } @@ -154,6 +156,11 @@ struct StatGridView: View { .foregroundStyle(P.inkMuted) .lineLimit(2) } + if let aim = tile.aim { + Text(aim) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + } } .padding(LoopDesign.Space.s3) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) @@ -176,6 +183,18 @@ extension LoopHistoryMath.RangeBand { } } + /// The consensus target for the band. High and low are aims for all time above or below range, + /// so they say so; the others are the band's own. + var localizedTarget: String { + switch self { + case .veryHigh: return ConsensusTarget.under(5) + case .high: return String(format: NSLocalizedString("%1$@ above range", comment: "Time in range High row: the consensus target for all time above range, high and very high together. (1: target, e.g. < 25%)"), ConsensusTarget.under(25)) + case .inRange: return ConsensusTarget.over(70) + case .low: return String(format: NSLocalizedString("%1$@ below range", comment: "Time in range Low row: the consensus target for all time below range, low and very low together. (1: target, e.g. < 4%)"), ConsensusTarget.under(4)) + case .veryLow: return ConsensusTarget.under(1) + } + } + /// Consensus thresholds with the user's decimal separator, e.g. "3,9–10,0". func localizedThresholds(_ unit: GlucoseUnitStyle) -> String { let decimals = unit.isMmol ? 1 : 0 @@ -200,6 +219,18 @@ private func percentText(_ value: Double, decimals: Int = 0) -> String { String(format: NSLocalizedString("%1$@%%", comment: "A percentage. (1: number)"), LoopFormat.number(value, decimals: decimals)) } +/// The international consensus targets for CGM metrics, written as "< 4%", "> 70%" or "≤ 36%". +enum ConsensusTarget { + static func under(_ percent: Double) -> String { "< " + percentText(percent) } + static func over(_ percent: Double) -> String { "> " + percentText(percent) } + static func atMost(_ percent: Double) -> String { "≤ " + percentText(percent) } + + /// "aim < 7%", under a figure. + static func aim(_ target: String) -> String { + String(format: NSLocalizedString("aim %1$@", comment: "Trends: the consensus target under a figure, in lower case. (1: target, e.g. < 7%)"), target) + } +} + struct TimeInRangeView: View { @Environment(\.dynamicTypeSize) private var typeSize @@ -273,10 +304,14 @@ struct TimeInRangeView: View { thresholds } Spacer(minLength: LoopDesign.Space.s1) - Text(String(format: NSLocalizedString("%1$@ h", comment: "Time in range row: average hours a day spent in the band, abbreviated. (1: hours with one decimal)"), - LoopFormat.number(hours, decimals: 1))) - .font(LoopDesign.Typeface.footnote.monospacedDigit()) - .foregroundStyle(P.inkMuted) + VStack(alignment: .trailing, spacing: 0) { + Text(String(format: NSLocalizedString("%1$@ h", comment: "Time in range row: average hours a day spent in the band, abbreviated. (1: hours with one decimal)"), + LoopFormat.number(hours, decimals: 1))) + .font(LoopDesign.Typeface.footnote.monospacedDigit()) + Text(band.localizedTarget) + .font(LoopDesign.Typeface.caption) + } + .foregroundStyle(P.inkMuted) } if typeSize.isAccessibilitySize { thresholds @@ -285,7 +320,8 @@ struct TimeInRangeView: View { .accessibilityElement(children: .ignore) .accessibilityLabel(String(format: NSLocalizedString("%1$@, %2$@ %3$@: %4$@ percent, %5$@ hours a day", comment: "VoiceOver label for a Time in range row. (1: band name, e.g. In range, 2: glucose thresholds, e.g. 3.9–10.0, 3: glucose unit, 4: percentage number, 5: average hours a day with one decimal)"), band.localizedName, band.localizedThresholds(unit), unit.unitName, - LoopFormat.number(Double(percentages[band] ?? 0), decimals: 0), LoopFormat.number(hours, decimals: 1))) + LoopFormat.number(Double(percentages[band] ?? 0), decimals: 0), LoopFormat.number(hours, decimals: 1)) + + ", " + ConsensusTarget.aim(band.localizedTarget)) } } diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift index 377573ad32..a4ab7331ac 100644 --- a/Loop/Redesign/TrendsScreen.swift +++ b/Loop/Redesign/TrendsScreen.swift @@ -170,13 +170,15 @@ final class TrendsModel: ObservableObject { if let stats = s.stats { tiles.append(StatTile(label: averageLabel, value: unit.format(stats.meanMgdl), unit: unit.unitName, detail: String(format: NSLocalizedString("SD %1$@ · CV %2$@%%", comment: "Trends Average tile detail: standard deviation and coefficient of variation of glucose. (1: standard deviation in the glucose unit, 2: coefficient of variation as a percentage number)"), - unit.format(stats.sdMgdl), LoopFormat.number(stats.cv * 100, decimals: 0)))) + unit.format(stats.sdMgdl), LoopFormat.number(stats.cv * 100, decimals: 0)), + aim: averageAim(unit))) let enough = s.cgmActive * s.interval.duration / 86400 >= 10 tiles.append(StatTile(label: gmiLabel, value: LoopFormat.number(stats.gmiPercent, decimals: 1), unit: "%", detail: enough ? String(format: NSLocalizedString("%1$@ mmol/mol", comment: "Trends GMI tile detail: the GMI in IFCC units. (1: value)"), LoopFormat.number(stats.gmiMmolMol, decimals: 0)) - : NSLocalizedString("Needs 10 days of CGM", comment: "Trends GMI tile detail when fewer than 10 days of CGM data are available"))) + : NSLocalizedString("Needs 10 days of CGM", comment: "Trends GMI tile detail when fewer than 10 days of CGM data are available"), + aim: ConsensusTarget.aim(ConsensusTarget.under(7)))) } else { tiles.append(StatTile(label: averageLabel, value: "–", unit: unit.unitName, detail: NSLocalizedString("No CGM data", comment: "Trends Average tile detail when the period has no glucose readings"))) @@ -204,6 +206,12 @@ final class TrendsModel: ObservableObject { return tiles } + /// "aim < 8.6, CV ≤ 36%": the consensus targets for mean glucose (154 mg/dL) and its variability. + private func averageAim(_ unit: GlucoseUnitStyle) -> String { + String(format: NSLocalizedString("aim %1$@, CV %2$@", comment: "Trends Average tile: the consensus targets for average glucose and its coefficient of variation, in lower case. (1: glucose target, e.g. < 8.6; 2: CV target, e.g. ≤ 36%)"), + "< " + (unit.isMmol ? LoopFormat.number(8.6, decimals: 1) : "154"), ConsensusTarget.atMost(36)) + } + /// "5 of 14 days" when only part of the period is averaged: days without data, or today while it runs. func daysNote(_ s: Summary) -> String? { let insulinDays = LoopHistoryMath.averagingDays(s.totals).count @@ -268,14 +276,14 @@ struct TrendsView: View { LoopCard { CardHeader(title: NSLocalizedString("Time in range", comment: "Trends card title: share of glucose readings in each range")) { Text(String(format: NSLocalizedString("%1$@%% CGM active", comment: "Trends Time in range card: share of the period with CGM readings. (1: percentage number)"), - LoopFormat.number(s.cgmActive * 100, decimals: 0))) + LoopFormat.number(s.cgmActive * 100, decimals: 0)) + + " · " + ConsensusTarget.aim(ConsensusTarget.over(70))) } if s.glucose.isEmpty { ChartPlaceholder(message: NSLocalizedString("No CGM data in this period", comment: "Trends chart placeholder when the period has no glucose readings")) } else { TimeInRangeView(fractions: s.timeInRange, unit: s.unit) } - Footnote(String(format: NSLocalizedString("Aim: over 70%% in range; under 25%% above and 4%% below; under 5%% very high and 1%% very low; CGM active over 70%%.", comment: "Trends Time in range card footnote: the consensus targets for time in range, above and below range, very high, very low and CGM wear"))) } if s.day == nil { patternCard(s) @@ -365,10 +373,6 @@ struct TrendsView: View { } StatGridView(tiles: model.tiles(s)) .opacity(model.loading ? 0.5 : 1) - if s.stats != nil { - Footnote(String(format: NSLocalizedString("Aim: average under %1$@ %2$@, GMI under 7%%, CV 36%% or less.", comment: "Trends Summary footnote: the consensus targets for average glucose, GMI and glucose variability (CV). (1: glucose value, 2: glucose unit)"), - s.unit.isMmol ? LoopFormat.number(8.6, decimals: 1) : "154", s.unit.unitName)) - } } } From 915bd7af9d0798b7ae013417474c07a748a15782 Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:36 +0000 Subject: [PATCH 07/15] Use Loop's existing colours in Trends Removes the 17 colour sets. Marks use the existing glucose, insulin, carbs and accent colours, text uses primary and secondary, and GlucoseBand.agpColor gains dark variants. The period selector is a system segmented Picker. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- .../accent-ink.colorset/Contents.json | 38 ----- .../agp-inner.colorset/Contents.json | 38 ----- .../agp-outer.colorset/Contents.json | 38 ----- .../basal-edge.colorset/Contents.json | 38 ----- .../basal.colorset/Contents.json | 38 ----- .../bolus.colorset/Contents.json | 38 ----- .../carbs-ink.colorset/Contents.json | 38 ----- .../control-thumb.colorset/Contents.json | 38 ----- .../glucose-ink.colorset/Contents.json | 38 ----- .../ink-muted.colorset/Contents.json | 38 ----- .../insulin-ink.colorset/Contents.json | 38 ----- .../range-high.colorset/Contents.json | 38 ----- .../range-in.colorset/Contents.json | 38 ----- .../range-low.colorset/Contents.json | 38 ----- .../range-very-high.colorset/Contents.json | 38 ----- .../range-very-low.colorset/Contents.json | 38 ----- .../target-band.colorset/Contents.json | 38 ----- Loop/Localizable.xcstrings | 143 ++++++++++++++++++ Loop/Redesign/DayCharts.swift | 14 +- Loop/Redesign/LoopDesign.swift | 43 ++---- Loop/Redesign/TrendsComponents.swift | 38 ++--- Loop/Redesign/TrendsScreen.swift | 5 +- .../Statistics/GlucoseBand+Display.swift | 20 ++- 23 files changed, 189 insertions(+), 720 deletions(-) delete mode 100644 Loop/DerivedAssetsBase.xcassets/accent-ink.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/agp-inner.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/agp-outer.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/basal-edge.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/basal.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/carbs-ink.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/control-thumb.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/glucose-ink.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/ink-muted.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/insulin-ink.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/range-high.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/range-in.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/range-low.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/range-very-high.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/range-very-low.colorset/Contents.json delete mode 100644 Loop/DerivedAssetsBase.xcassets/target-band.colorset/Contents.json diff --git a/Loop/DerivedAssetsBase.xcassets/accent-ink.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/accent-ink.colorset/Contents.json deleted file mode 100644 index af109ce19a..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/accent-ink.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.000", - "green": "0.384", - "blue": "0.800", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.039", - "green": "0.518", - "blue": "1.000", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/agp-inner.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/agp-inner.colorset/Contents.json deleted file mode 100644 index 2ddc0f5f90..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/agp-inner.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.000", - "green": "0.690", - "blue": "1.000", - "alpha": "0.340" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.388", - "green": "0.729", - "blue": "1.000", - "alpha": "0.380" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/agp-outer.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/agp-outer.colorset/Contents.json deleted file mode 100644 index 270771d475..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/agp-outer.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.000", - "green": "0.690", - "blue": "1.000", - "alpha": "0.160" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.388", - "green": "0.729", - "blue": "1.000", - "alpha": "0.180" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/basal-edge.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/basal-edge.colorset/Contents.json deleted file mode 100644 index fa72f9b84c..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/basal-edge.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.890", - "green": "0.725", - "blue": "0.514", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.431", - "green": "0.322", - "blue": "0.196", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/basal.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/basal.colorset/Contents.json deleted file mode 100644 index 76f27a3287..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/basal.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.988", - "green": "0.890", - "blue": "0.761", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.263", - "green": "0.188", - "blue": "0.106", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json deleted file mode 100644 index 30b11e52a8..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.663", - "green": "0.282", - "blue": "0.039", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "1.000", - "green": "0.639", - "blue": "0.278", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/carbs-ink.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/carbs-ink.colorset/Contents.json deleted file mode 100644 index df21da3860..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/carbs-ink.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.184", - "green": "0.490", - "blue": "0.086", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.349", - "green": "0.894", - "blue": "0.200", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/control-thumb.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/control-thumb.colorset/Contents.json deleted file mode 100644 index 34b15edebe..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/control-thumb.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "1.000", - "green": "1.000", - "blue": "1.000", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.388", - "green": "0.388", - "blue": "0.400", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/glucose-ink.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/glucose-ink.colorset/Contents.json deleted file mode 100644 index b704d8af81..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/glucose-ink.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.000", - "green": "0.439", - "blue": "0.722", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.388", - "green": "0.729", - "blue": "1.000", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/ink-muted.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/ink-muted.colorset/Contents.json deleted file mode 100644 index 6eda3190f3..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/ink-muted.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.424", - "green": "0.424", - "blue": "0.439", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.682", - "green": "0.682", - "blue": "0.698", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/insulin-ink.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/insulin-ink.colorset/Contents.json deleted file mode 100644 index 1acea580ea..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/insulin-ink.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.541", - "green": "0.227", - "blue": "0.024", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "1.000", - "green": "0.639", - "blue": "0.278", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/range-high.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/range-high.colorset/Contents.json deleted file mode 100644 index 28f578bf71..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/range-high.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.949", - "green": "0.718", - "blue": "0.020", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "1.000", - "green": "0.839", - "blue": "0.039", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/range-in.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/range-in.colorset/Contents.json deleted file mode 100644 index 3d0f452a75..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/range-in.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.184", - "green": "0.659", - "blue": "0.310", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.196", - "green": "0.843", - "blue": "0.294", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/range-low.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/range-low.colorset/Contents.json deleted file mode 100644 index 1349d6a59e..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/range-low.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.898", - "green": "0.282", - "blue": "0.302", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "1.000", - "green": "0.624", - "blue": "0.604", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/range-very-high.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/range-very-high.colorset/Contents.json deleted file mode 100644 index b2851438d1..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/range-very-high.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.541", - "green": "0.380", - "blue": "0.000", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.788", - "green": "0.541", - "blue": "0.000", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/range-very-low.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/range-very-low.colorset/Contents.json deleted file mode 100644 index 46e9e70555..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/range-very-low.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.608", - "green": "0.110", - "blue": "0.122", - "alpha": "1.000" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "1.000", - "green": "0.271", - "blue": "0.227", - "alpha": "1.000" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/DerivedAssetsBase.xcassets/target-band.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/target-band.colorset/Contents.json deleted file mode 100644 index 6ff45863c5..0000000000 --- a/Loop/DerivedAssetsBase.xcassets/target-band.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors": [ - { - "color": { - "color-space": "srgb", - "components": { - "red": "0.000", - "green": "0.690", - "blue": "1.000", - "alpha": "0.140" - } - }, - "idiom": "universal" - }, - { - "appearances": [ - { - "appearance": "luminosity", - "value": "dark" - } - ], - "color": { - "color-space": "srgb", - "components": { - "red": "0.388", - "green": "0.729", - "blue": "1.000", - "alpha": "0.180" - } - }, - "idiom": "universal" - } - ], - "info": { - "author": "xcode", - "version": 1 - } -} \ No newline at end of file diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 0d8566cd09..07ceedc6ef 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -12194,6 +12194,149 @@ } } }, + "Period" : { + "comment" : "Trends: VoiceOver label of the picker that chooses how many days the screen covers", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الفترة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Období" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periode" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeitraum" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periodo" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jakso" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Période" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "תקופה" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Időszak" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periodo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "期間" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periode" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Periode" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Okres" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Período" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Perioadă" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Период" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obdobie" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Period" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dönem" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Період" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Khoảng thời gian" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "时段" + } + } + } + }, "Previous period" : { "comment" : "VoiceOver label for the Trends button that moves back one period", "localizations" : { diff --git a/Loop/Redesign/DayCharts.swift b/Loop/Redesign/DayCharts.swift index 3f24f63466..58bf768f5e 100644 --- a/Loop/Redesign/DayCharts.swift +++ b/Loop/Redesign/DayCharts.swift @@ -95,11 +95,7 @@ struct DayTimelineCard: View { var body: some View { LoopCard { CardHeader(title: title) { - HStack(spacing: 4) { - Text(LoopFormat.totalUnits(history.totalUnits)).foregroundStyle(P.insulinInk) - Text(verbatim: "·") - Text(LoopFormat.grams(history.carbGrams)).foregroundStyle(P.carbsInk) - } + Text(verbatim: "\(LoopFormat.totalUnits(history.totalUnits)) · \(LoopFormat.grams(history.carbGrams))") } InsulinTimelineView(history: history, unit: unit, targetBands: targetBands, now: now, height: height) ChartLegend(items: Self.legend) @@ -227,7 +223,7 @@ struct InsulinTimelineView: View { if let grams = carbLabels[carb.id] { Text(LoopFormat.grams(grams)) .font(LoopDesign.Typeface.caption) - .foregroundStyle(P.carbsInk) + .foregroundStyle(P.ink) } } } @@ -266,7 +262,7 @@ struct InsulinTimelineView: View { if bolusLabels.contains(bolus.id) { Text(LoopFormat.number(bolus.units, decimals: 1)) .font(LoopDesign.Typeface.caption) - .foregroundStyle(P.insulinInk) + .foregroundStyle(P.ink) } } } @@ -327,7 +323,7 @@ struct InsulinTimelineView: View { private func scrubRows(at date: Date) -> [ScrubCallout.Row] { var rows: [ScrubCallout.Row] = [.init(label: nil, value: LoopFormat.time(date), color: P.ink)] if let glucose = nearest(history.glucose.map { ($0.date, $0.mgdl) }, to: date, within: 10 * 60) { - rows.append(.init(label: NSLocalizedString("Glucose", comment: "Chart callout label for the glucose reading at the touched time"), value: "\(unit.format(glucose)) \(unit.unitName)", color: P.glucoseInk)) + rows.append(.init(label: NSLocalizedString("Glucose", comment: "Chart callout label for the glucose reading at the touched time"), value: "\(unit.format(glucose)) \(unit.unitName)", color: P.ink)) } if let segment = LoopHistoryMath.basalRate(at: date, in: history.basal) { let value = segment.kind == .suspended @@ -340,7 +336,7 @@ struct InsulinTimelineView: View { rows.append(.init(label: bolus.automatic ? NSLocalizedString("Automatic bolus", comment: "Chart callout label for a bolus Loop delivered automatically near the touched time") : NSLocalizedString("Bolus", comment: "Chart callout label for a bolus near the touched time"), - value: LoopFormat.doseUnits(bolus.units), color: P.insulinInk)) + value: LoopFormat.doseUnits(bolus.units), color: P.ink)) } return rows } diff --git a/Loop/Redesign/LoopDesign.swift b/Loop/Redesign/LoopDesign.swift index 0fb74edffd..985ea599f1 100644 --- a/Loop/Redesign/LoopDesign.swift +++ b/Loop/Redesign/LoopDesign.swift @@ -8,14 +8,12 @@ import SwiftUI import UIKit -/// The tokens from the Loop design system (`design/project/tokens.json`). +/// Colours, spacing and type for the Trends screen. /// -/// Quantity colours reuse Loop's existing assets (`glucose`, `insulin`, `carbs`, `fresh`, `warning`, `accent`) -/// so a DerivedAssetsOverride still recolours them. Grounds and text map onto the iOS system colours they -/// were taken from; the rest are colour sets in DerivedAssetsBase.xcassets named after their token. -/// -/// Insulin and carb amounts are set in `insulinInk` and `carbsInk` wherever they stand alone. Glucose, basal, -/// counts and figures beside a legend swatch stay `ink`. Red is only for something that needs action now. +/// Every colour is one Loop already has: the `glucose`, `insulin`, `carbs`, `fresh` and `warning` assets, so a +/// DerivedAssetsOverride still recolours them, and the iOS system colours for grounds and text. Bands and fills are +/// those colours at reduced opacity. Text is `label` or `secondaryLabel`; colour is for marks, not for numbers. +/// Red is only for something that needs action now. enum LoopDesign { enum Palette { @@ -23,40 +21,25 @@ enum LoopDesign { static let surface = Color(uiColor: .systemGroupedBackground) static let surfaceRaised = Color(uiColor: .secondarySystemGroupedBackground) static let surfaceSunken = Color(uiColor: .systemGray5) - static let controlThumb = Color("control-thumb") static let separator = Color(uiColor: .opaqueSeparator) static let grid = Color(uiColor: .systemGray5) // Text static let ink = Color(uiColor: .label) - static let inkMuted = Color("ink-muted") - static let accentInk = Color("accent-ink") + static let inkMuted = Color(uiColor: .secondaryLabel) // Quantities static let glucose = Color("glucose") - static let glucoseInk = Color("glucose-ink") - static let targetBand = Color("target-band") - static let agpOuter = Color("agp-outer") - static let agpInner = Color("agp-inner") - /// Insulin as a quantity and an action: the Bolus button and the active insulin curve. + static let targetBand = glucose.opacity(0.14) + static let agpOuter = glucose.opacity(0.16) + static let agpInner = glucose.opacity(0.34) static let insulin = Color("insulin") - /// Every insulin amount set as text, whether a bolus, a day's total or active insulin. - static let insulinInk = Color("insulin-ink") - static let basal = Color("basal") - static let basalEdge = Color("basal-edge") - /// Basal is a pale, quiet orange with a tan edge; bolus is insulin's orange made deep. Both stay orange, - /// and their lightness is far enough apart (3:1 even where they touch) to tell them apart at a glance. - static let bolus = Color("bolus") + /// Bolus is insulin's colour; basal is the same colour faded, with a stronger edge along its top. + static let bolus = insulin + static let basal = insulin.opacity(0.25) + static let basalEdge = insulin.opacity(0.6) static let basalScheduled = inkMuted static let carbs = Color("carbs") - static let carbsInk = Color("carbs-ink") - - // Time in range ladder, top to bottom - static let rangeVeryHigh = Color("range-very-high") - static let rangeHigh = Color("range-high") - static let rangeIn = Color("range-in") - static let rangeLow = Color("range-low") - static let rangeVeryLow = Color("range-very-low") // Loop state static let statusAutomated = Color("fresh") diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift index d31bc56e85..638531e885 100644 --- a/Loop/Redesign/TrendsComponents.swift +++ b/Loop/Redesign/TrendsComponents.swift @@ -55,29 +55,15 @@ struct RangeSelectorView: View { var body: some View { VStack(spacing: LoopDesign.Space.s3) { - HStack(spacing: 2) { + Picker(NSLocalizedString("Period", comment: "Trends: VoiceOver label of the picker that chooses how many days the screen covers"), + selection: $period.animation(.snappy(duration: 0.2))) { ForEach(TrendsPeriod.allCases) { option in - Button { - withAnimation(.snappy(duration: 0.2)) { period = option } - } label: { - Text(option.label) - .font(.footnote.weight(.semibold)) - .foregroundStyle(P.ink) - .frame(maxWidth: .infinity, minHeight: 30) - .background { - if option == period { - Capsule().fill(P.controlThumb).shadow(color: .black.opacity(0.12), radius: 3, y: 1) - } - } - .contentShape(Capsule()) - } - .buttonStyle(.plain) - .accessibilityLabel(option.accessibilityName) - .accessibilityAddTraits(option == period ? .isSelected : []) + Text(option.label) + .accessibilityLabel(option.accessibilityName) + .tag(option) } } - .padding(2) - .background(P.surfaceSunken, in: Capsule()) + .pickerStyle(.segmented) HStack { StepButton(systemName: "chevron.left", label: NSLocalizedString("Previous period", comment: "VoiceOver label for the Trends button that moves back one period")) { step(-1) } @@ -242,11 +228,11 @@ struct TimeInRangeView: View { static func color(_ band: LoopHistoryMath.RangeBand) -> Color { switch band { - case .veryHigh: return P.rangeVeryHigh - case .high: return P.rangeHigh - case .inRange: return P.rangeIn - case .low: return P.rangeLow - case .veryLow: return P.rangeVeryLow + case .veryHigh: return GlucoseBand.veryHigh.agpColor + case .high: return GlucoseBand.high.agpColor + case .inRange: return GlucoseBand.target.agpColor + case .low: return GlucoseBand.low.agpColor + case .veryLow: return GlucoseBand.veryLow.agpColor } } @@ -541,7 +527,7 @@ struct InsulinSplitView: View { Spacer() Text(LoopFormat.totalUnits(split.total)) .font(LoopDesign.Typeface.statValueSmall) - .foregroundStyle(LoopDesign.Palette.insulinInk) + .foregroundStyle(LoopDesign.Palette.ink) } if let usual { Text(usual) diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift index a4ab7331ac..d647d5c410 100644 --- a/Loop/Redesign/TrendsScreen.swift +++ b/Loop/Redesign/TrendsScreen.swift @@ -195,8 +195,7 @@ final class TrendsModel: ObservableObject { detail: oneDay ? String(format: NSLocalizedString("%lld entries", comment: "Trends Carbs tile detail for a one-day period: how many carb entries were made that day. (1: number)"), s.carbEntries) : String(format: NSLocalizedString("%1$@ entries", comment: "Trends Carbs per day tile detail: average number of carb entries per day, a fractional value such as 2.5, so not pluralised. (1: number with one decimal)"), - LoopFormat.number(Double(s.carbEntries) / s.days, decimals: 1)), - valueColor: P.carbsInk)) + LoopFormat.number(Double(s.carbEntries) / s.days, decimals: 1)))) tiles.append(StatTile(label: oneDay ? NSLocalizedString("Boluses", comment: "Trends summary tile for a one-day period: how many manual boluses were given that day") : NSLocalizedString("Boluses per day", comment: "Trends summary tile: average number of manual boluses per day"), @@ -299,7 +298,7 @@ struct TrendsView: View { } else if let error = model.loadError { LoopCard { ChartPlaceholder(message: String(format: NSLocalizedString("Couldn't load Trends: %1$@", comment: "Trends error message when the history can't be read. (1: error description)"), error)) - Button(NSLocalizedString("Retry", comment: "Button to try loading Trends again after an error")) { model.reload() }.foregroundStyle(P.accentInk) + Button(NSLocalizedString("Retry", comment: "Button to try loading Trends again after an error")) { model.reload() } } } else { ProgressView().frame(maxWidth: .infinity, minHeight: 300) diff --git a/Loop/Views/Statistics/GlucoseBand+Display.swift b/Loop/Views/Statistics/GlucoseBand+Display.swift index 5c4beec4ba..a088563197 100644 --- a/Loop/Views/Statistics/GlucoseBand+Display.swift +++ b/Loop/Views/Statistics/GlucoseBand+Display.swift @@ -25,18 +25,26 @@ private func bandEdge(mgdl: Double, mmol: Double, _ preference: DisplayGlucosePr } /// captūrAGP / IDC report palette for the glucose bands, matched to the standard -/// Ambulatory Glucose Profile report colors. +/// Ambulatory Glucose Profile report colors. In dark mode each band is lifted so it +/// stays clear against a dark card, and very low becomes the brighter red. extension GlucoseBand { var agpColor: Color { switch self { - case .veryLow: return Color(red: 0.60, green: 0.13, blue: 0.15) // dark red - case .low: return Color(red: 0.85, green: 0.26, blue: 0.24) // red - case .target: return Color(red: 0.36, green: 0.65, blue: 0.37) // green - case .high: return Color(red: 0.91, green: 0.77, blue: 0.26) // gold / yellow - case .veryHigh: return Color(red: 0.85, green: 0.55, blue: 0.24) // orange + case .veryLow: return Self.color(light: (0.60, 0.13, 0.15), dark: (1.00, 0.27, 0.23)) // dark red + case .low: return Self.color(light: (0.85, 0.26, 0.24), dark: (1.00, 0.62, 0.60)) // red + case .target: return Self.color(light: (0.36, 0.65, 0.37), dark: (0.20, 0.84, 0.29)) // green + case .high: return Self.color(light: (0.91, 0.77, 0.26), dark: (1.00, 0.84, 0.04)) // gold / yellow + case .veryHigh: return Self.color(light: (0.85, 0.55, 0.24), dark: (0.79, 0.54, 0.00)) // orange } } + private static func color(light: (Double, Double, Double), dark: (Double, Double, Double)) -> Color { + Color(uiColor: UIColor { traits in + let (red, green, blue) = traits.userInterfaceStyle == .dark ? dark : light + return UIColor(red: red, green: green, blue: blue, alpha: 1) + }) + } + /// The band's range, written in the unit the user reads glucose in. func rangeDescription(_ preference: DisplayGlucosePreference) -> String { func edge(_ mgdl: Double, _ mmol: Double) -> String { From f9af46ec84240691f3ff015116f9bf732581b592 Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:37 +0000 Subject: [PATCH 08/15] Read closed and open loop from stored settings Loop status reads dosingEnabled from SettingsStore, including the record before the period, instead of a UserDefaults record. A manual temp basal counts as open loop, as on the home screen. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- Loop/Localizable.xcstrings | 3621 +++++-------------- Loop/Managers/LoopAppManager.swift | 8 - Loop/Redesign/LoopHistoryMath.swift | 31 +- Loop/Redesign/LoopHistoryModels.swift | 1 - Loop/Redesign/LoopHistoryProvider.swift | 95 +- Loop/Redesign/TrendsScreen.swift | 15 +- LoopTests/Models/LoopHistoryMathTests.swift | 41 +- 7 files changed, 943 insertions(+), 2869 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 07ceedc6ef..f5faaf7562 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -6325,6729 +6325,5013 @@ } } }, - "Closed loop off" : { - "comment" : "Reason shown for a stretch of time when Loop was in open loop because closed loop was turned off", + "COB" : { + "comment" : "Abbreviation for carbs on board (active carbohydrate) Abbreviation for carbs on board; label of a tile on the Now screen", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الحلقة المغلقة متوقفة" + "value" : "COB" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Uzavřená smyčka vypnuta" + "value" : "COB" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Lukket Loop slået fra" + "value" : "COB" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Closed Loop aus" + "value" : "COB" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Circuito cerrado desactivado" + "value" : "COB" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Suljettu säätö pois" + "value" : "COB" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Boucle fermée désactivée" + "value" : "GA" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "Closed Loop כבוי" + "value" : "COB" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Zárt hurok kikapcsolva" + "value" : "COB" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Loop chiuso disattivato" + "value" : "COB" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "クローズドループ オフ" + "value" : "COB" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Lukket Loop av" + "value" : "COB" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Actieve loop uit" + "value" : "COB" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pętla zamknięta wyłączona" + "value" : "COB" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Ciclo fechado desligado" + "value" : "COB" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Buclă închisă oprită" + "value" : "COB" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Замкнутый цикл выключен" + "value" : "COB" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Uzavretá slučka vypnutá" + "value" : "COB" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Sluten loop av" + "value" : "COB" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kapalı döngü kapalı" + "value" : "COB" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Замкнутий цикл вимкнено" + "value" : "COB" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Vòng lặp kín tắt" + "value" : "COB" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "闭环已关闭" + "value" : "COB" } } } }, - "COB" : { - "comment" : "Abbreviation for carbs on board (active carbohydrate) Abbreviation for carbs on board; label of a tile on the Now screen", + "Couldn't load Trends: %1$@" : { + "comment" : "Trends error message when the history can't be read. (1: error description)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "تعذّر تحميل الاتجاهات: %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Nelze načíst trendy: %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Kunne ikke indlæse Tendenser: %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Trends konnten nicht geladen werden: %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "No se pudieron cargar las tendencias: %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Trendejä ei voitu ladata: %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "GA" + "value" : "Impossible de charger les tendances : %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "לא ניתן היה לטעון מגמות: %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Nem sikerült betölteni a trendeket: %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Impossibile caricare le tendenze: %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "トレンドを読み込めませんでした:%1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Kunne ikke laste inn Trender: %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Trends kunnen niet worden geladen: %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Nie udało się wczytać trendów: %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Não foi possível carregar as tendências: %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Nu s-au putut încărca tendințele: %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Не удалось загрузить тренды: %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Trendy sa nedajú načítať: %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Kunde inte läsa in Trender: %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Eğilimler yüklenemedi: %1$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Не вдалося завантажити тренди: %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Không thể tải Xu hướng: %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "无法载入趋势:%1$@" } } } }, - "Configuration error" : { - "comment" : "Reason Loop could not run: its settings are incomplete", + "Covers %1$@, from the start of Loop's records, which it keeps for %2$@." : { + "comment" : "Trends Loop status footnote when Loop's records of its runs and settings start partway through the period. (1: date range covered; 2: how long Loop keeps them, e.g. 90 days)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "خطأ في الإعدادات" + "value" : "يغطي %1$@، من بداية سجلات Loop التي يحتفظ بها لمدة %2$@." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba konfigurace" + "value" : "Pokrývá %1$@, od začátku záznamů Loopu, které uchovává %2$@." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurationsfejl" + "value" : "Dækker %1$@, fra starten af Loops registreringer, som gemmes i %2$@." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurationsfehler" + "value" : "Deckt %1$@ ab, ab Beginn der Aufzeichnungen von Loop, die %2$@ lang gespeichert werden." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Error de configuración" + "value" : "Cubre %1$@, desde el inicio de los registros de Loop, que se conservan durante %2$@." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Määritysvirhe" + "value" : "Kattaa %1$@, Loopin tietojen alusta; Loop säilyttää niitä %2$@." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Erreur de configuration" + "value" : "Couvre %1$@, depuis le début des enregistrements de Loop, conservés pendant %2$@." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שגיאת תצורה" + "value" : "מכסה %1$@, מתחילת הרישומים של Loop, שנשמרים למשך %2$@." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurációs hiba" + "value" : "Lefedi: %1$@, a Loop rögzítéseinek kezdetétől; a Loop %2$@ időtartamig őrzi meg őket." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Errore di configurazione" + "value" : "Copre %1$@, dall'inizio delle registrazioni di Loop, conservate per %2$@." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "設定エラー" + "value" : "%1$@を表示(Loopの記録の開始以降。Loopは%2$@保存します)。" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurasjonsfeil" + "value" : "Dekker %1$@, fra starten av Loops registreringer, som lagres i %2$@." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Configuratiefout" + "value" : "Beslaat %1$@, vanaf het begin van de gegevens van Loop, die %2$@ worden bewaard." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Błąd konfiguracji" + "value" : "Obejmuje %1$@, od początku zapisów Loop, które są przechowywane przez %2$@." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Erro de configuração" + "value" : "Cobre %1$@, desde o início dos registros do Loop, mantidos por %2$@." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Eroare de configurare" + "value" : "Acoperă %1$@, de la începutul înregistrărilor Loop, păstrate %2$@." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ошибка настроек" + "value" : "Охватывает %1$@, с начала записей Loop, которые хранятся %2$@." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba konfigurácie" + "value" : "Pokrýva %1$@, od začiatku záznamov Loopu, ktoré uchováva %2$@." } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Konfigurationsfel" + "value" : "Täcker %1$@, från början av Loops registreringar, som sparas i %2$@." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yapılandırma hatası" + "value" : "%1$@ dönemini kapsar; Loop kayıtlarının başlangıcından itibaren, kayıtlar %2$@ saklanır." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Помилка налаштувань" + "value" : "Охоплює %1$@, від початку записів Loop, які зберігаються %2$@." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Lỗi cấu hình" + "value" : "Bao gồm %1$@, từ khi bắt đầu dữ liệu của Loop, được lưu trong %2$@." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "配置错误" + "value" : "涵盖 %1$@,从 Loop 记录开始起;Loop 会保留这些记录 %2$@。" } } } }, - "Connection error" : { - "comment" : "Reason Loop could not run: no connected devices, or a device connection failed", + "Daily pattern" : { + "comment" : "Trends card title: the ambulatory glucose profile, glucose by time of day across the period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "خطأ في الاتصال" + "value" : "النمط اليومي" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba připojení" + "value" : "Denní profil" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Forbindelsesfejl" + "value" : "Døgnmønster" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Verbindungsfehler" + "value" : "Tagesprofil" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Error de conexión" + "value" : "Patrón diario" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Yhteysvirhe" + "value" : "Vuorokausiprofiili" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Erreur de connexion" + "value" : "Profil journalier" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שגיאת חיבור" + "value" : "דפוס יומי" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kapcsolódási hiba" + "value" : "Napi mintázat" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Errore di connessione" + "value" : "Andamento giornaliero" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "接続エラー" + "value" : "日内パターン" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Tilkoblingsfeil" + "value" : "Døgnmønster" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verbindingsfout" + "value" : "Dagpatroon" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Błąd połączenia" + "value" : "Profil dobowy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Erro de conexão" + "value" : "Padrão diário" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Eroare de conexiune" + "value" : "Tipar zilnic" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ошибка подключения" + "value" : "Суточный профиль" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba pripojenia" + "value" : "Denný profil" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Anslutningsfel" + "value" : "Dygnsmönster" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bağlantı hatası" + "value" : "Günlük örüntü" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Помилка підключення" + "value" : "Добовий профіль" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Lỗi kết nối" + "value" : "Mẫu hằng ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "连接错误" + "value" : "每日模式" } } } }, - "Couldn't load Trends: %1$@" : { - "comment" : "Trends error message when the history can't be read. (1: error description)", + "Daily pattern over %1$@. Median highest %2$@ at %3$@, lowest %4$@ at %5$@" : { + "comment" : "VoiceOver label for the Trends Daily pattern chart. (1: days with readings, e.g. 14 days; 2: highest median glucose; 3: time of day; 4: lowest median glucose; 5: time of day)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تعذّر تحميل الاتجاهات: %1$@" + "value" : "النمط اليومي خلال %1$@. أعلى وسيط %2$@ عند %3$@، وأدنى وسيط %4$@ عند %5$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nelze načíst trendy: %1$@" + "value" : "Denní profil za %1$@. Nejvyšší medián %2$@ v %3$@, nejnižší %4$@ v %5$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke indlæse Tendenser: %1$@" + "value" : "Døgnmønster over %1$@. Median højest %2$@ kl. %3$@, lavest %4$@ kl. %5$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Trends konnten nicht geladen werden: %1$@" + "value" : "Tagesprofil über %1$@. Median am höchsten %2$@ um %3$@, am niedrigsten %4$@ um %5$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "No se pudieron cargar las tendencias: %1$@" + "value" : "Patrón diario de %1$@. Mediana más alta %2$@ a las %3$@, más baja %4$@ a las %5$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Trendejä ei voitu ladata: %1$@" + "value" : "Vuorokausiprofiili %1$@ ajalta. Korkein mediaani %2$@ klo %3$@, matalin %4$@ klo %5$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Impossible de charger les tendances : %1$@" + "value" : "Profil journalier sur %1$@. Médiane la plus haute %2$@ à %3$@, la plus basse %4$@ à %5$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "לא ניתן היה לטעון מגמות: %1$@" + "value" : "דפוס יומי במשך %1$@. חציון גבוה ביותר %2$@ ב-%3$@, נמוך ביותר %4$@ ב-%5$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nem sikerült betölteni a trendeket: %1$@" + "value" : "Napi mintázat, %1$@. Legmagasabb medián %2$@ ekkor: %3$@, legalacsonyabb %4$@ ekkor: %5$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Impossibile caricare le tendenze: %1$@" + "value" : "Andamento giornaliero su %1$@. Mediana più alta %2$@ alle %3$@, più bassa %4$@ alle %5$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "トレンドを読み込めませんでした:%1$@" + "value" : "%1$@の日内パターン。中央値の最高は%3$@の%2$@、最低は%5$@の%4$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Kunne ikke laste inn Trender: %1$@" + "value" : "Døgnmønster over %1$@. Median høyest %2$@ kl. %3$@, lavest %4$@ kl. %5$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Trends kunnen niet worden geladen: %1$@" + "value" : "Dagpatroon over %1$@. Mediaan hoogst %2$@ om %3$@, laagst %4$@ om %5$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nie udało się wczytać trendów: %1$@" + "value" : "Profil dobowy z %1$@. Najwyższa mediana %2$@ o %3$@, najniższa %4$@ o %5$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Não foi possível carregar as tendências: %1$@" + "value" : "Padrão diário em %1$@. Mediana mais alta %2$@ às %3$@, mais baixa %4$@ às %5$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Nu s-au putut încărca tendințele: %1$@" + "value" : "Tipar zilnic pe %1$@. Mediana cea mai mare %2$@ la %3$@, cea mai mică %4$@ la %5$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Не удалось загрузить тренды: %1$@" + "value" : "Суточный профиль за %1$@. Наибольшая медиана %2$@ в %3$@, наименьшая %4$@ в %5$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Trendy sa nedajú načítať: %1$@" + "value" : "Denný profil za %1$@. Najvyšší medián %2$@ o %3$@, najnižší %4$@ o %5$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Kunde inte läsa in Trender: %1$@" + "value" : "Dygnsmönster över %1$@. Högsta median %2$@ kl. %3$@, lägsta %4$@ kl. %5$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Eğilimler yüklenemedi: %1$@" + "value" : "%1$@ boyunca günlük örüntü. En yüksek medyan %3$@ saatinde %2$@, en düşük %5$@ saatinde %4$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Не вдалося завантажити тренди: %1$@" + "value" : "Добовий профіль за %1$@. Найвища медіана %2$@ о %3$@, найнижча %4$@ о %5$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không thể tải Xu hướng: %1$@" + "value" : "Mẫu hằng ngày trong %1$@. Trung vị cao nhất %2$@ lúc %3$@, thấp nhất %4$@ lúc %5$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无法载入趋势:%1$@" + "value" : "%1$@的每日模式。中位数最高 %2$@(%3$@),最低 %4$@(%5$@)" } } } }, - "Covers %1$@: Loop keeps 7 days of runs, and closed or open loop is recorded from when this version of Loop first ran." : { - "comment" : "Trends Loop status footnote when the status covers only part of the period. (1: date range covered)", + "Daily pattern: not enough days" : { + "comment" : "VoiceOver label for the Trends Daily pattern chart when fewer than 5 days have readings", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يغطي %1$@: يحتفظ Loop بسجل تشغيلات 7 أيام، وتُسجَّل الحلقة المغلقة أو المفتوحة منذ أول تشغيل لهذا الإصدار من Loop." + "value" : "النمط اليومي: لا توجد أيام كافية" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Pokrývá %1$@: Loop uchovává běhy za 7 dní a uzavřená či otevřená smyčka se zaznamenává od prvního spuštění této verze Loop." + "value" : "Denní profil: málo dní" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Dækker %1$@: Loop gemmer kørsler i 7 dage, og lukket eller åbent Loop registreres fra første gang denne version af Loop kørte." + "value" : "Døgnmønster: ikke nok dage" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Umfasst %1$@: Loop speichert Durchläufe 7 Tage lang, und Closed oder Open Loop wird erst ab dem ersten Start dieser Loop-Version aufgezeichnet." + "value" : "Tagesprofil: nicht genug Tage" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Cubre %1$@: Loop guarda 7 días de ejecuciones, y el circuito cerrado o abierto se registra desde la primera vez que se ejecutó esta versión de Loop." + "value" : "Patrón diario: no hay suficientes días" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kattaa %1$@: Loop säilyttää 7 päivän ajot, ja suljettu tai avoin säätö tallennetaan siitä lähtien, kun tämä Loopin versio käynnistettiin ensimmäisen kerran." + "value" : "Vuorokausiprofiili: liian vähän päiviä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Couvre %1$@ : Loop conserve 7 jours d'exécutions, et la boucle fermée ou ouverte est enregistrée depuis le premier lancement de cette version de Loop." + "value" : "Profil journalier : pas assez de jours" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מכסה %1$@: Loop שומר 7 ימים של הרצות, ו-Closed Loop או Open Loop נרשמים מאז ההפעלה הראשונה של גרסה זו של Loop." + "value" : "דפוס יומי: אין מספיק ימים" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Lefedett időszak: %1$@. A Loop 7 napnyi futást őriz meg, a zárt vagy nyitott hurok pedig a Loop ezen verziójának első futtatásától kerül rögzítésre." + "value" : "Napi mintázat: nincs elég nap" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Copre %1$@: Loop conserva 7 giorni di esecuzioni e il loop chiuso o aperto è registrato dal primo avvio di questa versione di Loop." + "value" : "Andamento giornaliero: giorni insufficienti" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "対象期間 %1$@:Loopは実行記録を7日間保持し、クローズド/オープンループはこのバージョンのLoopを初めて起動した時点から記録されます。" + "value" : "日内パターン:日数が不足しています" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Dekker %1$@: Loop lagrer kjøringer i 7 dager, og lukket eller åpen Loop registreres fra første gang denne versjonen av Loop kjørte." + "value" : "Døgnmønster: ikke nok dager" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Omvat %1$@: Loop bewaart 7 dagen aan loop-runs, en actieve of open loop wordt vastgelegd vanaf de eerste keer dat deze versie van Loop draaide." + "value" : "Dagpatroon: niet genoeg dagen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Obejmuje %1$@: Loop przechowuje cykle z 7 dni, a pętla zamknięta lub otwarta jest zapisywana od pierwszego uruchomienia tej wersji Loop." + "value" : "Profil dobowy: za mało dni" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Cobre %1$@: o Loop guarda 7 dias de execuções, e o ciclo fechado ou aberto é registrado desde a primeira vez que esta versão do Loop foi executada." + "value" : "Padrão diário: dias insuficientes" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Acoperă %1$@: Loop păstrează 7 zile de rulări, iar bucla închisă sau deschisă este înregistrată de la prima rulare a acestei versiuni de Loop." + "value" : "Tipar zilnic: prea puține zile" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Охвачено: %1$@. Loop хранит запуски за 7 дней, а замкнутый или открытый цикл записывается с первого запуска этой версии Loop." + "value" : "Суточный профиль: недостаточно дней" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Pokrýva %1$@: Loop uchováva behy za 7 dní a uzavretá či otvorená slučka sa zaznamenáva od prvého spustenia tejto verzie Loop." + "value" : "Denný profil: málo dní" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Omfattar %1$@: Loop sparar 7 dagars körningar, och sluten eller öppen loop registreras från när den här versionen av Loop först kördes." + "value" : "Dygnsmönster: för få dagar" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kapsanan dönem %1$@: Loop 7 günlük çalışma kaydı tutar; kapalı veya açık döngü, Loop'un bu sürümünün ilk çalıştığı andan itibaren kaydedilir." + "value" : "Günlük örüntü: yeterli gün yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Охоплено: %1$@. Loop зберігає запуски за 7 днів, а замкнутий чи відкритий цикл записується з першого запуску цієї версії Loop." + "value" : "Добовий профіль: замало днів" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Bao gồm %1$@: Loop lưu các lần chạy trong 7 ngày, và trạng thái vòng lặp kín hoặc mở được ghi lại từ khi phiên bản Loop này chạy lần đầu." + "value" : "Mẫu hằng ngày: không đủ số ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "覆盖 %1$@:Loop 保留 7 天的运行记录,闭环或开环状态从此版本 Loop 首次运行时开始记录。" + "value" : "每日模式:天数不足" } } } }, - "Daily pattern" : { - "comment" : "Trends card title: the ambulatory glucose profile, glucose by time of day across the period", + "Day" : { + "comment" : "Appearance option and app icon name: the light (daytime) version", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النمط اليومي" + "value" : "نهار" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Denní profil" + "value" : "Den" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster" + "value" : "Dag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tagesprofil" + "value" : "Tag" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Patrón diario" + "value" : "Día" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vuorokausiprofiili" + "value" : "Päivä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Profil journalier" + "value" : "Jour" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דפוס יומי" + "value" : "יום" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Napi mintázat" + "value" : "Nap" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Andamento giornaliero" + "value" : "Giorno" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "日内パターン" + "value" : "日" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster" + "value" : "Dag" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dagpatroon" + "value" : "Dag" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Profil dobowy" + "value" : "Dzień" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Padrão diário" + "value" : "Dia" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Tipar zilnic" + "value" : "Zi" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Суточный профиль" + "value" : "День" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Denný profil" + "value" : "Deň" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Dygnsmönster" + "value" : "Dag" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Günlük örüntü" + "value" : "Gündüz" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Добовий профіль" + "value" : "День" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mẫu hằng ngày" + "value" : "Ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "每日模式" + "value" : "日" } } } }, - "Daily pattern over %1$@. Median highest %2$@ at %3$@, lowest %4$@ at %5$@" : { - "comment" : "VoiceOver label for the Trends Daily pattern chart. (1: days with readings, e.g. 14 days; 2: highest median glucose; 3: time of day; 4: lowest median glucose; 5: time of day)", + "Day (Trends period)" : { + "comment" : "Trends period picker: a period of one calendar day (not daytime)", "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Day" + } + }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النمط اليومي خلال %1$@. أعلى وسيط %2$@ عند %3$@، وأدنى وسيط %4$@ عند %5$@" + "value" : "يوم" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Denní profil za %1$@. Nejvyšší medián %2$@ v %3$@, nejnižší %4$@ v %5$@" + "value" : "Den" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster over %1$@. Median højest %2$@ kl. %3$@, lavest %4$@ kl. %5$@" + "value" : "Dag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tagesprofil über %1$@. Median am höchsten %2$@ um %3$@, am niedrigsten %4$@ um %5$@" + "value" : "Tag" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Patrón diario de %1$@. Mediana más alta %2$@ a las %3$@, más baja %4$@ a las %5$@" + "value" : "Día" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vuorokausiprofiili %1$@ ajalta. Korkein mediaani %2$@ klo %3$@, matalin %4$@ klo %5$@" + "value" : "Päivä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Profil journalier sur %1$@. Médiane la plus haute %2$@ à %3$@, la plus basse %4$@ à %5$@" + "value" : "Jour" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דפוס יומי במשך %1$@. חציון גבוה ביותר %2$@ ב-%3$@, נמוך ביותר %4$@ ב-%5$@" + "value" : "יום" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Napi mintázat, %1$@. Legmagasabb medián %2$@ ekkor: %3$@, legalacsonyabb %4$@ ekkor: %5$@" + "value" : "Nap" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Andamento giornaliero su %1$@. Mediana più alta %2$@ alle %3$@, più bassa %4$@ alle %5$@" + "value" : "Giorno" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@の日内パターン。中央値の最高は%3$@の%2$@、最低は%5$@の%4$@" + "value" : "日" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster over %1$@. Median høyest %2$@ kl. %3$@, lavest %4$@ kl. %5$@" + "value" : "Dag" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dagpatroon over %1$@. Mediaan hoogst %2$@ om %3$@, laagst %4$@ om %5$@" + "value" : "Dag" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Profil dobowy z %1$@. Najwyższa mediana %2$@ o %3$@, najniższa %4$@ o %5$@" + "value" : "Dzień" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Padrão diário em %1$@. Mediana mais alta %2$@ às %3$@, mais baixa %4$@ às %5$@" + "value" : "Dia" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Tipar zilnic pe %1$@. Mediana cea mai mare %2$@ la %3$@, cea mai mică %4$@ la %5$@" + "value" : "Zi" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Суточный профиль за %1$@. Наибольшая медиана %2$@ в %3$@, наименьшая %4$@ в %5$@" + "value" : "День" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Denný profil za %1$@. Najvyšší medián %2$@ o %3$@, najnižší %4$@ o %5$@" + "value" : "Deň" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Dygnsmönster över %1$@. Högsta median %2$@ kl. %3$@, lägsta %4$@ kl. %5$@" + "value" : "Dag" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@ boyunca günlük örüntü. En yüksek medyan %3$@ saatinde %2$@, en düşük %5$@ saatinde %4$@" + "value" : "Gün" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Добовий профіль за %1$@. Найвища медіана %2$@ о %3$@, найнижча %4$@ о %5$@" + "value" : "День" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mẫu hằng ngày trong %1$@. Trung vị cao nhất %2$@ lúc %3$@, thấp nhất %4$@ lúc %5$@" + "value" : "Ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "%1$@的每日模式。中位数最高 %2$@(%3$@),最低 %4$@(%5$@)" + "value" : "日" } } } }, - "Daily pattern: not enough days" : { - "comment" : "VoiceOver label for the Trends Daily pattern chart when fewer than 5 days have readings", + "In range" : { + "comment" : "Time in range band: glucose 70–180 mg/dL (3.9–10.0 mmol/L) Time-in-range band name: glucose 70–180 mg/dL (3.9–10.0 mmol/L)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النمط اليومي: لا توجد أيام كافية" + "value" : "ضمن النطاق" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Denní profil: málo dní" + "value" : "V rozsahu" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster: ikke nok dage" + "value" : "I området" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tagesprofil: nicht genug Tage" + "value" : "Im Zielbereich" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Patrón diario: no hay suficientes días" + "value" : "En rango" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vuorokausiprofiili: liian vähän päiviä" + "value" : "Tavoitealueella" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Profil journalier : pas assez de jours" + "value" : "Dans la cible" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דפוס יומי: אין מספיק ימים" + "value" : "בטווח" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Napi mintázat: nincs elég nap" + "value" : "Tartományban" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Andamento giornaliero: giorni insufficienti" + "value" : "Nell'intervallo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "日内パターン:日数が不足しています" + "value" : "範囲内" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Døgnmønster: ikke nok dager" + "value" : "I målområdet" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dagpatroon: niet genoeg dagen" + "value" : "Binnen bereik" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Profil dobowy: za mało dni" + "value" : "W zakresie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Padrão diário: dias insuficientes" + "value" : "No alvo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Tipar zilnic: prea puține zile" + "value" : "În interval" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Суточный профиль: недостаточно дней" + "value" : "В диапазоне" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Denný profil: málo dní" + "value" : "V rozsahu" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Dygnsmönster: för få dagar" + "value" : "Inom målområdet" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Günlük örüntü: yeterli gün yok" + "value" : "Aralıkta" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Добовий профіль: замало днів" + "value" : "У діапазоні" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mẫu hằng ngày: không đủ số ngày" + "value" : "Trong khoảng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "每日模式:天数不足" + "value" : "范围内" } } } }, - "Day" : { - "comment" : "Appearance option and app icon name: the light (daytime) version", + "Insulin per day" : { + "comment" : "Trends tile and card title: average total insulin per day", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "نهار" + "value" : "الأنسولين يوميًا" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Den" + "value" : "Inzulín za den" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin pr. dag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tag" + "value" : "Insulin pro Tag" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Día" + "value" : "Insulina por día" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Päivä" + "value" : "Insuliini päivässä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Jour" + "value" : "Insuline par jour" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "יום" + "value" : "אינסולין ליום" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nap" + "value" : "Inzulin naponta" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Giorno" + "value" : "Insulina al giorno" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "日" + "value" : "1日のインスリン量" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin per dag" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insuline per dag" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dzień" + "value" : "Insulina dziennie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Dia" + "value" : "Insulina por dia" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Zi" + "value" : "Insulină pe zi" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "День" + "value" : "Инсулин в день" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Deň" + "value" : "Inzulín za deň" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin per dag" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gündüz" + "value" : "Günlük insülin" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "День" + "value" : "Інсулін на день" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Ngày" + "value" : "Insulin mỗi ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "日" + "value" : "每日胰岛素" } } } }, - "Day (Trends period)" : { - "comment" : "Trends period picker: a period of one calendar day (not daytime)", + "Insulin per day, average %1$@" : { + "comment" : "VoiceOver label for the Trends Insulin per day chart. (1: average insulin per day, e.g. 32.1 U)", "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Day" - } - }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يوم" + "value" : "الأنسولين يوميًا، المتوسط %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Den" + "value" : "Inzulín za den, průměr %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin pr. dag, gennemsnit %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Tag" + "value" : "Insulin pro Tag, Durchschnitt %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Día" + "value" : "Insulina por día, promedio %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Päivä" + "value" : "Insuliini päivässä, keskiarvo %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Jour" + "value" : "Insuline par jour, moyenne %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "יום" + "value" : "אינסולין ליום, ממוצע %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nap" + "value" : "Inzulin naponta, átlag %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Giorno" + "value" : "Insulina al giorno, media %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "日" + "value" : "1日のインスリン量、平均 %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin per dag, gjennomsnitt %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insuline per dag, gemiddeld %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dzień" + "value" : "Insulina dziennie, średnio %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Dia" + "value" : "Insulina por dia, média %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Zi" + "value" : "Insulină pe zi, medie %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "День" + "value" : "Инсулин в день, в среднем %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Deň" + "value" : "Inzulín za deň, priemer %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Dag" + "value" : "Insulin per dag, medel %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gün" + "value" : "Günlük insülin, ortalama %1$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "День" + "value" : "Інсулін на день, у середньому %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Ngày" + "value" : "Insulin mỗi ngày, trung bình %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "日" + "value" : "每日胰岛素,平均 %1$@" } } } }, - "Glucose too old" : { - "comment" : "Reason Loop could not run: the latest glucose reading was too old", + "Insulin per day, weekly average, average %1$@" : { + "comment" : "VoiceOver label for the Trends Insulin per day chart when bars are weekly averages. (1: average insulin per day, e.g. 32.1 U)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "قراءة السكر قديمة جدًا" + "value" : "الأنسولين يوميًا، متوسط أسبوعي، المتوسط %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Glukóza je příliš stará" + "value" : "Inzulín za den, týdenní průměr, průměr %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Glukosemåling for gammel" + "value" : "Insulin pr. dag, ugegennemsnit, gennemsnit %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Blutzuckerwert zu alt" + "value" : "Insulin pro Tag, Wochendurchschnitt, Durchschnitt %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Glucosa demasiado antigua" + "value" : "Insulina por día, promedio semanal, promedio %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoosi liian vanha" + "value" : "Insuliini päivässä, viikkokeskiarvo, keskiarvo %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Glycémie trop ancienne" + "value" : "Insuline par jour, moyenne hebdomadaire, moyenne %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "קריאת הסוכר ישנה מדי" + "value" : "אינסולין ליום, ממוצע שבועי, ממוצע %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Túl régi glükózérték" + "value" : "Inzulin naponta, heti átlag, átlag %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemia troppo vecchia" + "value" : "Insulina al giorno, media settimanale, media %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "血糖値が古すぎます" + "value" : "1日のインスリン量、週平均、平均 %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Blodsukkermåling for gammel" + "value" : "Insulin per dag, ukesgjennomsnitt, gjennomsnitt %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Glucosewaarde te oud" + "value" : "Insuline per dag, weekgemiddelde, gemiddeld %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoza zbyt stara" + "value" : "Insulina dziennie, średnia tygodniowa, średnio %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Glicose muito antiga" + "value" : "Insulina por dia, média semanal, média %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemie prea veche" + "value" : "Insulină pe zi, medie săptămânală, medie %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Данные глюкозы устарели" + "value" : "Инсулин в день, среднее за неделю, в среднем %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Glykémia je príliš stará" + "value" : "Inzulín za deň, týždenný priemer, priemer %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Glukosvärdet är för gammalt" + "value" : "Insulin per dag, veckomedel, medel %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kan şekeri verisi çok eski" + "value" : "Günlük insülin, haftalık ortalama, ortalama %1$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Дані глюкози застаріли" + "value" : "Інсулін на день, середнє за тиждень, у середньому %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Dữ liệu đường huyết quá cũ" + "value" : "Insulin mỗi ngày, trung bình theo tuần, trung bình %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "血糖数据过旧" + "value" : "每日胰岛素,按周平均,平均 %1$@" } } } }, - "In range" : { - "comment" : "Time in range band: glucose 70–180 mg/dL (3.9–10.0 mmol/L) Time-in-range band name: glucose 70–180 mg/dL (3.9–10.0 mmol/L)", + "Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@" : { + "comment" : "VoiceOver summary of the day's insulin timeline chart with no glucose readings. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "ضمن النطاق" + "value" : "مخطط الأنسولين، %1$@، Bolus %2$@، القاعدية %3$@، الكارب %4$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "V rozsahu" + "value" : "Časová osa inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "I området" + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kulhydrater %4$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Im Zielbereich" + "value" : "Insulin-Zeitleiste, %1$@, Bolus %2$@, Basal %3$@, KH %4$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "En rango" + "value" : "Cronología de insulina, %1$@, bolo %2$@, basal %3$@, carbohidratos %4$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Tavoitealueella" + "value" : "Insuliinin aikajana, %1$@, bolus %2$@, basaali %3$@, hiilihydraatit %4$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Dans la cible" + "value" : "Chronologie de l'insuline, %1$@, bolus %2$@, basal %3$@, glucides %4$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "בטווח" + "value" : "ציר זמן אינסולין, %1$@, Bolus %2$@, Basal %3$@, פחמימות %4$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Tartományban" + "value" : "Inzulin idővonal, %1$@, bólus %2$@, basal %3$@, szénhidrát %4$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nell'intervallo" + "value" : "Cronologia insulina, %1$@, bolo %2$@, basale %3$@, carboidrati %4$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "範囲内" + "value" : "インスリンのタイムライン、%1$@、ボーラス %2$@、ベーサル %3$@、カーボ %4$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "I målområdet" + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, karbohydrater %4$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Binnen bereik" + "value" : "Insulinetijdlijn, %1$@, bolus %2$@, basaal %3$@, koolhydraten %4$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "W zakresie" + "value" : "Oś czasu insuliny, %1$@, bolus %2$@, baza %3$@, węglowodany %4$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "No alvo" + "value" : "Linha do tempo de insulina, %1$@, bolus %2$@, basal %3$@, carbs %4$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "În interval" + "value" : "Cronologie insulină, %1$@, bolus %2$@, bazală %3$@, carbohidrați %4$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "В диапазоне" + "value" : "Хронология инсулина, %1$@, болюс %2$@, базал %3$@, углеводы %4$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "V rozsahu" + "value" : "Časová os inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inom målområdet" + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kolhydrater %4$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Aralıkta" + "value" : "İnsülin zaman çizelgesi, %1$@, bolus %2$@, bazal %3$@, karb %4$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "У діапазоні" + "value" : "Хронологія інсуліну, %1$@, болюс %2$@, базал %3$@, вуглеводи %4$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trong khoảng" + "value" : "Dòng thời gian insulin, %1$@, Bolus %2$@, Basal %3$@, Carbs %4$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "范围内" + "value" : "胰岛素时间线,%1$@,大剂量 %2$@,基础 %3$@,碳水 %4$@" } } } }, - "Insulin per day" : { - "comment" : "Trends tile and card title: average total insulin per day", + "Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@, latest glucose %5$@ %6$@" : { + "comment" : "VoiceOver summary of the day's insulin timeline chart. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g; 5: latest glucose value; 6: glucose unit, mmol/L or mg/dL)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الأنسولين يوميًا" + "value" : "مخطط الأنسولين، %1$@، Bolus %2$@، القاعدية %3$@، الكارب %4$@، آخر قراءة سكر %5$@ %6$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulín za den" + "value" : "Časová osa inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@, poslední glukóza %5$@ %6$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin pr. dag" + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kulhydrater %4$@, seneste glukose %5$@ %6$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin pro Tag" + "value" : "Insulin-Zeitleiste, %1$@, Bolus %2$@, Basal %3$@, KH %4$@, letzter Blutzucker %5$@ %6$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina por día" + "value" : "Cronología de insulina, %1$@, bolo %2$@, basal %3$@, carbohidratos %4$@, última glucosa %5$@ %6$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Insuliini päivässä" + "value" : "Insuliinin aikajana, %1$@, bolus %2$@, basaali %3$@, hiilihydraatit %4$@, viimeisin glukoosi %5$@ %6$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Insuline par jour" + "value" : "Chronologie de l'insuline, %1$@, bolus %2$@, basal %3$@, glucides %4$@, dernière glycémie %5$@ %6$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אינסולין ליום" + "value" : "ציר זמן אינסולין, %1$@, Bolus %2$@, Basal %3$@, פחמימות %4$@, סוכר אחרון %5$@ %6$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulin naponta" + "value" : "Inzulin idővonal, %1$@, bólus %2$@, basal %3$@, szénhidrát %4$@, legutóbbi glükóz %5$@ %6$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina al giorno" + "value" : "Cronologia insulina, %1$@, bolo %2$@, basale %3$@, carboidrati %4$@, ultima glicemia %5$@ %6$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "1日のインスリン量" + "value" : "インスリンのタイムライン、%1$@、ボーラス %2$@、ベーサル %3$@、カーボ %4$@、最新の血糖値 %5$@ %6$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin per dag" + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, karbohydrater %4$@, siste blodsukker %5$@ %6$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Insuline per dag" + "value" : "Insulinetijdlijn, %1$@, bolus %2$@, basaal %3$@, koolhydraten %4$@, laatste glucose %5$@ %6$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina dziennie" + "value" : "Oś czasu insuliny, %1$@, bolus %2$@, baza %3$@, węglowodany %4$@, ostatnia glukoza %5$@ %6$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina por dia" + "value" : "Linha do tempo de insulina, %1$@, bolus %2$@, basal %3$@, carbs %4$@, última glicose %5$@ %6$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Insulină pe zi" + "value" : "Cronologie insulină, %1$@, bolus %2$@, bazală %3$@, carbohidrați %4$@, ultima glicemie %5$@ %6$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Инсулин в день" + "value" : "Хронология инсулина, %1$@, болюс %2$@, базал %3$@, углеводы %4$@, последняя глюкоза %5$@ %6$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulín za deň" + "value" : "Časová os inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@, posledná glykémia %5$@ %6$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin per dag" + "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kolhydrater %4$@, senaste glukos %5$@ %6$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Günlük insülin" + "value" : "İnsülin zaman çizelgesi, %1$@, bolus %2$@, bazal %3$@, karb %4$@, son KŞ %5$@ %6$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Інсулін на день" + "value" : "Хронологія інсуліну, %1$@, болюс %2$@, базал %3$@, вуглеводи %4$@, остання глюкоза %5$@ %6$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin mỗi ngày" + "value" : "Dòng thời gian insulin, %1$@, Bolus %2$@, Basal %3$@, Carbs %4$@, đường huyết mới nhất %5$@ %6$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "每日胰岛素" + "value" : "胰岛素时间线,%1$@,大剂量 %2$@,基础 %3$@,碳水 %4$@,最新血糖 %5$@ %6$@" } } } }, - "Insulin per day, average %1$@" : { - "comment" : "VoiceOver label for the Trends Insulin per day chart. (1: average insulin per day, e.g. 32.1 U)", + "IOB" : { + "comment" : "Abbreviation for insulin on board (active insulin) Abbreviation for insulin on board; label of a tile on the Now screen", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الأنسولين يوميًا، المتوسط %1$@" + "value" : "IOB" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulín za den, průměr %1$@" + "value" : "IOB" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin pr. dag, gennemsnit %1$@" + "value" : "IOB" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin pro Tag, Durchschnitt %1$@" + "value" : "IOB" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina por día, promedio %1$@" + "value" : "IOB" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Insuliini päivässä, keskiarvo %1$@" + "value" : "IOB" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Insuline par jour, moyenne %1$@" + "value" : "IA" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אינסולין ליום, ממוצע %1$@" + "value" : "IOB" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulin naponta, átlag %1$@" + "value" : "IOB" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina al giorno, media %1$@" + "value" : "IOB" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "1日のインスリン量、平均 %1$@" + "value" : "IOB" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin per dag, gjennomsnitt %1$@" + "value" : "IOB" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Insuline per dag, gemiddeld %1$@" + "value" : "IOB" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina dziennie, średnio %1$@" + "value" : "IOB" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina por dia, média %1$@" + "value" : "IOB" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Insulină pe zi, medie %1$@" + "value" : "IOB" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Инсулин в день, в среднем %1$@" + "value" : "IOB" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulín za deň, priemer %1$@" + "value" : "IOB" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin per dag, medel %1$@" + "value" : "IOB" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Günlük insülin, ortalama %1$@" + "value" : "IOB" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Інсулін на день, у середньому %1$@" + "value" : "IOB" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin mỗi ngày, trung bình %1$@" + "value" : "IOB" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "每日胰岛素,平均 %1$@" + "value" : "IOB" } } } }, - "Insulin per day, weekly average, average %1$@" : { - "comment" : "VoiceOver label for the Trends Insulin per day chart when bars are weekly averages. (1: average insulin per day, e.g. 32.1 U)", + "Limited" : { + "comment" : "Trends Loop status: time closed loop was on but Loop wasn't completing runs", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الأنسولين يوميًا، متوسط أسبوعي، المتوسط %1$@" + "value" : "محدود" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulín za den, týdenní průměr, průměr %1$@" + "value" : "Omezeno" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin pr. dag, ugegennemsnit, gennemsnit %1$@" + "value" : "Begrænset" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin pro Tag, Wochendurchschnitt, Durchschnitt %1$@" + "value" : "Eingeschränkt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina por día, promedio semanal, promedio %1$@" + "value" : "Limitado" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Insuliini päivässä, viikkokeskiarvo, keskiarvo %1$@" + "value" : "Rajoitettu" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Insuline par jour, moyenne hebdomadaire, moyenne %1$@" + "value" : "Limitée" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אינסולין ליום, ממוצע שבועי, ממוצע %1$@" + "value" : "מוגבל" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulin naponta, heti átlag, átlag %1$@" + "value" : "Korlátozott" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina al giorno, media settimanale, media %1$@" + "value" : "Limitato" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "1日のインスリン量、週平均、平均 %1$@" + "value" : "制限あり" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin per dag, ukesgjennomsnitt, gjennomsnitt %1$@" + "value" : "Begrenset" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Insuline per dag, weekgemiddelde, gemiddeld %1$@" + "value" : "Beperkt" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina dziennie, średnia tygodniowa, średnio %1$@" + "value" : "Ograniczona" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Insulina por dia, média semanal, média %1$@" + "value" : "Limitado" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Insulină pe zi, medie săptămânală, medie %1$@" + "value" : "Limitat" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Инсулин в день, среднее за неделю, в среднем %1$@" + "value" : "Ограничен" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulín za deň, týždenný priemer, priemer %1$@" + "value" : "Obmedzená" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin per dag, veckomedel, medel %1$@" + "value" : "Begränsad" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Günlük insülin, haftalık ortalama, ortalama %1$@" + "value" : "Sınırlı" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Інсулін на день, середнє за тиждень, у середньому %1$@" + "value" : "Обмежено" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin mỗi ngày, trung bình theo tuần, trung bình %1$@" + "value" : "Hạn chế" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "每日胰岛素,按周平均,平均 %1$@" + "value" : "受限" } } } }, - "Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@" : { - "comment" : "VoiceOver summary of the day's insulin timeline chart with no glucose readings. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g)", + "Loop status" : { + "comment" : "Trends card title: how long Loop spent in closed loop, limited or open loop", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "مخطط الأنسولين، %1$@، Bolus %2$@، القاعدية %3$@، الكارب %4$@" + "value" : "حالة Loop" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Časová osa inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@" + "value" : "Stav Loop" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kulhydrater %4$@" + "value" : "Loop-status" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin-Zeitleiste, %1$@, Bolus %2$@, Basal %3$@, KH %4$@" + "value" : "Loop-Status" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Cronología de insulina, %1$@, bolo %2$@, basal %3$@, carbohidratos %4$@" + "value" : "Estado de Loop" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Insuliinin aikajana, %1$@, bolus %2$@, basaali %3$@, hiilihydraatit %4$@" + "value" : "Loopin tila" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Chronologie de l'insuline, %1$@, bolus %2$@, basal %3$@, glucides %4$@" + "value" : "État de Loop" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "ציר זמן אינסולין, %1$@, Bolus %2$@, Basal %3$@, פחמימות %4$@" + "value" : "מצב Loop" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulin idővonal, %1$@, bólus %2$@, basal %3$@, szénhidrát %4$@" + "value" : "Loop állapota" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Cronologia insulina, %1$@, bolo %2$@, basale %3$@, carboidrati %4$@" + "value" : "Stato di Loop" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "インスリンのタイムライン、%1$@、ボーラス %2$@、ベーサル %3$@、カーボ %4$@" + "value" : "Loopの状態" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, karbohydrater %4$@" + "value" : "Loop-status" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Insulinetijdlijn, %1$@, bolus %2$@, basaal %3$@, koolhydraten %4$@" + "value" : "Loopstatus" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Oś czasu insuliny, %1$@, bolus %2$@, baza %3$@, węglowodany %4$@" + "value" : "Stan Loop" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Linha do tempo de insulina, %1$@, bolus %2$@, basal %3$@, carbs %4$@" + "value" : "Status do Loop" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Cronologie insulină, %1$@, bolus %2$@, bazală %3$@, carbohidrați %4$@" + "value" : "Starea Loop" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Хронология инсулина, %1$@, болюс %2$@, базал %3$@, углеводы %4$@" + "value" : "Статус Loop" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Časová os inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@" + "value" : "Stav Loop" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kolhydrater %4$@" + "value" : "Loopstatus" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İnsülin zaman çizelgesi, %1$@, bolus %2$@, bazal %3$@, karb %4$@" + "value" : "Loop durumu" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Хронологія інсуліну, %1$@, болюс %2$@, базал %3$@, вуглеводи %4$@" + "value" : "Стан Loop" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Dòng thời gian insulin, %1$@, Bolus %2$@, Basal %3$@, Carbs %4$@" + "value" : "Trạng thái Loop" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "胰岛素时间线,%1$@,大剂量 %2$@,基础 %3$@,碳水 %4$@" + "value" : "Loop 状态" } } } }, - "Insulin timeline, %1$@, bolus %2$@, basal %3$@, carbs %4$@, latest glucose %5$@ %6$@" : { - "comment" : "VoiceOver summary of the day's insulin timeline chart. (1: weekday and date; 2: bolus total, e.g. 12.5 U; 3: basal total; 4: carbs total, e.g. 140 g; 5: latest glucose value; 6: glucose unit, mmol/L or mg/dL)", + "Manual %1$@" : { + "comment" : "Basal and bolus: bolus the person gave. (1: units, e.g. 16.2 U)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "مخطط الأنسولين، %1$@، Bolus %2$@، القاعدية %3$@، الكارب %4$@، آخر قراءة سكر %5$@ %6$@" + "value" : "يدوي %1$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Časová osa inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@, poslední glukóza %5$@ %6$@" + "value" : "Ručně %1$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kulhydrater %4$@, seneste glukose %5$@ %6$@" + "value" : "Manuel %1$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Insulin-Zeitleiste, %1$@, Bolus %2$@, Basal %3$@, KH %4$@, letzter Blutzucker %5$@ %6$@" + "value" : "Manuell %1$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Cronología de insulina, %1$@, bolo %2$@, basal %3$@, carbohidratos %4$@, última glucosa %5$@ %6$@" + "value" : "Manual %1$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Insuliinin aikajana, %1$@, bolus %2$@, basaali %3$@, hiilihydraatit %4$@, viimeisin glukoosi %5$@ %6$@" + "value" : "Manuaalinen %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Chronologie de l'insuline, %1$@, bolus %2$@, basal %3$@, glucides %4$@, dernière glycémie %5$@ %6$@" + "value" : "Manuel %1$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "ציר זמן אינסולין, %1$@, Bolus %2$@, Basal %3$@, פחמימות %4$@, סוכר אחרון %5$@ %6$@" + "value" : "ידני %1$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Inzulin idővonal, %1$@, bólus %2$@, basal %3$@, szénhidrát %4$@, legutóbbi glükóz %5$@ %6$@" + "value" : "Kézi %1$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Cronologia insulina, %1$@, bolo %2$@, basale %3$@, carboidrati %4$@, ultima glicemia %5$@ %6$@" + "value" : "Manuale %1$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "インスリンのタイムライン、%1$@、ボーラス %2$@、ベーサル %3$@、カーボ %4$@、最新の血糖値 %5$@ %6$@" + "value" : "手動 %1$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, karbohydrater %4$@, siste blodsukker %5$@ %6$@" + "value" : "Manuell %1$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Insulinetijdlijn, %1$@, bolus %2$@, basaal %3$@, koolhydraten %4$@, laatste glucose %5$@ %6$@" + "value" : "Handmatig %1$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Oś czasu insuliny, %1$@, bolus %2$@, baza %3$@, węglowodany %4$@, ostatnia glukoza %5$@ %6$@" + "value" : "Ręcznie %1$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Linha do tempo de insulina, %1$@, bolus %2$@, basal %3$@, carbs %4$@, última glicose %5$@ %6$@" + "value" : "Manual %1$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Cronologie insulină, %1$@, bolus %2$@, bazală %3$@, carbohidrați %4$@, ultima glicemie %5$@ %6$@" + "value" : "Manual %1$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Хронология инсулина, %1$@, болюс %2$@, базал %3$@, углеводы %4$@, последняя глюкоза %5$@ %6$@" + "value" : "Вручную %1$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Časová os inzulínu, %1$@, bolus %2$@, bazál %3$@, sacharidy %4$@, posledná glykémia %5$@ %6$@" + "value" : "Ručne %1$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Insulintidslinje, %1$@, bolus %2$@, basal %3$@, kolhydrater %4$@, senaste glukos %5$@ %6$@" + "value" : "Manuell %1$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İnsülin zaman çizelgesi, %1$@, bolus %2$@, bazal %3$@, karb %4$@, son KŞ %5$@ %6$@" + "value" : "Manuel %1$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Хронологія інсуліну, %1$@, болюс %2$@, базал %3$@, вуглеводи %4$@, остання глюкоза %5$@ %6$@" + "value" : "Вручну %1$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Dòng thời gian insulin, %1$@, Bolus %2$@, Basal %3$@, Carbs %4$@, đường huyết mới nhất %5$@ %6$@" + "value" : "Thủ công %1$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "胰岛素时间线,%1$@,大剂量 %2$@,基础 %3$@,碳水 %4$@,最新血糖 %5$@ %6$@" + "value" : "手动 %1$@" } } } }, - "Invalid future glucose" : { - "comment" : "Reason Loop could not run: a glucose reading had a timestamp in the future", + "Median" : { + "comment" : "Trends Daily pattern legend: the median glucose line", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "قراءة سكر مستقبلية غير صالحة" + "value" : "الوسيط" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Glukóza s budoucím časem" + "value" : "Medián" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ugyldig glukosemåling i fremtiden" + "value" : "Median" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ungültiger Blutzuckerwert in der Zukunft" + "value" : "Median" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Glucosa futura no válida" + "value" : "Mediana" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Virheellinen tulevaisuuden glukoosiarvo" + "value" : "Mediaani" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Glycémie future non valide" + "value" : "Médiane" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "קריאת סוכר עתידית לא תקינה" + "value" : "חציון" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Érvénytelen, jövőbeli glükózérték" + "value" : "Medián" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemia futura non valida" + "value" : "Mediana" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "未来時刻の無効な血糖値" + "value" : "中央値" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ugyldig blodsukkermåling i fremtiden" + "value" : "Median" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Ongeldige glucosewaarde in de toekomst" + "value" : "Mediaan" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoza z przyszłą datą" + "value" : "Mediana" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Glicose futura inválida" + "value" : "Mediana" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Glicemie invalidă, din viitor" + "value" : "Mediană" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Глюкоза с временем в будущем" + "value" : "Медиана" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Glykémia s budúcim časom" + "value" : "Medián" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Ogiltigt glukosvärde i framtiden" + "value" : "Median" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Geçersiz, ileri tarihli KŞ" + "value" : "Medyan" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Глюкоза з часом у майбутньому" + "value" : "Медіана" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Đường huyết có thời gian trong tương lai không hợp lệ" + "value" : "Trung vị" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无效的未来血糖" + "value" : "中位数" } } } }, - "IOB" : { - "comment" : "Abbreviation for insulin on board (active insulin) Abbreviation for insulin on board; label of a tile on the Now screen", + "Needs 10 days of CGM" : { + "comment" : "Trends GMI tile detail when fewer than 10 days of CGM data are available", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "يحتاج إلى 10 أيام من CGM" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Vyžaduje 10 dní CGM" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Kræver 10 dages CGM" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Benötigt 10 Tage CGM" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Requiere 10 días de CGM" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Vaatii 10 päivää CGM-dataa" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "IA" + "value" : "Nécessite 10 jours de CGM" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "נדרשים 10 ימי CGM" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "10 nap CGM-adat szükséges" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Servono 10 giorni di CGM" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "CGMデータが10日分必要" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Krever 10 dager med CGM" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Vereist 10 dagen CGM" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Wymaga 10 dni CGM" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Requer 10 dias de CGM" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Necesită 10 zile de CGM" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Нужно 10 дней CGM" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Vyžaduje 10 dní CGM" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Kräver 10 dagars CGM" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "10 günlük CGM gerekir" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Потрібно 10 днів CGM" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "Cần 10 ngày dữ liệu CGM" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "IOB" + "value" : "需要 10 天 CGM 数据" } } } }, - "Limited" : { - "comment" : "Trends Loop status: time closed loop was on but Loop wasn't completing runs", + "Next period" : { + "comment" : "VoiceOver label for the Trends button that moves forward one period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "محدود" + "value" : "الفترة التالية" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Omezeno" + "value" : "Další období" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Begrænset" + "value" : "Næste periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Eingeschränkt" + "value" : "Nächster Zeitraum" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Limitado" + "value" : "Período siguiente" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Rajoitettu" + "value" : "Seuraava jakso" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Limitée" + "value" : "Période suivante" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מוגבל" + "value" : "התקופה הבאה" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Korlátozott" + "value" : "Következő időszak" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Limitato" + "value" : "Periodo successivo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "制限あり" + "value" : "次の期間" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Begrenset" + "value" : "Neste periode" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Beperkt" + "value" : "Volgende periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ograniczona" + "value" : "Następny okres" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Limitado" + "value" : "Próximo período" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Limitat" + "value" : "Perioada următoare" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ограничен" + "value" : "Следующий период" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Obmedzená" + "value" : "Ďalšie obdobie" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Begränsad" + "value" : "Nästa period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sınırlı" + "value" : "Sonraki dönem" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Обмежено" + "value" : "Наступний період" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Hạn chế" + "value" : "Khoảng thời gian sau" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "受限" + "value" : "下一时段" } } } }, - "Loop didn't run" : { - "comment" : "Reason shown for a stretch of time when Loop did not complete a run and recorded no error", + "No CGM data" : { + "comment" : "Trends Average tile detail when the period has no glucose readings", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لم يعمل Loop" + "value" : "لا توجد بيانات CGM" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Loop neproběhl" + "value" : "Žádná data CGM" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Loop kørte ikke" + "value" : "Ingen CGM-data" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Loop ist nicht gelaufen" + "value" : "Keine CGM-Daten" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Loop no se ejecutó" + "value" : "Sin datos del CGM" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Loop ei ajanut" + "value" : "Ei CGM-dataa" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Loop ne s'est pas exécuté" + "value" : "Aucune donnée CGM" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "Loop לא רץ" + "value" : "אין נתוני CGM" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "A Loop nem futott" + "value" : "Nincs CGM-adat" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Loop non eseguito" + "value" : "Nessun dato CGM" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Loopが実行されませんでした" + "value" : "CGMデータなし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Loop kjørte ikke" + "value" : "Ingen CGM-data" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Loop heeft niet gedraaid" + "value" : "Geen CGM-gegevens" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Loop nie działał" + "value" : "Brak danych CGM" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "O Loop não executou" + "value" : "Sem dados do CGM" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Loop nu a rulat" + "value" : "Fără date CGM" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Loop не запускался" + "value" : "Нет данных CGM" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Loop neprebehol" + "value" : "Žiadne údaje CGM" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Loop kördes inte" + "value" : "Inga CGM-data" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Loop çalışmadı" + "value" : "CGM verisi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Loop не запускався" + "value" : "Немає даних CGM" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Loop không chạy" + "value" : "Không có dữ liệu CGM" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Loop 未运行" + "value" : "无 CGM 数据" } } } }, - "Loop status" : { - "comment" : "Trends card title: how long Loop spent in closed loop, limited or open loop", + "No CGM data %1$@ – %2$@" : { + "comment" : "Chart note: a gap in glucose readings. (1: start time; 2: end time)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حالة Loop" + "value" : "لا توجد بيانات CGM %1$@ – %2$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Stav Loop" + "value" : "Žádná data CGM %1$@ – %2$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Loop-status" + "value" : "Ingen CGM-data %1$@ – %2$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Loop-Status" + "value" : "Keine CGM-Daten %1$@ – %2$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Estado de Loop" + "value" : "Sin datos del CGM %1$@ – %2$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Loopin tila" + "value" : "Ei CGM-dataa %1$@ – %2$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "État de Loop" + "value" : "Aucune donnée CGM %1$@ – %2$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מצב Loop" + "value" : "אין נתוני CGM %1$@ – %2$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Loop állapota" + "value" : "Nincs CGM-adat %1$@ – %2$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Stato di Loop" + "value" : "Nessun dato CGM %1$@ – %2$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Loopの状態" + "value" : "CGMデータなし %1$@ – %2$@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Loop-status" + "value" : "Ingen CGM-data %1$@ – %2$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Loopstatus" + "value" : "Geen CGM-gegevens %1$@ – %2$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Stan Loop" + "value" : "Brak danych CGM %1$@ – %2$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Status do Loop" + "value" : "Sem dados do CGM %1$@ – %2$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Starea Loop" + "value" : "Fără date CGM %1$@ – %2$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Статус Loop" + "value" : "Нет данных CGM %1$@ – %2$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Stav Loop" + "value" : "Žiadne údaje CGM %1$@ – %2$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Loopstatus" + "value" : "Inga CGM-data %1$@ – %2$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Loop durumu" + "value" : "CGM verisi yok %1$@ – %2$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Стан Loop" + "value" : "Немає даних CGM %1$@ – %2$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trạng thái Loop" + "value" : "Không có dữ liệu CGM %1$@ – %2$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Loop 状态" + "value" : "无 CGM 数据 %1$@ – %2$@" } } } }, - "Loop status is recorded from when this version of Loop first ran." : { - "comment" : "Trends Loop status footnote when there is no Loop status history yet", + "No CGM data %1$@ – %2$@ and %3$@" : { + "comment" : "Chart note: the longest gap in glucose readings, plus the shorter ones. (1: start time; 2: end time; 3: number of shorter gaps, e.g. 2 shorter gaps)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تُسجَّل حالة Loop منذ أول تشغيل لهذا الإصدار من Loop." + "value" : "لا توجد بيانات CGM %1$@ – %2$@ و %3$@" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Stav Loop se zaznamenává od prvního spuštění této verze Loop." + "value" : "Žádná data CGM %1$@ – %2$@ a %3$@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Loop-status registreres fra første gang denne version af Loop kørte." + "value" : "Ingen CGM-data %1$@ – %2$@ og %3$@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Der Loop-Status wird ab dem ersten Start dieser Loop-Version aufgezeichnet." + "value" : "Keine CGM-Daten %1$@ – %2$@ und %3$@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "El estado de Loop se registra desde la primera vez que se ejecutó esta versión de Loop." + "value" : "Sin datos del CGM %1$@ – %2$@ y %3$@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Loopin tila tallennetaan siitä lähtien, kun tämä Loopin versio käynnistettiin ensimmäisen kerran." + "value" : "Ei CGM-dataa %1$@ – %2$@ ja %3$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "L'état de Loop est enregistré depuis le premier lancement de cette version de Loop." + "value" : "Aucune donnée CGM %1$@ – %2$@ et %3$@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מצב Loop נרשם מאז ההפעלה הראשונה של גרסה זו של Loop." + "value" : "אין נתוני CGM %1$@ – %2$@ ו-%3$@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "A Loop állapota a Loop ezen verziójának első futtatásától kerül rögzítésre." + "value" : "Nincs CGM-adat %1$@ – %2$@, és %3$@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Lo stato di Loop è registrato dal primo avvio di questa versione di Loop." + "value" : "Nessun dato CGM %1$@ – %2$@ e %3$@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Loopの状態は、このバージョンのLoopを初めて起動した時点から記録されます。" + "value" : "CGMデータなし %1$@ – %2$@(%3$@)" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Loop-status registreres fra første gang denne versjonen av Loop kjørte." + "value" : "Ingen CGM-data %1$@ – %2$@ og %3$@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "De loopstatus wordt vastgelegd vanaf de eerste keer dat deze versie van Loop draaide." + "value" : "Geen CGM-gegevens %1$@ – %2$@ en %3$@" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Stan Loop jest zapisywany od pierwszego uruchomienia tej wersji Loop." + "value" : "Brak danych CGM %1$@ – %2$@ oraz %3$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "O status do Loop é registrado desde a primeira vez que esta versão do Loop foi executada." + "value" : "Sem dados do CGM %1$@ – %2$@ e %3$@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Starea Loop este înregistrată de la prima rulare a acestei versiuni de Loop." + "value" : "Fără date CGM %1$@ – %2$@ și %3$@" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Статус Loop записывается с первого запуска этой версии Loop." + "value" : "Нет данных CGM %1$@ – %2$@ и ещё %3$@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Stav Loop sa zaznamenáva od prvého spustenia tejto verzie Loop." + "value" : "Žiadne údaje CGM %1$@ – %2$@ a %3$@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Loopstatus registreras från när den här versionen av Loop först kördes." + "value" : "Inga CGM-data %1$@ – %2$@ och %3$@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Loop durumu, Loop'un bu sürümünün ilk çalıştığı andan itibaren kaydedilir." + "value" : "CGM verisi yok %1$@ – %2$@ ve %3$@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Стан Loop записується з першого запуску цієї версії Loop." + "value" : "Немає даних CGM %1$@ – %2$@ і ще %3$@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trạng thái Loop được ghi lại từ khi phiên bản Loop này chạy lần đầu." + "value" : "Không có dữ liệu CGM %1$@ – %2$@ và %3$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Loop 状态从此版本 Loop 首次运行时开始记录。" + "value" : "无 CGM 数据 %1$@ – %2$@,%3$@" } } } }, - "Manual %1$@" : { - "comment" : "Basal and bolus: bolus the person gave. (1: units, e.g. 16.2 U)", + "No CGM data in this period" : { + "comment" : "Trends chart placeholder when the period has no glucose readings", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يدوي %1$@" + "value" : "لا توجد بيانات CGM في هذه الفترة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Ručně %1$@" + "value" : "Žádná data CGM v tomto období" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Manuel %1$@" + "value" : "Ingen CGM-data i denne periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Manuell %1$@" + "value" : "Keine CGM-Daten in diesem Zeitraum" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Manual %1$@" + "value" : "Sin datos del CGM en este período" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Manuaalinen %1$@" + "value" : "Ei CGM-dataa tällä jaksolla" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Manuel %1$@" + "value" : "Aucune donnée CGM sur cette période" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "ידני %1$@" + "value" : "אין נתוני CGM בתקופה זו" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kézi %1$@" + "value" : "Nincs CGM-adat ebben az időszakban" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Manuale %1$@" + "value" : "Nessun dato CGM in questo periodo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "手動 %1$@" + "value" : "この期間のCGMデータなし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Manuell %1$@" + "value" : "Ingen CGM-data i denne perioden" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Handmatig %1$@" + "value" : "Geen CGM-gegevens in deze periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ręcznie %1$@" + "value" : "Brak danych CGM w tym okresie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Manual %1$@" + "value" : "Sem dados do CGM neste período" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Manual %1$@" + "value" : "Fără date CGM în această perioadă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Вручную %1$@" + "value" : "Нет данных CGM за этот период" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Ručne %1$@" + "value" : "Žiadne údaje CGM v tomto období" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Manuell %1$@" + "value" : "Inga CGM-data under denna period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Manuel %1$@" + "value" : "Bu dönemde CGM verisi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Вручну %1$@" + "value" : "Немає даних CGM за цей період" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Thủ công %1$@" + "value" : "Không có dữ liệu CGM trong khoảng thời gian này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "手动 %1$@" + "value" : "此时段无 CGM 数据" } } } }, - "Median" : { - "comment" : "Trends Daily pattern legend: the median glucose line", + "No CGM data on this day" : { + "comment" : "Chart note: there are no glucose readings for the whole day", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الوسيط" + "value" : "لا توجد بيانات CGM في هذا اليوم" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Medián" + "value" : "Žádná data CGM v tento den" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Median" + "value" : "Ingen CGM-data denne dag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Median" + "value" : "Keine CGM-Daten an diesem Tag" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Mediana" + "value" : "Sin datos del CGM este día" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Mediaani" + "value" : "Ei CGM-dataa tänä päivänä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Médiane" + "value" : "Aucune donnée CGM ce jour-là" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "חציון" + "value" : "אין נתוני CGM ביום זה" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Medián" + "value" : "Nincs CGM-adat ezen a napon" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Mediana" + "value" : "Nessun dato CGM in questo giorno" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "中央値" + "value" : "この日のCGMデータなし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Median" + "value" : "Ingen CGM-data denne dagen" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Mediaan" + "value" : "Geen CGM-gegevens op deze dag" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Mediana" + "value" : "Brak danych CGM tego dnia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Mediana" + "value" : "Sem dados do CGM neste dia" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Mediană" + "value" : "Fără date CGM în această zi" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Медиана" + "value" : "Нет данных CGM за этот день" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Medián" + "value" : "Žiadne údaje CGM v tento deň" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Median" + "value" : "Inga CGM-data denna dag" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Medyan" + "value" : "Bu günde CGM verisi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Медіана" + "value" : "Немає даних CGM за цей день" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trung vị" + "value" : "Không có dữ liệu CGM trong ngày này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "中位数" + "value" : "这一天无 CGM 数据" } } } }, - "Missing data error" : { - "comment" : "Reason Loop could not run: data it needs was missing", + "No CGM data yet today" : { + "comment" : "Chart note: there are no glucose readings so far today", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "خطأ بيانات مفقودة" + "value" : "لا توجد بيانات CGM اليوم حتى الآن" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba chybějících dat" + "value" : "Dnes zatím žádná data CGM" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Fejl: manglende data" + "value" : "Ingen CGM-data endnu i dag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fehler: fehlende Daten" + "value" : "Heute noch keine CGM-Daten" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Error de datos faltantes" + "value" : "Aún no hay datos del CGM hoy" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Tietoja puuttuu" + "value" : "Ei vielä CGM-dataa tänään" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Erreur de données manquantes" + "value" : "Aucune donnée CGM aujourd'hui pour l'instant" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שגיאת נתונים חסרים" + "value" : "אין עדיין נתוני CGM היום" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Hiányzó adat" + "value" : "Ma még nincs CGM-adat" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Errore dati mancanti" + "value" : "Ancora nessun dato CGM oggi" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "データ欠落エラー" + "value" : "今日はまだCGMデータなし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Feil: manglende data" + "value" : "Ingen CGM-data ennå i dag" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Fout: ontbrekende gegevens" + "value" : "Vandaag nog geen CGM-gegevens" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Błąd brakujących danych" + "value" : "Dziś jeszcze brak danych CGM" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Erro de dados ausentes" + "value" : "Ainda sem dados do CGM hoje" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Eroare: date lipsă" + "value" : "Încă nu există date CGM azi" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ошибка: нет данных" + "value" : "Сегодня пока нет данных CGM" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba chýbajúcich údajov" + "value" : "Dnes zatiaľ žiadne údaje CGM" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Data saknas" + "value" : "Inga CGM-data än i dag" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Eksik veri hatası" + "value" : "Bugün henüz CGM verisi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Помилка: немає даних" + "value" : "Сьогодні ще немає даних CGM" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Lỗi thiếu dữ liệu" + "value" : "Hôm nay chưa có dữ liệu CGM" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "数据缺失错误" + "value" : "今天尚无 CGM 数据" } } } }, - "Needs 10 days of CGM" : { - "comment" : "Trends GMI tile detail when fewer than 10 days of CGM data are available", + "No insulin recorded in this period" : { + "comment" : "Trends Insulin per day chart placeholder when no insulin was delivered in the period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يحتاج إلى 10 أيام من CGM" + "value" : "لم يُسجَّل أنسولين في هذه الفترة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vyžaduje 10 dní CGM" + "value" : "V tomto období nebyl zaznamenán žádný inzulín" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Kræver 10 dages CGM" + "value" : "Intet insulin registreret i denne periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Benötigt 10 Tage CGM" + "value" : "Kein Insulin in diesem Zeitraum erfasst" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Requiere 10 días de CGM" + "value" : "No se registró insulina en este período" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vaatii 10 päivää CGM-dataa" + "value" : "Ei kirjattua insuliinia tällä jaksolla" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nécessite 10 jours de CGM" + "value" : "Aucune insuline enregistrée sur cette période" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "נדרשים 10 ימי CGM" + "value" : "לא נרשם אינסולין בתקופה זו" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "10 nap CGM-adat szükséges" + "value" : "Nincs rögzített inzulin ebben az időszakban" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Servono 10 giorni di CGM" + "value" : "Nessuna insulina registrata in questo periodo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "CGMデータが10日分必要" + "value" : "この期間のインスリン記録なし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Krever 10 dager med CGM" + "value" : "Ingen insulin registrert i denne perioden" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vereist 10 dagen CGM" + "value" : "Geen insuline vastgelegd in deze periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wymaga 10 dni CGM" + "value" : "Brak zapisanej insuliny w tym okresie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Requer 10 dias de CGM" + "value" : "Nenhuma insulina registrada neste período" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Necesită 10 zile de CGM" + "value" : "Nicio insulină înregistrată în această perioadă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нужно 10 дней CGM" + "value" : "За этот период инсулин не записан" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Vyžaduje 10 dní CGM" + "value" : "V tomto období nebol zaznamenaný žiadny inzulín" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Kräver 10 dagars CGM" + "value" : "Inget insulin registrerat under denna period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "10 günlük CGM gerekir" + "value" : "Bu dönemde kaydedilmiş insülin yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Потрібно 10 днів CGM" + "value" : "За цей період інсулін не записано" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cần 10 ngày dữ liệu CGM" + "value" : "Không có insulin được ghi lại trong khoảng thời gian này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "需要 10 天 CGM 数据" + "value" : "此时段内无胰岛素记录" } } } }, - "Next period" : { - "comment" : "VoiceOver label for the Trends button that moves forward one period", + "No loop runs recorded in this period" : { + "comment" : "Trends Loop status placeholder when there is no Loop run history for the period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الفترة التالية" + "value" : "لم تُسجَّل تشغيلات Loop في هذه الفترة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Další období" + "value" : "V tomto období nebyly zaznamenány žádné běhy Loop" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Næste periode" + "value" : "Ingen Loop-kørsler registreret i denne periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nächster Zeitraum" + "value" : "Keine Loop-Durchläufe in diesem Zeitraum erfasst" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Período siguiente" + "value" : "No se registraron ejecuciones de Loop en este período" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Seuraava jakso" + "value" : "Ei Loop-ajoja tällä jaksolla" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Période suivante" + "value" : "Aucune exécution de Loop enregistrée sur cette période" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "התקופה הבאה" + "value" : "לא נרשמו הרצות Loop בתקופה זו" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Következő időszak" + "value" : "Nincs rögzített Loop-futás ebben az időszakban" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Periodo successivo" + "value" : "Nessuna esecuzione di Loop registrata in questo periodo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "次の期間" + "value" : "この期間のLoop実行記録なし" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Neste periode" + "value" : "Ingen Loop-kjøringer registrert i denne perioden" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Volgende periode" + "value" : "Geen loop-runs vastgelegd in deze periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Następny okres" + "value" : "Brak zapisanych cykli Loop w tym okresie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Próximo período" + "value" : "Nenhuma execução do Loop registrada neste período" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Perioada următoare" + "value" : "Nicio rulare Loop înregistrată în această perioadă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Следующий период" + "value" : "За этот период запуски Loop не записаны" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Ďalšie obdobie" + "value" : "V tomto období neboli zaznamenané žiadne behy Loop" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Nästa period" + "value" : "Inga loopkörningar registrerade under denna period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sonraki dönem" + "value" : "Bu dönemde kaydedilmiş Loop çalışması yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Наступний період" + "value" : "За цей період запуски Loop не записано" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Khoảng thời gian sau" + "value" : "Không có lần chạy Loop nào được ghi lại trong khoảng thời gian này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "下一时段" + "value" : "此时段内无 Loop 运行记录" } } } }, - "No CGM data" : { - "comment" : "Trends Average tile detail when the period has no glucose readings", + "Not enough days for a pattern: %1$@ of 5" : { + "comment" : "Trends Daily pattern footnote when fewer than 5 days have readings; the chart shows raw readings instead. (1: days with readings)", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد بيانات CGM" + "value" : "أيام غير كافية لنمط: %1$@ من 5" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Žádná data CGM" + "value" : "Málo dní pro profil: %1$@ z 5" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data" + "value" : "Ikke nok dage til et mønster: %1$@ af 5" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine CGM-Daten" + "value" : "Nicht genug Tage für ein Profil: %1$@ von 5" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin datos del CGM" + "value" : "No hay suficientes días para un patrón: %1$@ de 5" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei CGM-dataa" + "value" : "Liian vähän päiviä profiiliin: %1$@ / 5" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Aucune donnée CGM" + "value" : "Pas assez de jours pour un profil : %1$@ sur 5" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין נתוני CGM" + "value" : "אין מספיק ימים לדפוס: %1$@ מתוך 5" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs CGM-adat" + "value" : "Nincs elég nap a mintázathoz: %1$@ / 5" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nessun dato CGM" + "value" : "Giorni insufficienti per un andamento: %1$@ su 5" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "CGMデータなし" + "value" : "パターン作成には日数不足:%1$@ / 5日" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data" + "value" : "Ikke nok dager for et mønster: %1$@ av 5" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Geen CGM-gegevens" + "value" : "Niet genoeg dagen voor een patroon: %1$@ van 5" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Brak danych CGM" + "value" : "Za mało dni na profil: %1$@ z 5" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Sem dados do CGM" + "value" : "Dias insuficientes para um padrão: %1$@ de 5" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Fără date CGM" + "value" : "Prea puține zile pentru un tipar: %1$@ din 5" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Нет данных CGM" + "value" : "Мало дней для профиля: %1$@ из 5" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Žiadne údaje CGM" + "value" : "Málo dní pre profil: %1$@ z 5" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inga CGM-data" + "value" : "För få dagar för ett mönster: %1$@ av 5" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "CGM verisi yok" + "value" : "Örüntü için yeterli gün yok: %1$@ / 5" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Немає даних CGM" + "value" : "Замало днів для профілю: %1$@ з 5" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không có dữ liệu CGM" + "value" : "Không đủ ngày để tạo mẫu: %1$@ / 5" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "无 CGM 数据" + "value" : "天数不足以生成模式:%1$@ / 5" } } } }, - "No CGM data %1$@ – %2$@" : { - "comment" : "Chart note: a gap in glucose readings. (1: start time; 2: end time)", + "Now" : { + "comment" : "Title at the top of the Now screen, showing current glucose and Loop status Title of the Now tab in the redesigned interface, showing current glucose and Loop status", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "لا توجد بيانات CGM %1$@ – %2$@" + "value" : "الآن" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Žádná data CGM %1$@ – %2$@" + "value" : "Nyní" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen CGM-data %1$@ – %2$@" + "value" : "Nu" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Keine CGM-Daten %1$@ – %2$@" + "value" : "Jetzt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin datos del CGM %1$@ – %2$@" + "value" : "Ahora" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei CGM-dataa %1$@ – %2$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucune donnée CGM %1$@ – %2$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אין נתוני CGM %1$@ – %2$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs CGM-adat %1$@ – %2$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nessun dato CGM %1$@ – %2$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "CGMデータなし %1$@ – %2$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen CGM-data %1$@ – %2$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen CGM-gegevens %1$@ – %2$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brak danych CGM %1$@ – %2$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sem dados do CGM %1$@ – %2$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fără date CGM %1$@ – %2$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нет данных CGM %1$@ – %2$@" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žiadne údaje CGM %1$@ – %2$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inga CGM-data %1$@ – %2$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "CGM verisi yok %1$@ – %2$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Немає даних CGM %1$@ – %2$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không có dữ liệu CGM %1$@ – %2$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无 CGM 数据 %1$@ – %2$@" - } - } - } - }, - "No CGM data %1$@ – %2$@ and %3$@" : { - "comment" : "Chart note: the longest gap in glucose readings, plus the shorter ones. (1: start time; 2: end time; 3: number of shorter gaps, e.g. 2 shorter gaps)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "لا توجد بيانات CGM %1$@ – %2$@ و %3$@" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žádná data CGM %1$@ – %2$@ a %3$@" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen CGM-data %1$@ – %2$@ og %3$@" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine CGM-Daten %1$@ – %2$@ und %3$@" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin datos del CGM %1$@ – %2$@ y %3$@" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ei CGM-dataa %1$@ – %2$@ ja %3$@" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucune donnée CGM %1$@ – %2$@ et %3$@" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אין נתוני CGM %1$@ – %2$@ ו-%3$@" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs CGM-adat %1$@ – %2$@, és %3$@" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nessun dato CGM %1$@ – %2$@ e %3$@" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "CGMデータなし %1$@ – %2$@(%3$@)" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen CGM-data %1$@ – %2$@ og %3$@" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen CGM-gegevens %1$@ – %2$@ en %3$@" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brak danych CGM %1$@ – %2$@ oraz %3$@" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sem dados do CGM %1$@ – %2$@ e %3$@" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fără date CGM %1$@ – %2$@ și %3$@" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нет данных CGM %1$@ – %2$@ и ещё %3$@" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žiadne údaje CGM %1$@ – %2$@ a %3$@" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inga CGM-data %1$@ – %2$@ och %3$@" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "CGM verisi yok %1$@ – %2$@ ve %3$@" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Немає даних CGM %1$@ – %2$@ і ще %3$@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không có dữ liệu CGM %1$@ – %2$@ và %3$@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无 CGM 数据 %1$@ – %2$@,%3$@" - } - } - } - }, - "No CGM data in this period" : { - "comment" : "Trends chart placeholder when the period has no glucose readings", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "لا توجد بيانات CGM في هذه الفترة" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žádná data CGM v tomto období" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen CGM-data i denne periode" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine CGM-Daten in diesem Zeitraum" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin datos del CGM en este período" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ei CGM-dataa tällä jaksolla" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucune donnée CGM sur cette période" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אין נתוני CGM בתקופה זו" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs CGM-adat ebben az időszakban" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nessun dato CGM in questo periodo" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "この期間のCGMデータなし" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen CGM-data i denne perioden" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen CGM-gegevens in deze periode" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brak danych CGM w tym okresie" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sem dados do CGM neste período" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fără date CGM în această perioadă" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нет данных CGM за этот период" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žiadne údaje CGM v tomto období" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inga CGM-data under denna period" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu dönemde CGM verisi yok" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Немає даних CGM за цей період" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không có dữ liệu CGM trong khoảng thời gian này" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "此时段无 CGM 数据" - } - } - } - }, - "No CGM data on this day" : { - "comment" : "Chart note: there are no glucose readings for the whole day", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "لا توجد بيانات CGM في هذا اليوم" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žádná data CGM v tento den" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen CGM-data denne dag" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine CGM-Daten an diesem Tag" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin datos del CGM este día" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ei CGM-dataa tänä päivänä" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucune donnée CGM ce jour-là" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אין נתוני CGM ביום זה" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs CGM-adat ezen a napon" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nessun dato CGM in questo giorno" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "この日のCGMデータなし" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen CGM-data denne dagen" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen CGM-gegevens op deze dag" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brak danych CGM tego dnia" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sem dados do CGM neste dia" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fără date CGM în această zi" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нет данных CGM за этот день" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žiadne údaje CGM v tento deň" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inga CGM-data denna dag" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu günde CGM verisi yok" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Немає даних CGM за цей день" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không có dữ liệu CGM trong ngày này" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "这一天无 CGM 数据" - } - } - } - }, - "No CGM data yet today" : { - "comment" : "Chart note: there are no glucose readings so far today", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "لا توجد بيانات CGM اليوم حتى الآن" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dnes zatím žádná data CGM" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen CGM-data endnu i dag" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Heute noch keine CGM-Daten" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aún no hay datos del CGM hoy" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ei vielä CGM-dataa tänään" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucune donnée CGM aujourd'hui pour l'instant" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אין עדיין נתוני CGM היום" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ma még nincs CGM-adat" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ancora nessun dato CGM oggi" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "今日はまだCGMデータなし" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen CGM-data ennå i dag" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vandaag nog geen CGM-gegevens" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dziś jeszcze brak danych CGM" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ainda sem dados do CGM hoje" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Încă nu există date CGM azi" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сегодня пока нет данных CGM" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dnes zatiaľ žiadne údaje CGM" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inga CGM-data än i dag" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bugün henüz CGM verisi yok" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сьогодні ще немає даних CGM" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hôm nay chưa có dữ liệu CGM" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "今天尚无 CGM 数据" - } - } - } - }, - "No insulin recorded in this period" : { - "comment" : "Trends Insulin per day chart placeholder when no insulin was delivered in the period", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "لم يُسجَّل أنسولين في هذه الفترة" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "V tomto období nebyl zaznamenán žádný inzulín" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intet insulin registreret i denne periode" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kein Insulin in diesem Zeitraum erfasst" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No se registró insulina en este período" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ei kirjattua insuliinia tällä jaksolla" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucune insuline enregistrée sur cette période" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "לא נרשם אינסולין בתקופה זו" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs rögzített inzulin ebben az időszakban" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nessuna insulina registrata in questo periodo" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "この期間のインスリン記録なし" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen insulin registrert i denne perioden" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen insuline vastgelegd in deze periode" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brak zapisanej insuliny w tym okresie" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nenhuma insulina registrada neste período" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nicio insulină înregistrată în această perioadă" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "За этот период инсулин не записан" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "V tomto období nebol zaznamenaný žiadny inzulín" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inget insulin registrerat under denna period" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu dönemde kaydedilmiş insülin yok" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "За цей період інсулін не записано" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không có insulin được ghi lại trong khoảng thời gian này" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "此时段内无胰岛素记录" - } - } - } - }, - "No loop runs recorded in this period" : { - "comment" : "Trends Loop status placeholder when there is no Loop run history for the period", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "لم تُسجَّل تشغيلات Loop في هذه الفترة" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "V tomto období nebyly zaznamenány žádné běhy Loop" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen Loop-kørsler registreret i denne periode" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Loop-Durchläufe in diesem Zeitraum erfasst" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No se registraron ejecuciones de Loop en este período" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ei Loop-ajoja tällä jaksolla" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucune exécution de Loop enregistrée sur cette période" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "לא נרשמו הרצות Loop בתקופה זו" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs rögzített Loop-futás ebben az időszakban" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nessuna esecuzione di Loop registrata in questo periodo" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "この期間のLoop実行記録なし" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen Loop-kjøringer registrert i denne perioden" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen loop-runs vastgelegd in deze periode" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brak zapisanych cykli Loop w tym okresie" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nenhuma execução do Loop registrada neste período" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nicio rulare Loop înregistrată în această perioadă" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "За этот период запуски Loop не записаны" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "V tomto období neboli zaznamenané žiadne behy Loop" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inga loopkörningar registrerade under denna period" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu dönemde kaydedilmiş Loop çalışması yok" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "За цей період запуски Loop не записано" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không có lần chạy Loop nào được ghi lại trong khoảng thời gian này" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "此时段内无 Loop 运行记录" - } - } - } - }, - "Not enough days for a pattern: %1$@ of 5" : { - "comment" : "Trends Daily pattern footnote when fewer than 5 days have readings; the chart shows raw readings instead. (1: days with readings)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "أيام غير كافية لنمط: %1$@ من 5" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Málo dní pro profil: %1$@ z 5" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ikke nok dage til et mønster: %1$@ af 5" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nicht genug Tage für ein Profil: %1$@ von 5" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No hay suficientes días para un patrón: %1$@ de 5" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Liian vähän päiviä profiiliin: %1$@ / 5" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pas assez de jours pour un profil : %1$@ sur 5" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אין מספיק ימים לדפוס: %1$@ מתוך 5" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs elég nap a mintázathoz: %1$@ / 5" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Giorni insufficienti per un andamento: %1$@ su 5" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "パターン作成には日数不足:%1$@ / 5日" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ikke nok dager for et mønster: %1$@ av 5" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Niet genoeg dagen voor een patroon: %1$@ van 5" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Za mało dni na profil: %1$@ z 5" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dias insuficientes para um padrão: %1$@ de 5" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Prea puține zile pentru un tipar: %1$@ din 5" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Мало дней для профиля: %1$@ из 5" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Málo dní pre profil: %1$@ z 5" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "För få dagar för ett mönster: %1$@ av 5" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Örüntü için yeterli gün yok: %1$@ / 5" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Замало днів для профілю: %1$@ з 5" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không đủ ngày để tạo mẫu: %1$@ / 5" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "天数不足以生成模式:%1$@ / 5" - } - } - } - }, - "Now" : { - "comment" : "Title at the top of the Now screen, showing current glucose and Loop status Title of the Now tab in the redesigned interface, showing current glucose and Loop status", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الآن" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nyní" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nu" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jetzt" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ahora" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nyt" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Maintenant" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "עכשיו" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Most" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ora" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "現在" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nå" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nu" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teraz" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Agora" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Acum" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сейчас" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Teraz" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nu" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Şimdi" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Зараз" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hiện tại" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "当前" - } - } - } - }, - "Open loop / manual" : { - "comment" : "Trends Loop status: time closed loop was switched off", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "حلقة مفتوحة / يدوي" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Otevřená smyčka / ručně" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Åbent Loop / manuel" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open Loop / manuell" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Circuito abierto / manual" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avoin säätö / manuaalinen" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Boucle ouverte / manuel" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open Loop / ידני" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nyitott hurok / kézi" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loop aperto / manuale" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "オープンループ/手動" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Åpen Loop / manuell" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open loop / handmatig" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pętla otwarta / ręcznie" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ciclo aberto / manual" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Buclă deschisă / manual" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Открытый цикл / вручную" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Otvorená slučka / ručne" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Öppen loop / manuell" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Açık döngü / manuel" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Відкритий цикл / вручну" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vòng lặp mở / thủ công" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "开环 / 手动" - } - } - } - }, - "per day, weekly average" : { - "comment" : "Trends Insulin per day card subtitle when each bar is a week's average daily total", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "يوميًا، متوسط أسبوعي" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "za den, týdenní průměr" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "pr. dag, ugegennemsnit" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "pro Tag, Wochendurchschnitt" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "por día, promedio semanal" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "päivässä, viikkokeskiarvo" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "par jour, moyenne hebdomadaire" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "ליום, ממוצע שבועי" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "naponta, heti átlag" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "al giorno, media settimanale" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "1日あたり、週平均" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "per dag, ukesgjennomsnitt" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "per dag, weekgemiddelde" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "dziennie, średnia tygodniowa" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "por dia, média semanal" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "pe zi, medie săptămânală" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "в день, среднее за неделю" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "za deň, týždenný priemer" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "per dag, veckomedel" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "günlük, haftalık ortalama" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "на день, середнє за тиждень" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "mỗi ngày, trung bình theo tuần" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "每日,按周平均" - } - } - } - }, - "Period" : { - "comment" : "Trends: VoiceOver label of the picker that chooses how many days the screen covers", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الفترة" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Období" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Periode" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zeitraum" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Periodo" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jakso" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Période" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "תקופה" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Időszak" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Periodo" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "期間" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Periode" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Periode" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Okres" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Período" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Perioadă" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Период" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Obdobie" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Period" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dönem" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Період" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Khoảng thời gian" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "时段" - } - } - } - }, - "Previous period" : { - "comment" : "VoiceOver label for the Trends button that moves back one period", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "الفترة السابقة" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Předchozí období" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Forrige periode" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vorheriger Zeitraum" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Período anterior" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Edellinen jakso" + "value" : "Nyt" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Période précédente" + "value" : "Maintenant" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "התקופה הקודמת" + "value" : "עכשיו" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Előző időszak" + "value" : "Most" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Periodo precedente" + "value" : "Ora" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "前の期間" + "value" : "現在" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Forrige periode" + "value" : "Nå" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vorige periode" + "value" : "Nu" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Poprzedni okres" + "value" : "Teraz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Período anterior" + "value" : "Agora" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Perioada anterioară" + "value" : "Acum" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Предыдущий период" + "value" : "Сейчас" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Predchádzajúce obdobie" + "value" : "Teraz" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Föregående period" + "value" : "Nu" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Önceki dönem" + "value" : "Şimdi" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Попередній період" + "value" : "Зараз" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Khoảng thời gian trước" + "value" : "Hiện tại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "上一时段" + "value" : "当前" } } } }, - "Pump data too old" : { - "comment" : "Reason Loop could not run: the latest pump data was too old", + "Open loop / manual" : { + "comment" : "Trends Loop status: time closed loop was switched off", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "بيانات المضخة قديمة جدًا" + "value" : "حلقة مفتوحة / يدوي" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Data pumpy jsou příliš stará" + "value" : "Otevřená smyčka / ručně" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpedata for gamle" + "value" : "Åbent Loop / manuel" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpendaten zu alt" + "value" : "Open Loop / manuell" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Datos de la microinfusadora demasiado antiguos" + "value" : "Circuito abierto / manual" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpun tiedot liian vanhoja" + "value" : "Avoin säätö / manuaalinen" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Données de la pompe trop anciennes" + "value" : "Boucle ouverte / manuel" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "נתוני המשאבה ישנים מדי" + "value" : "Open Loop / ידני" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Túl régi pumpaadatok" + "value" : "Nyitott hurok / kézi" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Dati pompa troppo vecchi" + "value" : "Loop aperto / manuale" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ポンプデータが古すぎます" + "value" : "オープンループ/手動" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpedata for gamle" + "value" : "Åpen Loop / manuell" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pompgegevens te oud" + "value" : "Open loop / handmatig" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Dane pompy zbyt stare" + "value" : "Pętla otwarta / ręcznie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Dados da bomba muito antigos" + "value" : "Ciclo aberto / manual" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Date pompă prea vechi" + "value" : "Buclă deschisă / manual" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Данные помпы устарели" + "value" : "Открытый цикл / вручную" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Údaje pumpy sú príliš staré" + "value" : "Otvorená slučka / ručne" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpdata för gamla" + "value" : "Öppen loop / manuell" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Pompa verisi çok eski" + "value" : "Açık döngü / manuel" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Дані помпи застаріли" + "value" : "Відкритий цикл / вручну" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Dữ liệu bơm quá cũ" + "value" : "Vòng lặp mở / thủ công" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "胰岛素泵数据过旧" + "value" : "开环 / 手动" } } } }, - "Pump manager error" : { - "comment" : "Reason Loop could not run: the pump reported an error", + "per day, weekly average" : { + "comment" : "Trends Insulin per day card subtitle when each bar is a week's average daily total", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "خطأ في مدير المضخة" + "value" : "يوميًا، متوسط أسبوعي" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba správce pumpy" + "value" : "za den, týdenní průměr" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpefejl" + "value" : "pr. dag, ugegennemsnit" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpenfehler" + "value" : "pro Tag, Wochendurchschnitt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Error del gestor de la microinfusadora" + "value" : "por día, promedio semanal" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpunhallinnan virhe" + "value" : "päivässä, viikkokeskiarvo" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Erreur du gestionnaire de pompe" + "value" : "par jour, moyenne hebdomadaire" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שגיאת מנהל המשאבה" + "value" : "ליום, ממוצע שבועי" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpakezelő-hiba" + "value" : "naponta, heti átlag" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Errore del gestore pompa" + "value" : "al giorno, media settimanale" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ポンプマネージャのエラー" + "value" : "1日あたり、週平均" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpefeil" + "value" : "per dag, ukesgjennomsnitt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pompfout" + "value" : "per dag, weekgemiddelde" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Błąd menedżera pompy" + "value" : "dziennie, średnia tygodniowa" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Erro do gerenciador da bomba" + "value" : "por dia, média semanal" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Eroare manager pompă" + "value" : "pe zi, medie săptămânală" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ошибка менеджера помпы" + "value" : "в день, среднее за неделю" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Chyba správcu pumpy" + "value" : "za deň, týždenný priemer" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Fel i pumphanteraren" + "value" : "per dag, veckomedel" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Pompa yöneticisi hatası" + "value" : "günlük, haftalık ortalama" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Помилка менеджера помпи" + "value" : "на день, середнє за тиждень" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Lỗi trình quản lý bơm" + "value" : "mỗi ngày, trung bình theo tuần" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "胰岛素泵管理器错误" + "value" : "每日,按周平均" } } } }, - "Pump suspended" : { - "comment" : "Reason Loop could not run: insulin delivery on the pump was suspended", + "Period" : { + "comment" : "Trends: VoiceOver label of the picker that chooses how many days the screen covers", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "المضخة متوقفة مؤقتًا" + "value" : "الفترة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpa pozastavena" + "value" : "Období" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpe suspenderet" + "value" : "Periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpe unterbrochen" + "value" : "Zeitraum" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Microinfusadora suspendida" + "value" : "Periodo" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Pumppu pysäytetty" + "value" : "Jakso" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Pompe suspendue" + "value" : "Période" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "המשאבה מושהית" + "value" : "תקופה" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpa felfüggesztve" + "value" : "Időszak" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Pompa sospesa" + "value" : "Periodo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ポンプ一時停止中" + "value" : "期間" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpe stanset" + "value" : "Periode" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Pomp onderbroken" + "value" : "Periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pompa wstrzymana" + "value" : "Okres" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Bomba suspensa" + "value" : "Período" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Pompă suspendată" + "value" : "Perioadă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Помпа приостановлена" + "value" : "Период" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Pumpa pozastavená" + "value" : "Obdobie" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Pump pausad" + "value" : "Period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Pompa askıya alındı" + "value" : "Dönem" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Помпу призупинено" + "value" : "Період" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Bơm đã tạm ngưng" + "value" : "Khoảng thời gian" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "胰岛素泵已暂停" + "value" : "时段" } } } }, - "Recommendation expired" : { - "comment" : "Reason Loop could not run: its dosing recommendation expired before it was used", + "Previous period" : { + "comment" : "VoiceOver label for the Trends button that moves back one period", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "انتهت صلاحية التوصية" + "value" : "الفترة السابقة" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Doporučení vypršelo" + "value" : "Předchozí období" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Anbefaling udløbet" + "value" : "Forrige periode" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Empfehlung abgelaufen" + "value" : "Vorheriger Zeitraum" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Recomendación caducada" + "value" : "Período anterior" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Suositus vanhentunut" + "value" : "Edellinen jakso" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Recommandation expirée" + "value" : "Période précédente" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "תוקף ההמלצה פג" + "value" : "התקופה הקודמת" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Lejárt javaslat" + "value" : "Előző időszak" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Suggerimento scaduto" + "value" : "Periodo precedente" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "推奨の有効期限切れ" + "value" : "前の期間" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Anbefaling utløpt" + "value" : "Forrige periode" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Advies verlopen" + "value" : "Vorige periode" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zalecenie wygasło" + "value" : "Poprzedni okres" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Recomendação expirada" + "value" : "Período anterior" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Recomandare expirată" + "value" : "Perioada anterioară" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Рекомендация устарела" + "value" : "Предыдущий период" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Odporúčanie vypršalo" + "value" : "Predchádzajúce obdobie" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Rekommendationen har gått ut" + "value" : "Föregående period" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Önerinin süresi doldu" + "value" : "Önceki dönem" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Рекомендація застаріла" + "value" : "Попередній період" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Khuyến nghị đã hết hạn" + "value" : "Khoảng thời gian trước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "建议已过期" + "value" : "上一时段" } } } @@ -15483,149 +13767,6 @@ } } }, - "Unknown error" : { - "comment" : "Reason Loop could not run: an unexpected error", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "خطأ غير معروف" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Neznámá chyba" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukendt fejl" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unbekannter Fehler" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Error desconocido" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tuntematon virhe" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erreur inconnue" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "שגיאה לא ידועה" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ismeretlen hiba" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Errore sconosciuto" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "不明なエラー" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ukjent feil" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onbekende fout" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nieznany błąd" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erro desconhecido" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eroare necunoscută" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Неизвестная ошибка" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Neznáma chyba" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Okänt fel" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bilinmeyen hata" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Невідома помилка" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lỗi không xác định" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "未知错误" - } - } - } - }, "Usually %1$@ by %2$@" : { "comment" : "Trends: the median insulin delivered by this time of day over the last 14 days. (1: units, e.g. 28.4 U, 2: time of day)", "localizations" : { diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 6c95612287..662e8ac732 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -518,14 +518,6 @@ class LoopAppManager: NSObject { analyticsServicesManager.application(didFinishLaunchingWithOptions: launchOptions) - // Loop's automation history only goes back 36 hours, so the Trends screen keeps its own longer record. - // The debounce keeps a quick off-and-on from being recorded as open loop. - ObservablePublishers.tracking(settingsManager, keyPath: \.dosingEnabled) - .debounce(for: .seconds(15), scheduler: RunLoop.main) - .removeDuplicates() - .sink { LoopModeHistory.shared.record(closedLoop: $0) } - .store(in: &cancellables) - state = state.next await loopDataManager.updateDisplayState() diff --git a/Loop/Redesign/LoopHistoryMath.swift b/Loop/Redesign/LoopHistoryMath.swift index cfdca173dc..60cf93662b 100644 --- a/Loop/Redesign/LoopHistoryMath.swift +++ b/Loop/Redesign/LoopHistoryMath.swift @@ -299,19 +299,22 @@ enum LoopHistoryMath { struct LoopRun { let date: Date let succeeded: Bool - let error: String? + /// The run stopped because a manual temp basal was running, which Loop shows as open loop. + var manualTempBasal = false } - struct ModeChange: Codable, Equatable { + /// Closed loop switched on or off, from Loop's stored settings. + struct ModeChange: Equatable { let date: Date let closedLoop: Bool } /// Splits `interval` into closed, limited and open stretches. /// - /// Each 5-minute slot is open when closed loop was switched off (from `modeChanges`), closed when Loop completed a - /// run in the 15 minutes before the slot ends, and limited otherwise. Before the first mode change the setting is - /// unknown, so the result starts at the later of `coverageStart` and the first mode change. + /// Each 5-minute slot is open when closed loop was switched off (from `modeChanges`), or when Loop's latest run in + /// the 15 minutes before the slot ends stopped for a manual temp basal. Otherwise it is closed when Loop completed a + /// run in those 15 minutes, and limited when it didn't. Before the first mode change the setting is unknown, so the + /// result starts at the later of `coverageStart` and the first mode change. static func loopModeSpans(interval: DateInterval, runs: [LoopRun], modeChanges: [ModeChange], coverageStart: Date?, recency: TimeInterval = 15 * 60, slot: TimeInterval = 5 * 60) -> [LoopModeSpan] { guard let coverageStart, let firstChange = modeChanges.map(\.date).min() else { return [] } @@ -322,8 +325,8 @@ enum LoopHistoryMath { let changes = modeChanges.sorted { $0.date < $1.date } var spans: [LoopModeSpan] = [] var runIndex = 0 + var lastRun: LoopRun? = nil var lastSuccess: Date? = nil - var lastError: String? = nil var changeIndex = 0 var closedSetting: Bool? = nil @@ -332,11 +335,9 @@ enum LoopHistoryMath { let slotEnd = min(t.addingTimeInterval(slot), interval.end) while runIndex < sortedRuns.count && sortedRuns[runIndex].date <= slotEnd { let run = sortedRuns[runIndex] + lastRun = run if run.succeeded { lastSuccess = run.date - lastError = nil - } else { - lastError = run.error ?? lastError } runIndex += 1 } @@ -346,22 +347,20 @@ enum LoopHistoryMath { } let mode: LoopModeSpan.Mode - let cause: String? if closedSetting == false { mode = .open - cause = NSLocalizedString("Closed loop off", comment: "Reason shown for a stretch of time when Loop was in open loop because closed loop was turned off") + } else if let lastRun, lastRun.manualTempBasal, slotEnd.timeIntervalSince(lastRun.date) <= recency { + mode = .open } else if let lastSuccess, slotEnd.timeIntervalSince(lastSuccess) <= recency { mode = .closed - cause = nil } else { mode = .limited - cause = lastError ?? NSLocalizedString("Loop didn't run", comment: "Reason shown for a stretch of time when Loop did not complete a run and recorded no error") } - if let last = spans.last, last.mode == mode, last.cause == cause, last.end == t { - spans[spans.count - 1] = LoopModeSpan(start: last.start, end: slotEnd, mode: mode, cause: cause) + if let last = spans.last, last.mode == mode, last.end == t { + spans[spans.count - 1] = LoopModeSpan(start: last.start, end: slotEnd, mode: mode) } else { - spans.append(LoopModeSpan(start: t, end: slotEnd, mode: mode, cause: cause)) + spans.append(LoopModeSpan(start: t, end: slotEnd, mode: mode)) } t = slotEnd } diff --git a/Loop/Redesign/LoopHistoryModels.swift b/Loop/Redesign/LoopHistoryModels.swift index f443ecbbdf..956f91cbfe 100644 --- a/Loop/Redesign/LoopHistoryModels.swift +++ b/Loop/Redesign/LoopHistoryModels.swift @@ -189,7 +189,6 @@ struct LoopModeSpan: Equatable { let start: Date let end: Date let mode: Mode - let cause: String? var duration: TimeInterval { end.timeIntervalSince(start) } } diff --git a/Loop/Redesign/LoopHistoryProvider.swift b/Loop/Redesign/LoopHistoryProvider.swift index c0c771bc21..850a462599 100644 --- a/Loop/Redesign/LoopHistoryProvider.swift +++ b/Loop/Redesign/LoopHistoryProvider.swift @@ -53,6 +53,11 @@ final class LoopHistoryProvider { Date().addingTimeInterval(-Bundle.main.localCacheDuration) } + /// How many days Loop keeps in its own stores (`LOOP_LOCAL_CACHE_DURATION_DAYS`). + var localHistoryDays: Int { + Int((Bundle.main.localCacheDuration / 86400).rounded()) + } + @MainActor var glucoseTargetRange: ClosedRange? { guard let range = temporaryPresetsManager.effectiveCorrectionRange() else { return nil } @@ -130,10 +135,7 @@ final class LoopHistoryProvider { // Decode only the fields a run needs, and only Loop's local window. runs = try await store.getDosingDecisions(start: start, end: Date(), as: LoopRunRecord.self) .filter { $0.reason == "loop" } - .map { record in - LoopHistoryMath.LoopRun(date: record.date, succeeded: (record.errors ?? []).isEmpty, - error: record.errors?.first.map { Self.describe(issue: $0.id) }) - } + .map { Self.run(date: $0.date, issues: ($0.errors ?? []).map(\.id)) } } else { runs = try await anchoredLoopRuns() } @@ -142,6 +144,10 @@ final class LoopHistoryProvider { return (runs, runs.first?.date) } + private static func run(date: Date, issues: [String]) -> LoopHistoryMath.LoopRun { + LoopHistoryMath.LoopRun(date: date, succeeded: issues.isEmpty, manualTempBasal: issues.first == "manualTempBasalRunning") + } + private struct LoopRunRecord: Decodable { struct Issue: Decodable { let id: String } let date: Date @@ -164,43 +170,26 @@ final class LoopHistoryProvider { } try Task.checkCancellation() anchor = page.0 - runs += page.1.filter { $0.reason == "loop" }.map { decision in - LoopHistoryMath.LoopRun(date: decision.date, succeeded: decision.errors.isEmpty, - error: decision.errors.first.map { Self.describe(issue: $0.id) }) - } + runs += page.1.filter { $0.reason == "loop" }.map { Self.run(date: $0.date, issues: $0.errors.map(\.id)) } if page.1.count < 1000 { break } } return runs } - /// "glucoseTooOld" → "Glucose too old". Loop's own issue ids are localised; any other id falls back to the - /// id split into English words. - static func describe(issue id: String) -> String { - switch id { - case "configurationError": return NSLocalizedString("Configuration error", comment: "Reason Loop could not run: its settings are incomplete") - case "connectionError": return NSLocalizedString("Connection error", comment: "Reason Loop could not run: no connected devices, or a device connection failed") - case "missingDataError": return NSLocalizedString("Missing data error", comment: "Reason Loop could not run: data it needs was missing") - case "glucoseTooOld": return NSLocalizedString("Glucose too old", comment: "Reason Loop could not run: the latest glucose reading was too old") - case "invalidFutureGlucose": return NSLocalizedString("Invalid future glucose", comment: "Reason Loop could not run: a glucose reading had a timestamp in the future") - case "pumpDataTooOld": return NSLocalizedString("Pump data too old", comment: "Reason Loop could not run: the latest pump data was too old") - case "recommendationExpired": return NSLocalizedString("Recommendation expired", comment: "Reason Loop could not run: its dosing recommendation expired before it was used") - case "pumpSuspended": return NSLocalizedString("Pump suspended", comment: "Reason Loop could not run: insulin delivery on the pump was suspended") - case "pumpManagerError": return NSLocalizedString("Pump manager error", comment: "Reason Loop could not run: the pump reported an error") - case "unknownError": return NSLocalizedString("Unknown error", comment: "Reason Loop could not run: an unexpected error") - default: break + /// When closed loop was switched on or off, from the settings Loop stores on every change: the setting in force at + /// `interval.start`, then each change after it. Empty when Loop has no settings store. + func loopModeChanges(in interval: DateInterval) async throws -> [LoopHistoryMath.ModeChange] { + guard let store = settingsManager.settingsStore else { return [] } + var changes: [LoopHistoryMath.ModeChange] = [] + if let previous = try await store.getStoredSettings(end: interval.start, limit: 1).first { + changes.append(.init(date: interval.start, closedLoop: previous.dosingEnabled)) } - var words: [String] = [] - var current = "" - for character in id { - if character.isUppercase, !current.isEmpty { - words.append(current) - current = "" - } - current.append(character) + // Newest first from the store; every settings save is a record, so keep only the ones that switch the mode. + for settings in try await store.getStoredSettings(start: interval.start, end: interval.end).reversed() + where changes.last?.closedLoop != settings.dosingEnabled { + changes.append(.init(date: settings.date, closedLoop: settings.dosingEnabled)) } - words.append(current) - let sentence = words.joined(separator: " ").lowercased() - return sentence.prefix(1).uppercased() + sentence.dropFirst() + return changes } // MARK: - Loop's stores @@ -360,44 +349,6 @@ final class LoopHistoryProvider { } } -// MARK: - Loop mode history - -/// Records when closed loop is switched on and off, because Loop's stored runs don't say which mode they ran in. -/// The Trends screen can only call time "open loop" from the first record on. -final class LoopModeHistory { - static let shared = LoopModeHistory() - - private let defaults: UserDefaults - private let key = "com.loopkit.Loop.redesign.loopModeChanges" - private let maximumAge: TimeInterval = .days(100) - - init(defaults: UserDefaults = .standard) { - self.defaults = defaults - } - - var changes: [LoopHistoryMath.ModeChange] { - guard let data = defaults.data(forKey: key), - let changes = try? JSONDecoder().decode([LoopHistoryMath.ModeChange].self, from: data) else { - return [] - } - return changes - } - - func record(closedLoop: Bool, at date: Date = Date()) { - var changes = self.changes - guard changes.last?.closedLoop != closedLoop else { return } - changes.append(.init(date: date, closedLoop: closedLoop)) - // Keep the last change before the retention window: it establishes the mode at its start. - let cutoff = date.addingTimeInterval(-maximumAge) - if let anchor = changes.lastIndex(where: { $0.date < cutoff }), anchor > 0 { - changes.removeFirst(anchor) - } - if let data = try? JSONEncoder().encode(changes) { - defaults.set(data, forKey: key) - } - } -} - // MARK: - Historical targets extension LoopDataManager { diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift index d647d5c410..3f49ffca6d 100644 --- a/Loop/Redesign/TrendsScreen.swift +++ b/Loop/Redesign/TrendsScreen.swift @@ -97,6 +97,7 @@ final class TrendsModel: ObservableObject { let fetchWindow = oneDay ? DateInterval(start: interval.start, end: Calendar.current.date(byAdding: .day, value: 1, to: interval.start)!) : interval async let historyFetch = provider.dayHistory(fetchWindow) async let runsFetch = provider.loopRuns() + async let modeFetch = provider.loopModeChanges(in: interval) var history = try await historyFetch let (runs, coverageStart) = try await runsFetch if oneDay { @@ -155,7 +156,7 @@ final class TrendsModel: ObservableObject { summary.typicalByNow = await provider.typicalInsulinByNow() } - summary.loopSpans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: LoopModeHistory.shared.changes, + summary.loopSpans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: try await modeFetch, coverageStart: coverageStart) return summary } @@ -395,13 +396,13 @@ struct TrendsView: View { LoopFormat.totalUnits(today.total), LoopFormat.totalUnits(usual.typical.median), LoopFormat.time(usual.at)) } + /// When Loop's records of its runs and settings start inside the period, say so, and how long it keeps them. private func coverageNote(_ s: TrendsModel.Summary) -> String? { - guard let start = s.loopSpans.first?.start else { - return NSLocalizedString("Loop status is recorded from when this version of Loop first ran.", comment: "Trends Loop status footnote when there is no Loop status history yet") - } - guard start > s.interval.start else { return nil } - return String(format: NSLocalizedString("Covers %1$@: Loop keeps 7 days of runs, and closed or open loop is recorded from when this version of Loop first ran.", comment: "Trends Loop status footnote when the status covers only part of the period. (1: date range covered)"), - LoopFormat.range(start, s.interval.end)) + guard let start = s.loopSpans.first?.start, start > s.interval.start else { return nil } + let kept = String.localizedStringWithFormat(NSLocalizedString("%lld days", comment: "A number of days, e.g. a Trends period or how many days are averaged. (1: number of days)"), + model.provider.localHistoryDays) + return String(format: NSLocalizedString("Covers %1$@, from the start of Loop's records, which it keeps for %2$@.", comment: "Trends Loop status footnote when Loop's records of its runs and settings start partway through the period. (1: date range covered; 2: how long Loop keeps them, e.g. 90 days)"), + LoopFormat.range(start, s.interval.end), kept) } } diff --git a/LoopTests/Models/LoopHistoryMathTests.swift b/LoopTests/Models/LoopHistoryMathTests.swift index a5519a513a..68a322dc5b 100644 --- a/LoopTests/Models/LoopHistoryMathTests.swift +++ b/LoopTests/Models/LoopHistoryMathTests.swift @@ -149,7 +149,7 @@ class LoopHistoryMathTests: XCTestCase { func testLoopModesStartAtFirstModeRecord() { let interval = DateInterval(start: at(0), end: at(60)) - let runs = stride(from: 0.0, through: 60, by: 5).map { LoopHistoryMath.LoopRun(date: at($0), succeeded: true, error: nil) } + let runs = stride(from: 0.0, through: 60, by: 5).map { LoopHistoryMath.LoopRun(date: at($0), succeeded: true) } let spans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: [.init(date: at(30), closedLoop: true)], coverageStart: at(0)) XCTAssertEqual(spans.first?.start, at(30), "Before the first record the mode is unknown and left out") @@ -161,17 +161,29 @@ class LoopHistoryMathTests: XCTestCase { func testLoopModesClassifyOpenAndLimited() { let interval = DateInterval(start: at(0), end: at(90)) // Runs succeed until 20 min, then fail with an error, then stop. - var runs = stride(from: 0.0, through: 20, by: 5).map { LoopHistoryMath.LoopRun(date: at($0), succeeded: true, error: nil) } - runs.append(.init(date: at(25), succeeded: false, error: "Glucose too old")) + var runs = stride(from: 0.0, through: 20, by: 5).map { LoopHistoryMath.LoopRun(date: at($0), succeeded: true) } + runs.append(.init(date: at(25), succeeded: false)) let changes: [LoopHistoryMath.ModeChange] = [.init(date: at(0), closedLoop: true), .init(date: at(60), closedLoop: false)] let spans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: changes, coverageStart: at(0)) XCTAssertEqual(spans.map(\.mode), [.closed, .limited, .open]) - XCTAssertEqual(spans[1].cause, "Glucose too old") XCTAssertEqual(spans[1].start, at(35), "Limited starts once the last good run is over 15 minutes old") XCTAssertEqual(spans[2].start, at(55), "The slot ending at the switch-off counts as open") } + func testManualTempBasalCountsAsOpenLoop() { + let interval = DateInterval(start: at(0), end: at(60)) + // Closed loop stays on; from 20 min a manual temp basal holds every run, until it ends at 40 min. + let runs = stride(from: 0.0, through: 60, by: 5).map { minute in + LoopHistoryMath.LoopRun(date: at(minute), succeeded: !(20...40).contains(minute), manualTempBasal: (20...40).contains(minute)) + } + let spans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: [.init(date: at(0), closedLoop: true)], + coverageStart: at(0)) + XCTAssertEqual(spans.map(\.mode), [.closed, .open, .closed]) + XCTAssertEqual(spans[1].start, at(15), "The slot holding the first held run is open") + XCTAssertEqual(spans[2].start, at(40), "Closed again with the first good run after it") + } + // MARK: Calendar func testDaysAndQuarterMarksOnClockChangeDay() throws { @@ -291,27 +303,6 @@ class LoopHistoryMathTests: XCTestCase { XCTAssertEqual(LoopFormat.signed(0.0001), "+0.00") } - func testModeHistoryRetainsTheModeBeforeTheRetentionWindow() throws { - let suite = "LoopHistoryMathTests-\(UUID().uuidString)" - let defaults = try XCTUnwrap(UserDefaults(suiteName: suite)) - defer { defaults.removePersistentDomain(forName: suite) } - let history = LoopModeHistory(defaults: defaults) - history.record(closedLoop: false, at: at(0)) - history.record(closedLoop: true, at: at(60)) - let switchDate = at(200 * 1440) - history.record(closedLoop: false, at: switchDate) - XCTAssertEqual(history.changes, [.init(date: at(60), closedLoop: true), .init(date: switchDate, closedLoop: false)]) - let interval = DateInterval(start: switchDate.addingTimeInterval(-3600), end: switchDate) - let spans = LoopHistoryMath.loopModeSpans(interval: interval, runs: [], modeChanges: history.changes, - coverageStart: interval.start) - XCTAssertEqual(spans.first?.start, interval.start, "Retaining the old mode must preserve recent coverage") - } - - func testIssueDescriptions() { - XCTAssertEqual(LoopHistoryProvider.describe(issue: "glucoseTooOld"), "Glucose too old") - XCTAssertEqual(LoopHistoryProvider.describe(issue: "pumpDataTooOld"), "Pump data too old") - } - // MARK: Day history func testDayHistoryClippedToNowDropsLaterEventsAndCutsBasal() { From 553ff34d24642e4ecbadcb3713df017f035799f3 Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:37 +0000 Subject: [PATCH 09/15] Follow the cache duration and mark partial older periods The Loop status note uses localCacheDuration instead of 7 days. Scheduled basal and the manual and automatic split cover only days Loop has itself, with a day count, and boluses of unknown kind are no longer counted as manual. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- Loop/Localizable.xcstrings | 286 ++++++++++++++++++++++++ Loop/Redesign/LoopHistoryModels.swift | 3 +- Loop/Redesign/LoopHistoryProvider.swift | 13 +- Loop/Redesign/TrendsComponents.swift | 43 +++- Loop/Redesign/TrendsScreen.swift | 46 +++- 5 files changed, 359 insertions(+), 32 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index f5faaf7562..5cc44e5e62 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -3608,6 +3608,149 @@ } } }, + "+ %1$@ automatic, %2$@ unknown" : { + "comment" : "Trends Boluses per day tile detail when some boluses don't say whether they were manual or automatic, such as another app's in Apple Health. (1: automatic boluses per day; 2: boluses of unknown kind per day; both fractional, such as 4.2)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ تلقائية، %2$@ غير معروفة" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatických, %2$@ neznámých" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatiske, %2$@ ukendte" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatisch, %2$@ unbekannt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automáticos, %2$@ desconocidos" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automaattista, %2$@ tuntematonta" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatiques, %2$@ inconnus" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ אוטומטיים, %2$@ לא ידועים" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatikus, %2$@ ismeretlen" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatici, %2$@ sconosciuti" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ 自動 %1$@、不明 %2$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatiske, %2$@ ukjente" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatisch, %2$@ onbekend" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatycznych, %2$@ nieznanych" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automáticos, %2$@ desconhecidos" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automate, %2$@ necunoscute" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ автоматических, %2$@ неизвестных" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatických, %2$@ neznámych" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ automatiska, %2$@ okända" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ otomatik, %2$@ bilinmeyen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ автоматичних, %2$@ невідомих" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ %1$@ tự động, %2$@ không rõ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "+ 自动 %1$@,未知 %2$@" + } + } + } + }, "25–75%" : { "comment" : "Trends Daily pattern legend: band between the 25th and 75th percentiles of glucose", "localizations" : { @@ -10764,6 +10907,149 @@ } } }, + "Older days come from Apple Health, which doesn't record the basal schedule, or whether another app's bolus was manual or automatic." : { + "comment" : "Trends Insulin per day footnote when the period reaches back past Loop's own records, so scheduled basal or the manual and automatic split can't be shown", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الأيام الأقدم مأخوذة من Apple Health، الذي لا يسجّل جدول القاعدي، ولا ما إذا كانت جرعة تطبيق آخر يدوية أم تلقائية." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Starší dny pocházejí z Apple Health, který nezaznamenává plán bazálu ani to, zda byl bolus z jiné aplikace ruční, nebo automatický." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ældre dage kommer fra Apple Health, som ikke registrerer basalplanen, eller om en anden apps bolus var manuel eller automatisk." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ältere Tage stammen aus Apple Health. Dort sind weder das Basalprogramm noch die Angabe gespeichert, ob ein Bolus aus einer anderen App manuell oder automatisch war." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los días anteriores provienen de Apple Health, que no registra el programa basal ni si un bolo de otra app fue manual o automático." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vanhemmat päivät tulevat Apple Healthista, joka ei tallenna basaaliohjelmaa eikä sitä, oliko toisen apin bolus manuaalinen vai automaattinen." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les jours plus anciens viennent d'Apple Santé, qui n'enregistre ni le programme basal, ni si un bolus d'une autre app était manuel ou automatique." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ימים ישנים יותר מגיעים מ-Apple Health, שאינו שומר את תוכנית הבזאלי, או אם בולוס מאפליקציה אחרת היה ידני או אוטומטי." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "A régebbi napok az Apple Egészségből származnak, amely nem rögzíti a bázisprogramot, sem azt, hogy egy másik app bólusa kézi vagy automatikus volt-e." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "I giorni precedenti provengono da Apple Salute, che non registra il programma basale, né se un bolo di un'altra app era manuale o automatico." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "それ以前の日はAppleヘルスケアのデータです。ベーサルのスケジュールや、他のAppのボーラスが手動か自動かは記録されません。" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eldre dager kommer fra Apple Helse, som ikke registrerer basalplanen, eller om en annen apps bolus var manuell eller automatisk." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Oudere dagen komen uit Apple Gezondheid, dat het basaalschema niet vastlegt, en ook niet of een bolus van een andere app handmatig of automatisch was." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Starsze dni pochodzą z Apple Zdrowie, które nie zapisuje harmonogramu bazy ani tego, czy bolus z innej aplikacji był ręczny, czy automatyczny." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Os dias mais antigos vêm do app Saúde, que não registra a programação basal nem se o bolus de outro app foi manual ou automático." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zilele mai vechi provin din Apple Sănătate, care nu înregistrează programul bazal sau dacă un bolus din altă aplicație a fost manual ori automat." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Более ранние дни взяты из Apple Здоровье, где не хранятся базальная программа и то, был ли болюс из другого приложения ручным или автоматическим." + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Staršie dni pochádzajú z Apple Zdravie, ktoré nezaznamenáva plán bazálu ani to, či bol bolus z inej aplikácie manuálny, alebo automatický." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Äldre dagar kommer från Apple Hälsa, som inte registrerar basalschemat, eller om en annan apps bolus var manuell eller automatisk." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha eski günler Apple Sağlık'tan gelir; bazal programı veya başka bir uygulamanın bolusunun manuel mi otomatik mi olduğu orada kaydedilmez." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Давніші дні взято з Apple Здоровʼя, де не зберігаються базальна програма та те, чи був болюс з іншої програми ручним чи автоматичним." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Các ngày cũ hơn lấy từ Apple Sức khỏe, nơi không ghi lại lịch basal, hay liều bolus của ứng dụng khác là thủ công hay tự động." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "较早的日子来自 Apple“健康”,其中不记录基础率计划,也不记录其他 App 的大剂量是手动还是自动。" + } + } + } + }, "Open loop / manual" : { "comment" : "Trends Loop status: time closed loop was switched off", "localizations" : { diff --git a/Loop/Redesign/LoopHistoryModels.swift b/Loop/Redesign/LoopHistoryModels.swift index 956f91cbfe..fc8e73b378 100644 --- a/Loop/Redesign/LoopHistoryModels.swift +++ b/Loop/Redesign/LoopHistoryModels.swift @@ -174,7 +174,8 @@ extension InsulinSplit { enum HistorySource { /// Loop's own stores: manual versus automatic, temp basals and the schedule are all known. case loop - /// Apple Health, for days older than Loop keeps. Doses are split into basal and bolus only. + /// Apple Health, for days older than Loop keeps. There is no basal schedule, and only the boluses Loop wrote say + /// whether they were automatic. case health } diff --git a/Loop/Redesign/LoopHistoryProvider.swift b/Loop/Redesign/LoopHistoryProvider.swift index 850a462599..4a15d13259 100644 --- a/Loop/Redesign/LoopHistoryProvider.swift +++ b/Loop/Redesign/LoopHistoryProvider.swift @@ -14,9 +14,10 @@ import os /// Reads glucose, insulin, carbs and loop runs for the redesigned screens. /// -/// Loop keeps `LOOP_LOCAL_CACHE_DURATION_DAYS` (7) days in its own stores. Inside that window everything comes from -/// those stores, so manual and automatic doses, temp basals and the schedule are known. Older days come from Apple -/// Health, which has glucose, carbs and insulin split only into basal and bolus. +/// Loop keeps `LOOP_LOCAL_CACHE_DURATION_DAYS` (90 by default) days in its own stores. Inside that window everything +/// comes from those stores, so manual and automatic doses, temp basals and the schedule are known. Older days come from +/// Apple Health, which has glucose, carbs and insulin split into basal and bolus, and whether Loop gave a bolus +/// automatically only for the doses Loop wrote, but not the basal schedule. final class LoopHistoryProvider { private let deviceManager: DeviceDataManager @@ -64,12 +65,6 @@ final class LoopHistoryProvider { return range.lowerBound.doubleValue(for: .milligramsPerDeciliter)...range.upperBound.doubleValue(for: .milligramsPerDeciliter) } - /// What the basal schedule delivers in a day, for days the stored profile history doesn't reach. - @MainActor - var scheduledBasalPerDay: Double? { - loopDataManager.settingsProvider.settings.basalRateSchedule?.total() - } - /// The median insulin delivered from midnight to this time of day over the last 14 days. func typicalInsulinByNow() async -> (typical: TypicalInsulinDelivery, at: Date)? { guard let history = try? await loopDataManager.insulinDeliveryHistory(days: 14), diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift index 638531e885..cd42de493b 100644 --- a/Loop/Redesign/TrendsComponents.swift +++ b/Loop/Redesign/TrendsComponents.swift @@ -484,6 +484,12 @@ struct InsulinSplitView: View { var partial = false /// What the basal schedule delivers over the same time, shown under the basal row. var scheduledBasal: Double? + /// How many of the days the schedule covers, e.g. "20 of 90 days", when that is fewer than the split's. + var scheduledNote: String? + /// Manual and automatic bolus over only the days that say which each bolus was, when some days don't. + var classifiedSplit: InsulinSplit? + /// How many days `classifiedSplit` covers, e.g. "60 of 90 days". + var classifiedNote: String? /// Today against a usual day, e.g. "Usually 28.4 U by 19:46", under the total. var usual: String? /// The split as one capsule, where no chart above already draws it. @@ -499,16 +505,19 @@ struct InsulinSplitView: View { VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { row(.basal, NSLocalizedString("Basal", comment: "Basal and bolus row: insulin delivered as basal"), split.basal, split.basalPercent) if let scheduledBasal { - part(.dash(LoopDesign.Palette.basalScheduled), String(format: NSLocalizedString("Scheduled %1$@", comment: "Basal and bolus: what the basal schedule delivers over the same time, under the basal row. (1: units, e.g. 18.0 U)"), LoopFormat.totalUnits(scheduledBasal))) + part(.dash(LoopDesign.Palette.basalScheduled), Self.scheduledText(scheduledBasal, note: scheduledNote)) .font(LoopDesign.Typeface.footnote.monospacedDigit()) .foregroundStyle(LoopDesign.Palette.inkMuted) .padding(.leading, 10 + LoopDesign.Space.s2) } row(.solid(LoopDesign.Palette.bolus), NSLocalizedString("Bolus", comment: "Basal and bolus row: insulin delivered as bolus, manual and automatic"), split.bolus, split.bolusPercent) - if split.isClassified && split.bolus > 0 { + if let detail = bolusDetail, split.bolus > 0 { FlowLayout(spacing: LoopDesign.Space.s3, lineSpacing: 2) { - part(.solid(LoopDesign.Palette.bolus), String(format: NSLocalizedString("Manual %1$@", comment: "Basal and bolus: bolus the person gave. (1: units, e.g. 16.2 U)"), LoopFormat.totalUnits(split.manualBolus))) - part(.hatch(LoopDesign.Palette.bolus), String(format: NSLocalizedString("Automatic %1$@", comment: "Basal and bolus: bolus Loop gave automatically. (1: units, e.g. 5.4 U)"), LoopFormat.totalUnits(split.automaticBolus))) + part(.solid(LoopDesign.Palette.bolus), String(format: NSLocalizedString("Manual %1$@", comment: "Basal and bolus: bolus the person gave. (1: units, e.g. 16.2 U)"), LoopFormat.totalUnits(detail.manualBolus))) + part(.hatch(LoopDesign.Palette.bolus), String(format: NSLocalizedString("Automatic %1$@", comment: "Basal and bolus: bolus Loop gave automatically. (1: units, e.g. 5.4 U)"), LoopFormat.totalUnits(detail.automaticBolus))) + if let classifiedNote { + Text(verbatim: "· \(classifiedNote)") + } } .font(LoopDesign.Typeface.footnote.monospacedDigit()) .foregroundStyle(LoopDesign.Palette.inkMuted) @@ -538,7 +547,19 @@ struct InsulinSplitView: View { } } .accessibilityElement(children: .ignore) - .accessibilityLabel(Self.accessibilityLabel(split, partial: partial, scheduledBasal: scheduledBasal, usual: usual)) + .accessibilityLabel(Self.accessibilityLabel(split, partial: partial, scheduledBasal: scheduledBasal.map { Self.scheduledText($0, note: scheduledNote) }, + bolusDetail: bolusDetail, bolusDetailNote: classifiedNote, usual: usual)) + } + + /// The whole split's manual and automatic bolus, or only the days that record it; nil when nothing does. + private var bolusDetail: InsulinSplit? { + classifiedSplit ?? (split.isClassified ? split : nil) + } + + /// "Scheduled 18.0 U", or "Scheduled 18.0 U · 20 of 90 days" when the schedule is known for only some days. + private static func scheduledText(_ units: Double, note: String?) -> String { + let text = String(format: NSLocalizedString("Scheduled %1$@", comment: "Basal and bolus: what the basal schedule delivers over the same time, under the basal row. (1: units, e.g. 18.0 U)"), LoopFormat.totalUnits(units)) + return note.map { "\(text) · \($0)" } ?? text } private func row(_ swatch: LegendItem.Swatch, _ label: String, _ units: Double, _ percent: Int) -> some View { @@ -565,21 +586,23 @@ struct InsulinSplitView: View { } } - static func accessibilityLabel(_ split: InsulinSplit, partial: Bool, scheduledBasal: Double? = nil, usual: String? = nil) -> String { + static func accessibilityLabel(_ split: InsulinSplit, partial: Bool, scheduledBasal: String? = nil, bolusDetail: InsulinSplit? = nil, + bolusDetailNote: String? = nil, usual: String? = nil) -> String { var parts = [ String(format: NSLocalizedString("Basal %1$@, %2$@ percent", comment: "VoiceOver: basal units and share. (1: units, 2: percentage number)"), LoopFormat.totalUnits(split.basal), "\(split.basalPercent)"), String(format: NSLocalizedString("Bolus %1$@, %2$@ percent", comment: "VoiceOver: bolus units and share. (1: units, 2: percentage number)"), LoopFormat.totalUnits(split.bolus), "\(split.bolusPercent)"), ] - if split.isClassified && split.bolus > 0 { - parts.append(String(format: NSLocalizedString("%1$@ manual, %2$@ automatic", comment: "VoiceOver: how the bolus divides. (1: manual units, 2: automatic units)"), - LoopFormat.totalUnits(split.manualBolus), LoopFormat.totalUnits(split.automaticBolus))) + if let detail = bolusDetail, split.bolus > 0 { + let text = String(format: NSLocalizedString("%1$@ manual, %2$@ automatic", comment: "VoiceOver: how the bolus divides. (1: manual units, 2: automatic units)"), + LoopFormat.totalUnits(detail.manualBolus), LoopFormat.totalUnits(detail.automaticBolus)) + parts.append(bolusDetailNote.map { "\(text), \($0)" } ?? text) } parts.append(String(format: partial ? NSLocalizedString("Total so far %1$@", comment: "VoiceOver: today's total insulin so far. (1: units)") : NSLocalizedString("Total %1$@", comment: "VoiceOver: total insulin. (1: units)"), LoopFormat.totalUnits(split.total))) if let scheduledBasal { - parts.insert(String(format: NSLocalizedString("Scheduled %1$@", comment: "Basal and bolus: what the basal schedule delivers over the same time, under the basal row. (1: units, e.g. 18.0 U)"), LoopFormat.totalUnits(scheduledBasal)), at: 1) + parts.insert(scheduledBasal, at: 1) } if let usual { parts.append(usual) diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift index 3f49ffca6d..995f894df2 100644 --- a/Loop/Redesign/TrendsScreen.swift +++ b/Loop/Redesign/TrendsScreen.swift @@ -33,13 +33,20 @@ final class TrendsModel: ObservableObject { var carbsByDay: [Date: Double] = [:] var manualBoluses = 0 var automaticBoluses = 0 + /// Boluses whose record doesn't say whether Loop or the person gave them, such as another app's in Apple Health. + var unknownBoluses = 0 var loopSpans: [LoopModeSpan] = [] var targetRange: ClosedRange? /// The whole calendar day, for a one-day period: drawn as one timeline. var day: DayHistory? var dayTargets: [TargetBand] = [] - /// What the basal schedule delivers over the same days the split averages. + /// What the basal schedule delivers per day, over the averaged days Loop has its schedule for. var scheduledBasal: Double? + /// The averaged days `scheduledBasal` covers: Apple Health doesn't record the schedule for older days. + var scheduledDays = 0 + /// Manual and automatic bolus per day over the averaged days that say which each bolus was, when some don't. + var classifiedSplit: InsulinSplit? + var classifiedDays = 0 /// The median insulin by this time of day over the last 14 days, when the period ends now. var typicalByNow: (typical: TypicalInsulinDelivery, at: Date)? @@ -139,18 +146,21 @@ final class TrendsModel: ObservableObject { summary.carbGrams = carbs.reduce(0) { $0 + $1.grams } summary.carbEntries = carbs.count let boluses = history.boluses.filter { inAveragedDays($0.date) } - summary.manualBoluses = boluses.filter { !$0.automatic }.count - summary.automaticBoluses = boluses.filter(\.automatic).count + summary.manualBoluses = boluses.filter { $0.automaticKnown && !$0.automatic }.count + summary.automaticBoluses = boluses.filter { $0.automaticKnown && $0.automatic }.count + summary.unknownBoluses = boluses.filter { !$0.automaticKnown }.count - // Days the stored profile history doesn't reach (older ones, read from Health) use today's schedule. + // Only days Loop has its schedule for: older days come from Health, which doesn't record it. let spans = Dictionary(uniqueKeysWithValues: LoopHistoryMath.days(in: interval).map { ($0.start, $0) }) - let perDay = provider.scheduledBasalPerDay - let scheduled = insulinDays.compactMap { day -> Double? in - guard let span = spans[day.day] else { return nil } - return LoopHistoryMath.scheduledUnits(history.scheduledBasal, over: span) ?? perDay.map { $0 * span.duration / 86400 } - } - if !scheduled.isEmpty, scheduled.count == insulinDays.count { + let scheduled = insulinDays.compactMap { day in spans[day.day].flatMap { LoopHistoryMath.scheduledUnits(history.scheduledBasal, over: $0) } } + if !scheduled.isEmpty { summary.scheduledBasal = scheduled.reduce(0, +) / Double(scheduled.count) + summary.scheduledDays = scheduled.count + } + let classified = insulinDays.filter(\.split.isClassified) + if !classified.isEmpty, classified.count < insulinDays.count { + summary.classifiedSplit = InsulinSplit.average(classified.map(\.split)) + summary.classifiedDays = classified.count } if interval.end.timeIntervalSinceNow > -300 { summary.typicalByNow = await provider.typicalInsulinByNow() @@ -201,8 +211,12 @@ final class TrendsModel: ObservableObject { ? NSLocalizedString("Boluses", comment: "Trends summary tile for a one-day period: how many manual boluses were given that day") : NSLocalizedString("Boluses per day", comment: "Trends summary tile: average number of manual boluses per day"), value: LoopFormat.number(Double(s.manualBoluses) / s.days, decimals: countDecimals), unit: nil, - detail: String(format: NSLocalizedString("+ %1$@ automatic", comment: "Trends Boluses per day tile detail: average number of automatic boluses per day on top of the manual ones, a fractional value such as 4.2. (1: number with one decimal)"), - LoopFormat.number(Double(s.automaticBoluses) / s.days, decimals: countDecimals)))) + detail: s.unknownBoluses > 0 + ? String(format: NSLocalizedString("+ %1$@ automatic, %2$@ unknown", comment: "Trends Boluses per day tile detail when some boluses don't say whether they were manual or automatic, such as another app's in Apple Health. (1: automatic boluses per day; 2: boluses of unknown kind per day; both fractional, such as 4.2)"), + LoopFormat.number(Double(s.automaticBoluses) / s.days, decimals: countDecimals), + LoopFormat.number(Double(s.unknownBoluses) / s.days, decimals: countDecimals)) + : String(format: NSLocalizedString("+ %1$@ automatic", comment: "Trends Boluses per day tile detail: average number of automatic boluses per day on top of the manual ones, a fractional value such as 4.2. (1: number with one decimal)"), + LoopFormat.number(Double(s.automaticBoluses) / s.days, decimals: countDecimals)))) return tiles } @@ -333,12 +347,20 @@ struct TrendsView: View { InsulinDailyTotalsView(totals: weekly ? LoopHistoryMath.weekly(s.totals) : s.totals, weekly: weekly, average: s.insulinPerDay) } // Under the chart, the rows name its marks, so there is no separate legend. + let averaged = LoopHistoryMath.averagingDays(s.totals).count InsulinSplitView(split: s.split, partial: s.splitIsPartial, scheduledBasal: s.scheduledBasal, + scheduledNote: s.scheduledDays < averaged ? daysOf(s.scheduledDays, averaged) : nil, + classifiedSplit: s.classifiedSplit, + classifiedNote: s.classifiedSplit != nil ? daysOf(s.classifiedDays, averaged) : nil, usual: s.day != nil && s.splitIsPartial ? usualByNow(s) : nil, showsBar: s.day != nil, averageLine: s.day == nil) if s.day == nil, let today = todaySoFar(s) { Footnote(today) } + // Days read from Apple Health have no schedule, and other apps' boluses there don't say which they were. + if s.interval.start < model.provider.localHistoryStart, s.scheduledBasal == nil || (s.split.bolus > 0 && !s.split.isClassified && s.classifiedSplit == nil) { + Footnote(NSLocalizedString("Older days come from Apple Health, which doesn't record the basal schedule, or whether another app's bolus was manual or automatic.", comment: "Trends Insulin per day footnote when the period reaches back past Loop's own records, so scheduled basal or the manual and automatic split can't be shown")) + } } else { ChartPlaceholder(message: NSLocalizedString("No insulin recorded in this period", comment: "Trends Insulin per day chart placeholder when no insulin was delivered in the period"), height: 80) } From 918ab1112d123988ba2f102b29361868ae79984c Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:37 +0000 Subject: [PATCH 10/15] Stack Trends rows at the largest text sizes Time in range, basal and bolus, and Loop status rows stack at accessibility sizes instead of spilling or breaking words. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- Loop/Redesign/TrendsComponents.swift | 117 ++++++++++++++++++--------- 1 file changed, 77 insertions(+), 40 deletions(-) diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift index cd42de493b..1d5b08c6a6 100644 --- a/Loop/Redesign/TrendsComponents.swift +++ b/Loop/Redesign/TrendsComponents.swift @@ -274,33 +274,43 @@ struct TimeInRangeView: View { private func row(_ band: LoopHistoryMath.RangeBand) -> some View { let fraction = fractions[band] ?? 0 let hours = fraction * 24 - let thresholds = Text(band.localizedThresholds(unit)) - .font(LoopDesign.Typeface.footnote) - .foregroundStyle(P.inkMuted) - return VStack(alignment: .leading, spacing: 0) { - HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { - Text(percentText(Double(percentages[band] ?? 0))) - .font(LoopDesign.Typeface.statValueSmall) - .foregroundStyle(P.ink) - .frame(minWidth: typeSize.isAccessibilitySize ? nil : 52, alignment: .trailing) - Text(band.localizedName) - .font(LoopDesign.Typeface.subhead) - .foregroundStyle(P.ink) - if !typeSize.isAccessibilitySize { - thresholds + let hoursText = String(format: NSLocalizedString("%1$@ h", comment: "Time in range row: average hours a day spent in the band, abbreviated. (1: hours with one decimal)"), + LoopFormat.number(hours, decimals: 1)) + let percent = Text(percentText(Double(percentages[band] ?? 0))) + .font(LoopDesign.Typeface.statValueSmall) + .foregroundStyle(P.ink) + let name = Text(band.localizedName) + .font(LoopDesign.Typeface.subhead) + .foregroundStyle(P.ink) + return Group { + if typeSize.isAccessibilitySize { + // One figure per line, so nothing is squeezed into a column. + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { + percent + name + } + Text(verbatim: "\(band.localizedThresholds(unit)) · \(hoursText) · \(band.localizedTarget)") + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + .fixedSize(horizontal: false, vertical: true) } - Spacer(minLength: LoopDesign.Space.s1) - VStack(alignment: .trailing, spacing: 0) { - Text(String(format: NSLocalizedString("%1$@ h", comment: "Time in range row: average hours a day spent in the band, abbreviated. (1: hours with one decimal)"), - LoopFormat.number(hours, decimals: 1))) - .font(LoopDesign.Typeface.footnote.monospacedDigit()) - Text(band.localizedTarget) - .font(LoopDesign.Typeface.caption) + } else { + HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { + percent.frame(minWidth: 52, alignment: .trailing) + name + Text(band.localizedThresholds(unit)) + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + Spacer(minLength: LoopDesign.Space.s1) + VStack(alignment: .trailing, spacing: 0) { + Text(hoursText) + .font(LoopDesign.Typeface.footnote.monospacedDigit()) + Text(band.localizedTarget) + .font(LoopDesign.Typeface.caption) + } + .foregroundStyle(P.inkMuted) } - .foregroundStyle(P.inkMuted) - } - if typeSize.isAccessibilitySize { - thresholds } } .accessibilityElement(children: .ignore) @@ -496,6 +506,7 @@ struct InsulinSplitView: View { var showsBar = true /// The total is the dashed average line of the chart above, so it carries that line's swatch. var averageLine = false + @Environment(\.dynamicTypeSize) private var typeSize var body: some View { VStack(alignment: .leading, spacing: LoopDesign.Space.s3) { @@ -563,19 +574,26 @@ struct InsulinSplitView: View { } private func row(_ swatch: LegendItem.Swatch, _ label: String, _ units: Double, _ percent: Int) -> some View { - HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { - LegendSwatch(swatch: swatch).alignmentGuide(.firstTextBaseline) { $0[.bottom] - 1 } - Text(label) - .font(LoopDesign.Typeface.subhead) - .foregroundStyle(LoopDesign.Palette.ink) - Spacer() + let amount = HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { Text(LoopFormat.totalUnits(units)) .font(LoopDesign.Typeface.body.weight(.semibold).monospacedDigit()) .foregroundStyle(LoopDesign.Palette.ink) Text(verbatim: "\(percent)%") .font(LoopDesign.Typeface.footnote.monospacedDigit()) .foregroundStyle(LoopDesign.Palette.inkMuted) - .frame(minWidth: 38, alignment: .trailing) + .frame(minWidth: typeSize.isAccessibilitySize ? nil : 38, alignment: .trailing) + } + return AdaptiveRow(spacing: typeSize.isAccessibilitySize ? 0 : LoopDesign.Space.s2) { + HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { + LegendSwatch(swatch: swatch).alignmentGuide(.firstTextBaseline) { $0[.bottom] - 1 } + Text(label) + .font(LoopDesign.Typeface.subhead) + .foregroundStyle(LoopDesign.Palette.ink) + } + if !typeSize.isAccessibilitySize { + Spacer() + } + amount } } @@ -724,6 +742,7 @@ struct InsulinDailyTotalsView: View { struct LoopModeBarView: View { let spans: [LoopModeSpan] + @Environment(\.dynamicTypeSize) private var typeSize private var total: TimeInterval { spans.reduce(0) { $0 + $1.duration } } @@ -783,18 +802,36 @@ struct LoopModeBarView: View { private func row(_ mode: LoopModeSpan.Mode) -> some View { let duration = LoopHistoryMath.duration(of: mode, in: spans) - return HStack(spacing: LoopDesign.Space.s2) { + let name = HStack(spacing: LoopDesign.Space.s2) { Circle().fill(Self.color(mode)).frame(width: 8, height: 8) Text(Self.name(mode)) .font(LoopDesign.Typeface.subhead) .foregroundStyle(P.ink) - Text("· \(LoopFormat.duration(duration))") - .font(LoopDesign.Typeface.footnote) - .foregroundStyle(P.inkMuted) - Spacer() - Text(percentText(fraction(mode) * 100, decimals: 1)) - .font(LoopDesign.Typeface.statValueSmall) - .foregroundStyle(P.ink) + } + let time = Text(verbatim: "· \(LoopFormat.duration(duration))") + .font(LoopDesign.Typeface.footnote) + .foregroundStyle(P.inkMuted) + let percent = Text(percentText(fraction(mode) * 100, decimals: 1)) + .font(LoopDesign.Typeface.statValueSmall) + .foregroundStyle(P.ink) + return Group { + if typeSize.isAccessibilitySize { + VStack(alignment: .leading, spacing: 0) { + name + HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { + percent + time + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } else { + HStack(spacing: LoopDesign.Space.s2) { + name + time + Spacer() + percent + } + } } .accessibilityElement(children: .combine) } From 66260a52960d107b0c8e3a7426973552c7511c04 Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:37 +0000 Subject: [PATCH 11/15] Remove the unused ValueTile Moves the smooth-curve helper into SetupComparisonCharts, its remaining user. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- Loop/Redesign/LoopDesign.swift | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/Loop/Redesign/LoopDesign.swift b/Loop/Redesign/LoopDesign.swift index 985ea599f1..b76fb9e97d 100644 --- a/Loop/Redesign/LoopDesign.swift +++ b/Loop/Redesign/LoopDesign.swift @@ -257,33 +257,6 @@ struct AdaptiveRow: View { } } -// MARK: - Tiles - -/// A small sunken tile with a label over a value, as used under the timeline. -struct ValueTile: View { - let label: String - @ViewBuilder var value: Value - - var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(label) - .font(LoopDesign.Typeface.footnote) - .foregroundStyle(LoopDesign.Palette.inkMuted) - .lineLimit(1) - .minimumScaleFactor(0.7) - .allowsTightening(true) - // A value with a long unit ("0.04 単位") shrinks to fit rather than breaking mid-word. - value - .lineLimit(1) - .minimumScaleFactor(0.6) - } - .padding(LoopDesign.Space.s3) - .frame(maxWidth: .infinity, minHeight: 64, maxHeight: .infinity, alignment: .topLeading) - .background(LoopDesign.Palette.surfaceSunken, in: RoundedRectangle(cornerRadius: LoopDesign.Radius.md, style: .continuous)) - .accessibilityElement(children: .combine) - } -} - /// The round previous and next buttons beside a date or period. struct StepButton: View { let systemName: String From b92a4ca5ce73682daf817482db309f32242a88a2 Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:37 +0000 Subject: [PATCH 12/15] Fix issues found testing the review changes Reads Apple Health for days before Loop's first local records, per kind of data, without overlap. Classifies each bolus from its own record and shows unknown. Always shows GMI with its IFCC value, with a note when the period is short or coverage is low. Counts time-in-range hours over CGM-covered time, restores dark-mode tile backgrounds and matches the Settings chevron. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- Loop/Localizable.xcstrings | 902 +++++++++++++------- Loop/Redesign/LoopDesign.swift | 10 +- Loop/Redesign/LoopHistoryMath.swift | 42 +- Loop/Redesign/LoopHistoryModels.swift | 7 +- Loop/Redesign/LoopHistoryProvider.swift | 165 ++-- Loop/Redesign/TrendsComponents.swift | 96 ++- Loop/Redesign/TrendsScreen.swift | 54 +- Loop/Views/SettingsView.swift | 14 +- LoopTests/Models/LoopHistoryMathTests.swift | 70 ++ 9 files changed, 927 insertions(+), 433 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 5cc44e5e62..f1fc5cfd4f 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -1300,6 +1300,149 @@ } } }, + "%1$@ of unknown kind\n+ %2$@ automatic" : { + "comment" : "Trends Boluses per day tile detail when some boluses don't say whether they were manual or automatic, such as another app's in Apple Health, on two lines: how many of the figure above are of unknown kind, then the automatic boluses on top. (1: boluses of unknown kind per day; 2: automatic boluses per day; both fractional, such as 4.2)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ من نوع غير معروف\n+ %2$@ تلقائية" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ neznámého druhu\n+ %2$@ automatických" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ af ukendt type\n+ %2$@ automatiske" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ unbekannter Art\n+ %2$@ automatisch" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ de tipo desconocido\n+ %2$@ automáticos" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ tuntematonta tyyppiä\n+ %2$@ automaattista" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ de type inconnu\n+ %2$@ automatiques" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ מסוג לא ידוע\n+ %2$@ אוטומטיים" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ismeretlen típusú\n+ %2$@ automatikus" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ di tipo sconosciuto\n+ %2$@ automatici" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "種類不明 %1$@\n+ 自動 %2$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ av ukjent type\n+ %2$@ automatiske" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ van onbekend type\n+ %2$@ automatisch" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ nieznanego rodzaju\n+ %2$@ automatycznych" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ de tipo desconhecido\n+ %2$@ automáticos" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ de tip necunoscut\n+ %2$@ automate" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ неизвестного типа\n+ %2$@ автоматических" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ neznámeho druhu\n+ %2$@ automatických" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ av okänd typ\n+ %2$@ automatiska" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ bilinmeyen türde\n+ %2$@ otomatik" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ невідомого типу\n+ %2$@ автоматичних" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ không rõ loại\n+ %2$@ tự động" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "类型未知 %1$@\n+ 自动 %2$@" + } + } + } + }, "%1$@ U/h" : { "comment" : "Format string for an insulin rate in units per hour. (1: rate)", "localizations" : { @@ -1443,6 +1586,149 @@ } } }, + "%1$@ unknown" : { + "comment" : "Basal and bolus: bolus whose record doesn't say whether it was manual or automatic, such as another app's in Apple Health, after the manual and automatic amounts. (1: units, e.g. 3.2 U)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ غير معروف" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ neznámý" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ukendt" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ unbekannt" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ desconocido" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ tuntematon" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ inconnu" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ לא ידוע" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ismeretlen" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ sconosciuto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "不明 %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ ukjent" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ onbekend" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ nieznany" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ desconhecido" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ necunoscut" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ неизвестно" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ neznámy" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ okänd" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ bilinmeyen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ невідомо" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ không rõ" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未知 %1$@" + } + } + } + }, "%1$@%%" : { "comment" : "A percentage. (1: number)", "localizations" : { @@ -3608,149 +3894,6 @@ } } }, - "+ %1$@ automatic, %2$@ unknown" : { - "comment" : "Trends Boluses per day tile detail when some boluses don't say whether they were manual or automatic, such as another app's in Apple Health. (1: automatic boluses per day; 2: boluses of unknown kind per day; both fractional, such as 4.2)", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ تلقائية، %2$@ غير معروفة" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatických, %2$@ neznámých" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatiske, %2$@ ukendte" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatisch, %2$@ unbekannt" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automáticos, %2$@ desconocidos" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automaattista, %2$@ tuntematonta" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatiques, %2$@ inconnus" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ אוטומטיים, %2$@ לא ידועים" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatikus, %2$@ ismeretlen" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatici, %2$@ sconosciuti" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ 自動 %1$@、不明 %2$@" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatiske, %2$@ ukjente" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatisch, %2$@ onbekend" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatycznych, %2$@ nieznanych" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automáticos, %2$@ desconhecidos" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automate, %2$@ necunoscute" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ автоматических, %2$@ неизвестных" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatických, %2$@ neznámych" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ automatiska, %2$@ okända" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ otomatik, %2$@ bilinmeyen" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ автоматичних, %2$@ невідомих" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ %1$@ tự động, %2$@ không rõ" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "+ 自动 %1$@,未知 %2$@" - } - } - } - }, "25–75%" : { "comment" : "Trends Daily pattern legend: band between the 25th and 75th percentiles of glucose", "localizations" : { @@ -5348,121 +5491,264 @@ "de" : { "stringUnit" : { "state" : "translated", - "value" : "Abgegebene Basalrate" + "value" : "Abgegebene Basalrate" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal administrada" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Annettu basaali" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal administré" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal שניתן" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Leadott basal" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basale erogata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "注入済みベーサル" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gitt basal" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toegediend basaal" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podana baza" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal entregue" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazală administrată" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поданный базал" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Podaný bazál" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Given basal" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verilen bazal" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Поданий базал" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Basal đã tiêm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已输注基础" + } + } + } + }, + "Best over 14 days with %1$@ CGM data" : { + "comment" : "Trends GMI tile caption when the period is shorter than 14 days or CGM readings cover less than 70% of it: the consensus for when GMI is representative. (1: share of CGM data, e.g. 70%)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الأفضل على مدى 14 يومًا مع %1$@ من بيانات CGM" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nejlépe za 14 dní s %1$@ dat CGM" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bedst over 14 dage med %1$@ CGM-data" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Am besten über 14 Tage mit %1$@ CGM-Daten" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Basal administrada" + "value" : "Mejor en 14 días con %1$@ de datos de CGM" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Annettu basaali" + "value" : "Paras 14 päivän ajalta, kun CGM-dataa on %1$@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Basal administré" + "value" : "Idéalement sur 14 jours avec %1$@ de données CGM" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "Basal שניתן" + "value" : "מומלץ על פני 14 ימים עם %1$@ נתוני CGM" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Leadott basal" + "value" : "Legjobb 14 nap alatt, %1$@ CGM-adattal" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Basale erogata" + "value" : "Ideale su 14 giorni con il %1$@ di dati CGM" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "注入済みベーサル" + "value" : "14日間・CGMデータ%1$@以上が目安" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Gitt basal" + "value" : "Best over 14 dager med %1$@ CGM-data" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Toegediend basaal" + "value" : "Het best over 14 dagen met %1$@ CGM-gegevens" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Podana baza" + "value" : "Najlepiej z 14 dni z %1$@ danych CGM" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Basal entregue" + "value" : "Ideal em 14 dias com %1$@ de dados de CGM" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Bazală administrată" + "value" : "Ideal pe 14 zile cu %1$@ date CGM" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Поданный базал" + "value" : "Лучше за 14 дней с %1$@ данных CGM" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Podaný bazál" + "value" : "Najlepšie za 14 dní s %1$@ údajov CGM" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Given basal" + "value" : "Bäst över 14 dagar med %1$@ CGM-data" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Verilen bazal" + "value" : "En iyisi %1$@ CGM verisiyle 14 gün" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Поданий базал" + "value" : "Найкраще за 14 днів із %1$@ даних CGM" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Basal đã tiêm" + "value" : "Tốt nhất trong 14 ngày với %1$@ dữ liệu CGM" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "已输注基础" + "value" : "最好基于 14 天、%1$@ 的 CGM 数据" } } } @@ -5611,7 +5897,7 @@ } }, "Boluses" : { - "comment" : "Trends summary tile for a one-day period: how many manual boluses were given that day", + "comment" : "Trends summary tile for a one-day period: how many boluses were given that day, other than Loop's automatic ones", "localizations" : { "ar" : { "stringUnit" : { @@ -5754,7 +6040,7 @@ } }, "Boluses per day" : { - "comment" : "Trends summary tile: average number of manual boluses per day", + "comment" : "Trends summary tile: average number of boluses per day, other than Loop's automatic ones", "localizations" : { "ar" : { "stringUnit" : { @@ -9191,149 +9477,6 @@ } } }, - "Needs 10 days of CGM" : { - "comment" : "Trends GMI tile detail when fewer than 10 days of CGM data are available", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "يحتاج إلى 10 أيام من CGM" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vyžaduje 10 dní CGM" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kræver 10 dages CGM" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Benötigt 10 Tage CGM" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requiere 10 días de CGM" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vaatii 10 päivää CGM-dataa" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nécessite 10 jours de CGM" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "נדרשים 10 ימי CGM" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "10 nap CGM-adat szükséges" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Servono 10 giorni di CGM" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "CGMデータが10日分必要" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Krever 10 dager med CGM" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vereist 10 dagen CGM" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wymaga 10 dni CGM" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Requer 10 dias de CGM" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Necesită 10 zile de CGM" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нужно 10 дней CGM" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Vyžaduje 10 dní CGM" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kräver 10 dagars CGM" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "10 günlük CGM gerekir" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Потрібно 10 днів CGM" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cần 10 ngày dữ liệu CGM" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "需要 10 天 CGM 数据" - } - } - } - }, "Next period" : { "comment" : "VoiceOver label for the Trends button that moves forward one period", "localizations" : { @@ -11336,6 +11479,149 @@ } } }, + "per day, weekly average · %1$@" : { + "comment" : "Trends Insulin per day card subtitle when each bar is a week's average daily total and only some days of the period are averaged. (1: days averaged, e.g. 4 of 90 days)", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "يوميًا، متوسط أسبوعي · %1$@" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "za den, týdenní průměr · %1$@" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "pr. dag, ugegennemsnit · %1$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "pro Tag, Wochendurchschnitt · %1$@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "por día, promedio semanal · %1$@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "päivässä, viikkokeskiarvo · %1$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "par jour, moyenne hebdomadaire · %1$@" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "ליום, ממוצע שבועי · %1$@" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "naponta, heti átlag · %1$@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "al giorno, media settimanale · %1$@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "1日あたり、週平均 · %1$@" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "per dag, ukesgjennomsnitt · %1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "per dag, weekgemiddelde · %1$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "dziennie, średnia tygodniowa · %1$@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "por dia, média semanal · %1$@" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "pe zi, medie săptămânală · %1$@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "в день, среднее за неделю · %1$@" + } + }, + "sk" : { + "stringUnit" : { + "state" : "translated", + "value" : "za deň, týždenný priemer · %1$@" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "per dag, veckomedel · %1$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "günlük, haftalık ortalama · %1$@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "на день, середнє за тиждень · %1$@" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "mỗi ngày, trung bình theo tuần · %1$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "每日,按周平均 · %1$@" + } + } + } + }, "Period" : { "comment" : "Trends: VoiceOver label of the picker that chooses how many days the screen covers", "localizations" : { diff --git a/Loop/Redesign/LoopDesign.swift b/Loop/Redesign/LoopDesign.swift index b76fb9e97d..da50af217b 100644 --- a/Loop/Redesign/LoopDesign.swift +++ b/Loop/Redesign/LoopDesign.swift @@ -20,9 +20,12 @@ enum LoopDesign { // Grounds static let surface = Color(uiColor: .systemGroupedBackground) static let surfaceRaised = Color(uiColor: .secondarySystemGroupedBackground) - static let surfaceSunken = Color(uiColor: .systemGray5) + /// A fill rather than a solid grey, so tiles still show on a card in a sheet, where dark mode's raised card is + /// the same grey as `systemGray5`. + static let surfaceSunken = Color(uiColor: .secondarySystemFill) static let separator = Color(uiColor: .opaqueSeparator) - static let grid = Color(uiColor: .systemGray5) + /// One step lighter than the card in dark mode, for the same reason. + static let grid = Color(uiColor: UIColor { $0.userInterfaceStyle == .dark ? .systemGray4 : .systemGray5 }) // Text static let ink = Color(uiColor: .label) @@ -34,7 +37,8 @@ enum LoopDesign { static let agpOuter = glucose.opacity(0.16) static let agpInner = glucose.opacity(0.34) static let insulin = Color("insulin") - /// Bolus is insulin's colour; basal is the same colour faded, with a stronger edge along its top. + /// Bolus is insulin's colour; basal is the same colour faded, with a stronger edge along its top. One hue can't + /// give 3:1 between them, so charts keep a gap of the card's colour wherever basal and bolus meet. static let bolus = insulin static let basal = insulin.opacity(0.25) static let basalEdge = insulin.opacity(0.6) diff --git a/Loop/Redesign/LoopHistoryMath.swift b/Loop/Redesign/LoopHistoryMath.swift index 60cf93662b..b570c8074e 100644 --- a/Loop/Redesign/LoopHistoryMath.swift +++ b/Loop/Redesign/LoopHistoryMath.swift @@ -39,6 +39,18 @@ enum LoopHistoryMath { return min(1, Double(points.count) / expected) } + /// Whether GMI is as representative as the consensus asks: a period of at least 14 calendar days (today counts, + /// so "14 d" does while it runs), with CGM data for at least 70% of it. + static func gmiIsRepresentative(periodDays: Int, cgmActive: Double) -> Bool { + periodDays >= 14 && cgmActive >= 0.7 + } + + /// Hours a day with CGM readings: what a band's share of readings is a share of. One day gives that day's hours + /// with readings, so far when it is today; a longer period gives the average over its days. + static func cgmHoursPerDay(cgmActive: Double, over interval: DateInterval, periodDays: Int) -> Double { + periodDays <= 1 ? cgmActive * interval.duration / 3600 : cgmActive * 24 + } + // MARK: Time in range enum RangeBand: Int, CaseIterable, Identifiable { @@ -148,6 +160,24 @@ enum LoopHistoryMath { return covered >= span.duration - 60 ? units : nil } + // MARK: Loop's stores and Apple Health + + /// Where one kind of data switches from Apple Health to Loop's own records: the later of the start of Loop's + /// local history and its first record of that kind. With no records of that kind, it is `now`, so all of it + /// comes from Health. Loop writes what it records to Health too, so reading Health only before this point and + /// Loop's stores only from it counts nothing twice. + static func healthBoundary(localHistoryStart: Date, firstLocalRecord: Date?, now: Date) -> Date { + max(localHistoryStart, firstLocalRecord ?? now) + } + + /// `window` cut at `boundary`: the part before it is read from Apple Health, the part from it on from Loop's + /// stores. Either is nil when the window has none of it; the two never overlap. + static func split(_ window: DateInterval, at boundary: Date) -> (health: DateInterval?, local: DateInterval?) { + let health = window.start < boundary ? DateInterval(start: window.start, end: min(window.end, boundary)) : nil + let local = window.end > boundary ? DateInterval(start: max(window.start, boundary), end: window.end) : nil + return (health, local) + } + // MARK: Days /// Midnight-to-midnight intervals covering `interval`, oldest first, the last one cut off at `interval.end`. @@ -316,6 +346,7 @@ enum LoopHistoryMath { /// run in those 15 minutes, and limited when it didn't. Before the first mode change the setting is unknown, so the /// result starts at the later of `coverageStart` and the first mode change. static func loopModeSpans(interval: DateInterval, runs: [LoopRun], modeChanges: [ModeChange], coverageStart: Date?, + manualTempBasals: [DateInterval] = [], recency: TimeInterval = 15 * 60, slot: TimeInterval = 5 * 60) -> [LoopModeSpan] { guard let coverageStart, let firstChange = modeChanges.map(\.date).min() else { return [] } let start = max(interval.start, coverageStart, firstChange) @@ -329,6 +360,8 @@ enum LoopHistoryMath { var lastSuccess: Date? = nil var changeIndex = 0 var closedSetting: Bool? = nil + let manualTemps = manualTempBasals.sorted { $0.start < $1.start } + var manualIndex = 0 var t = start while t < interval.end { @@ -346,8 +379,15 @@ enum LoopHistoryMath { changeIndex += 1 } + // A manual temp basal holds Loop open for as long as the pump runs it, whatever a later run failed on. + let middle = t.addingTimeInterval(slotEnd.timeIntervalSince(t) / 2) + while manualIndex < manualTemps.count && manualTemps[manualIndex].end <= middle { + manualIndex += 1 + } + let inManualTemp = manualIndex < manualTemps.count && manualTemps[manualIndex].start <= middle + let mode: LoopModeSpan.Mode - if closedSetting == false { + if closedSetting == false || inManualTemp { mode = .open } else if let lastRun, lastRun.manualTempBasal, slotEnd.timeIntervalSince(lastRun.date) <= recency { mode = .open diff --git a/Loop/Redesign/LoopHistoryModels.swift b/Loop/Redesign/LoopHistoryModels.swift index fc8e73b378..0d16a7036d 100644 --- a/Loop/Redesign/LoopHistoryModels.swift +++ b/Loop/Redesign/LoopHistoryModels.swift @@ -109,6 +109,9 @@ struct DayHistory { var carbs: [CarbEvent] = [] /// Where the data came from, so older days can say they are missing Loop's own detail. var source: HistorySource = .loop + /// Where Loop's own dose records take over from Apple Health: before it, insulin has no schedule, and only the + /// boluses Loop wrote say whether they were automatic. + var localInsulinStart = Date.distantPast var bolusUnits: Double { boluses.reduce(0) { $0 + $1.units } } var basalUnits: Double { basal.reduce(0) { $0 + $1.units } } @@ -298,7 +301,9 @@ extension BolusEvent { guard dose.type == .bolus, dose.startDate >= interval.start, dose.startDate < interval.end else { return nil } self.init(id: dose.syncIdentifier ?? "\(dose.startDate.timeIntervalSince1970)", date: dose.startDate, units: dose.unitsInDeliverableIncrements, automatic: dose.automatic ?? false, - manuallyEntered: dose.manuallyEntered, dose: dose) + manuallyEntered: dose.manuallyEntered, dose: dose, + // A dose imported without Loop's automatic flag, or not yet reconciled, is of unknown kind. + automaticKnown: dose.automatic != nil) } } diff --git a/Loop/Redesign/LoopHistoryProvider.swift b/Loop/Redesign/LoopHistoryProvider.swift index 4a15d13259..b5b31702e2 100644 --- a/Loop/Redesign/LoopHistoryProvider.swift +++ b/Loop/Redesign/LoopHistoryProvider.swift @@ -14,10 +14,11 @@ import os /// Reads glucose, insulin, carbs and loop runs for the redesigned screens. /// -/// Loop keeps `LOOP_LOCAL_CACHE_DURATION_DAYS` (90 by default) days in its own stores. Inside that window everything -/// comes from those stores, so manual and automatic doses, temp basals and the schedule are known. Older days come from -/// Apple Health, which has glucose, carbs and insulin split into basal and bolus, and whether Loop gave a bolus -/// automatically only for the doses Loop wrote, but not the basal schedule. +/// Loop keeps `LOOP_LOCAL_CACHE_DURATION_DAYS` (90 by default) days in its own stores. From the start of that window, +/// or from Loop's first record of a kind when that is later, everything comes from those stores, so manual and +/// automatic doses, temp basals and the schedule are known. Earlier days come from Apple Health, which has glucose, +/// carbs and insulin split into basal and bolus, and whether Loop gave a bolus automatically only for the doses Loop +/// wrote, but not the basal schedule. final class LoopHistoryProvider { private let deviceManager: DeviceDataManager @@ -30,7 +31,7 @@ final class LoopHistoryProvider { private let glucoseStore: GlucoseStore private let healthStore = HKHealthStore() private let log = OSLog(category: "LoopHistoryProvider") - private let recordsStartCache = OSAllocatedUnfairLock<(fetched: Date, value: Date)?>(initialState: nil) + private let recordStartsCache = OSAllocatedUnfairLock<(fetched: Date, starts: LocalRecordStarts)?>(initialState: nil) @MainActor init(deviceManager: DeviceDataManager, loopDataManager: LoopDataManager, settingsManager: SettingsManager, temporaryPresetsManager: TemporaryPresetsManager) { @@ -87,37 +88,85 @@ final class LoopHistoryProvider { // MARK: - Days - /// Glucose, insulin and carbs in `window`. + /// Glucose, insulin and carbs in `window`. Each kind comes from Apple Health before Loop's own records of it + /// begin, and from Loop's stores after, so a Loop younger than its cache still shows what Health has for the + /// days before it. func dayHistory(_ window: DateInterval) async throws -> DayHistory { var history = DayHistory(window: window) - let boundary = localHistoryStart - - if window.end > boundary { - let local = DateInterval(start: max(window.start, boundary), end: window.end) - async let glucose = localGlucose(local) - async let insulin = localInsulin(local) - async let carbs = localCarbs(local) - history.glucose = try await glucose - (history.boluses, history.basal, history.scheduledBasal) = try await insulin - history.carbs = try await carbs + let starts = try await localRecordStarts() + let now = Date(), localStart = localHistoryStart + let boundary = { LoopHistoryMath.healthBoundary(localHistoryStart: localStart, firstLocalRecord: $0, now: now) } + let glucoseBoundary = boundary(starts.glucose), insulinBoundary = boundary(starts.insulin), carbBoundary = boundary(starts.carbs) + let glucose = LoopHistoryMath.split(window, at: glucoseBoundary) + let insulin = LoopHistoryMath.split(window, at: insulinBoundary) + let carbs = LoopHistoryMath.split(window, at: carbBoundary) + + async let localGlucose = localGlucose(glucose.local) + async let localInsulin = localInsulin(insulin.local, recordsStart: starts.insulin) + async let localCarbs = localCarbs(carbs.local) + async let healthGlucose = healthGlucose(glucose.health) + async let healthInsulin = healthInsulin(insulin.health) + async let healthCarbs = healthCarbs(carbs.health) + + let (localBoluses, localBasal, scheduled) = try await localInsulin + let (healthBoluses, healthBasal) = try await healthInsulin + history.glucose = try await healthGlucose + localGlucose + history.boluses = healthBoluses + localBoluses + history.basal = healthBasal + localBasal + history.scheduledBasal = scheduled + history.carbs = try await healthCarbs + localCarbs + history.localInsulinStart = insulinBoundary + if window.end <= min(glucoseBoundary, insulinBoundary, carbBoundary) { + history.source = .health } + try Task.checkCancellation() + return history + } - if window.start < boundary { - let older = DateInterval(start: window.start, end: min(window.end, boundary)) - async let glucose = healthGlucose(older) - async let insulin = healthInsulin(older) - async let carbs = healthCarbs(older) - let (boluses, basal) = try await insulin - history.glucose = try await glucose + history.glucose - history.boluses = boluses + history.boluses - history.basal = basal + history.basal - history.carbs = try await carbs + history.carbs - if window.end <= boundary { - history.source = .health + /// Where Loop's own records of each kind begin, inside its local history; nil for a kind it has none of. + struct LocalRecordStarts: Equatable { + var glucose: Date? + var insulin: Date? + var carbs: Date? + } + + /// A found start is kept for a few minutes, so each period doesn't search again. A kind with no records yet is + /// searched every time, because its first record can arrive at any moment. + func localRecordStarts() async throws -> LocalRecordStarts { + let now = Date(), start = localHistoryStart + let cached = recordStartsCache.withLock { $0 }.flatMap { now.timeIntervalSince($0.fetched) < 300 ? $0 : nil } + var starts = cached?.starts ?? LocalRecordStarts() + let glucoseStore = self.glucoseStore, doseStore = self.doseStore + if starts.glucose == nil { + starts.glucose = try await Self.firstRecord(from: start, to: now) { chunk in + try await glucoseStore.getGlucoseSamples(start: chunk.start, end: chunk.end).filter { !$0.isDisplayOnly }.map(\.startDate) } } - try Task.checkCancellation() - return history + if starts.insulin == nil { + starts.insulin = try await Self.firstRecord(from: start, to: now) { chunk in + try await doseStore.getNormalizedDoseEntries(start: chunk.start, end: chunk.end).map(\.startDate) + } + } + if starts.carbs == nil { + starts.carbs = try await carbStore.getCarbEntries(start: start, end: now, dateAscending: true, fetchLimit: 1).first?.startDate + } + let found = starts + recordStartsCache.withLock { $0 = (fetched: cached?.fetched ?? now, starts: found) } + return found + } + + /// The earliest date `dates` gives from `start` to `end`, read a week at a time so that finding it doesn't load + /// all of the local history. + private static func firstRecord(from start: Date, to end: Date, _ dates: (DateInterval) async throws -> [Date]) async throws -> Date? { + var chunkStart = start + while chunkStart < end { + let chunk = DateInterval(start: chunkStart, end: min(end, chunkStart.addingTimeInterval(.days(7)))) + if let first = try await dates(chunk).min() { + return first + } + chunkStart = chunk.end + } + return nil } // MARK: - Loop runs @@ -187,9 +236,18 @@ final class LoopHistoryProvider { return changes } + /// When manual temp basals ran in `interval`, from Loop's dose records, ended where the pump ended or cancelled + /// them. Loop shows the loop as open while one runs. + func manualTempBasals(in interval: DateInterval) async throws -> [DateInterval] { + try await doseStore.getNormalizedDoseEntries(start: interval.start, end: interval.end) + .filter { $0.type == .tempBasal && $0.automatic == false && $0.endDate > $0.startDate } + .map { DateInterval(start: $0.startDate, end: $0.endDate) } + } + // MARK: - Loop's stores - private func localGlucose(_ interval: DateInterval) async throws -> [GlucosePoint] { + private func localGlucose(_ interval: DateInterval?) async throws -> [GlucosePoint] { + guard let interval else { return [] } do { let samples = try await glucoseStore.getGlucoseSamples(start: interval.start, end: interval.end) return samples @@ -201,34 +259,13 @@ final class LoopHistoryProvider { } } - /// Doses for `interval`, with the start of Loop's insulin records (its earliest local dose). A found start is kept - /// for a few minutes so day pages and periods don't each read the whole local history; when it isn't known, - /// one read from the start of the local history serves both. No dose before `interval.end` says nothing about - /// later days, so that isn't kept. - private func doses(for interval: DateInterval) async throws -> (doses: [DoseEntry], recordsStart: Date?) { - let localStart = localHistoryStart - if interval.start <= localStart { - let doses = try await doseStore.getNormalizedDoseEntries(start: interval.start, end: interval.end) - return (doses, doses.map(\.startDate).min()) - } - if let cached = recordsStartCache.withLock({ $0 }), Date().timeIntervalSince(cached.fetched) < 300 { - return (try await doseStore.getNormalizedDoseEntries(start: interval.start, end: interval.end), cached.value) - } - let local = try await doseStore.getNormalizedDoseEntries(start: localStart, end: interval.end) - let recordsStart = local.map(\.startDate).min() - if let recordsStart { - recordsStartCache.withLock { $0 = (fetched: Date(), value: recordsStart) } - } - return (local.filter { - $0.startDate < interval.end && ($0.type == .bolus ? $0.startDate >= interval.start : $0.endDate > interval.start) - }, recordsStart) - } - - private func localInsulin(_ interval: DateInterval) async throws -> ([BolusEvent], [BasalSegment], [ScheduledBasalPoint]) { + /// Doses, basal and the schedule for `interval`. `recordsStart` is Loop's earliest local dose: after it, + /// unrecorded time runs at the pump's programmed rate. + private func localInsulin(_ interval: DateInterval?, recordsStart: Date?) async throws -> ([BolusEvent], [BasalSegment], [ScheduledBasalPoint]) { + guard let interval else { return ([], [], []) } let doses: [DoseEntry] - let recordsStart: Date? do { - (doses, recordsStart) = try await self.doses(for: interval) + doses = try await doseStore.getNormalizedDoseEntries(start: interval.start, end: interval.end) } catch { os_log("Dose fetch failed: %{public}@", log: log, type: .error, String(describing: error)) throw error @@ -257,7 +294,8 @@ final class LoopHistoryProvider { return (boluses, basal, scheduled) } - private func localCarbs(_ interval: DateInterval) async throws -> [CarbEvent] { + private func localCarbs(_ interval: DateInterval?) async throws -> [CarbEvent] { + guard let interval else { return [] } do { let entries = try await carbStore.getCarbEntries(start: interval.start, end: interval.end) return entries.map { entry in @@ -277,8 +315,8 @@ final class LoopHistoryProvider { // MARK: - Apple Health - private func healthSamples(_ type: HKQuantityType, _ interval: DateInterval, options: HKQueryOptions = .strictStartDate) async throws -> [HKQuantitySample] { - guard interval.duration > 0 else { return [] } + private func healthSamples(_ type: HKQuantityType, _ interval: DateInterval?, options: HKQueryOptions = .strictStartDate) async throws -> [HKQuantitySample] { + guard let interval, interval.duration > 0 else { return [] } let log = self.log return try await withCheckedThrowingContinuation { continuation in let predicate = HKQuery.predicateForSamples(withStart: interval.start, end: interval.end, options: options) @@ -296,7 +334,7 @@ final class LoopHistoryProvider { } /// Several apps can write the same CGM reading to Health; keep one reading per 2½ minutes. - private func healthGlucose(_ interval: DateInterval) async throws -> [GlucosePoint] { + private func healthGlucose(_ interval: DateInterval?) async throws -> [GlucosePoint] { let samples = try await healthSamples(HKQuantityType(.bloodGlucose), interval) var points: [GlucosePoint] = [] let own = HKSource.default().bundleIdentifier @@ -309,7 +347,8 @@ final class LoopHistoryProvider { return points } - private func healthInsulin(_ interval: DateInterval) async throws -> ([BolusEvent], [BasalSegment]) { + private func healthInsulin(_ interval: DateInterval?) async throws -> ([BolusEvent], [BasalSegment]) { + guard let interval else { return ([], []) } let samples = try await healthSamples(HKQuantityType(.insulinDelivery), interval, options: []) var boluses: [BolusEvent] = [] var basal: [BasalSegment] = [] @@ -331,7 +370,7 @@ final class LoopHistoryProvider { return (boluses, basal) } - private func healthCarbs(_ interval: DateInterval) async throws -> [CarbEvent] { + private func healthCarbs(_ interval: DateInterval?) async throws -> [CarbEvent] { let samples = try await healthSamples(HKQuantityType(.dietaryCarbohydrates), interval) let own = HKSource.default().bundleIdentifier return samples diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift index 1d5b08c6a6..a7a7385c28 100644 --- a/Loop/Redesign/TrendsComponents.swift +++ b/Loop/Redesign/TrendsComponents.swift @@ -93,6 +93,8 @@ struct StatTile: Identifiable { var valueColor: Color = LoopDesign.Palette.ink /// The consensus target for the figure, e.g. "aim < 7%". var aim: String? = nil + /// When the figure is less representative than usual, e.g. GMI over a short period. + var caveat: String? = nil var id: String { label } } @@ -147,6 +149,12 @@ struct StatGridView: View { .font(LoopDesign.Typeface.footnote) .foregroundStyle(P.inkMuted) } + if let caveat = tile.caveat { + Text(caveat) + .font(LoopDesign.Typeface.caption) + .foregroundStyle(P.inkMuted) + .fixedSize(horizontal: false, vertical: true) + } } .padding(LoopDesign.Space.s3) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) @@ -210,6 +218,7 @@ enum ConsensusTarget { static func under(_ percent: Double) -> String { "< " + percentText(percent) } static func over(_ percent: Double) -> String { "> " + percentText(percent) } static func atMost(_ percent: Double) -> String { "≤ " + percentText(percent) } + static func percent(_ percent: Double) -> String { percentText(percent) } /// "aim < 7%", under a figure. static func aim(_ target: String) -> String { @@ -220,8 +229,10 @@ enum ConsensusTarget { struct TimeInRangeView: View { @Environment(\.dynamicTypeSize) private var typeSize - /// Fractions per band, 0–1. + /// Fractions per band, 0–1, of the time with CGM readings. let fractions: [LoopHistoryMath.RangeBand: Double] + /// Hours a day with CGM readings, so a band's hours leave out time without them. + let hoursPerDay: Double let unit: GlucoseUnitStyle private var percentages: [LoopHistoryMath.RangeBand: Int] { LoopHistoryMath.roundedPercentages(fractions) } @@ -240,7 +251,7 @@ struct TimeInRangeView: View { HStack(alignment: .top, spacing: LoopDesign.Space.s4) { bar .frame(width: 14) - VStack(spacing: LoopDesign.Space.s2) { + VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { ForEach(LoopHistoryMath.RangeBand.allCases) { band in row(band) } @@ -273,7 +284,7 @@ struct TimeInRangeView: View { private func row(_ band: LoopHistoryMath.RangeBand) -> some View { let fraction = fractions[band] ?? 0 - let hours = fraction * 24 + let hours = fraction * hoursPerDay let hoursText = String(format: NSLocalizedString("%1$@ h", comment: "Time in range row: average hours a day spent in the band, abbreviated. (1: hours with one decimal)"), LoopFormat.number(hours, decimals: 1)) let percent = Text(percentText(Double(percentages[band] ?? 0))) @@ -295,6 +306,7 @@ struct TimeInRangeView: View { .foregroundStyle(P.inkMuted) .fixedSize(horizontal: false, vertical: true) } + .frame(maxWidth: .infinity, alignment: .leading) } else { HStack(alignment: .firstTextBaseline, spacing: LoopDesign.Space.s2) { percent.frame(minWidth: 52, alignment: .trailing) @@ -496,10 +508,6 @@ struct InsulinSplitView: View { var scheduledBasal: Double? /// How many of the days the schedule covers, e.g. "20 of 90 days", when that is fewer than the split's. var scheduledNote: String? - /// Manual and automatic bolus over only the days that say which each bolus was, when some days don't. - var classifiedSplit: InsulinSplit? - /// How many days `classifiedSplit` covers, e.g. "60 of 90 days". - var classifiedNote: String? /// Today against a usual day, e.g. "Usually 28.4 U by 19:46", under the total. var usual: String? /// The split as one capsule, where no chart above already draws it. @@ -515,19 +523,24 @@ struct InsulinSplitView: View { } VStack(alignment: .leading, spacing: LoopDesign.Space.s2) { row(.basal, NSLocalizedString("Basal", comment: "Basal and bolus row: insulin delivered as basal"), split.basal, split.basalPercent) + // No swatch: nothing on the chart draws the schedule. if let scheduledBasal { - part(.dash(LoopDesign.Palette.basalScheduled), Self.scheduledText(scheduledBasal, note: scheduledNote)) + Text(Self.scheduledText(scheduledBasal, note: scheduledNote)) .font(LoopDesign.Typeface.footnote.monospacedDigit()) .foregroundStyle(LoopDesign.Palette.inkMuted) .padding(.leading, 10 + LoopDesign.Space.s2) } row(.solid(LoopDesign.Palette.bolus), NSLocalizedString("Bolus", comment: "Basal and bolus row: insulin delivered as bolus, manual and automatic"), split.bolus, split.bolusPercent) - if let detail = bolusDetail, split.bolus > 0 { + // Manual, automatic and unknown add up to the bolus row. Unknown has no swatch of its own: it is drawn + // as plain bolus, like manual. + if split.bolus >= 0.005 { FlowLayout(spacing: LoopDesign.Space.s3, lineSpacing: 2) { - part(.solid(LoopDesign.Palette.bolus), String(format: NSLocalizedString("Manual %1$@", comment: "Basal and bolus: bolus the person gave. (1: units, e.g. 16.2 U)"), LoopFormat.totalUnits(detail.manualBolus))) - part(.hatch(LoopDesign.Palette.bolus), String(format: NSLocalizedString("Automatic %1$@", comment: "Basal and bolus: bolus Loop gave automatically. (1: units, e.g. 5.4 U)"), LoopFormat.totalUnits(detail.automaticBolus))) - if let classifiedNote { - Text(verbatim: "· \(classifiedNote)") + if showsBolusDetail { + part(.solid(LoopDesign.Palette.bolus), String(format: NSLocalizedString("Manual %1$@", comment: "Basal and bolus: bolus the person gave. (1: units, e.g. 16.2 U)"), LoopFormat.totalUnits(split.manualBolus))) + part(.hatch(LoopDesign.Palette.bolus), String(format: NSLocalizedString("Automatic %1$@", comment: "Basal and bolus: bolus Loop gave automatically. (1: units, e.g. 5.4 U)"), LoopFormat.totalUnits(split.automaticBolus))) + } + if !split.isClassified { + Text(Self.unknownText(split.unclassifiedBolus)) } } .font(LoopDesign.Typeface.footnote.monospacedDigit()) @@ -559,12 +572,17 @@ struct InsulinSplitView: View { } .accessibilityElement(children: .ignore) .accessibilityLabel(Self.accessibilityLabel(split, partial: partial, scheduledBasal: scheduledBasal.map { Self.scheduledText($0, note: scheduledNote) }, - bolusDetail: bolusDetail, bolusDetailNote: classifiedNote, usual: usual)) + showsBolusDetail: showsBolusDetail, usual: usual)) + } + + /// Manual and automatic, when any bolus says which it was; a split that is all unknown shows only that. + private var showsBolusDetail: Bool { + split.manualBolus + split.automaticBolus >= 0.005 } - /// The whole split's manual and automatic bolus, or only the days that record it; nil when nothing does. - private var bolusDetail: InsulinSplit? { - classifiedSplit ?? (split.isClassified ? split : nil) + /// "3.2 U unknown": bolus whose record doesn't say whether it was manual or automatic. + private static func unknownText(_ units: Double) -> String { + String(format: NSLocalizedString("%1$@ unknown", comment: "Basal and bolus: bolus whose record doesn't say whether it was manual or automatic, such as another app's in Apple Health, after the manual and automatic amounts. (1: units, e.g. 3.2 U)"), LoopFormat.totalUnits(units)) } /// "Scheduled 18.0 U", or "Scheduled 18.0 U · 20 of 90 days" when the schedule is known for only some days. @@ -604,16 +622,18 @@ struct InsulinSplitView: View { } } - static func accessibilityLabel(_ split: InsulinSplit, partial: Bool, scheduledBasal: String? = nil, bolusDetail: InsulinSplit? = nil, - bolusDetailNote: String? = nil, usual: String? = nil) -> String { + static func accessibilityLabel(_ split: InsulinSplit, partial: Bool, scheduledBasal: String? = nil, showsBolusDetail: Bool = false, + usual: String? = nil) -> String { var parts = [ String(format: NSLocalizedString("Basal %1$@, %2$@ percent", comment: "VoiceOver: basal units and share. (1: units, 2: percentage number)"), LoopFormat.totalUnits(split.basal), "\(split.basalPercent)"), String(format: NSLocalizedString("Bolus %1$@, %2$@ percent", comment: "VoiceOver: bolus units and share. (1: units, 2: percentage number)"), LoopFormat.totalUnits(split.bolus), "\(split.bolusPercent)"), ] - if let detail = bolusDetail, split.bolus > 0 { + if showsBolusDetail { let text = String(format: NSLocalizedString("%1$@ manual, %2$@ automatic", comment: "VoiceOver: how the bolus divides. (1: manual units, 2: automatic units)"), - LoopFormat.totalUnits(detail.manualBolus), LoopFormat.totalUnits(detail.automaticBolus)) - parts.append(bolusDetailNote.map { "\(text), \($0)" } ?? text) + LoopFormat.totalUnits(split.manualBolus), LoopFormat.totalUnits(split.automaticBolus)) + parts.append(split.isClassified ? text : "\(text), \(unknownText(split.unclassifiedBolus))") + } else if !split.isClassified { + parts.append(unknownText(split.unclassifiedBolus)) } parts.append(String(format: partial ? NSLocalizedString("Total so far %1$@", comment: "VoiceOver: today's total insulin so far. (1: units)") @@ -645,6 +665,27 @@ struct InsulinDailyTotalsView: View { return max(10, Self.niceStep(top / 4) * 5) } + struct BolusPart: Equatable { + let start: Double + let end: Double + let automatic: Bool + } + + /// Where a day's bolus parts sit above its basal: manual (with bolus of unknown kind), then automatic. Each + /// starts a gap of the card's colour above the part below it, so no two parts touch. A part taller than two gaps + /// gives the gap from its own height; a shorter one moves up by a gap instead, so it stays visible. + static func bolusParts(_ day: LoopHistoryMath.DayTotal, gap: Double) -> [BolusPart] { + var parts: [BolusPart] = [] + var below = day.basal + for (units, automatic) in [(day.split.manualBolus + day.split.unclassifiedBolus, false), (day.split.automaticBolus, true)] where units > 0 { + let start = below + gap + let end = units > gap * 2 ? below + units : start + units + parts.append(BolusPart(start: start, end: end, automatic: automatic)) + below = end + } + return parts + } + /// 1, 2 or 5 times a power of ten, at or above `raw`. static func niceStep(_ raw: Double) -> Double { guard raw > 0 else { return 1 } @@ -671,14 +712,9 @@ struct InsulinDailyTotalsView: View { .foregroundStyle(P.basal) BarMark(x: .value("Day", day.day, unit: unitWidth), yStart: .value("Basal", max(0, day.basal - yMax * 0.006)), yEnd: .value("Edge", day.basal), width: .ratio(0.7)) .foregroundStyle(P.basalEdge) - // A thin gap under each bolus part, so it never touches the part below it - let gap = yMax * 0.01 - let manualTop = day.basal + day.split.manualBolus + day.split.unclassifiedBolus - BarMark(x: .value("Day", day.day, unit: unitWidth), yStart: .value("Basal", day.basal + (manualTop - day.basal > gap * 2 ? gap : 0)), yEnd: .value("Bolus", manualTop), width: .ratio(0.7)) - .foregroundStyle(P.bolus) - if day.split.automaticBolus > 0 { - BarMark(x: .value("Day", day.day, unit: unitWidth), yStart: .value("Bolus", manualTop + (day.total - manualTop > gap * 2 ? gap : 0)), yEnd: .value("Total", day.total), width: .ratio(0.7)) - .foregroundStyle(Hatch.paint(P.bolus, scheme: scheme)) + ForEach(Self.bolusParts(day, gap: yMax * 0.015), id: \.automatic) { part in + BarMark(x: .value("Day", day.day, unit: unitWidth), yStart: .value("Bolus start", part.start), yEnd: .value("Bolus end", part.end), width: .ratio(0.7)) + .foregroundStyle(part.automatic ? AnyShapeStyle(Hatch.paint(P.bolus, scheme: scheme)) : AnyShapeStyle(P.bolus)) } } // The average's value is the Total row under the chart. diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift index 995f894df2..8eec875119 100644 --- a/Loop/Redesign/TrendsScreen.swift +++ b/Loop/Redesign/TrendsScreen.swift @@ -23,6 +23,8 @@ final class TrendsModel: ObservableObject { var cgmCoveredDays: Int? var stats: LoopHistoryMath.GlucoseStats? var cgmActive: Double = 0 + /// Hours a day with CGM readings, which the time in range shares are of. + var cgmHoursPerDay: Double = 0 var timeInRange: [LoopHistoryMath.RangeBand: Double] = [:] var agp: [LoopHistoryMath.AGPSlot] = [] var agpDays = 0 @@ -44,9 +46,8 @@ final class TrendsModel: ObservableObject { var scheduledBasal: Double? /// The averaged days `scheduledBasal` covers: Apple Health doesn't record the schedule for older days. var scheduledDays = 0 - /// Manual and automatic bolus per day over the averaged days that say which each bolus was, when some don't. - var classifiedSplit: InsulinSplit? - var classifiedDays = 0 + /// Some of the insulin comes from Apple Health, before Loop's own dose records begin. + var readsHealthInsulin = false /// The median insulin by this time of day over the last 14 days, when the period ends now. var typicalByNow: (typical: TypicalInsulinDelivery, at: Date)? @@ -105,6 +106,7 @@ final class TrendsModel: ObservableObject { async let historyFetch = provider.dayHistory(fetchWindow) async let runsFetch = provider.loopRuns() async let modeFetch = provider.loopModeChanges(in: interval) + async let manualTempFetch = provider.manualTempBasals(in: interval) var history = try await historyFetch let (runs, coverageStart) = try await runsFetch if oneDay { @@ -117,6 +119,8 @@ final class TrendsModel: ObservableObject { summary.glucose = cgm summary.stats = LoopHistoryMath.glucoseStats(cgm) summary.cgmActive = LoopHistoryMath.cgmActiveFraction(cgm, over: interval) + summary.cgmHoursPerDay = LoopHistoryMath.cgmHoursPerDay(cgmActive: summary.cgmActive, over: interval, + periodDays: LoopHistoryMath.days(in: interval).count) summary.timeInRange = LoopHistoryMath.timeInRange(cgm) summary.agp = LoopHistoryMath.agp(cgm) summary.agpDays = LoopHistoryMath.daysWithReadings(cgm) @@ -157,17 +161,13 @@ final class TrendsModel: ObservableObject { summary.scheduledBasal = scheduled.reduce(0, +) / Double(scheduled.count) summary.scheduledDays = scheduled.count } - let classified = insulinDays.filter(\.split.isClassified) - if !classified.isEmpty, classified.count < insulinDays.count { - summary.classifiedSplit = InsulinSplit.average(classified.map(\.split)) - summary.classifiedDays = classified.count - } + summary.readsHealthInsulin = interval.start < history.localInsulinStart if interval.end.timeIntervalSinceNow > -300 { summary.typicalByNow = await provider.typicalInsulinByNow() } summary.loopSpans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: try await modeFetch, - coverageStart: coverageStart) + coverageStart: coverageStart, manualTempBasals: try await manualTempFetch) return summary } @@ -183,13 +183,14 @@ final class TrendsModel: ObservableObject { detail: String(format: NSLocalizedString("SD %1$@ · CV %2$@%%", comment: "Trends Average tile detail: standard deviation and coefficient of variation of glucose. (1: standard deviation in the glucose unit, 2: coefficient of variation as a percentage number)"), unit.format(stats.sdMgdl), LoopFormat.number(stats.cv * 100, decimals: 0)), aim: averageAim(unit))) - let enough = s.cgmActive * s.interval.duration / 86400 >= 10 + // Always shown, as Statistics did. The consensus says how representative it is: 14 days, 70% CGM data. tiles.append(StatTile(label: gmiLabel, value: LoopFormat.number(stats.gmiPercent, decimals: 1), unit: "%", - detail: enough - ? String(format: NSLocalizedString("%1$@ mmol/mol", comment: "Trends GMI tile detail: the GMI in IFCC units. (1: value)"), - LoopFormat.number(stats.gmiMmolMol, decimals: 0)) - : NSLocalizedString("Needs 10 days of CGM", comment: "Trends GMI tile detail when fewer than 10 days of CGM data are available"), - aim: ConsensusTarget.aim(ConsensusTarget.under(7)))) + detail: String(format: NSLocalizedString("%1$@ mmol/mol", comment: "Trends GMI tile detail: the GMI in IFCC units. (1: value)"), + LoopFormat.number(stats.gmiMmolMol, decimals: 0)), + aim: ConsensusTarget.aim(ConsensusTarget.under(7)), + caveat: LoopHistoryMath.gmiIsRepresentative(periodDays: s.totals.count, cgmActive: s.cgmActive) ? nil + : String(format: NSLocalizedString("Best over 14 days with %1$@ CGM data", comment: "Trends GMI tile caption when the period is shorter than 14 days or CGM readings cover less than 70% of it: the consensus for when GMI is representative. (1: share of CGM data, e.g. 70%)"), + ConsensusTarget.percent(70)))) } else { tiles.append(StatTile(label: averageLabel, value: "–", unit: unit.unitName, detail: NSLocalizedString("No CGM data", comment: "Trends Average tile detail when the period has no glucose readings"))) @@ -208,13 +209,14 @@ final class TrendsModel: ObservableObject { : String(format: NSLocalizedString("%1$@ entries", comment: "Trends Carbs per day tile detail: average number of carb entries per day, a fractional value such as 2.5, so not pluralised. (1: number with one decimal)"), LoopFormat.number(Double(s.carbEntries) / s.days, decimals: 1)))) tiles.append(StatTile(label: oneDay - ? NSLocalizedString("Boluses", comment: "Trends summary tile for a one-day period: how many manual boluses were given that day") - : NSLocalizedString("Boluses per day", comment: "Trends summary tile: average number of manual boluses per day"), - value: LoopFormat.number(Double(s.manualBoluses) / s.days, decimals: countDecimals), unit: nil, + ? NSLocalizedString("Boluses", comment: "Trends summary tile for a one-day period: how many boluses were given that day, other than Loop's automatic ones") + : NSLocalizedString("Boluses per day", comment: "Trends summary tile: average number of boluses per day, other than Loop's automatic ones"), + // Every bolus not known to be automatic, so one of unknown kind still counts. + value: LoopFormat.number(Double(s.manualBoluses + s.unknownBoluses) / s.days, decimals: countDecimals), unit: nil, detail: s.unknownBoluses > 0 - ? String(format: NSLocalizedString("+ %1$@ automatic, %2$@ unknown", comment: "Trends Boluses per day tile detail when some boluses don't say whether they were manual or automatic, such as another app's in Apple Health. (1: automatic boluses per day; 2: boluses of unknown kind per day; both fractional, such as 4.2)"), - LoopFormat.number(Double(s.automaticBoluses) / s.days, decimals: countDecimals), - LoopFormat.number(Double(s.unknownBoluses) / s.days, decimals: countDecimals)) + ? String(format: NSLocalizedString("%1$@ of unknown kind\n+ %2$@ automatic", comment: "Trends Boluses per day tile detail when some boluses don't say whether they were manual or automatic, such as another app's in Apple Health, on two lines: how many of the figure above are of unknown kind, then the automatic boluses on top. (1: boluses of unknown kind per day; 2: automatic boluses per day; both fractional, such as 4.2)"), + LoopFormat.number(Double(s.unknownBoluses) / s.days, decimals: countDecimals), + LoopFormat.number(Double(s.automaticBoluses) / s.days, decimals: countDecimals)) : String(format: NSLocalizedString("+ %1$@ automatic", comment: "Trends Boluses per day tile detail: average number of automatic boluses per day on top of the manual ones, a fractional value such as 4.2. (1: number with one decimal)"), LoopFormat.number(Double(s.automaticBoluses) / s.days, decimals: countDecimals)))) return tiles @@ -296,7 +298,7 @@ struct TrendsView: View { if s.glucose.isEmpty { ChartPlaceholder(message: NSLocalizedString("No CGM data in this period", comment: "Trends chart placeholder when the period has no glucose readings")) } else { - TimeInRangeView(fractions: s.timeInRange, unit: s.unit) + TimeInRangeView(fractions: s.timeInRange, hoursPerDay: s.cgmHoursPerDay, unit: s.unit) } } if s.day == nil { @@ -333,7 +335,9 @@ struct TrendsView: View { } } else { CardHeader(title: NSLocalizedString("Insulin per day", comment: "Trends tile and card title: average total insulin per day")) { - if weekly { + if weekly, let note = model.daysNote(s) { + Text(String(format: NSLocalizedString("per day, weekly average · %1$@", comment: "Trends Insulin per day card subtitle when each bar is a week's average daily total and only some days of the period are averaged. (1: days averaged, e.g. 4 of 90 days)"), note)) + } else if weekly { Text(NSLocalizedString("per day, weekly average", comment: "Trends Insulin per day card subtitle when each bar is a week's average daily total")) } else if let note = model.daysNote(s) { Text(String(format: NSLocalizedString("average of %1$@", comment: "Trends Insulin per day card subtitle when only some days of the period are averaged, such as a day still in progress. (1: days averaged, e.g. 13 of 14 days)"), note)) @@ -350,15 +354,13 @@ struct TrendsView: View { let averaged = LoopHistoryMath.averagingDays(s.totals).count InsulinSplitView(split: s.split, partial: s.splitIsPartial, scheduledBasal: s.scheduledBasal, scheduledNote: s.scheduledDays < averaged ? daysOf(s.scheduledDays, averaged) : nil, - classifiedSplit: s.classifiedSplit, - classifiedNote: s.classifiedSplit != nil ? daysOf(s.classifiedDays, averaged) : nil, usual: s.day != nil && s.splitIsPartial ? usualByNow(s) : nil, showsBar: s.day != nil, averageLine: s.day == nil) if s.day == nil, let today = todaySoFar(s) { Footnote(today) } // Days read from Apple Health have no schedule, and other apps' boluses there don't say which they were. - if s.interval.start < model.provider.localHistoryStart, s.scheduledBasal == nil || (s.split.bolus > 0 && !s.split.isClassified && s.classifiedSplit == nil) { + if s.readsHealthInsulin, s.scheduledBasal == nil || !s.split.isClassified { Footnote(NSLocalizedString("Older days come from Apple Health, which doesn't record the basal schedule, or whether another app's bolus was manual or automatic.", comment: "Trends Insulin per day footnote when the period reaches back past Loop's own records, so scheduled basal or the manual and automatic split can't be shown")) } } else { diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 656a2099b2..7dc0d4ef21 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -350,6 +350,8 @@ extension SettingsView { if viewModel.historyProvider != nil { LargeButton( action: { sheet = .trends }, + // Opens a sheet, but sits among navigation rows, so it shows their chevron. + arrowLikeNavigationLink: true, imageView: Image(systemName: "chart.bar.doc.horizontal") .resizable() .aspectRatio(contentMode: .fit) @@ -790,6 +792,7 @@ fileprivate struct LargeButton: View { let action: () -> Void var includeArrow: Bool + var arrowLikeNavigationLink: Bool let imageView: Content let secondaryImageView: SecondaryContent let label: String @@ -798,6 +801,7 @@ fileprivate struct LargeButton: View { init( action: @escaping () -> Void, includeArrow: Bool = true, + arrowLikeNavigationLink: Bool = false, imageView: Content, secondaryImageView: SecondaryContent = EmptyView(), label: String, @@ -805,6 +809,7 @@ fileprivate struct LargeButton: View { ) { self.action = action self.includeArrow = includeArrow + self.arrowLikeNavigationLink = arrowLikeNavigationLink self.imageView = imageView self.secondaryImageView = secondaryImageView self.label = label @@ -841,7 +846,14 @@ fileprivate struct LargeButton: View { if includeArrow { // TODO: Ick. I can't use a NavigationLink because we're not Navigating, but this seems worse somehow. - Image(systemName: "chevron.right").foregroundColor(.gray).font(.footnote) + if arrowLikeNavigationLink { + Image(systemName: "chevron.forward") + .font(.subheadline.weight(.medium)) + .foregroundStyle(Color(uiColor: .tertiaryLabel)) + .padding(.trailing, 1) + } else { + Image(systemName: "chevron.right").foregroundColor(.gray).font(.footnote) + } } } .padding(EdgeInsets(top: topBottomPadding, leading: 0, bottom: topBottomPadding, trailing: 0)) diff --git a/LoopTests/Models/LoopHistoryMathTests.swift b/LoopTests/Models/LoopHistoryMathTests.swift index 68a322dc5b..4dead9fcc5 100644 --- a/LoopTests/Models/LoopHistoryMathTests.swift +++ b/LoopTests/Models/LoopHistoryMathTests.swift @@ -133,6 +133,62 @@ class LoopHistoryMathTests: XCTestCase { XCTAssertNil(LoopHistoryMath.cgmCoveredDays([], in: interval, periodDays: 14)) } + func testGMIIsRepresentativeFromFourteenDaysAtSeventyPercent() { + // "14 d" ending today still counts as 14 days, and exactly 70% passes. + XCTAssertTrue(LoopHistoryMath.gmiIsRepresentative(periodDays: 14, cgmActive: 0.73)) + XCTAssertTrue(LoopHistoryMath.gmiIsRepresentative(periodDays: 14, cgmActive: 0.7)) + XCTAssertTrue(LoopHistoryMath.gmiIsRepresentative(periodDays: 90, cgmActive: 0.95)) + // Each condition on its own is enough to add the caption. + XCTAssertFalse(LoopHistoryMath.gmiIsRepresentative(periodDays: 14, cgmActive: 0.69)) + XCTAssertFalse(LoopHistoryMath.gmiIsRepresentative(periodDays: 7, cgmActive: 1)) + XCTAssertFalse(LoopHistoryMath.gmiIsRepresentative(periodDays: 1, cgmActive: 1)) + } + + func testBandHoursCountOnlyTimeWithReadings() { + // Today until 15:00 with readings 15% of the time: 2.25 h with readings, not 24. + let today = DateInterval(start: at(0), end: at(15 * 60)) + XCTAssertEqual(LoopHistoryMath.cgmHoursPerDay(cgmActive: 0.15, over: today, periodDays: 1), 2.25, accuracy: 1e-9) + // A longer period averages over its days. + let fortnight = DateInterval(start: at(0), end: at(14 * 1440)) + XCTAssertEqual(LoopHistoryMath.cgmHoursPerDay(cgmActive: 0.75, over: fortnight, periodDays: 14), 18, accuracy: 1e-9) + } + + func testHealthBoundaryIsTheLaterOfLocalHistoryAndFirstRecord() { + let localStart = at(0), now = at(90 * 1440) + // Loop began recording 20 days into its 90-day history: Health covers the days before. + XCTAssertEqual(LoopHistoryMath.healthBoundary(localHistoryStart: localStart, firstLocalRecord: at(20 * 1440), now: now), at(20 * 1440)) + // A record older than the local history has been purged up to its start. + XCTAssertEqual(LoopHistoryMath.healthBoundary(localHistoryStart: localStart, firstLocalRecord: at(-60), now: now), localStart) + // No records of a kind: all of it comes from Health. + XCTAssertEqual(LoopHistoryMath.healthBoundary(localHistoryStart: localStart, firstLocalRecord: nil, now: now), now) + } + + func testHealthAndLocalPartsMeetWithoutOverlap() { + let window = DateInterval(start: at(0), end: at(1440)) + let straddling = LoopHistoryMath.split(window, at: at(600)) + XCTAssertEqual(straddling.health, DateInterval(start: at(0), end: at(600))) + XCTAssertEqual(straddling.local, DateInterval(start: at(600), end: at(1440))) + let allLocal = LoopHistoryMath.split(window, at: at(-60)) + XCTAssertNil(allLocal.health) + XCTAssertEqual(allLocal.local, window) + let allHealth = LoopHistoryMath.split(window, at: at(1440)) + XCTAssertEqual(allHealth.health, window) + XCTAssertNil(allHealth.local) + } + + func testBolusPartsNeverTouchAndKeepTheirHeight() { + let day = LoopHistoryMath.DayTotal(day: at(0), split: InsulinSplit(basal: 20, manualBolus: 10, automaticBolus: 0.2, unclassifiedBolus: 2)) + let parts = InsulinDailyTotalsView.bolusParts(day, gap: 0.5) + XCTAssertEqual(parts.map(\.automatic), [false, true]) + // Manual and unknown together, a gap above basal, keeping its top. + XCTAssertEqual(parts[0].start, 20.5, accuracy: 1e-9) + XCTAssertEqual(parts[0].end, 32, accuracy: 1e-9) + // Automatic is shorter than two gaps, so it moves up a gap and keeps its full height. + XCTAssertEqual(parts[1].start, 32.5, accuracy: 1e-9) + XCTAssertEqual(parts[1].end - parts[1].start, 0.2, accuracy: 1e-9) + XCTAssertTrue(InsulinDailyTotalsView.bolusParts(LoopHistoryMath.DayTotal(day: at(0), basal: 20, bolus: 0), gap: 0.5).isEmpty) + } + func testScheduledUnitsNeedTheWholeSpanCovered() { let points = [ScheduledBasalPoint(start: at(0), end: at(120), rate: 1.0), ScheduledBasalPoint(start: at(120), end: at(240), rate: 0.5)] XCTAssertEqual(try XCTUnwrap(LoopHistoryMath.scheduledUnits(points, over: DateInterval(start: at(60), end: at(180)))), 1.5, accuracy: 1e-9) @@ -184,6 +240,20 @@ class LoopHistoryMathTests: XCTestCase { XCTAssertEqual(spans[2].start, at(40), "Closed again with the first good run after it") } + func testManualTempBasalStaysOpenWhenALaterRunFailsForAnotherReason() { + let interval = DateInterval(start: at(0), end: at(60)) + // A manual temp basal runs 20–40 min. The run at 25 is held by it; from 30 glucose is stale, so later runs fail + // on that before Loop checks for the manual temp basal. + let runs = stride(from: 0.0, through: 60, by: 5).map { minute in + LoopHistoryMath.LoopRun(date: at(minute), succeeded: minute < 20 || minute > 40, manualTempBasal: minute == 25) + } + let spans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: [.init(date: at(0), closedLoop: true)], + coverageStart: at(0), manualTempBasals: [DateInterval(start: at(20), end: at(40))]) + XCTAssertEqual(spans.map(\.mode), [.closed, .open, .closed]) + XCTAssertEqual(spans[1].start, at(20)) + XCTAssertEqual(spans[1].end, at(40), "Open for the whole manual temp basal, not just after the run it held") + } + // MARK: Calendar func testDaysAndQuarterMarksOnClockChangeDay() throws { From 150196b600a58fbcd399766077d9775814a55867 Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 19:29:37 +0000 Subject: [PATCH 13/15] Tighten dose boundaries and use a darker bolus colour Limits the first-record scan to each week, treats logged injections as manual, lets a recorded manual temp basal end win, refreshes cached first records when stores change and lets the Boluses detail wrap. Adds one bolus colour set, so bolus stands apart from basal in light mode. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QHnVP6NkV9kiyLRBAeMMt9 --- .../bolus.colorset/Contents.json | 38 +++++++++ Loop/Localizable.xcstrings | 66 +++++++--------- Loop/Redesign/LoopDesign.swift | 12 +-- Loop/Redesign/LoopHistoryMath.swift | 23 +++++- Loop/Redesign/LoopHistoryModels.swift | 17 ++-- Loop/Redesign/LoopHistoryProvider.swift | 78 +++++++++++++++---- Loop/Redesign/TrendsComponents.swift | 5 +- Loop/Redesign/TrendsScreen.swift | 19 +++-- LoopTests/Models/LoopHistoryMathTests.swift | 58 ++++++++++++++ 9 files changed, 237 insertions(+), 79 deletions(-) create mode 100644 Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json diff --git a/Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json b/Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json new file mode 100644 index 0000000000..30b11e52a8 --- /dev/null +++ b/Loop/DerivedAssetsBase.xcassets/bolus.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "red": "0.663", + "green": "0.282", + "blue": "0.039", + "alpha": "1.000" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "red": "1.000", + "green": "0.639", + "blue": "0.278", + "alpha": "1.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index f1fc5cfd4f..4b942877de 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -1301,7 +1301,7 @@ } }, "%1$@ of unknown kind\n+ %2$@ automatic" : { - "comment" : "Trends Boluses per day tile detail when some boluses don't say whether they were manual or automatic, such as another app's in Apple Health, on two lines: how many of the figure above are of unknown kind, then the automatic boluses on top. (1: boluses of unknown kind per day; 2: automatic boluses per day; both fractional, such as 4.2)", + "comment" : "Trends Boluses per day tile detail when some boluses don't say whether they were manual or automatic, such as some in Apple Health, on two lines: how many of the figure above are of unknown kind, then the automatic boluses on top. (1: boluses of unknown kind per day; 2: automatic boluses per day; both fractional, such as 4.2)", "localizations" : { "ar" : { "stringUnit" : { @@ -1587,7 +1587,7 @@ } }, "%1$@ unknown" : { - "comment" : "Basal and bolus: bolus whose record doesn't say whether it was manual or automatic, such as another app's in Apple Health, after the manual and automatic amounts. (1: units, e.g. 3.2 U)", + "comment" : "Basal and bolus: bolus whose record doesn't say whether it was manual or automatic, such as some in Apple Health, after the manual and automatic amounts. (1: units, e.g. 3.2 U)", "localizations" : { "ar" : { "stringUnit" : { @@ -11050,145 +11050,145 @@ } } }, - "Older days come from Apple Health, which doesn't record the basal schedule, or whether another app's bolus was manual or automatic." : { - "comment" : "Trends Insulin per day footnote when the period reaches back past Loop's own records, so scheduled basal or the manual and automatic split can't be shown", + "Older days come from Apple Health, which doesn't record the basal schedule, and some boluses there don't say whether they were manual or automatic." : { + "comment" : "Trends Insulin per day footnote when the period reaches back past Loop's own records into Apple Health, so scheduled basal can't be shown, or some boluses there have no record of whether they were manual or automatic", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الأيام الأقدم مأخوذة من Apple Health، الذي لا يسجّل جدول القاعدي، ولا ما إذا كانت جرعة تطبيق آخر يدوية أم تلقائية." + "value" : "الأيام الأقدم مأخوذة من Apple Health، الذي لا يسجّل جدول القاعدي، وبعض الجرعات هناك لا تذكر ما إذا كانت يدوية أم تلقائية." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Starší dny pocházejí z Apple Health, který nezaznamenává plán bazálu ani to, zda byl bolus z jiné aplikace ruční, nebo automatický." + "value" : "Starší dny pocházejí z Apple Health, který nezaznamenává plán bazálu, a u některých bolusů tam není uvedeno, zda byly ruční, nebo automatické." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ældre dage kommer fra Apple Health, som ikke registrerer basalplanen, eller om en anden apps bolus var manuel eller automatisk." + "value" : "Ældre dage kommer fra Apple Health, som ikke registrerer basalplanen, og nogle boluser der angiver ikke, om de var manuelle eller automatiske." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Ältere Tage stammen aus Apple Health. Dort sind weder das Basalprogramm noch die Angabe gespeichert, ob ein Bolus aus einer anderen App manuell oder automatisch war." + "value" : "Ältere Tage stammen aus Apple Health. Dort ist das Basalprogramm nicht gespeichert, und bei manchen Boli fehlt die Angabe, ob sie manuell oder automatisch waren." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Los días anteriores provienen de Apple Health, que no registra el programa basal ni si un bolo de otra app fue manual o automático." + "value" : "Los días anteriores provienen de Apple Health, que no registra el programa basal, y algunos bolos allí no indican si fueron manuales o automáticos." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Vanhemmat päivät tulevat Apple Healthista, joka ei tallenna basaaliohjelmaa eikä sitä, oliko toisen apin bolus manuaalinen vai automaattinen." + "value" : "Vanhemmat päivät tulevat Apple Healthista, joka ei tallenna basaaliohjelmaa, eikä osa sen boluksista kerro, olivatko ne manuaalisia vai automaattisia." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Les jours plus anciens viennent d'Apple Santé, qui n'enregistre ni le programme basal, ni si un bolus d'une autre app était manuel ou automatique." + "value" : "Les jours plus anciens viennent d'Apple Santé, qui n'enregistre pas le programme basal, et certains bolus n'y indiquent pas s'ils étaient manuels ou automatiques." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "ימים ישנים יותר מגיעים מ-Apple Health, שאינו שומר את תוכנית הבזאלי, או אם בולוס מאפליקציה אחרת היה ידני או אוטומטי." + "value" : "ימים ישנים יותר מגיעים מ-Apple Health, שאינו שומר את תוכנית הבזאלי, וחלק מהבולוסים שם אינם מציינים אם היו ידניים או אוטומטיים." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "A régebbi napok az Apple Egészségből származnak, amely nem rögzíti a bázisprogramot, sem azt, hogy egy másik app bólusa kézi vagy automatikus volt-e." + "value" : "A régebbi napok az Apple Egészségből származnak, amely nem rögzíti a bázisprogramot, és ott egyes bólusoknál nincs feltüntetve, hogy kézi vagy automatikus volt-e." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "I giorni precedenti provengono da Apple Salute, che non registra il programma basale, né se un bolo di un'altra app era manuale o automatico." + "value" : "I giorni precedenti provengono da Apple Salute, che non registra il programma basale, e alcuni boli lì non indicano se erano manuali o automatici." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "それ以前の日はAppleヘルスケアのデータです。ベーサルのスケジュールや、他のAppのボーラスが手動か自動かは記録されません。" + "value" : "それ以前の日はAppleヘルスケアのデータです。ベーサルのスケジュールは記録されず、一部のボーラスは手動か自動かが記録されていません。" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Eldre dager kommer fra Apple Helse, som ikke registrerer basalplanen, eller om en annen apps bolus var manuell eller automatisk." + "value" : "Eldre dager kommer fra Apple Helse, som ikke registrerer basalplanen, og noen boluser der oppgir ikke om de var manuelle eller automatiske." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Oudere dagen komen uit Apple Gezondheid, dat het basaalschema niet vastlegt, en ook niet of een bolus van een andere app handmatig of automatisch was." + "value" : "Oudere dagen komen uit Apple Gezondheid, dat het basaalschema niet vastlegt, en bij sommige bolussen daar staat niet of ze handmatig of automatisch waren." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Starsze dni pochodzą z Apple Zdrowie, które nie zapisuje harmonogramu bazy ani tego, czy bolus z innej aplikacji był ręczny, czy automatyczny." + "value" : "Starsze dni pochodzą z Apple Zdrowie, które nie zapisuje harmonogramu bazy, a przy niektórych bolusach nie ma tam informacji, czy były ręczne, czy automatyczne." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Os dias mais antigos vêm do app Saúde, que não registra a programação basal nem se o bolus de outro app foi manual ou automático." + "value" : "Os dias mais antigos vêm do app Saúde, que não registra a programação basal, e alguns bolus ali não informam se foram manuais ou automáticos." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Zilele mai vechi provin din Apple Sănătate, care nu înregistrează programul bazal sau dacă un bolus din altă aplicație a fost manual ori automat." + "value" : "Zilele mai vechi provin din Apple Sănătate, care nu înregistrează programul bazal, iar unele bolusuri de acolo nu arată dacă au fost manuale sau automate." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Более ранние дни взяты из Apple Здоровье, где не хранятся базальная программа и то, был ли болюс из другого приложения ручным или автоматическим." + "value" : "Более ранние дни взяты из Apple Здоровье, где не хранится базальная программа, а у некоторых болюсов не указано, были ли они ручными или автоматическими." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Staršie dni pochádzajú z Apple Zdravie, ktoré nezaznamenáva plán bazálu ani to, či bol bolus z inej aplikácie manuálny, alebo automatický." + "value" : "Staršie dni pochádzajú z Apple Zdravie, ktoré nezaznamenáva plán bazálu, a pri niektorých bolusoch tam nie je uvedené, či boli manuálne, alebo automatické." } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Äldre dagar kommer från Apple Hälsa, som inte registrerar basalschemat, eller om en annan apps bolus var manuell eller automatisk." + "value" : "Äldre dagar kommer från Apple Hälsa, som inte registrerar basalschemat, och vissa bolusar där anger inte om de var manuella eller automatiska." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Daha eski günler Apple Sağlık'tan gelir; bazal programı veya başka bir uygulamanın bolusunun manuel mi otomatik mi olduğu orada kaydedilmez." + "value" : "Daha eski günler Apple Sağlık'tan gelir; bazal program orada kaydedilmez ve bazı bolusların manuel mi otomatik mi olduğu belirtilmez." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Давніші дні взято з Apple Здоровʼя, де не зберігаються базальна програма та те, чи був болюс з іншої програми ручним чи автоматичним." + "value" : "Давніші дні взято з Apple Здоровʼя, де не зберігається базальна програма, а для деяких болюсів не вказано, чи були вони ручними чи автоматичними." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Các ngày cũ hơn lấy từ Apple Sức khỏe, nơi không ghi lại lịch basal, hay liều bolus của ứng dụng khác là thủ công hay tự động." + "value" : "Các ngày cũ hơn lấy từ Apple Sức khỏe, nơi không ghi lại lịch basal, và một số liều bolus ở đó không cho biết là thủ công hay tự động." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "较早的日子来自 Apple“健康”,其中不记录基础率计划,也不记录其他 App 的大剂量是手动还是自动。" + "value" : "较早的日子来自 Apple“健康”,其中不记录基础率计划,部分大剂量也没有记录是手动还是自动。" } } } @@ -44714,18 +44714,6 @@ "state" : "translated", "value" : "OK" } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } } } }, diff --git a/Loop/Redesign/LoopDesign.swift b/Loop/Redesign/LoopDesign.swift index da50af217b..f17bae7236 100644 --- a/Loop/Redesign/LoopDesign.swift +++ b/Loop/Redesign/LoopDesign.swift @@ -10,9 +10,9 @@ import UIKit /// Colours, spacing and type for the Trends screen. /// -/// Every colour is one Loop already has: the `glucose`, `insulin`, `carbs`, `fresh` and `warning` assets, so a +/// Every colour but one is one Loop already has: the `glucose`, `insulin`, `carbs`, `fresh` and `warning` assets, so a /// DerivedAssetsOverride still recolours them, and the iOS system colours for grounds and text. Bands and fills are -/// those colours at reduced opacity. Text is `label` or `secondaryLabel`; colour is for marks, not for numbers. +/// those colours at reduced opacity. The one addition is `bolus`, a darker insulin for light mode. Text is `label` or `secondaryLabel`; colour is for marks, not for numbers. /// Red is only for something that needs action now. enum LoopDesign { @@ -37,9 +37,11 @@ enum LoopDesign { static let agpOuter = glucose.opacity(0.16) static let agpInner = glucose.opacity(0.34) static let insulin = Color("insulin") - /// Bolus is insulin's colour; basal is the same colour faded, with a stronger edge along its top. One hue can't - /// give 3:1 between them, so charts keep a gap of the card's colour wherever basal and bolus meet. - static let bolus = insulin + /// Basal is insulin's colour faded, with a stronger edge along its top. Insulin's own orange is too light for + /// a bolus on it in light mode, where it is under 2:1 against faded basal, so bolus has its own asset: a deep + /// orange in light mode, insulin's light orange in dark. Charts also keep a gap of the card's colour wherever + /// basal and bolus meet. + static let bolus = Color("bolus") static let basal = insulin.opacity(0.25) static let basalEdge = insulin.opacity(0.6) static let basalScheduled = inkMuted diff --git a/Loop/Redesign/LoopHistoryMath.swift b/Loop/Redesign/LoopHistoryMath.swift index b570c8074e..40a3e49bee 100644 --- a/Loop/Redesign/LoopHistoryMath.swift +++ b/Loop/Redesign/LoopHistoryMath.swift @@ -170,6 +170,17 @@ enum LoopHistoryMath { max(localHistoryStart, firstLocalRecord ?? now) } + /// The earliest record that lies in `chunk`, from records given as start and end. A store can return records + /// outside the range it was asked for, such as a pump's dose still in progress, so only those overlapping the + /// chunk count. A record crossing into the chunk keeps its real start; one with no duration, such as a bolus, + /// counts by its start. + static func firstRecord(in chunk: DateInterval, of records: [(start: Date, end: Date)]) -> Date? { + records + .filter { $0.start < chunk.end && ($0.end > chunk.start || ($0.end <= $0.start && $0.start >= chunk.start)) } + .map(\.start) + .min() + } + /// `window` cut at `boundary`: the part before it is read from Apple Health, the part from it on from Loop's /// stores. Either is nil when the window has none of it; the two never overlap. static func split(_ window: DateInterval, at boundary: Date) -> (health: DateInterval?, local: DateInterval?) { @@ -341,9 +352,10 @@ enum LoopHistoryMath { /// Splits `interval` into closed, limited and open stretches. /// - /// Each 5-minute slot is open when closed loop was switched off (from `modeChanges`), or when Loop's latest run in - /// the 15 minutes before the slot ends stopped for a manual temp basal. Otherwise it is closed when Loop completed a - /// run in those 15 minutes, and limited when it didn't. Before the first mode change the setting is unknown, so the + /// Each 5-minute slot is open when closed loop was switched off (from `modeChanges`), or while a manual temp basal + /// from `manualTempBasals` runs. A run that stopped for a manual temp basal with no dose record of it holds the + /// loop open for 15 minutes; where the record exists, its end or cancellation decides instead. Otherwise a slot is + /// closed when Loop completed a run in the 15 minutes before it ends, and limited when it didn't. Before the first mode change the setting is unknown, so the /// result starts at the later of `coverageStart` and the first mode change. static func loopModeSpans(interval: DateInterval, runs: [LoopRun], modeChanges: [ModeChange], coverageStart: Date?, manualTempBasals: [DateInterval] = [], @@ -357,11 +369,13 @@ enum LoopHistoryMath { var spans: [LoopModeSpan] = [] var runIndex = 0 var lastRun: LoopRun? = nil + var lastRunHasDoseRecord = false var lastSuccess: Date? = nil var changeIndex = 0 var closedSetting: Bool? = nil let manualTemps = manualTempBasals.sorted { $0.start < $1.start } var manualIndex = 0 + let recorded = { (date: Date) in manualTemps.contains { $0.start <= date && date <= $0.end } } var t = start while t < interval.end { @@ -369,6 +383,7 @@ enum LoopHistoryMath { while runIndex < sortedRuns.count && sortedRuns[runIndex].date <= slotEnd { let run = sortedRuns[runIndex] lastRun = run + lastRunHasDoseRecord = run.manualTempBasal && recorded(run.date) if run.succeeded { lastSuccess = run.date } @@ -389,7 +404,7 @@ enum LoopHistoryMath { let mode: LoopModeSpan.Mode if closedSetting == false || inManualTemp { mode = .open - } else if let lastRun, lastRun.manualTempBasal, slotEnd.timeIntervalSince(lastRun.date) <= recency { + } else if let lastRun, lastRun.manualTempBasal, !lastRunHasDoseRecord, slotEnd.timeIntervalSince(lastRun.date) <= recency { mode = .open } else if let lastSuccess, slotEnd.timeIntervalSince(lastSuccess) <= recency { mode = .closed diff --git a/Loop/Redesign/LoopHistoryModels.swift b/Loop/Redesign/LoopHistoryModels.swift index 0d16a7036d..f5aae9857d 100644 --- a/Loop/Redesign/LoopHistoryModels.swift +++ b/Loop/Redesign/LoopHistoryModels.swift @@ -26,13 +26,19 @@ struct BolusEvent: Equatable, Identifiable { let units: Double /// Issued by Loop (automatic bolus) rather than by the person. let automatic: Bool - /// Logged in Loop by hand rather than delivered by the pump; these can be deleted. + /// Logged by hand, such as an injection, rather than delivered by the pump. Always a manual dose. let manuallyEntered: Bool /// Kept so a manually entered dose can be deleted from the dose store. var dose: DoseEntry? = nil - /// False when the record doesn't say whether Loop or the person gave it, such as another app's doses in Apple Health. + /// False when the record doesn't say whether it was automatic or given by the person, such as some doses in Apple Health. var automaticKnown = true + /// Whether a record says what kind of bolus it is: it carries the automatic flag, or it was logged by hand, which + /// makes it manual. + static func kindKnown(automatic: Bool?, manuallyEntered: Bool) -> Bool { + automatic != nil || manuallyEntered + } + static func == (lhs: BolusEvent, rhs: BolusEvent) -> Bool { lhs.id == rhs.id && lhs.date == rhs.date && lhs.units == rhs.units && lhs.automatic == rhs.automatic } @@ -300,10 +306,11 @@ extension BolusEvent { init?(dose: DoseEntry, in interval: DateInterval) { guard dose.type == .bolus, dose.startDate >= interval.start, dose.startDate < interval.end else { return nil } self.init(id: dose.syncIdentifier ?? "\(dose.startDate.timeIntervalSince1970)", date: dose.startDate, - units: dose.unitsInDeliverableIncrements, automatic: dose.automatic ?? false, + units: dose.unitsInDeliverableIncrements, automatic: !dose.manuallyEntered && (dose.automatic ?? false), manuallyEntered: dose.manuallyEntered, dose: dose, - // A dose imported without Loop's automatic flag, or not yet reconciled, is of unknown kind. - automaticKnown: dose.automatic != nil) + // A dose imported without the automatic flag, or not yet reconciled, is of unknown kind; one logged + // by hand is manual. + automaticKnown: BolusEvent.kindKnown(automatic: dose.automatic, manuallyEntered: dose.manuallyEntered)) } } diff --git a/Loop/Redesign/LoopHistoryProvider.swift b/Loop/Redesign/LoopHistoryProvider.swift index b5b31702e2..a9ad524c1a 100644 --- a/Loop/Redesign/LoopHistoryProvider.swift +++ b/Loop/Redesign/LoopHistoryProvider.swift @@ -23,7 +23,8 @@ final class LoopHistoryProvider { private let deviceManager: DeviceDataManager private let loopDataManager: LoopDataManager - private let settingsManager: SettingsManager + /// Read once on the main actor, which owns `SettingsManager`; the store itself is safe to use from anywhere. + private let settingsStore: SettingsStore? private let temporaryPresetsManager: TemporaryPresetsManager private let dosingDecisionStore: DosingDecisionStoreProtocol private let doseStore: DoseStore @@ -31,18 +32,40 @@ final class LoopHistoryProvider { private let glucoseStore: GlucoseStore private let healthStore = HKHealthStore() private let log = OSLog(category: "LoopHistoryProvider") - private let recordStartsCache = OSAllocatedUnfairLock<(fetched: Date, starts: LocalRecordStarts)?>(initialState: nil) + private let recordStartsCache = OSAllocatedUnfairLock(initialState: RecordStartsCache()) + private var storeObservers: [NSObjectProtocol] = [] @MainActor init(deviceManager: DeviceDataManager, loopDataManager: LoopDataManager, settingsManager: SettingsManager, temporaryPresetsManager: TemporaryPresetsManager) { self.deviceManager = deviceManager self.loopDataManager = loopDataManager - self.settingsManager = settingsManager + self.settingsStore = settingsManager.settingsStore self.temporaryPresetsManager = temporaryPresetsManager self.dosingDecisionStore = loopDataManager.dosingDecisionStore self.doseStore = deviceManager.doseStore self.carbStore = deviceManager.carbStore self.glucoseStore = deviceManager.glucoseStore + + // A record added or edited earlier than a cached start would otherwise stay hidden until the cache expires, + // so any change to a store searches for that kind's start again. + let cache = recordStartsCache + func forget(_ name: Notification.Name, of store: AnyObject, _ clear: @escaping @Sendable (inout LocalRecordStarts) -> Void) -> NSObjectProtocol { + NotificationCenter.default.addObserver(forName: name, object: store, queue: nil) { _ in + cache.withLock { cached in + clear(&cached.starts) + cached.generation += 1 + } + } + } + storeObservers = [ + forget(GlucoseStore.glucoseSamplesDidChange, of: glucoseStore) { $0.glucose = nil }, + forget(DoseStore.valuesDidChange, of: doseStore) { $0.insulin = nil }, + forget(CarbStore.carbEntriesDidChange, of: carbStore) { $0.carbs = nil }, + ] + } + + deinit { + storeObservers.forEach(NotificationCenter.default.removeObserver) } @MainActor @@ -75,7 +98,7 @@ final class LoopHistoryProvider { /// The targets that actually applied, including dated presets and changes to the stored profile. func glucoseTargetBands(in interval: DateInterval) async throws -> [TargetBand] { - if let store = settingsManager.settingsStore { + if let store = settingsStore { let previous = try await store.getStoredSettings(end: interval.start, limit: 1).first if previous?.glucoseTargetRangeSchedule == nil { let changes = try await store.getStoredSettings(start: interval.start, end: interval.end) @@ -130,12 +153,21 @@ final class LoopHistoryProvider { var carbs: Date? } - /// A found start is kept for a few minutes, so each period doesn't search again. A kind with no records yet is - /// searched every time, because its first record can arrive at any moment. + private struct RecordStartsCache { + var fetched: Date? + var starts = LocalRecordStarts() + /// Counts store changes, so a search that overlapped one doesn't save what it found. + var generation = 0 + } + + /// A found start is kept for a few minutes, so each period doesn't search again, and forgotten as soon as that + /// kind's store changes. A kind with no records yet is searched every time, because its first record can arrive at + /// any moment. func localRecordStarts() async throws -> LocalRecordStarts { let now = Date(), start = localHistoryStart - let cached = recordStartsCache.withLock { $0 }.flatMap { now.timeIntervalSince($0.fetched) < 300 ? $0 : nil } - var starts = cached?.starts ?? LocalRecordStarts() + let cached = recordStartsCache.withLock { $0 } + let fresh = cached.fetched.map { now.timeIntervalSince($0) < 300 } ?? false + var starts = fresh ? cached.starts : LocalRecordStarts() let glucoseStore = self.glucoseStore, doseStore = self.doseStore if starts.glucose == nil { starts.glucose = try await Self.firstRecord(from: start, to: now) { chunk in @@ -144,14 +176,19 @@ final class LoopHistoryProvider { } if starts.insulin == nil { starts.insulin = try await Self.firstRecord(from: start, to: now) { chunk in - try await doseStore.getNormalizedDoseEntries(start: chunk.start, end: chunk.end).map(\.startDate) + let doses = try await doseStore.getNormalizedDoseEntries(start: chunk.start, end: chunk.end) + return LoopHistoryMath.firstRecord(in: chunk, of: doses.map { (start: $0.startDate, end: $0.endDate) }).map { [$0] } ?? [] } } if starts.carbs == nil { starts.carbs = try await carbStore.getCarbEntries(start: start, end: now, dateAscending: true, fetchLimit: 1).first?.startDate } let found = starts - recordStartsCache.withLock { $0 = (fetched: cached?.fetched ?? now, starts: found) } + recordStartsCache.withLock { latest in + guard latest.generation == cached.generation else { return } + latest.fetched = fresh ? cached.fetched : now + latest.starts = found + } return found } @@ -223,7 +260,7 @@ final class LoopHistoryProvider { /// When closed loop was switched on or off, from the settings Loop stores on every change: the setting in force at /// `interval.start`, then each change after it. Empty when Loop has no settings store. func loopModeChanges(in interval: DateInterval) async throws -> [LoopHistoryMath.ModeChange] { - guard let store = settingsManager.settingsStore else { return [] } + guard let store = settingsStore else { return [] } var changes: [LoopHistoryMath.ModeChange] = [] if let previous = try await store.getStoredSettings(end: interval.start, limit: 1).first { changes.append(.init(date: interval.start, closedLoop: previous.dosingEnabled)) @@ -239,7 +276,9 @@ final class LoopHistoryProvider { /// When manual temp basals ran in `interval`, from Loop's dose records, ended where the pump ended or cancelled /// them. Loop shows the loop as open while one runs. func manualTempBasals(in interval: DateInterval) async throws -> [DateInterval] { - try await doseStore.getNormalizedDoseEntries(start: interval.start, end: interval.end) + // The last run before the period may have stopped for a temp basal that ended before the period too. + // Keep its record so the run-based fallback cannot reopen the loop after that recorded end. + try await doseStore.getNormalizedDoseEntries(start: interval.start.addingTimeInterval(-.minutes(15)), end: interval.end) .filter { $0.type == .tempBasal && $0.automatic == false && $0.endDate > $0.startDate } .map { DateInterval(start: $0.startDate, end: $0.endDate) } } @@ -326,7 +365,12 @@ final class LoopHistoryProvider { os_log("Health query for %{public}@ failed: %{public}@", log: log, type: .error, type.identifier, String(describing: error)) continuation.resume(throwing: error) } else { - continuation.resume(returning: (samples as? [HKQuantitySample]) ?? []) + let quantities = (samples as? [HKQuantitySample]) ?? [] + // Match the local stores' half-open ranges, including at the Health/local boundary. Basal + // queries use no strict start so segments crossing into the interval can still be clamped. + continuation.resume(returning: options.contains(.strictStartDate) + ? quantities.filter { $0.startDate >= interval.start && $0.startDate < interval.end } + : quantities) } } healthStore.execute(query) @@ -357,14 +401,16 @@ final class LoopHistoryProvider { let units = sample.quantity.doubleValue(for: .internationalUnit()) let reason = (sample.metadata?[HKMetadataKeyInsulinDeliveryReason] as? NSNumber)?.intValue let automaticFlag = (sample.metadata?["com.loopkit.InsulinKit.MetadataKeyAutomaticallyIssued"] as? NSNumber)?.boolValue - let automatic = automaticFlag ?? false + let manuallyEntered = (sample.metadata?["com.loopkit.InsulinKit.MetadataKeyManuallyEntered"] as? NSNumber)?.boolValue ?? false + let automatic = !manuallyEntered && (automaticFlag ?? false) if reason == HKInsulinDeliveryReason.basal.rawValue { if let segment = BasalSegment(healthSample: sample, in: interval) { basal.append(segment) } } else if sample.startDate >= interval.start && sample.startDate < interval.end { - boluses.append(BolusEvent(id: sample.uuid.uuidString, date: sample.startDate, units: units, automatic: automatic, manuallyEntered: false, - automaticKnown: automaticFlag != nil)) + boluses.append(BolusEvent(id: sample.uuid.uuidString, date: sample.startDate, units: units, automatic: automatic, + manuallyEntered: manuallyEntered, + automaticKnown: BolusEvent.kindKnown(automatic: automaticFlag, manuallyEntered: manuallyEntered))) } } return (boluses, basal) diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift index a7a7385c28..fadcb4d7f8 100644 --- a/Loop/Redesign/TrendsComponents.swift +++ b/Loop/Redesign/TrendsComponents.swift @@ -139,10 +139,11 @@ struct StatGridView: View { } } if let detail = tile.detail { + // Not capped: a detail can run to two lines of its own, which wrap again at large sizes. Text(detail) .font(LoopDesign.Typeface.footnote) .foregroundStyle(P.inkMuted) - .lineLimit(2) + .fixedSize(horizontal: false, vertical: true) } if let aim = tile.aim { Text(aim) @@ -582,7 +583,7 @@ struct InsulinSplitView: View { /// "3.2 U unknown": bolus whose record doesn't say whether it was manual or automatic. private static func unknownText(_ units: Double) -> String { - String(format: NSLocalizedString("%1$@ unknown", comment: "Basal and bolus: bolus whose record doesn't say whether it was manual or automatic, such as another app's in Apple Health, after the manual and automatic amounts. (1: units, e.g. 3.2 U)"), LoopFormat.totalUnits(units)) + String(format: NSLocalizedString("%1$@ unknown", comment: "Basal and bolus: bolus whose record doesn't say whether it was manual or automatic, such as some in Apple Health, after the manual and automatic amounts. (1: units, e.g. 3.2 U)"), LoopFormat.totalUnits(units)) } /// "Scheduled 18.0 U", or "Scheduled 18.0 U · 20 of 90 days" when the schedule is known for only some days. diff --git a/Loop/Redesign/TrendsScreen.swift b/Loop/Redesign/TrendsScreen.swift index 8eec875119..a13f831165 100644 --- a/Loop/Redesign/TrendsScreen.swift +++ b/Loop/Redesign/TrendsScreen.swift @@ -35,7 +35,7 @@ final class TrendsModel: ObservableObject { var carbsByDay: [Date: Double] = [:] var manualBoluses = 0 var automaticBoluses = 0 - /// Boluses whose record doesn't say whether Loop or the person gave them, such as another app's in Apple Health. + /// Boluses whose record doesn't say whether they were automatic or given by the person, such as some in Apple Health. var unknownBoluses = 0 var loopSpans: [LoopModeSpan] = [] var targetRange: ClosedRange? @@ -178,6 +178,9 @@ final class TrendsModel: ObservableObject { var tiles: [StatTile] = [] let averageLabel = NSLocalizedString("Average", comment: "Trends summary tile: mean glucose over the period") let gmiLabel = NSLocalizedString("GMI", comment: "Trends summary tile: abbreviation for Glucose Management Indicator, an estimate of HbA1c from CGM data") + let gmiCaveat = LoopHistoryMath.gmiIsRepresentative(periodDays: s.totals.count, cgmActive: s.cgmActive) ? nil + : String(format: NSLocalizedString("Best over 14 days with %1$@ CGM data", comment: "Trends GMI tile caption when the period is shorter than 14 days or CGM readings cover less than 70% of it: the consensus for when GMI is representative. (1: share of CGM data, e.g. 70%)"), + ConsensusTarget.percent(70)) if let stats = s.stats { tiles.append(StatTile(label: averageLabel, value: unit.format(stats.meanMgdl), unit: unit.unitName, detail: String(format: NSLocalizedString("SD %1$@ · CV %2$@%%", comment: "Trends Average tile detail: standard deviation and coefficient of variation of glucose. (1: standard deviation in the glucose unit, 2: coefficient of variation as a percentage number)"), @@ -188,13 +191,13 @@ final class TrendsModel: ObservableObject { detail: String(format: NSLocalizedString("%1$@ mmol/mol", comment: "Trends GMI tile detail: the GMI in IFCC units. (1: value)"), LoopFormat.number(stats.gmiMmolMol, decimals: 0)), aim: ConsensusTarget.aim(ConsensusTarget.under(7)), - caveat: LoopHistoryMath.gmiIsRepresentative(periodDays: s.totals.count, cgmActive: s.cgmActive) ? nil - : String(format: NSLocalizedString("Best over 14 days with %1$@ CGM data", comment: "Trends GMI tile caption when the period is shorter than 14 days or CGM readings cover less than 70% of it: the consensus for when GMI is representative. (1: share of CGM data, e.g. 70%)"), - ConsensusTarget.percent(70)))) + caveat: gmiCaveat)) } else { tiles.append(StatTile(label: averageLabel, value: "–", unit: unit.unitName, detail: NSLocalizedString("No CGM data", comment: "Trends Average tile detail when the period has no glucose readings"))) - tiles.append(StatTile(label: gmiLabel, value: "–", unit: "%", detail: nil)) + tiles.append(StatTile(label: gmiLabel, value: "–", unit: "%", + detail: String(format: NSLocalizedString("%1$@ mmol/mol", comment: "Trends GMI tile detail: the GMI in IFCC units. (1: value)"), "–"), + caveat: gmiCaveat)) } // One day's figures are that day's own, so its tiles drop "per day" and show whole counts. let oneDay = s.day != nil @@ -214,7 +217,7 @@ final class TrendsModel: ObservableObject { // Every bolus not known to be automatic, so one of unknown kind still counts. value: LoopFormat.number(Double(s.manualBoluses + s.unknownBoluses) / s.days, decimals: countDecimals), unit: nil, detail: s.unknownBoluses > 0 - ? String(format: NSLocalizedString("%1$@ of unknown kind\n+ %2$@ automatic", comment: "Trends Boluses per day tile detail when some boluses don't say whether they were manual or automatic, such as another app's in Apple Health, on two lines: how many of the figure above are of unknown kind, then the automatic boluses on top. (1: boluses of unknown kind per day; 2: automatic boluses per day; both fractional, such as 4.2)"), + ? String(format: NSLocalizedString("%1$@ of unknown kind\n+ %2$@ automatic", comment: "Trends Boluses per day tile detail when some boluses don't say whether they were manual or automatic, such as some in Apple Health, on two lines: how many of the figure above are of unknown kind, then the automatic boluses on top. (1: boluses of unknown kind per day; 2: automatic boluses per day; both fractional, such as 4.2)"), LoopFormat.number(Double(s.unknownBoluses) / s.days, decimals: countDecimals), LoopFormat.number(Double(s.automaticBoluses) / s.days, decimals: countDecimals)) : String(format: NSLocalizedString("+ %1$@ automatic", comment: "Trends Boluses per day tile detail: average number of automatic boluses per day on top of the manual ones, a fractional value such as 4.2. (1: number with one decimal)"), @@ -359,9 +362,9 @@ struct TrendsView: View { if s.day == nil, let today = todaySoFar(s) { Footnote(today) } - // Days read from Apple Health have no schedule, and other apps' boluses there don't say which they were. + // Days read from Apple Health have no schedule, and some boluses there don't say which kind they were. if s.readsHealthInsulin, s.scheduledBasal == nil || !s.split.isClassified { - Footnote(NSLocalizedString("Older days come from Apple Health, which doesn't record the basal schedule, or whether another app's bolus was manual or automatic.", comment: "Trends Insulin per day footnote when the period reaches back past Loop's own records, so scheduled basal or the manual and automatic split can't be shown")) + Footnote(NSLocalizedString("Older days come from Apple Health, which doesn't record the basal schedule, and some boluses there don't say whether they were manual or automatic.", comment: "Trends Insulin per day footnote when the period reaches back past Loop's own records into Apple Health, so scheduled basal can't be shown, or some boluses there have no record of whether they were manual or automatic")) } } else { ChartPlaceholder(message: NSLocalizedString("No insulin recorded in this period", comment: "Trends Insulin per day chart placeholder when no insulin was delivered in the period"), height: 80) diff --git a/LoopTests/Models/LoopHistoryMathTests.swift b/LoopTests/Models/LoopHistoryMathTests.swift index 4dead9fcc5..901261e514 100644 --- a/LoopTests/Models/LoopHistoryMathTests.swift +++ b/LoopTests/Models/LoopHistoryMathTests.swift @@ -163,6 +163,39 @@ class LoopHistoryMathTests: XCTestCase { XCTAssertEqual(LoopHistoryMath.healthBoundary(localHistoryStart: localStart, firstLocalRecord: nil, now: now), now) } + func testFirstRecordIgnoresRecordsOutsideTheChunk() { + let chunk = DateInterval(start: at(0), end: at(7 * 1440)) + // A dose still in progress today comes back with an older week's query; it isn't that week's first record. + let inProgress = (start: at(30 * 1440), end: at(30 * 1440 + 30)) + XCTAssertNil(LoopHistoryMath.firstRecord(in: chunk, of: [inProgress])) + // A basal crossing into the chunk keeps its real start, and a bolus counts by its start. + let crossing = (start: at(-60), end: at(60)) + let bolus = (start: at(120), end: at(120)) + XCTAssertEqual(LoopHistoryMath.firstRecord(in: chunk, of: [inProgress, bolus, crossing]), at(-60)) + XCTAssertEqual(LoopHistoryMath.firstRecord(in: chunk, of: [inProgress, bolus]), at(120)) + // A bolus before the chunk, or at its end, is outside it. + XCTAssertNil(LoopHistoryMath.firstRecord(in: chunk, of: [(start: at(-5), end: at(-5)), (start: chunk.end, end: chunk.end)])) + } + + func testLoggedInjectionsAreManualAndMissingFlagsUnknown() { + let window = DateInterval(start: at(0), end: at(60)) + for (automatic, entered, manual, auto, unknown) in [ + (nil, true, 1.0, 0.0, 0.0), + (true, true, 1.0, 0.0, 0.0), + (false, false, 1.0, 0.0, 0.0), + (true, false, 0.0, 1.0, 0.0), + (nil, false, 0.0, 0.0, 1.0), + ] as [(Bool?, Bool, Double, Double, Double)] { + let dose = DoseEntry(type: .bolus, startDate: at(5), value: 1, unit: .units, decisionId: nil, + automatic: automatic, manuallyEntered: entered) + guard let event = BolusEvent(dose: dose, in: window) else { return XCTFail("Dose must be in the window") } + let split = InsulinSplit(basal: 0, boluses: [event]) + XCTAssertEqual(split.manualBolus, manual) + XCTAssertEqual(split.automaticBolus, auto) + XCTAssertEqual(split.unclassifiedBolus, unknown) + } + } + func testHealthAndLocalPartsMeetWithoutOverlap() { let window = DateInterval(start: at(0), end: at(1440)) let straddling = LoopHistoryMath.split(window, at: at(600)) @@ -254,6 +287,31 @@ class LoopHistoryMathTests: XCTestCase { XCTAssertEqual(spans[1].end, at(40), "Open for the whole manual temp basal, not just after the run it held") } + func testRecordedCancellationEndsManualTempBasalBeforeTheRunFallback() { + let interval = DateInterval(start: at(0), end: at(30)) + // A good run at 0; a manual temp basal from 3 holds the run at 5; it's cancelled at 12, and no runs follow. + let runs = [LoopHistoryMath.LoopRun(date: at(0), succeeded: true), .init(date: at(5), succeeded: false, manualTempBasal: true)] + let changes: [LoopHistoryMath.ModeChange] = [.init(date: at(0), closedLoop: true)] + let spans = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: changes, + coverageStart: at(0), manualTempBasals: [DateInterval(start: at(3), end: at(12))]) + XCTAssertEqual(spans.map(\.mode), [.closed, .open, .closed, .limited]) + XCTAssertEqual(spans[1].start, at(5)) + XCTAssertEqual(spans[1].end, at(10), "Open while the recorded temp basal runs, not for 15 minutes after the run it held") + XCTAssertEqual(spans[3].start, at(15), "Limited once the last good run is over 15 minutes old") + + // A period starting after the cancellation still needs the preceding dose record. This can happen at + // midnight, while the last held run is recent enough for the fallback to apply. + let nextPeriod = LoopHistoryMath.loopModeSpans(interval: DateInterval(start: at(15), end: at(30)), runs: runs, + modeChanges: changes, coverageStart: at(0), + manualTempBasals: [DateInterval(start: at(3), end: at(12))]) + XCTAssertEqual(nextPeriod.map(\.mode), [.limited]) + + // With no dose record, the held run alone keeps the loop open for 15 minutes. + let unrecorded = LoopHistoryMath.loopModeSpans(interval: interval, runs: runs, modeChanges: changes, coverageStart: at(0)) + XCTAssertEqual(unrecorded.map(\.mode), [.open, .limited]) + XCTAssertEqual(unrecorded[1].start, at(20)) + } + // MARK: Calendar func testDaysAndQuarterMarksOnClockChangeDay() throws { From 0081d73cb08b0fc4be4aac4fb41a53b8fa0c2b97 Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 20:08:12 +0000 Subject: [PATCH 14/15] Keep unrecorded schedule history unknown in Trends --- Loop/Redesign/LoopHistoryProvider.swift | 57 +++++++++++---- LoopTests/Managers/LoopDataManagerTests.swift | 69 +++++++++++++++++-- 2 files changed, 104 insertions(+), 22 deletions(-) diff --git a/Loop/Redesign/LoopHistoryProvider.swift b/Loop/Redesign/LoopHistoryProvider.swift index a9ad524c1a..ad9ec1021e 100644 --- a/Loop/Redesign/LoopHistoryProvider.swift +++ b/Loop/Redesign/LoopHistoryProvider.swift @@ -98,15 +98,42 @@ final class LoopHistoryProvider { /// The targets that actually applied, including dated presets and changes to the stored profile. func glucoseTargetBands(in interval: DateInterval) async throws -> [TargetBand] { - if let store = settingsStore { - let previous = try await store.getStoredSettings(end: interval.start, limit: 1).first - if previous?.glucoseTargetRangeSchedule == nil { - let changes = try await store.getStoredSettings(start: interval.start, end: interval.end) - // LoopKit's historical query requires at least one valid profile, even if settings records exist. - guard changes.contains(where: { $0.glucoseTargetRangeSchedule != nil }) else { return [] } + guard let settingsStore else { return [] } + return try await loopDataManager.glucoseTargetBands(in: interval, settingsStore: settingsStore) + } + + /// SettingsStore's schedule queries extend the first profile backwards to the requested start. Reporting must + /// instead leave time before the first saved profile, and settings records without a profile, unknown. + private static func settingsHistory(in interval: DateInterval, from store: SettingsStore) async throws -> [(settings: StoredSettings, interval: DateInterval)] { + var records = Array(try await store.getStoredSettings(start: interval.start, end: interval.end).reversed()) + if let previous = try await store.getStoredSettings(end: interval.start, limit: 1).first { + records.insert(previous, at: 0) + } + return records.enumerated().compactMap { index, settings in + let start = max(interval.start, settings.date) + let end = min(interval.end, index + 1 < records.count ? records[index + 1].date : interval.end) + guard end > start else { return nil } + return (settings, DateInterval(start: start, end: end)) + } + } + + static func basalHistory(in interval: DateInterval, from store: SettingsStore) async throws -> [AbsoluteScheduleValue] { + let history = try await settingsHistory(in: interval, from: store) + return history.flatMap { record in + record.settings.basalRateSchedule?.truncatingBetween(start: record.interval.start, end: record.interval.end) ?? [] + } + } + + static func targetHistory(in interval: DateInterval, from store: SettingsStore) async throws -> [TargetBand] { + let history = try await settingsHistory(in: interval, from: store) + return history.flatMap { record -> [TargetBand] in + guard let schedule = record.settings.glucoseTargetRangeSchedule else { return [] } + return schedule.truncatingBetween(start: record.interval.start, end: record.interval.end).map { + let range = $0.value.quantityRange(for: schedule.unit) + return TargetBand(start: $0.startDate, end: $0.endDate, + range: range.lowerBound.doubleValue(for: .milligramsPerDeciliter)...range.upperBound.doubleValue(for: .milligramsPerDeciliter)) } } - return try await loopDataManager.glucoseTargetBands(in: interval) } // MARK: - Days @@ -312,7 +339,12 @@ final class LoopHistoryProvider { let boluses = doses.compactMap { BolusEvent(dose: $0, in: interval) } - let basalHistory = try await loopDataManager.settingsProvider.getBasalHistory(startDate: interval.start, endDate: interval.end) + let basalHistory: [AbsoluteScheduleValue] + if let store = settingsStore { + basalHistory = try await Self.basalHistory(in: interval, from: store) + } else { + basalHistory = [] + } let temporaryPresetsManager = self.temporaryPresetsManager let overrides = await MainActor.run { temporaryPresetsManager.presetHistory.getOverrideHistory(startDate: interval.start, endDate: interval.end) @@ -433,13 +465,8 @@ final class LoopHistoryProvider { extension LoopDataManager { /// A historical chart must use the profile and presets from that time, rather than repeat today's targets. - func glucoseTargetBands(in interval: DateInterval) async throws -> [TargetBand] { - guard settingsProvider.settings.glucoseTargetRangeSchedule != nil else { return [] } - let schedule = try await settingsProvider.getTargetRangeHistory(startDate: interval.start, endDate: interval.end) - var bands = schedule.map { - TargetBand(start: max($0.startDate, interval.start), end: min($0.endDate, interval.end), - range: $0.value.lowerBound.doubleValue(for: .milligramsPerDeciliter)...$0.value.upperBound.doubleValue(for: .milligramsPerDeciliter)) - } + func glucoseTargetBands(in interval: DateInterval, settingsStore: SettingsStore) async throws -> [TargetBand] { + var bands = try await LoopHistoryProvider.targetHistory(in: interval, from: settingsStore) let overrides = temporaryPresetsManager.presetHistory.getOverrideHistory(startDate: interval.start, endDate: interval.end) for override in overrides.sorted(by: { $0.startDate < $1.startDate }) { guard let target = override.settings.targetRange, diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index f6e9fa4949..24388d1a5b 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -700,10 +700,12 @@ class LoopDataManagerTests: XCTestCase { LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: low)...LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: high) } let window = DateInterval(start: d(-.hours(3)), end: d(-.minutes(30))) - settingsProvider.targetRangeHistory = [ - AbsoluteScheduleValue(startDate: window.start, endDate: d(-.hours(2)), value: target(100, 110)), - AbsoluteScheduleValue(startDate: d(-.hours(2)), endDate: window.end, value: target(120, 130)), - ] + let store = try await reportingSettingsStore([ + StoredSettings(date: window.start, glucoseTargetRangeSchedule: GlucoseRangeSchedule( + unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0, value: DoubleRange(minValue: 100, maxValue: 110))], timeZone: .utcTimeZone)), + StoredSettings(date: d(-.hours(2)), glucoseTargetRangeSchedule: GlucoseRangeSchedule( + unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0, value: DoubleRange(minValue: 120, maxValue: 130))], timeZone: .utcTimeZone)), + ]) var preset = TemporaryScheduleOverride(context: .custom, settings: TemporaryPresetSettings(targetRange: target(150, 160), insulinNeedsScaleFactor: 1), startDate: d(-.minutes(150)), duration: .finite(.hours(2)), @@ -711,7 +713,7 @@ class LoopDataManagerTests: XCTestCase { preset.actualEnd = .early(d(-.minutes(90))) temporaryPresetsManager.presetHistory.recordOverride(preset, at: d(-.minutes(150))) - let bands = try await loopDataManager.glucoseTargetBands(in: window) + let bands = try await loopDataManager.glucoseTargetBands(in: window, settingsStore: store) XCTAssertEqual(bands, [ TargetBand(start: window.start, end: d(-.minutes(150)), range: 100...110), TargetBand(start: d(-.minutes(150)), end: d(-.minutes(90)), range: 150...160), @@ -720,11 +722,64 @@ class LoopDataManagerTests: XCTestCase { } func testTargetBandsAreUnknownBeforeTherapyIsConfigured() async throws { - settingsProvider.settings.glucoseTargetRangeSchedule = nil - let bands = try await loopDataManager.glucoseTargetBands(in: DateInterval(start: d(-.hours(3)), end: now)) + // Today's profile must not fill in a period whose saved settings had no profile. + let store = try await reportingSettingsStore([StoredSettings(date: d(-.hours(4)))]) + let bands = try await loopDataManager.glucoseTargetBands(in: DateInterval(start: d(-.hours(3)), end: now), settingsStore: store) XCTAssertTrue(bands.isEmpty) } + func testReportingSchedulesLeaveUnrecordedHistoryUnknown() async throws { + let window = DateInterval(start: d(-.hours(4)), end: now) + let first = window.start.addingTimeInterval(.hours(1)) + let missing = first.addingTimeInterval(.hours(1)) + let resumed = missing.addingTimeInterval(.hours(1)) + func profile(at date: Date, rate: Double, target: Double) -> StoredSettings { + StoredSettings(date: date, glucoseTargetRangeSchedule: GlucoseRangeSchedule( + unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0, value: DoubleRange(minValue: target, maxValue: target + 10))], timeZone: .utcTimeZone), + basalRateSchedule: BasalRateSchedule(dailyItems: [RepeatingScheduleValue(startTime: 0, value: rate)], timeZone: .utcTimeZone)) + } + let store = try await reportingSettingsStore([ + profile(at: first, rate: 1, target: 100), + StoredSettings(date: missing), + profile(at: resumed, rate: 2, target: 120), + ]) + + let basal = try await LoopHistoryProvider.basalHistory(in: window, from: store) + let schedule = basal.map { LoopHistoryMath.ScheduleValue(start: $0.startDate, end: $0.endDate, rate: $0.value) } + let delivered = LoopHistoryMath.basalSegments(window: window, schedule: schedule, deliveries: [], + programmedRates: schedule, recordsStart: window.start) + XCTAssertEqual(delivered.reduce(0) { $0 + $1.units }, 3, accuracy: 1e-9, + "Only the two recorded hours can supply inferred basal, not the earlier time or missing profile") + XCTAssertNil(LoopHistoryMath.scheduledUnits(basal.map { ScheduledBasalPoint(start: $0.startDate, end: $0.endDate, rate: $0.value) }, over: window)) + let targets = try await loopDataManager.glucoseTargetBands(in: window, settingsStore: store) + XCTAssertEqual(targets, [TargetBand(start: first, end: missing, range: 100...110), + TargetBand(start: resumed, end: window.end, range: 120...130)]) + + // A query starting during a known profile still includes the setting saved before its start. + let inside = DateInterval(start: first.addingTimeInterval(.minutes(30)), end: missing) + let partial = try await LoopHistoryProvider.basalHistory(in: inside, from: store) + XCTAssertEqual(partial.first?.startDate, inside.start) + XCTAssertEqual(partial.reduce(0) { $0 + $1.value * $1.endDate.timeIntervalSince($1.startDate) / 3600 }, 0.5, accuracy: 1e-9) + let before = DateInterval(start: window.start, end: first) + let earlierBasal = try await LoopHistoryProvider.basalHistory(in: before, from: store) + let earlierTargets = try await loopDataManager.glucoseTargetBands(in: before, settingsStore: store) + XCTAssertTrue(earlierBasal.isEmpty) + XCTAssertTrue(earlierTargets.isEmpty) + } + + private func reportingSettingsStore(_ settings: [StoredSettings]) async throws -> SettingsStore { + let store = SettingsStore(store: PersistenceController.mock(), expireAfter: .days(3650)) + for record in settings { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + store.storeSettings(record) { error in + if let error { continuation.resume(throwing: error) } + else { continuation.resume() } + } + } + } + return store + } + func testFetchDataWithHighInsulinNeedsPresetMitigation() async throws { var input = try await loopDataManager.fetchData(for: now) XCTAssertEqual(input.targetMgdl.count, 1) From 596606a22dc6046142469ea9d3815a1812fd687d Mon Sep 17 00:00:00 2001 From: Garry Casey Date: Tue, 3 Nov 2026 20:08:12 +0000 Subject: [PATCH 15/15] Keep Trends chart labels readable at accessibility sizes --- Loop/Redesign/DayCharts.swift | 5 ++++- Loop/Redesign/LoopDesign.swift | 7 +++++-- Loop/Redesign/TrendsComponents.swift | 27 +++++++++++++++++++++------ 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Loop/Redesign/DayCharts.swift b/Loop/Redesign/DayCharts.swift index 58bf768f5e..900af50953 100644 --- a/Loop/Redesign/DayCharts.swift +++ b/Loop/Redesign/DayCharts.swift @@ -118,6 +118,7 @@ struct InsulinTimelineView: View { @State private var selection: Date? @Environment(\.colorScheme) private var colorScheme + @Environment(\.dynamicTypeSize) private var typeSize private static let insulinTop = 30.0 private static let basalTop = 12.0 @@ -284,7 +285,9 @@ struct InsulinTimelineView: View { .chartXScale(domain: window.start...window.end) .chartYScale(domain: 0...104) .chartXAxis { - AxisMarks(values: DayAxis.quarterDayMarks(in: window)) { value in + AxisMarks(values: DayAxis.quarterDayMarks(in: window).filter { + !typeSize.isAccessibilitySize || Calendar.current.component(.hour, from: $0).isMultiple(of: 12) + }) { value in AxisValueLabel { if let date = value.as(Date.self) { Text(DayAxis.hourLabel(date)) diff --git a/Loop/Redesign/LoopDesign.swift b/Loop/Redesign/LoopDesign.swift index f17bae7236..931563a6b4 100644 --- a/Loop/Redesign/LoopDesign.swift +++ b/Loop/Redesign/LoopDesign.swift @@ -100,14 +100,17 @@ struct LoopCard: View { struct CardHeader: View { let title: String @ViewBuilder var trailing: Trailing + @Environment(\.dynamicTypeSize) private var typeSize var body: some View { - HStack(alignment: .firstTextBaseline) { + AdaptiveRow(spacing: typeSize.isAccessibilitySize ? LoopDesign.Space.s1 : LoopDesign.Space.s2) { Text(title) .font(LoopDesign.Typeface.headline) .foregroundStyle(LoopDesign.Palette.ink) .accessibilityAddTraits(.isHeader) - Spacer(minLength: LoopDesign.Space.s2) + if !typeSize.isAccessibilitySize { + Spacer(minLength: LoopDesign.Space.s2) + } trailing .font(LoopDesign.Typeface.footnote) .foregroundStyle(LoopDesign.Palette.inkMuted) diff --git a/Loop/Redesign/TrendsComponents.swift b/Loop/Redesign/TrendsComponents.swift index fadcb4d7f8..08e8346260 100644 --- a/Loop/Redesign/TrendsComponents.swift +++ b/Loop/Redesign/TrendsComponents.swift @@ -343,6 +343,7 @@ struct TrendsAGPChart: View { let unit: GlucoseUnitStyle let targetRange: ClosedRange? var height: CGFloat = 180 + @Environment(\.dynamicTypeSize) private var typeSize private func v(_ mgdl: Double) -> Double { min(max(unit.value(mgdl), unit.agpDomain.lowerBound), unit.agpDomain.upperBound) @@ -414,7 +415,7 @@ struct TrendsAGPChart: View { .chartXScale(domain: 0...1440) .chartYScale(domain: unit.agpDomain) .chartXAxis { - AxisMarks(values: Self.hourTicks) { value in + AxisMarks(values: typeSize.isAccessibilitySize ? [0, 720, 1440] : Self.hourTicks) { value in AxisGridLine().foregroundStyle(P.grid) AxisValueLabel { if let minute = value.as(Int.self) { Text(Self.label(minute: minute)) } @@ -424,7 +425,8 @@ struct TrendsAGPChart: View { } } .chartYAxis { - AxisMarks(position: .leading, values: unit.agpTicks) { _ in + AxisMarks(position: .leading, values: typeSize.isAccessibilitySize + ? [unit.agpTicks[0], unit.agpTicks[2], unit.agpTicks[4]] : unit.agpTicks) { _ in AxisGridLine().foregroundStyle(P.grid) AxisValueLabel().font(LoopDesign.Typeface.caption).foregroundStyle(P.inkMuted) } @@ -660,6 +662,7 @@ struct InsulinDailyTotalsView: View { let average: Double var height: CGFloat = 170 @Environment(\.colorScheme) private var scheme + @Environment(\.dynamicTypeSize) private var typeSize private var yMax: Double { let top = max(totals.map(\.total).max() ?? 0, average) @@ -726,7 +729,8 @@ struct InsulinDailyTotalsView: View { .chartYScale(domain: 0...yMax) .chartXAxis { AxisMarks(values: axisDates) { value in - AxisValueLabel(centered: true) { + AxisValueLabel(centered: !typeSize.isAccessibilitySize, + anchor: typeSize.isAccessibilitySize ? (value.index == 0 ? .topLeading : .topTrailing) : nil) { if let date = value.as(Date.self) { Text(axisLabel(date)) .multilineTextAlignment(.center) @@ -737,9 +741,16 @@ struct InsulinDailyTotalsView: View { } } .chartYAxis { - AxisMarks(position: .leading) { _ in - AxisGridLine().foregroundStyle(P.grid) - AxisValueLabel().font(LoopDesign.Typeface.caption).foregroundStyle(P.inkMuted) + if typeSize.isAccessibilitySize { + AxisMarks(position: .leading, values: [0, yMax / 2, yMax]) { _ in + AxisGridLine().foregroundStyle(P.grid) + AxisValueLabel().font(LoopDesign.Typeface.caption).foregroundStyle(P.inkMuted) + } + } else { + AxisMarks(position: .leading) { _ in + AxisGridLine().foregroundStyle(P.grid) + AxisValueLabel().font(LoopDesign.Typeface.caption).foregroundStyle(P.inkMuted) + } } } .accessibilityElement(children: .ignore) @@ -750,6 +761,10 @@ struct InsulinDailyTotalsView: View { } private var axisDates: [Date] { + // Full dates at the largest text sizes need the width between the two ends of the chart. + if typeSize.isAccessibilitySize, let first = totals.first?.day, let last = totals.last?.day { + return first == last ? [first] : [first, last] + } if weekly { let stride = max(1, Int(ceil(Double(totals.count) / 6))) return totals.enumerated().compactMap { index, total in index % stride == 0 ? total.day : nil }