diff --git a/.gitignore b/.gitignore index 35849edfe..c4c82bab5 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,6 @@ docs/LiveActivityTestPlan.md CLAUDE.md node_modules/ + +# Python helper scripts +__pycache__/ diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 560cd2f8e..f6c94d172 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -313,6 +313,7 @@ knownRegions = ( en, Base, + tr, ); mainGroup = FC97880B2485969B00A7906C; packageReferences = ( diff --git a/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift b/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift index b603f6152..e901e3b7c 100644 --- a/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift +++ b/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift @@ -16,7 +16,8 @@ struct AddAlarmSheet: View { private func matches(_ type: AlarmType) -> Bool { let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines) guard !query.isEmpty else { return true } - return type.rawValue.localizedCaseInsensitiveContains(query) + return type.displayName.localizedCaseInsensitiveContains(query) + || type.rawValue.localizedCaseInsensitiveContains(query) || type.blurb.localizedCaseInsensitiveContains(query) || type.group.rawValue.localizedCaseInsensitiveContains(query) } diff --git a/LoopFollow/Alarm/AddAlarm/AlarmTile.swift b/LoopFollow/Alarm/AddAlarm/AlarmTile.swift index 4b4ab452f..a81a1f117 100644 --- a/LoopFollow/Alarm/AddAlarm/AlarmTile.swift +++ b/LoopFollow/Alarm/AddAlarm/AlarmTile.swift @@ -13,7 +13,7 @@ struct AlarmTile: View { Image(systemName: type.icon) .font(.title2) .foregroundColor(.accentColor) - Text(type.rawValue) + Text(type.displayName) .font(.subheadline) .multilineTextAlignment(.center) .lineLimit(2) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index f429aa04a..024778605 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -11,7 +11,13 @@ protocol DayNightDisplayable { extension DayNightDisplayable where Self: RawRepresentable, Self.RawValue == String { var displayName: String { - rawValue == "always" ? "Day & Night" : rawValue.capitalized + switch rawValue { + case "always": String(localized: "Day & Night", comment: "Alarm schedule option: at any time of day") + case "day": String(localized: "Day", comment: "Alarm schedule option: daytime only") + case "night": String(localized: "Night", comment: "Alarm schedule option: nighttime only") + case "never": String(localized: "Never", comment: "Alarm schedule option: never") + default: rawValue.capitalized + } } } @@ -277,7 +283,7 @@ struct Alarm: Identifiable, Codable, Equatable { } }() - AlarmManager.shared.sendNotification(title: type.rawValue, actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze") + AlarmManager.shared.sendNotification(title: type.displayName, actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze") if playSound { AlarmSound.setSoundFile(soundFile) @@ -289,7 +295,7 @@ struct Alarm: Identifiable, Codable, Equatable { init(type: AlarmType) { self.type = type - name = type.rawValue + name = type.displayName switch type { case .buildExpire: @@ -448,30 +454,30 @@ extension AlarmType { var blurb: String { switch self { - case .low: return "Alerts when BG goes below a limit." - case .high: return "Alerts when BG rises above a limit." - case .fastDrop: return "Rapid downward BG trend." - case .fastRise: return "Rapid upward BG trend." - case .missedReading: return "No CGM data for X minutes." - case .iob: return "High insulin-on-board." - case .cob: return "High carbs-on-board." - case .missedBolus: return "Carbs without bolus." - case .futureCarbs: return "Reminder when future carbs are due." - case .recBolus: return "Recommended bolus issued." - case .battery: return "Phone battery low." - case .batteryDrop: return "Battery drops quickly." - case .pump: return "Reservoir level low." - case .pumpBattery: return "Pump battery low." - case .pumpChange: return "Pump change due." - case .sensorChange: return "Sensor change due." - case .notLooping: return "Loop hasn’t completed." - case .buildExpire: return "Looping-app build expiring." - case .dbSize: return "Nightscout database filling up." - case .overrideStart: return "Override just started." - case .overrideEnd: return "Override ended." - case .tempTargetStart: return "Temp target started." - case .tempTargetEnd: return "Temp target ended." - case .temporary: return "One-time BG limit alert." + case .low: return String(localized: "Alerts when BG goes below a limit.", comment: "Alarm type blurb") + case .high: return String(localized: "Alerts when BG rises above a limit.", comment: "Alarm type blurb") + case .fastDrop: return String(localized: "Rapid downward BG trend.", comment: "Alarm type blurb") + case .fastRise: return String(localized: "Rapid upward BG trend.", comment: "Alarm type blurb") + case .missedReading: return String(localized: "No CGM data for X minutes.", comment: "Alarm type blurb") + case .iob: return String(localized: "High insulin-on-board.", comment: "Alarm type blurb") + case .cob: return String(localized: "High carbs-on-board.", comment: "Alarm type blurb") + case .missedBolus: return String(localized: "Carbs without bolus.", comment: "Alarm type blurb") + case .futureCarbs: return String(localized: "Reminder when future carbs are due.", comment: "Alarm type blurb") + case .recBolus: return String(localized: "Recommended bolus issued.", comment: "Alarm type blurb") + case .battery: return String(localized: "Phone battery low.", comment: "Alarm type blurb") + case .batteryDrop: return String(localized: "Battery drops quickly.", comment: "Alarm type blurb") + case .pump: return String(localized: "Reservoir level low.", comment: "Alarm type blurb") + case .pumpBattery: return String(localized: "Pump battery low.", comment: "Alarm type blurb") + case .pumpChange: return String(localized: "Pump change due.", comment: "Alarm type blurb") + case .sensorChange: return String(localized: "Sensor change due.", comment: "Alarm type blurb") + case .notLooping: return String(localized: "Loop hasn’t completed.", comment: "Alarm type blurb") + case .buildExpire: return String(localized: "Looping-app build expiring.", comment: "Alarm type blurb") + case .dbSize: return String(localized: "Nightscout database filling up.", comment: "Alarm type blurb") + case .overrideStart: return String(localized: "Override just started.", comment: "Alarm type blurb") + case .overrideEnd: return String(localized: "Override ended.", comment: "Alarm type blurb") + case .tempTargetStart: return String(localized: "Temp target started.", comment: "Alarm type blurb") + case .tempTargetEnd: return String(localized: "Temp target ended.", comment: "Alarm type blurb") + case .temporary: return String(localized: "One-time BG limit alert.", comment: "Alarm type blurb") } } } diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 14901c5a1..2c8287384 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -39,7 +39,7 @@ struct AlarmEditor: View { } } } - .navigationTitle(alarm.type.rawValue) + .navigationTitle(alarm.type.displayName) } .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) } diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift index e78de27a2..acc9e68f2 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift @@ -74,7 +74,7 @@ struct AlarmAudioSection: View { } struct AlarmEnumMenuPicker: View { - let title: String + let title: LocalizedStringKey @Binding var selection: E var allowed: [E] @@ -101,7 +101,7 @@ struct AlarmEnumMenuPicker: Vi } extension AlarmEnumMenuPicker where E: CaseIterable { - init(title: String, selection: Binding) { + init(title: LocalizedStringKey, selection: Binding) { self.title = title _selection = selection allowed = Array(E.allCases) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift index e9650d69d..fa32664db 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift @@ -5,10 +5,10 @@ import SwiftUI struct AlarmBGLimitSection: View { // ────────── Public API ────────── - let header: String? - let footer: String? + let header: LocalizedStringKey? + let footer: LocalizedStringKey? let toggleText: String - let pickerTitle: String + let pickerTitle: LocalizedStringKey let range: ClosedRange let defaultOnValue: Double @@ -16,10 +16,10 @@ struct AlarmBGLimitSection: View { // ──────────────────────────────── init( - header: String? = nil, - footer: String? = nil, + header: LocalizedStringKey? = nil, + footer: LocalizedStringKey? = nil, toggleText: String, - pickerTitle: String, + pickerTitle: LocalizedStringKey, range: ClosedRange, defaultOnValue: Double? = nil, value: Binding @@ -60,8 +60,8 @@ struct AlarmBGLimitSection: View { var body: some View { Section( - header: header.map(Text.init), - footer: footer.map(Text.init) + header: header.map { Text($0) }, + footer: footer.map { Text($0) } ) { Toggle(toggleText, isOn: isOn) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index 25ed6624b..7dd13ae4e 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -7,9 +7,9 @@ import SwiftUI struct AlarmBGSection: View { // MARK: – public parameters - let header: String? - let footer: String? - let title: String + let header: LocalizedStringKey? + let footer: LocalizedStringKey? + let title: LocalizedStringKey let range: ClosedRange // MARK: – underlying optional binding @@ -19,9 +19,9 @@ struct AlarmBGSection: View { // MARK: – designated initialiser init( - header: String? = nil, - footer: String? = nil, - title: String, + header: LocalizedStringKey? = nil, + footer: LocalizedStringKey? = nil, + title: LocalizedStringKey, range: ClosedRange, value: Binding ) { @@ -45,8 +45,8 @@ struct AlarmBGSection: View { var body: some View { Section( - header: header.map(Text.init), - footer: footer.map(Text.init) + header: header.map { Text($0) }, + footer: footer.map { Text($0) } ) { BGPicker( title: title, diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift index 671f90f10..da2316b6c 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift @@ -6,9 +6,9 @@ import SwiftUI struct AlarmStepperSection: View { // MARK: – public parameters - let header: String? - let footer: String? - let title: String + let header: LocalizedStringKey? + let footer: LocalizedStringKey? + let title: LocalizedStringKey let range: ClosedRange let step: Double let unitLabel: String? @@ -21,9 +21,9 @@ struct AlarmStepperSection: View { // MARK: – designated initialiser (Double?) init( - header: String? = nil, - footer: String? = nil, - title: String, + header: LocalizedStringKey? = nil, + footer: LocalizedStringKey? = nil, + title: LocalizedStringKey, range: ClosedRange, step: Double, unitLabel: String? = nil, @@ -42,9 +42,9 @@ struct AlarmStepperSection: View { /// Same API but for **`Binding`** — it bridges to Double internally. init( - header: String? = nil, - footer: String? = nil, - title: String, + header: LocalizedStringKey? = nil, + footer: LocalizedStringKey? = nil, + title: LocalizedStringKey, range: ClosedRange, step: Double, unitLabel: String? = nil, @@ -90,8 +90,8 @@ struct AlarmStepperSection: View { var body: some View { Section( - header: header.map(Text.init), - footer: footer.map(Text.init) + header: header.map { Text($0) }, + footer: footer.map { Text($0) } ) { Stepper(value: nonOptional, in: range, step: step) { HStack { diff --git a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift index 2250e1160..599d2477b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift @@ -5,7 +5,7 @@ import SwiftUI struct InfoBanner: View { /// Main explanatory text - let text: String + let text: LocalizedStringKey /// Optional alarm type whose icon you’d like to show. /// If `nil`, we fall back to the standard “info” symbol. diff --git a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift index 4d0f8e968..b2b425497 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift @@ -31,7 +31,7 @@ enum SoundFile: Hashable, Identifiable { .replacingOccurrences(of: "_", with: " ") .replacingOccurrences(of: " ", with: " ") case let .custom(uuid): - return CustomSoundStore.shared.displayName(for: uuid) ?? "Custom Sound" + return CustomSoundStore.shared.displayName(for: uuid) ?? String(localized: "Custom Sound") } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 3121f8424..d48733e0b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -9,8 +9,7 @@ struct BuildExpireAlarmEditor: View { var body: some View { Group { InfoBanner( - text: "Sends a reminder before the looping-app build you’re following reaches its " - + "TestFlight or Xcode expiry date. Works with Trio 0.4 and later." + text: "Sends a reminder before the looping-app build you’re following reaches its TestFlight or Xcode expiry date. Works with Trio 0.4 and later." ) AlarmGeneralSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index 76ae5219f..c832a4582 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -9,9 +9,7 @@ struct FastRiseAlarmEditor: View { var body: some View { Group { InfoBanner( - text: "Alerts when glucose readings rise rapidly. For example, " - + "three straight readings each climbing by at least the amount " - + "you set. Optionally limit alerts to only fire above a certain BG.", + text: "Alerts when glucose readings rise rapidly. For example, three straight readings each climbing by at least the amount you set. Optionally limit alerts to only fire above a certain BG.", alarmType: alarm.type ) @@ -27,8 +25,7 @@ struct FastRiseAlarmEditor: View { AlarmStepperSection( header: "Consecutive Rises", - footer: "Number of rises—each meeting the rate above—" - + "required before an alert fires.", + footer: "Number of rises—each meeting the rate above—required before an alert fires.", title: "Rises in a row", range: 1 ... 3, step: 1, @@ -37,8 +34,7 @@ struct FastRiseAlarmEditor: View { AlarmBGLimitSection( header: "BG Limit", - footer: "When enabled, this alert only fires if the glucose is " - + "above the limit you set.", + footer: "When enabled, this alert only fires if the glucose is above the limit you set.", toggleText: "Use BG Limit", pickerTitle: "Rising above", range: 40 ... 300, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FutureCarbsAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FutureCarbsAlarmEditor.swift index 0df1e2177..35a18b90c 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FutureCarbsAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FutureCarbsAlarmEditor.swift @@ -9,9 +9,7 @@ struct FutureCarbsAlarmEditor: View { var body: some View { Group { InfoBanner( - text: "Alerts when a future-dated carb entry's scheduled time arrives — " + - "a reminder to start eating. Use the max lookahead to ignore " + - "fat/protein entries that are typically scheduled further ahead.", + text: "Alerts when a future-dated carb entry's scheduled time arrives — a reminder to start eating. Use the max lookahead to ignore fat/protein entries that are typically scheduled further ahead.", alarmType: alarm.type ) @@ -19,8 +17,7 @@ struct FutureCarbsAlarmEditor: View { AlarmStepperSection( header: "Max Lookahead", - footer: "Only track carb entries scheduled up to this many minutes " + - "in the future. Entries beyond this window are ignored.", + footer: "Only track carb entries scheduled up to this many minutes in the future. Entries beyond this window are ignored.", title: "Lookahead", range: 5 ... 120, step: 5, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 1adf217aa..e2ca342db 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -9,8 +9,7 @@ struct HighBgAlarmEditor: View { var body: some View { Group { InfoBanner( - text: "Alerts when glucose stays above the limit " - + "you set below. Use Persistent if you want to ignore brief spikes." + text: "Alerts when glucose stays above the limit you set below. Use Persistent if you want to ignore brief spikes." ) AlarmGeneralSection(alarm: $alarm) @@ -25,8 +24,7 @@ struct HighBgAlarmEditor: View { AlarmStepperSection( header: "Persistent High", - footer: "How long glucose must remain above the threshold before the " - + "alarm actually fires. Set to 0 for an immediate alert.", + footer: "How long glucose must remain above the threshold before the alarm actually fires. Set to 0 for an immediate alert.", title: "Persistent for", range: 0 ... 120, step: 5, @@ -36,8 +34,7 @@ struct HighBgAlarmEditor: View { Section( header: Text("FALLING BG"), - footer: Text("Stay silent while BG is falling. The alert only sounds " - + "when the latest reading is flat or still rising.") + footer: Text("Stay silent while BG is falling. The alert only sounds when the latest reading is flat or still rising.") ) { Toggle("Skip if BG is falling", isOn: $alarm.suppressIfFalling) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift index ba714fabe..0fad4ff07 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift @@ -9,8 +9,7 @@ struct IOBAlarmEditor: View { var body: some View { Group { InfoBanner( - text: "Alerts when insulin-on-board is high, or when several " - + "boluses in quick succession exceed the limits you set.", + text: "Alerts when insulin-on-board is high, or when several boluses in quick succession exceed the limits you set.", alarmType: alarm.type ) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index a21db2239..8894b1f23 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -22,8 +22,7 @@ struct LowBgAlarmEditor: View { AlarmStepperSection( header: "PERSISTENCE", - footer: "Glucose must stay below the threshold for this many minutes " - + "before the alert sounds. Set 0 to alert immediately.", + footer: "Glucose must stay below the threshold for this many minutes before the alert sounds. Set 0 to alert immediately.", title: "Persistent", range: 0 ... 120, step: 5, @@ -33,9 +32,7 @@ struct LowBgAlarmEditor: View { AlarmStepperSection( header: "PREDICTION", - footer: "Look ahead this many minutes in the forecast; " - + "if any future value is at or below the threshold, " - + "you’ll be warned early. Set 0 to disable.", + footer: "Look ahead this many minutes in the forecast; if any future value is at or below the threshold, you’ll be warned early. Set 0 to disable.", title: "Predictive", range: 0 ... 60, step: 5, @@ -45,8 +42,7 @@ struct LowBgAlarmEditor: View { Section( header: Text("RISING BG"), - footer: Text("Stay silent while BG is rising. The alert only sounds " - + "when the latest reading is flat or still falling.") + footer: Text("Stay silent while BG is rising. The alert only sounds when the latest reading is flat or still falling.") ) { Toggle("Skip if BG is rising", isOn: $alarm.suppressIfRising) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift index 10f1226b4..756e63c9c 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift @@ -9,9 +9,7 @@ struct MissedBolusAlarmEditor: View { var body: some View { Group { InfoBanner( - text: "Alerts when carbs are logged but no bolus is delivered " + - "within the delay below. Allows small-carb / treatment " + - "exclusions and pre-bolus detection.", + text: "Alerts when carbs are logged but no bolus is delivered within the delay below. Allows small-carb / treatment exclusions and pre-bolus detection.", alarmType: alarm.type ) @@ -19,8 +17,7 @@ struct MissedBolusAlarmEditor: View { AlarmStepperSection( header: "Delay", - footer: "Minutes to wait after the carb entry before checking " + - "for a bolus.", + footer: "Minutes to wait after the carb entry before checking for a bolus.", title: "Delay", range: 5 ... 60, step: 5, @@ -30,8 +27,7 @@ struct MissedBolusAlarmEditor: View { AlarmStepperSection( header: "Pre-bolus", - footer: "Count boluses given up to this many minutes before " + - "the carb entry as valid.", + footer: "Count boluses given up to this many minutes before the carb entry as valid.", title: "Pre-Bolus Time", range: 0 ... 45, step: 5, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift index 678e18f28..d557e3f05 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -11,16 +11,14 @@ struct NotLoopingAlarmEditor: View { var body: some View { Group { InfoBanner( - text: "Alerts when no successful loop has occurred for the time " - + "you set below.", alarmType: alarm.type + text: "Alerts when no successful loop has occurred for the time you set below.", alarmType: alarm.type ) AlarmGeneralSection(alarm: $alarm) AlarmStepperSection( header: "No Loop for…", - footer: "Number of minutes since the last successful loop. " - + "When this time has elapsed, the alarm becomes eligible.", + footer: "Number of minutes since the last successful loop. When this time has elapsed, the alarm becomes eligible.", title: "Elapsed time", range: 16 ... 61, step: 5, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift index d08ee68ed..b39b500e9 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PhoneBatteryAlarmEditor.swift @@ -27,8 +27,7 @@ struct PhoneBatteryAlarmEditor: View { Section( header: Text("CHARGING"), - footer: Text("Stay silent while the phone is charging. Requires the " - + "uploader to report charging status; if it doesn't, the alert still sounds.") + footer: Text("Stay silent while the phone is charging. Requires the uploader to report charging status; if it doesn't, the alert still sounds.") ) { Toggle("Skip while charging", isOn: $alarm.suppressIfCharging) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift index dedd0cc0a..4f9a91120 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift @@ -9,9 +9,7 @@ struct PumpChangeAlarmEditor: View { var body: some View { Group { InfoBanner( - text: "Alerts when the pump / cannula is within the time " - + "window you choose below (relative to the 3-day change " - + "limit).", + text: "Alerts when the pump / cannula is within the time window you choose below (relative to the 3-day change limit).", alarmType: alarm.type ) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift index 85525d4e4..48c4ba292 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift @@ -17,8 +17,7 @@ struct PumpVolumeAlarmEditor: View { AlarmStepperSection( header: "Trigger Level", - footer: "An alert fires once the reservoir is at this value " - + "or lower.", + footer: "An alert fires once the reservoir is at this value or lower.", title: "Reservoir Below", range: 1 ... 50, step: 1, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift index 5d2f66b4d..5acd6d535 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift @@ -9,8 +9,7 @@ struct RecBolusAlarmEditor: View { var body: some View { Group { InfoBanner( - text: "Alerts when the recommended bolus equals or exceeds the " + - "threshold you set below.", + text: "Alerts when the recommended bolus equals or exceeds the threshold you set below.", alarmType: alarm.type ) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift index 9f425de4c..f7f968417 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift @@ -28,8 +28,7 @@ struct SensorAgeAlarmEditor: View { AlarmStepperSection( header: "Sensor Lifetime", - footer: "Number of days your CGM sensor lasts " + - "(e.g. 10 for Dexcom G6, 15 for G7 15-day).", + footer: "Number of days your CGM sensor lasts (e.g. 10 for Dexcom G6, 15 for G7 15-day).", title: "Lifetime", range: 7 ... 15, step: 1, @@ -39,8 +38,7 @@ struct SensorAgeAlarmEditor: View { AlarmStepperSection( header: "Early Reminder", - footer: "Number of hours before the \(lifetimeDays)-day mark that the alert " + - "will fire.", + footer: "Number of hours before the \(lifetimeDays)-day mark that the alert will fire.", title: "Reminder Time", range: 1 ... 24, step: 1, diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 9748f99c3..8f737757b 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -30,6 +30,7 @@ struct AlarmListView: View { let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines) guard !query.isEmpty else { return true } return alarm.name.localizedCaseInsensitiveContains(query) + || alarm.type.displayName.localizedCaseInsensitiveContains(query) || alarm.type.rawValue.localizedCaseInsensitiveContains(query) } diff --git a/LoopFollow/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift index 3f328bfa4..27e28e04c 100644 --- a/LoopFollow/Alarm/AlarmSettingsView.swift +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -120,9 +120,7 @@ struct AlarmSettingsView: View { Section( header: Text("Day / Night Schedule"), - footer: Text("Pick when your day period begins and when your night period begins. " + - "Any time from your Day-starts time up until your Night-starts time will count as day; " + - "from Night-starts until the next Day-starts will count as night.") + footer: Text("Pick when your day period begins and when your night period begins. Any time from your Day-starts time up until your Night-starts time will count as day; from Night-starts until the next Day-starts will count as night.") ) { DatePicker( "Day starts", diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift index 9cda0ff3c..5f6034d28 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift @@ -19,10 +19,10 @@ enum TimeUnit { /// A user-facing label var label: String { switch self { - case .minute: return "min" - case .hour: return "hours" - case .day: return "days" - case .none: return "none" + case .minute: return String(localized: "min", comment: "Alarm time unit") + case .hour: return String(localized: "hours", comment: "Alarm time unit") + case .day: return String(localized: "days", comment: "Alarm time unit") + case .none: return String(localized: "none", comment: "Alarm time unit") } } } diff --git a/LoopFollow/Alarm/AlarmType/AlarmType.swift b/LoopFollow/Alarm/AlarmType/AlarmType.swift index 45c2aa9af..9d08a60cd 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType.swift @@ -50,3 +50,36 @@ extension AlarmType { } } } + +extension AlarmType { + /// User-facing, localized name. `rawValue` is persisted in Storage and must never be + /// shown to the user or translated; every display site goes through this property. + var displayName: String { + switch self { + case .temporary: String(localized: "Temporary Alert", comment: "Alarm type name") + case .iob: String(localized: "IOB Alert", comment: "Alarm type name") + case .cob: String(localized: "COB Alert", comment: "Alarm type name") + case .low: String(localized: "Low BG Alert", comment: "Alarm type name") + case .high: String(localized: "High BG Alert", comment: "Alarm type name") + case .fastDrop: String(localized: "Fast Drop Alert", comment: "Alarm type name") + case .fastRise: String(localized: "Fast Rise Alert", comment: "Alarm type name") + case .missedReading: String(localized: "Missed Reading Alert", comment: "Alarm type name") + case .notLooping: String(localized: "Not Looping Alert", comment: "Alarm type name") + case .missedBolus: String(localized: "Missed Bolus Alert", comment: "Alarm type name") + case .futureCarbs: String(localized: "Future Carbs Alert", comment: "Alarm type name") + case .sensorChange: String(localized: "Sensor Change Alert", comment: "Alarm type name") + case .pumpChange: String(localized: "Pump Change Alert", comment: "Alarm type name") + case .pump: String(localized: "Pump Insulin Alert", comment: "Alarm type name") + case .pumpBattery: String(localized: "Pump Battery Alert", comment: "Alarm type name") + case .battery: String(localized: "Low Battery", comment: "Alarm type name") + case .batteryDrop: String(localized: "Battery Drop", comment: "Alarm type name") + case .recBolus: String(localized: "Rec. Bolus", comment: "Alarm type name: recommended bolus") + case .overrideStart: String(localized: "Override Started", comment: "Alarm type name") + case .overrideEnd: String(localized: "Override Ended", comment: "Alarm type name") + case .tempTargetStart: String(localized: "Temp Target Started", comment: "Alarm type name") + case .tempTargetEnd: String(localized: "Temp Target Ended", comment: "Alarm type name") + case .buildExpire: String(localized: "Looping app expiration", comment: "Alarm type name") + case .dbSize: String(localized: "Nightscout Database Size", comment: "Alarm type name") + } + } +} diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index 3c364fc2c..69688045f 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -242,7 +242,9 @@ final class AppSceneDelegate: NSObject, UIWindowSceneDelegate { return false } Storage.shared.speakBG.value.toggle() - let message = Storage.shared.speakBG.value ? "BG Speak is now on" : "BG Speak is now off" + let message = Storage.shared.speakBG.value + ? String(localized: "BG Speak is now on") + : String(localized: "BG Speak is now off") speechSynthesizer.speak(AVSpeechUtterance(string: message)) return true } diff --git a/LoopFollow/Charts/BGChartModel.swift b/LoopFollow/Charts/BGChartModel.swift index 8acc2859c..be549a416 100644 --- a/LoopFollow/Charts/BGChartModel.swift +++ b/LoopFollow/Charts/BGChartModel.swift @@ -237,19 +237,19 @@ final class BGChartModel: ObservableObject { { var actionParts: [String] = [] if let bolusAmount = json["bolus-entry"] as? Double { - actionParts.append("Bolus: \(bolusAmount) U") + actionParts.append(String(localized: "Bolus: \(bolusAmount, specifier: "%g") U", comment: "Treatment pill detail")) } if let carbsAmount = json["carbs-entry"] as? Double { - actionParts.append("Carbs: \(carbsAmount) g") + actionParts.append(String(localized: "Carbs: \(carbsAmount, specifier: "%g") g", comment: "Treatment pill detail")) } if let absorptionTime = json["absorption-time"] as? Double { - actionParts.append("Absorption: \(absorptionTime) hrs") + actionParts.append(String(localized: "Absorption: \(absorptionTime, specifier: "%g") hrs", comment: "Treatment pill detail")) } if let otp = json["otp"] as? String { - actionParts.append("OTP: \(otp)") + actionParts.append(String(localized: "OTP: \(otp)", comment: "Treatment pill detail")) } if let enteredBy = json["entered-by"] as? String { - actionParts.append("From: \(enteredBy)") + actionParts.append(String(localized: "From: \(enteredBy)", comment: "Treatment pill detail: uploader name")) } if !actionParts.isEmpty { actionContext = " [" + actionParts.joined(separator: ", ") + "]" @@ -461,7 +461,7 @@ final class BGChartModel: ObservableObject { value: $0.value, sgv: Double($0.sgv), label: dose, - pillText: "Bolus\n\(dose)U\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + pillText: "\(String(localized: "Bolus", comment: "Chart pill"))\n\(dose)U\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" ) } carbs = Self.spread((showCarbs ? vc.carbData : []).map { @@ -475,7 +475,7 @@ final class BGChartModel: ObservableObject { value: $0.value, sgv: Double($0.sgv), label: label, - pillText: "Carbs\n\(grams)g\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + pillText: "\(String(localized: "Carbs", comment: "Chart pill"))\n\(grams)g\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" ) }, minGap: Spread.carbGap, maxShift: Spread.carbShift) let smbPoints = (showBolus ? vc.smbData : []).map { @@ -485,7 +485,7 @@ final class BGChartModel: ObservableObject { value: $0.value, sgv: Double($0.sgv), label: dose, - pillText: "SMB\n\(dose)U\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + pillText: "\(String(localized: "SMB", comment: "Chart pill"))\n\(dose)U\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" ) } (boluses, smbs) = Self.spreadTogether(bolusPoints, smbPoints, minGap: Spread.bolusGap, maxShift: Spread.bolusShift) @@ -495,17 +495,17 @@ final class BGChartModel: ObservableObject { value: Double($0.sgv), sgv: Double($0.sgv), label: "", - pillText: "BG Check\n\(Localizer.toDisplayUnits(String($0.sgv)))\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + pillText: "\(String(localized: "BG Check", comment: "Chart pill"))\n\(Localizer.toDisplayUnits(String($0.sgv)))\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" ) } suspends = (showOtherTreatments ? vc.suspendGraphData : []).map { - TreatmentPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), sgv: Double($0.sgv), label: "", pillText: "Suspend\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))") + TreatmentPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), sgv: Double($0.sgv), label: "", pillText: "\(String(localized: "Suspend", comment: "Pump suspended pill"))\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))") } resumes = (showOtherTreatments ? vc.resumeGraphData : []).map { - TreatmentPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), sgv: Double($0.sgv), label: "", pillText: "Resume\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))") + TreatmentPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), sgv: Double($0.sgv), label: "", pillText: "\(String(localized: "Resume", comment: "Pump resumed pill"))\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))") } sensorStarts = (showOtherTreatments ? vc.sensorStartGraphData : []).map { - TreatmentPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), sgv: Double($0.sgv), label: "", pillText: "Sensor Start\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))") + TreatmentPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), sgv: Double($0.sgv), label: "", pillText: "\(String(localized: "Sensor Start", comment: "Chart pill"))\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))") } notes = (showOtherTreatments ? vc.noteGraphData : []).map { TreatmentPoint( @@ -550,7 +550,7 @@ final class BGChartModel: ObservableObject { yBottom: yBottom, yTop: yTop, label: displayName, - pillText: "Override\n\(displayName)\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + pillText: "\(String(localized: "Override", comment: "Chart pill"))\n\(displayName)\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" ) } tempTargets = (showOtherTreatments ? vc.tempTargetGraphData : []).map { @@ -564,7 +564,7 @@ final class BGChartModel: ObservableObject { yBottom: yCenter - 5, yTop: yCenter + 5, label: "Temp Target", - pillText: "Temp Target\n\(Localizer.toDisplayUnits(target))\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" + pillText: "\(String(localized: "Temp Target", comment: "Chart pill"))\n\(Localizer.toDisplayUnits(target))\n\(pillTimeString(for: Date(timeIntervalSince1970: $0.date)))" ) } diff --git a/LoopFollow/Contact/ContactColorMode.swift b/LoopFollow/Contact/ContactColorMode.swift index 7d146040c..dd02ece4d 100644 --- a/LoopFollow/Contact/ContactColorMode.swift +++ b/LoopFollow/Contact/ContactColorMode.swift @@ -10,9 +10,9 @@ enum ContactColorMode: String, Codable, CaseIterable { var displayName: String { switch self { case .staticColor: - return "Static" + return String(localized: "Static", comment: "Contact color mode") case .dynamic: - return "Dynamic (BG Range)" + return String(localized: "Dynamic (BG Range)", comment: "Contact color mode") } } diff --git a/LoopFollow/Controllers/MainViewController+updateStats.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift index f3c4c0d10..3d3140c92 100644 --- a/LoopFollow/Controllers/MainViewController+updateStats.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -25,7 +25,9 @@ extension MainViewController { statsDisplayModel.highPercent = String(format: "%.1f%%", stats.percentHigh) statsDisplayModel.avgBG = Localizer.toDisplayUnits(String(format: "%.0f", stats.avgBG)) - statsDisplayModel.estA1CTitle = UnitSettingsStore.shared.glycemicMetricMode == .gmi ? "GMI:" : "Est. A1C:" + statsDisplayModel.estA1CTitle = UnitSettingsStore.shared.glycemicMetricMode == .gmi + ? String(localized: "GMI:") + : String(localized: "Est. A1C:") if UnitSettingsStore.shared.glycemicOutputUnit == .mmolMol { statsDisplayModel.estA1C = String(format: "%.0f", stats.a1C) } else { diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index a568746c7..8953747a2 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -292,9 +292,9 @@ extension MainViewController { self.updateBGTextAppearance() if latestBG <= globalVariables.minDisplayGlucose { - Observable.shared.bgText.value = "LOW" + Observable.shared.bgText.value = String(localized: "LOW", comment: "BG below sensor range") } else if latestBG >= globalVariables.maxDisplayGlucose { - Observable.shared.bgText.value = "HIGH" + Observable.shared.bgText.value = String(localized: "HIGH", comment: "BG above sensor range") } else { Observable.shared.bgText.value = Localizer.toDisplayUnits(String(latestBG)) } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 4cd3a65a1..72ab67eb8 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -46,7 +46,7 @@ extension MainViewController { IsNotLooping = true Observable.shared.isNotLooping.value = true - Observable.shared.loopStatusText.value = "⚠️ Not Looping!" + Observable.shared.loopStatusText.value = String(localized: "⚠️ Not Looping!") Observable.shared.loopStatusColor.value = .yellow #if !targetEnvironment(macCatalyst) LiveActivityManager.shared.refreshFromCurrentState(reason: "notLooping") diff --git a/LoopFollow/Controllers/StatsDisplayModel.swift b/LoopFollow/Controllers/StatsDisplayModel.swift index 72e60560a..acd29f646 100644 --- a/LoopFollow/Controllers/StatsDisplayModel.swift +++ b/LoopFollow/Controllers/StatsDisplayModel.swift @@ -9,9 +9,9 @@ class StatsDisplayModel: ObservableObject { @Published var highPercent: String = "" @Published var avgBG: String = "" @Published var estA1C: String = "" - @Published var estA1CTitle: String = "Est A1C:" + @Published var estA1CTitle = String(localized: "Est. A1C:") @Published var stdDev: String = "" - @Published var stdDevTitle: String = "Std Dev:" + @Published var stdDevTitle = String(localized: "Std Dev:") @Published var pieLow: Double = 0 @Published var pieRange: Double = 0 @Published var pieHigh: Double = 0 diff --git a/LoopFollow/Helpers/AppearanceMode.swift b/LoopFollow/Helpers/AppearanceMode.swift index 09feb73ba..f1d3e65cf 100644 --- a/LoopFollow/Helpers/AppearanceMode.swift +++ b/LoopFollow/Helpers/AppearanceMode.swift @@ -10,9 +10,9 @@ enum AppearanceMode: String, CaseIterable, Codable { var displayName: String { switch self { - case .system: return "System" - case .light: return "Light" - case .dark: return "Dark" + case .system: return String(localized: "System", comment: "Appearance mode") + case .light: return String(localized: "Light", comment: "Appearance mode") + case .dark: return String(localized: "Dark", comment: "Appearance mode") } } diff --git a/LoopFollow/Helpers/AuthService.swift b/LoopFollow/Helpers/AuthService.swift index d065054c8..aeaceba48 100644 --- a/LoopFollow/Helpers/AuthService.swift +++ b/LoopFollow/Helpers/AuthService.swift @@ -29,15 +29,15 @@ public enum AuthService { var error: NSError? guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { - var message = "Device authentication is not available. " + var message = String(localized: "Device authentication is not available. ") let biometryType = context.biometryType if biometryType == .none { - message += "Please enable Face ID, Touch ID, or set up a device passcode in Settings." + message += String(localized: "Please enable Face ID, Touch ID, or set up a device passcode in Settings.") } else if biometryType == .faceID { - message += "Face ID is not available. Please set up a device passcode in Settings." + message += String(localized: "Face ID is not available. Please set up a device passcode in Settings.") } else if biometryType == .touchID { - message += "Touch ID is not available. Please set up a device passcode in Settings." + message += String(localized: "Touch ID is not available. Please set up a device passcode in Settings.") } DispatchQueue.main.async { diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index 07a6246bc..be3066de8 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -17,19 +17,19 @@ class NightscoutUtils { var errorDescription: String? { switch self { case .emptyAddress: - return "The address is empty." + return String(localized: "The address is empty.", comment: "Nightscout error") case .invalidURL: - return "The URL is invalid." + return String(localized: "The URL is invalid.", comment: "Nightscout error") case .networkError: - return "A network error occurred." + return String(localized: "A network error occurred.", comment: "Nightscout error") case .siteNotFound: - return "The site was not found." + return String(localized: "The site was not found.", comment: "Nightscout error") case .invalidToken: - return "The token is invalid." + return String(localized: "The token is invalid.", comment: "Nightscout error") case .tokenRequired: - return "A token is required." + return String(localized: "A token is required.", comment: "Nightscout error") case .unknown: - return "An unknown error occurred." + return String(localized: "An unknown error occurred.", comment: "Nightscout error") } } } diff --git a/LoopFollow/Helpers/PredictionDisplayType.swift b/LoopFollow/Helpers/PredictionDisplayType.swift index eb79940f8..f0e119c29 100644 --- a/LoopFollow/Helpers/PredictionDisplayType.swift +++ b/LoopFollow/Helpers/PredictionDisplayType.swift @@ -7,8 +7,8 @@ enum PredictionDisplayType: String, CaseIterable, Codable { var displayName: String { switch self { - case .cone: return "Cone" - case .lines: return "Lines" + case .cone: return String(localized: "Cone", comment: "Prediction display style") + case .lines: return String(localized: "Lines", comment: "Prediction display style") } } } diff --git a/LoopFollow/Helpers/TabPosition.swift b/LoopFollow/Helpers/TabPosition.swift index 7268ec20e..01c7cb349 100644 --- a/LoopFollow/Helpers/TabPosition.swift +++ b/LoopFollow/Helpers/TabPosition.swift @@ -12,11 +12,11 @@ enum TabPosition: String, CaseIterable, Codable, Comparable { var displayName: String { switch self { - case .position1: return "Tab 1" - case .position2: return "Tab 2" - case .position3: return "Tab 3" - case .position4: return "Tab 4" - case .menu, .more, .disabled: return "Menu" + case .position1: return String(localized: "Tab 1", comment: "Tab position") + case .position2: return String(localized: "Tab 2", comment: "Tab position") + case .position3: return String(localized: "Tab 3", comment: "Tab position") + case .position4: return String(localized: "Tab 4", comment: "Tab position") + case .menu, .more, .disabled: return String(localized: "Menu", comment: "Tab position") } } @@ -67,13 +67,13 @@ enum TabItem: String, CaseIterable, Codable, Identifiable { var displayName: String { switch self { - case .home: return "Home" - case .alarms: return "Alarms" - case .remote: return "Remote" - case .nightscout: return "Nightscout" - case .snoozer: return "Snoozer" - case .treatments: return "Treatments" - case .stats: return "Statistics" + case .home: return String(localized: "Home", comment: "Tab / feature name") + case .alarms: return String(localized: "Alarms", comment: "Tab / feature name") + case .remote: return String(localized: "Remote", comment: "Tab / feature name") + case .nightscout: return String(localized: "Nightscout", comment: "Tab / feature name") + case .snoozer: return String(localized: "Snoozer", comment: "Tab / feature name") + case .treatments: return String(localized: "Treatments", comment: "Tab / feature name") + case .stats: return String(localized: "Statistics", comment: "Tab / feature name") } } diff --git a/LoopFollow/Helpers/TextFieldWithToolBar.swift b/LoopFollow/Helpers/TextFieldWithToolBar.swift index d65fe3dee..3273f20ef 100644 --- a/LoopFollow/Helpers/TextFieldWithToolBar.swift +++ b/LoopFollow/Helpers/TextFieldWithToolBar.swift @@ -177,20 +177,20 @@ public struct TextFieldWithToolBar: UIViewRepresentable { formatter.maximumFractionDigits = self.unit.preferredFractionDigits let step = pow(10.0, Double(-formatter.maximumFractionDigits)) - var message = "Value outside of guardrails: \(text)\n" + var message = String(localized: "Value outside of guardrails: \(text)\n") if let minValue = self.parent.minValue { let minVal = minValue.doubleValue(for: self.unit) let adjustedMin = ceil(minVal / step) * step let minQuantity = HKQuantity(unit: self.unit, doubleValue: adjustedMin) - message += "Minimum: \(self.format(quantity: minQuantity, for: self.unit))\n" + message += String(localized: "Minimum: \(self.format(quantity: minQuantity, for: self.unit))\n") } if let maxValue = self.parent.maxValue { let maxVal = maxValue.doubleValue(for: self.unit) let adjustedMax = floor(maxVal / step) * step let maxQuantity = HKQuantity(unit: self.unit, doubleValue: adjustedMax) - message += "Maximum: \(self.format(quantity: maxQuantity, for: self.unit))" + message += String(localized: "Maximum: \(self.format(quantity: maxQuantity, for: self.unit))") } self.onValidationError(message) diff --git a/LoopFollow/Helpers/Views/BGPicker.swift b/LoopFollow/Helpers/Views/BGPicker.swift index 86c99a706..3bb493f30 100644 --- a/LoopFollow/Helpers/Views/BGPicker.swift +++ b/LoopFollow/Helpers/Views/BGPicker.swift @@ -6,7 +6,7 @@ import SwiftUI /// Lets the user pick a BG-related number (mg/dL or mmol/L) inside any form row. struct BGPicker: View { - let title: String + let title: LocalizedStringKey let range: ClosedRange @Binding var value: Double diff --git a/LoopFollow/Helpers/Views/ErrorMessageView.swift b/LoopFollow/Helpers/Views/ErrorMessageView.swift index ecaf7f882..cdfca1225 100644 --- a/LoopFollow/Helpers/Views/ErrorMessageView.swift +++ b/LoopFollow/Helpers/Views/ErrorMessageView.swift @@ -5,8 +5,8 @@ import Foundation import SwiftUI struct ErrorMessageView: View { - var message: String - var buttonTitle: String? + var message: LocalizedStringKey + var buttonTitle: LocalizedStringKey? var buttonAction: (() -> Void)? var body: some View { diff --git a/LoopFollow/Helpers/Views/HKQuantityInputView.swift b/LoopFollow/Helpers/Views/HKQuantityInputView.swift index 674a6da88..8daa686ed 100644 --- a/LoopFollow/Helpers/Views/HKQuantityInputView.swift +++ b/LoopFollow/Helpers/Views/HKQuantityInputView.swift @@ -6,7 +6,7 @@ import HealthKit import SwiftUI struct HKQuantityInputView: View { - var label: String + var label: LocalizedStringKey @Binding var quantity: HKQuantity var unit: HKUnit var maxLength: Int diff --git a/LoopFollow/Helpers/Views/LoadingButtonView.swift b/LoopFollow/Helpers/Views/LoadingButtonView.swift index 7e14f0a53..6caa9fe8d 100644 --- a/LoopFollow/Helpers/Views/LoadingButtonView.swift +++ b/LoopFollow/Helpers/Views/LoadingButtonView.swift @@ -4,8 +4,8 @@ import SwiftUI struct LoadingButtonView: View { - var buttonText: String - var progressText: String + var buttonText: LocalizedStringKey + var progressText: LocalizedStringKey var isLoading: Bool var action: () -> Void var isDisabled: Bool = false diff --git a/LoopFollow/Helpers/Views/QuickPickSectionHeader.swift b/LoopFollow/Helpers/Views/QuickPickSectionHeader.swift index e966de81d..719f7f74c 100644 --- a/LoopFollow/Helpers/Views/QuickPickSectionHeader.swift +++ b/LoopFollow/Helpers/Views/QuickPickSectionHeader.swift @@ -4,7 +4,7 @@ import SwiftUI struct QuickPickSectionHeader: View { - let title: String + let title: LocalizedStringKey let infoText: String @State private var showInfo = false @@ -26,7 +26,7 @@ struct QuickPickSectionHeader: View { } private struct QuickPickInfoSheet: View { - let title: String + let title: LocalizedStringKey let text: String @Environment(\.dismiss) private var dismiss @@ -51,19 +51,7 @@ private struct QuickPickInfoSheet: View { } extension QuickPickSectionHeader { - static let bolusInfoText = """ - These buttons show your most-used recent bolus amounts. + static let bolusInfoText = String(localized: "These buttons show your most-used recent bolus amounts.\n\nThey're based on what you've sent before at similar times on similar days — so if you usually give 4 units before breakfast on weekdays, that button will show up on weekday mornings.\n\nTap a button to fill in the amount. Nothing is sent until you review and confirm.") - They're based on what you've sent before at similar times on similar days — so if you usually give 4 units before breakfast on weekdays, that button will show up on weekday mornings. - - Tap a button to fill in the amount. Nothing is sent until you review and confirm. - """ - - static let mealInfoText = """ - These buttons show your most-used recent meals. - - They're based on what you've sent before at similar times on similar days — so if you usually send the same breakfast on weekday mornings, it'll appear as an option. - - Tap a button to fill in the details. Nothing is sent until you review and confirm. - """ + static let mealInfoText = String(localized: "These buttons show your most-used recent meals.\n\nThey're based on what you've sent before at similar times on similar days — so if you usually send the same breakfast on weekday mornings, it'll appear as an option.\n\nTap a button to fill in the details. Nothing is sent until you review and confirm.") } diff --git a/LoopFollow/Helpers/Views/SimpleQRCodeScannerView.swift b/LoopFollow/Helpers/Views/SimpleQRCodeScannerView.swift index 672bb9991..f5d5fd32f 100644 --- a/LoopFollow/Helpers/Views/SimpleQRCodeScannerView.swift +++ b/LoopFollow/Helpers/Views/SimpleQRCodeScannerView.swift @@ -50,7 +50,7 @@ class SimpleQRCodeScannerViewController: UIViewController { target: self, action: #selector(cancelTapped) ) - navigationItem.title = "Scan QR Code" + navigationItem.title = String(localized: "Scan QR Code") setupCamera() } diff --git a/LoopFollow/InfoDisplaySettings/InfoRowSettingsView.swift b/LoopFollow/InfoDisplaySettings/InfoRowSettingsView.swift index 9387949cc..47f368967 100644 --- a/LoopFollow/InfoDisplaySettings/InfoRowSettingsView.swift +++ b/LoopFollow/InfoDisplaySettings/InfoRowSettingsView.swift @@ -91,9 +91,9 @@ struct InfoRowSettingsView: View { private func colorFooter(_ config: InfoColorConfig) -> String { switch config.direction { case .above: - return "The value turns yellow at or above the yellow level and red at or above the red level. In range it shows green. A visual cue only — it never triggers an alarm." + return String(localized: "The value turns yellow at or above the yellow level and red at or above the red level. In range it shows green. A visual cue only — it never triggers an alarm.", comment: "Info row color footer") case .below: - return "The value turns yellow at or below the yellow level and red at or below the red level. In range it shows green. A visual cue only — it never triggers an alarm." + return String(localized: "The value turns yellow at or below the yellow level and red at or below the red level. In range it shows green. A visual cue only — it never triggers an alarm.", comment: "Info row color footer") } } } diff --git a/LoopFollow/InfoTable/InfoType.swift b/LoopFollow/InfoTable/InfoType.swift index 173ca416e..145c443c1 100644 --- a/LoopFollow/InfoTable/InfoType.swift +++ b/LoopFollow/InfoTable/InfoType.swift @@ -8,27 +8,27 @@ enum InfoType: Int, CaseIterable, Codable { var name: String { switch self { - case .iob: return "IOB" - case .cob: return "COB" - case .basal: return "Basal" - case .override: return "Override" - case .battery: return "Battery" - case .pump: return "Pump" - case .pumpBattery: return "Pump Battery" - case .sage: return "SAGE" - case .cage: return "CAGE" - case .recBolus: return "Rec. Bolus" - case .minMax: return "Min/Max" - case .carbsToday: return "Carbs today" - case .autosens: return "Autosens" - case .profile: return "Profile" - case .target: return "Target" - case .isf: return "ISF" - case .carbRatio: return "CR" - case .updated: return "Updated" - case .tdd: return "TDD" - case .iage: return "IAGE" - case .dbSize: return "DB Size" + case .iob: return String(localized: "IOB", comment: "Info table row") + case .cob: return String(localized: "COB", comment: "Info table row") + case .basal: return String(localized: "Basal", comment: "Info table row") + case .override: return String(localized: "Override", comment: "Info table row") + case .battery: return String(localized: "Battery", comment: "Info table row") + case .pump: return String(localized: "Pump", comment: "Info table row") + case .pumpBattery: return String(localized: "Pump Battery", comment: "Info table row") + case .sage: return String(localized: "SAGE", comment: "Info table row") + case .cage: return String(localized: "CAGE", comment: "Info table row") + case .recBolus: return String(localized: "Rec. Bolus", comment: "Info table row") + case .minMax: return String(localized: "Min/Max", comment: "Info table row") + case .carbsToday: return String(localized: "Carbs today", comment: "Info table row") + case .autosens: return String(localized: "Autosens", comment: "Info table row") + case .profile: return String(localized: "Profile", comment: "Info table row") + case .target: return String(localized: "Target", comment: "Info table row") + case .isf: return String(localized: "ISF", comment: "Info table row") + case .carbRatio: return String(localized: "CR", comment: "Info table row") + case .updated: return String(localized: "Updated", comment: "Info table row") + case .tdd: return String(localized: "TDD", comment: "Info table row") + case .iage: return String(localized: "IAGE", comment: "Info table row") + case .dbSize: return String(localized: "DB Size", comment: "Info table row") } } diff --git a/LoopFollow/Log/SearchBar.swift b/LoopFollow/Log/SearchBar.swift index bb3704d2b..3b24dd24e 100644 --- a/LoopFollow/Log/SearchBar.swift +++ b/LoopFollow/Log/SearchBar.swift @@ -29,7 +29,7 @@ struct SearchBar: UIViewRepresentable { func makeUIView(context: UIViewRepresentableContext) -> UISearchBar { let searchBar = UISearchBar(frame: .zero) - searchBar.placeholder = "Search Log" + searchBar.placeholder = String(localized: "Search Log") searchBar.delegate = context.coordinator searchBar.autocapitalizationType = .none searchBar.searchBarStyle = .minimal diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 1065cc892..64ab54c77 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -33,7 +33,10 @@ class NightscoutSettingsViewModel: ObservableObject { } } - @Published var nightscoutStatus: String = "Checking..." + /// Status shown while a check is running; compared by `statusKind`, so it is one shared value. + static let checkingStatus = String(localized: "Checking...") + + @Published var nightscoutStatus = NightscoutSettingsViewModel.checkingStatus /// The most recent verification error, kept so the onboarding address page can /// tell "reachable Nightscout that needs a token" apart from "can't reach it". @@ -70,7 +73,7 @@ class NightscoutSettingsViewModel: ObservableObject { } } - @Published var webSocketStatus: String = "Disconnected" + @Published var webSocketStatus = String(localized: "Disconnected") var webSocketStatusColor: Color { switch NightscoutSocketManager.shared.connectionState { @@ -130,7 +133,7 @@ class NightscoutSettingsViewModel: ObservableObject { // "this is your API secret" verdict. provisionedTokenPending = false tokenIsVerifiedSecret = false - nightscoutStatus = "Checking..." + nightscoutStatus = Self.checkingStatus checkStatusWorkItem = DispatchWorkItem { self.checkStatusSubject.send() @@ -196,7 +199,7 @@ class NightscoutSettingsViewModel: ObservableObject { isConfirmingProvisionedToken = true provisionedTokenPending = false isConnected = false - nightscoutStatus = "Finishing connection…" + nightscoutStatus = String(localized: "Finishing connection…") nightscoutToken = token verifyProvisionedTokenLoop(attempt: 0) } @@ -213,7 +216,7 @@ class NightscoutSettingsViewModel: ObservableObject { Storage.shared.nsAdminAuth.value = nsAdminAuth self.updateStatusLabel(error: nil) } else if attempt + 1 < maxAttempts { - self.nightscoutStatus = "Finishing connection…" + self.nightscoutStatus = String(localized: "Finishing connection…") let delay = min(0.5 + Double(attempt) * 0.25, 2.0) DispatchQueue.main.asyncAfter(deadline: .now() + delay) { self.verifyProvisionedTokenLoop(attempt: attempt + 1) @@ -299,19 +302,19 @@ class NightscoutSettingsViewModel: ObservableObject { static func provisioningMessage(for error: Error) -> String { guard let nsError = error as? NightscoutUtils.NightscoutError else { - return "Could not create a token. Please try again." + return String(localized: "Could not create a token. Please try again.", comment: "Token creation error") } switch nsError { case .invalidToken: - return "That API secret was rejected. Check it and try again." + return String(localized: "That API secret was rejected. Check it and try again.", comment: "Token creation error") case .invalidURL, .emptyAddress: - return "Please enter a valid site URL first." + return String(localized: "Please enter a valid site URL first.", comment: "Token creation error") case .siteNotFound: - return "Couldn't reach that site. Check the URL." + return String(localized: "Couldn't reach that site. Check the URL.", comment: "Token creation error") case .networkError: - return "Network error. Check your connection and try again." + return String(localized: "Network error. Check your connection and try again.", comment: "Token creation error") case .tokenRequired, .unknown: - return "Could not create a token. Please try again." + return String(localized: "Could not create a token. Please try again.", comment: "Token creation error") } } @@ -321,19 +324,19 @@ class NightscoutSettingsViewModel: ObservableObject { isConnected = false switch error { case .invalidURL: - nightscoutStatus = "Invalid URL" + nightscoutStatus = String(localized: "Invalid URL") case .networkError: - nightscoutStatus = "Network Error" + nightscoutStatus = String(localized: "Network Error") case .invalidToken: - nightscoutStatus = "Invalid Token" + nightscoutStatus = String(localized: "Invalid Token") case .tokenRequired: - nightscoutStatus = "Token Required" + nightscoutStatus = String(localized: "Token Required") case .siteNotFound: - nightscoutStatus = "Site Not Found" + nightscoutStatus = String(localized: "Site Not Found") case .unknown: - nightscoutStatus = "Unknown Error" + nightscoutStatus = String(localized: "Unknown Error") case .emptyAddress: - nightscoutStatus = "Address Empty" + nightscoutStatus = String(localized: "Address Empty") } NightscoutSocketManager.shared.disconnect() @@ -351,12 +354,14 @@ class NightscoutSettingsViewModel: ObservableObject { tokenIsVerifiedSecret = false let authStatus: String if Storage.shared.nsAdminAuth.value { - authStatus = "Admin" + authStatus = String(localized: "Admin", comment: "Nightscout auth level") } else { - authStatus = "Read" + (Storage.shared.nsWriteAuth.value ? " & Write" : "") + authStatus = Storage.shared.nsWriteAuth.value + ? String(localized: "Read & Write", comment: "Nightscout auth level") + : String(localized: "Read", comment: "Nightscout auth level") } - nightscoutStatus = "OK (\(authStatus))" + nightscoutStatus = String(localized: "OK (\(authStatus))") if nightscoutURL != initialURL || nightscoutToken != initialToken { NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) @@ -386,7 +391,7 @@ class NightscoutSettingsViewModel: ObservableObject { if isConnected { return .connected } // Token created and correct, just not accepted by the site yet. if provisionedTokenPending { return .pending } - if nightscoutStatus == "Checking..." { return .checking } + if nightscoutStatus == Self.checkingStatus { return .checking } // The site is reachable and simply needs a token — that's an expected // step, not an error, so it's shown positively rather than red. if addressNeedsToken { return .needsToken } @@ -398,17 +403,19 @@ class NightscoutSettingsViewModel: ObservableObject { var friendlyStatus: String { switch statusKind { case .idle: - return "Enter your site address to connect." + return String(localized: "Enter your site address to connect.", comment: "Nightscout connection status") case .checking: - return isConfirmingProvisionedToken ? "Finishing connection…" : "Checking your connection…" + return isConfirmingProvisionedToken + ? String(localized: "Finishing connection…") + : String(localized: "Checking your connection…") case .needsToken: - return "Site found — it needs a token." + return String(localized: "Site found — it needs a token.", comment: "Nightscout connection status") case .pending: - return "Token created. Your site can take a few minutes to start accepting it — you can continue." + return String(localized: "Token created. Your site can take a few minutes to start accepting it — you can continue.", comment: "Nightscout connection status") case .connected: if Storage.shared.nsAdminAuth.value { return "Connected — admin access" } if Storage.shared.nsWriteAuth.value { return "Connected — read & write" } - return "Connected — read-only" + return String(localized: "Connected — read-only", comment: "Nightscout connection status") case .error: return nightscoutStatus } @@ -426,11 +433,11 @@ class NightscoutSettingsViewModel: ObservableObject { private func updateWebSocketStatus() { switch NightscoutSocketManager.shared.connectionState { - case .disconnected: webSocketStatus = "Disconnected" - case .connecting: webSocketStatus = "Connecting..." - case .connected: webSocketStatus = "Connected" - case .authenticated: webSocketStatus = "Connected" - case .error: webSocketStatus = "Error" + case .disconnected: webSocketStatus = String(localized: "Disconnected") + case .connecting: webSocketStatus = String(localized: "Connecting...") + case .connected: webSocketStatus = String(localized: "Connected") + case .authenticated: webSocketStatus = String(localized: "Connected") + case .error: webSocketStatus = String(localized: "Error") } } } diff --git a/LoopFollow/Onboarding/OnboardingNavFooter.swift b/LoopFollow/Onboarding/OnboardingNavFooter.swift index 1b0f3e2d4..a5218b7cb 100644 --- a/LoopFollow/Onboarding/OnboardingNavFooter.swift +++ b/LoopFollow/Onboarding/OnboardingNavFooter.swift @@ -7,7 +7,7 @@ import SwiftUI /// phases that manage their own internal pages (connect, alarms), so the controls /// look and behave identically everywhere. struct OnboardingNavFooter: View { - var continueTitle: String = "Continue" + var continueTitle: LocalizedStringKey = "Continue" var continueEnabled: Bool = true var showBack: Bool = true var onBack: () -> Void diff --git a/LoopFollow/Onboarding/OnboardingStepHeader.swift b/LoopFollow/Onboarding/OnboardingStepHeader.swift index 1b6395ef8..6617e0e48 100644 --- a/LoopFollow/Onboarding/OnboardingStepHeader.swift +++ b/LoopFollow/Onboarding/OnboardingStepHeader.swift @@ -11,8 +11,8 @@ import SwiftUI /// cards in a plain `VStack`. Callers add their own horizontal padding to match. struct OnboardingStepHeader: View { let systemImage: String - let title: String - let subtitle: String + let title: LocalizedStringKey + let subtitle: LocalizedStringKey var body: some View { // Left-aligned: justified/centered body copy is harder to read, so the diff --git a/LoopFollow/Onboarding/Steps/AlarmsStepView.swift b/LoopFollow/Onboarding/Steps/AlarmsStepView.swift index 5efe46da7..2c20f0643 100644 --- a/LoopFollow/Onboarding/Steps/AlarmsStepView.swift +++ b/LoopFollow/Onboarding/Steps/AlarmsStepView.swift @@ -136,7 +136,7 @@ struct AlarmsStepView: View { private func bgPicker( _ seed: Binding, - title: String, + title: LocalizedStringKey, range: ClosedRange, keyPath: WritableKeyPath ) -> some View { @@ -188,11 +188,11 @@ struct AlarmsStepView: View { let perReading = Localizer.formatQuantity(delta) if window == 1 { - return "Warns when glucose falls by at least \(perReading) \(unit) between two readings." + return String(localized: "Warns when glucose falls by at least \(perReading) \(unit) between two readings.", comment: "Onboarding fast-drop explanation") } let total = Localizer.formatQuantity(delta * Double(window)) let minutes = window * 5 - return "Warns when glucose falls by at least \(perReading) \(unit) on each of \(window) readings in a row — about \(total) \(unit) over roughly \(minutes) minutes." + return String(localized: "Warns when glucose falls by at least \(perReading) \(unit) on each of \(window, specifier: "%lld") readings in a row — about \(total) \(unit) over roughly \(minutes) minutes.", comment: "Onboarding fast-drop explanation") default: return nil } diff --git a/LoopFollow/Onboarding/Steps/UnitsStepView.swift b/LoopFollow/Onboarding/Steps/UnitsStepView.swift index 25138e611..4d7b49a82 100644 --- a/LoopFollow/Onboarding/Steps/UnitsStepView.swift +++ b/LoopFollow/Onboarding/Steps/UnitsStepView.swift @@ -48,7 +48,7 @@ struct UnitsStepView: View { onboarding.phaseProgress = .init(page: page.rawValue, count: Page.allCases.count) } - private func header(systemImage: String, title: String, subtitle: String) -> some View { + private func header(systemImage: String, title: LocalizedStringKey, subtitle: LocalizedStringKey) -> some View { Section { EmptyView() } header: { diff --git a/LoopFollow/Onboarding/Steps/WelcomeStepView.swift b/LoopFollow/Onboarding/Steps/WelcomeStepView.swift index d619d722d..849868452 100644 --- a/LoopFollow/Onboarding/Steps/WelcomeStepView.swift +++ b/LoopFollow/Onboarding/Steps/WelcomeStepView.swift @@ -67,7 +67,7 @@ struct WelcomeStepView: View { } } - private func primaryLabel(_ text: String) -> some View { + private func primaryLabel(_ text: LocalizedStringKey) -> some View { Text(text) .font(.body.weight(.semibold)) .frame(maxWidth: .infinity) diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift index 09222270c..1f98ae3f7 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift @@ -57,7 +57,7 @@ struct LoopAPNSBolusView: View { Text("\(String(format: "%.2f", recommendedBolus))U") .font(.headline) .foregroundColor(.primary) - Text("Calculated \(minutesSinceCalculation) minute\(minutesSinceCalculation == 1 ? "" : "s") ago") + Text("Calculated \(minutesSinceCalculation) minutes ago") .font(.caption) .foregroundColor(.secondary) } @@ -166,7 +166,7 @@ struct LoopAPNSBolusView: View { .padding(.horizontal, 8) .background(Color.green.opacity(0.1)) .cornerRadius(4) - Text("(" + (otpTimeRemaining.map { "\($0)s left" } ?? "-") + ")") + Text(verbatim: "(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") .font(.caption) .foregroundColor(.secondary) } @@ -206,7 +206,7 @@ struct LoopAPNSBolusView: View { // Validate APNS setup let apnsService = LoopAPNSService() if !apnsService.validateSetup() { - alertMessage = "Loop APNS setup is incomplete. Please configure all required fields in settings." + alertMessage = String(localized: "Loop APNS setup is incomplete. Please configure all required fields in settings.") alertType = .error showAlert = true } @@ -260,7 +260,7 @@ struct LoopAPNSBolusView: View { // Only show warning if calculation is between 5-12 minutes old if minutesSinceCalculation > 5 && minutesSinceCalculation < 12 && !showOldCalculationWarning { showOldCalculationWarning = true - alertMessage = "This recommended bolus was calculated \(minutesSinceCalculation) minutes ago. New treatments may have occurred since then. Proceed with caution." + alertMessage = String(localized: "This recommended bolus was calculated \(minutesSinceCalculation) minutes ago. New treatments may have occurred since then. Proceed with caution.") alertType = .oldCalculationWarning showAlert = true } @@ -337,7 +337,7 @@ struct LoopAPNSBolusView: View { private func sendInsulin() { guard insulinAmount.doubleValue(for: .internationalUnit()) > 0 else { - alertMessage = "Please enter a valid insulin amount" + alertMessage = String(localized: "Please enter a valid insulin amount") alertType = .error showAlert = true return @@ -348,7 +348,7 @@ struct LoopAPNSBolusView: View { let insulinValue = insulinAmount.doubleValue(for: .internationalUnit()) if insulinValue > maxBolus { - alertMessage = "Insulin amount (\(String(format: "%.2f", insulinValue))U) exceeds the maximum allowed (\(String(format: "%.2f", maxBolus))U). Please reduce the amount." + alertMessage = String(localized: "Insulin amount (\(insulinValue, specifier: "%.2f")U) exceeds the maximum allowed (\(maxBolus, specifier: "%.2f")U). Please reduce the amount.") alertType = .error showAlert = true return @@ -369,7 +369,7 @@ struct LoopAPNSBolusView: View { self.alertType = .error self.showAlert = true case .failed: - self.alertMessage = "Authentication failed" + self.alertMessage = String(localized: "Authentication failed") self.alertType = .error self.showAlert = true case .canceled: @@ -385,7 +385,7 @@ struct LoopAPNSBolusView: View { // Extract OTP from QR code URL guard let otpCode = TOTPGenerator.extractOTPFromURL(Storage.shared.loopAPNSQrCodeURL.value) else { - alertMessage = "Invalid QR code URL. Please re-scan the QR code in settings." + alertMessage = String(localized: "Invalid QR code URL. Please re-scan the QR code in settings.") alertType = .error isLoading = false showAlert = true diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index 537fa29d2..7d857df2e 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -290,7 +290,7 @@ struct LoopAPNSCarbsView: View { .padding(.horizontal, 8) .background(Color.green.opacity(0.1)) .cornerRadius(4) - Text("(" + (otpTimeRemaining.map { "\($0)s left" } ?? "-") + ")") + Text(verbatim: "(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") .font(.caption) .foregroundColor(.secondary) } @@ -405,7 +405,7 @@ struct LoopAPNSCarbsView: View { // Validate APNS setup let apnsService = LoopAPNSService() if !apnsService.validateSetup() { - alertMessage = "Loop APNS setup is incomplete. Please configure all required fields in settings." + alertMessage = String(localized: "Loop APNS setup is incomplete. Please configure all required fields in settings.") alertType = .error showAlert = true } @@ -486,7 +486,7 @@ struct LoopAPNSCarbsView: View { private func sendCarbs() { guard carbsAmount.doubleValue(for: .gram()) > 0 else { - alertMessage = "Please enter a valid carb amount" + alertMessage = String(localized: "Please enter a valid carb amount") alertType = .error showAlert = true return @@ -497,7 +497,7 @@ struct LoopAPNSCarbsView: View { let carbsValue = carbsAmount.doubleValue(for: .gram()) if carbsValue > maxCarbs { - alertMessage = "Carbs amount (\(Int(carbsValue))g) exceeds the maximum allowed (\(Int(maxCarbs))g). Please reduce the amount." + alertMessage = String(localized: "Carbs amount (\(Int(carbsValue))g) exceeds the maximum allowed (\(Int(maxCarbs))g). Please reduce the amount.") alertType = .error showAlert = true return @@ -509,14 +509,14 @@ struct LoopAPNSCarbsView: View { let latestAcceptedDate = now.addingTimeInterval(60 * 60 * Double(maxFutureHours)) if consumedDate < oldestAcceptedDate { - alertMessage = "Time must be within the prior \(maxPastHours) hours" + alertMessage = String(localized: "Time must be within the prior \(maxPastHours) hours") alertType = .error showAlert = true return } if consumedDate > latestAcceptedDate { - alertMessage = "Time must be within the next \(maxFutureHours) hour" + alertMessage = String(localized: "Time must be within the next \(maxFutureHours) hour") alertType = .error showAlert = true return @@ -531,7 +531,7 @@ struct LoopAPNSCarbsView: View { // Extract OTP from QR code URL guard let otpCode = TOTPGenerator.extractOTPFromURL(Storage.shared.loopAPNSQrCodeURL.value) else { - alertMessage = "Invalid QR code URL. Please re-scan the QR code in settings." + alertMessage = String(localized: "Invalid QR code URL. Please re-scan the QR code in settings.") alertType = .error isLoading = false showAlert = true diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift index 38dc4fb23..a001527c7 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift @@ -36,25 +36,25 @@ class LoopAPNSService { var errorDescription: String? { switch self { case .invalidConfiguration: - return "Loop APNS Configuration not valid" + return String(localized: "Loop APNS Configuration not valid", comment: "Loop APNS error") case .jwtError: - return "Failed generating JWT token, check APNS Key ID, APNS Key and Team ID" + return String(localized: "Failed generating JWT token, check APNS Key ID, APNS Key and Team ID", comment: "Loop APNS error") case .networkError: - return "Network error occurred" + return String(localized: "Network error occurred", comment: "Loop APNS error") case .invalidResponse: - return "Invalid response from server" + return String(localized: "Invalid response from server", comment: "Loop APNS error") case .noDeviceToken: - return "No device token found in profile" + return String(localized: "No device token found in profile", comment: "Loop APNS error") case .noBundleIdentifier: - return "No bundle identifier found in profile" + return String(localized: "No bundle identifier found in profile", comment: "Loop APNS error") case .unauthorized: - return "Unauthorized - check your API secret" + return String(localized: "Unauthorized - check your API secret", comment: "Loop APNS error") case .deviceTokenNotConfigured: - return "Device token not configured" + return String(localized: "Device token not configured", comment: "Loop APNS error") case .bundleIdentifierNotConfigured: - return "Bundle identifier not configured" + return String(localized: "Bundle identifier not configured", comment: "Loop APNS error") case .rateLimited: - return "Too many requests - please wait a few minutes before trying again" + return String(localized: "Too many requests - please wait a few minutes before trying again", comment: "Loop APNS error") } } } @@ -128,7 +128,7 @@ class LoopAPNSService { /// - completion: Completion handler with success status and error message func sendCarbsViaAPNS(payload: LoopAPNSPayload, completion: @escaping (Bool, String?) -> Void) { guard validateSetup() else { - let errorMessage = "Loop APNS Configuration not valid" + let errorMessage = String(localized: "Loop APNS Configuration not valid") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -197,7 +197,7 @@ class LoopAPNSService { /// - completion: Completion handler with success status and error message func sendBolusViaAPNS(payload: LoopAPNSPayload, completion: @escaping (Bool, String?) -> Void) { guard validateSetup() else { - let errorMessage = "Loop APNS Configuration not valid" + let errorMessage = String(localized: "Loop APNS Configuration not valid") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -308,10 +308,10 @@ class LoopAPNSService { /// Provides simple environment guidance for APNS configuration /// - Returns: String with simple guidance to try opposite setting private func getEnvironmentGuidance() -> String { - let currentSetting = storage.productionEnvironment.value ? "ON" : "OFF" - let trySetting = storage.productionEnvironment.value ? "OFF" : "ON" + let currentSetting = storage.productionEnvironment.value ? String(localized: "ON") : String(localized: "OFF") + let trySetting = storage.productionEnvironment.value ? String(localized: "OFF") : String(localized: "ON") - return "Try changing Production Environment from \(currentSetting) to \(trySetting) in your Loop APNS settings." + return String(localized: "Try changing Production Environment from \(currentSetting) to \(trySetting) in your Loop APNS settings.") } /// Sends an APNS notification @@ -333,7 +333,7 @@ class LoopAPNSService { ) { // Validate credentials first if let validationErrors = validateCredentials() { - let errorMessage = "Credential validation failed: \(validationErrors.joined(separator: ", "))" + let errorMessage = String(localized: "Credential validation failed: \(validationErrors.joined(separator: ", "))") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -341,7 +341,7 @@ class LoopAPNSService { // Create JWT token for APNS authentication guard let jwt = JWTManager.shared.getOrGenerateJWT(keyId: keyId, teamId: teamId, apnsKey: apnsKey) else { - let errorMessage = "Failed to generate JWT, please check that the APNS Key ID, APNS Key and Team ID are correct." + let errorMessage = String(localized: "Failed to generate JWT, please check that the APNS Key ID, APNS Key and Team ID are correct.") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -351,7 +351,7 @@ class LoopAPNSService { let isProduction = storage.productionEnvironment.value let apnsURL = isProduction ? "https://api.push.apple.com" : "https://api.sandbox.push.apple.com" guard let requestURL = URL(string: "\(apnsURL)/3/device/\(deviceToken)") else { - let errorMessage = "Failed to construct APNs URL" + let errorMessage = String(localized: "Failed to construct APNs URL") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -403,7 +403,7 @@ class LoopAPNSService { let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { - let errorMessage = "Failed to send push notification: \(error.localizedDescription)" + let errorMessage = String(localized: "Failed to send push notification: \(error.localizedDescription)") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -425,49 +425,49 @@ class LoopAPNSService { completion(true, nil) case 400: let environmentGuidance = self.getEnvironmentGuidance() - let errorMessage = "Bad request. The request was invalid or malformed. \(responseBodyMessage)\n\n\(environmentGuidance)" + let errorMessage = String(localized: "Bad request. The request was invalid or malformed. \(responseBodyMessage)\n\n\(environmentGuidance)") LogManager.shared.log(category: .apns, message: "APNS error 400: \(responseBodyMessage) - Check device token and environment settings") completion(false, errorMessage) case 403: JWTManager.shared.invalidateCache() - let errorMessage = "Authentication error. Check your certificate or authentication token. \(responseBodyMessage)" + let errorMessage = String(localized: "Authentication error. Check your certificate or authentication token. \(responseBodyMessage)") LogManager.shared.log(category: .apns, message: "APNS error 403: \(responseBodyMessage) - Check APNS key permissions for bundle ID") completion(false, errorMessage) case 404: - let errorMessage = "Invalid request: The :path value was incorrect. \(responseBodyMessage)" + let errorMessage = String(localized: "Invalid request: The :path value was incorrect. \(responseBodyMessage)") LogManager.shared.log(category: .apns, message: "APNS error 404: \(responseBodyMessage)") completion(false, errorMessage) case 405: - let errorMessage = "Invalid request: Only POST requests are supported. \(responseBodyMessage)" + let errorMessage = String(localized: "Invalid request: Only POST requests are supported. \(responseBodyMessage)") LogManager.shared.log(category: .apns, message: "APNS error 405: \(responseBodyMessage)") completion(false, errorMessage) case 410: - let errorMessage = "The device token is no longer active for the topic. \(responseBodyMessage)" + let errorMessage = String(localized: "The device token is no longer active for the topic. \(responseBodyMessage)") LogManager.shared.log(category: .apns, message: "APNS error 410: Device token is invalid or expired") completion(false, errorMessage) case 413: - let errorMessage = "Payload too large. The notification payload exceeded the size limit. \(responseBodyMessage)" + let errorMessage = String(localized: "Payload too large. The notification payload exceeded the size limit. \(responseBodyMessage)") LogManager.shared.log(category: .apns, message: "APNS error 413: \(responseBodyMessage)") completion(false, errorMessage) case 429: - let errorMessage = "Too many requests. \(responseBodyMessage)" + let errorMessage = String(localized: "Too many requests. \(responseBodyMessage)") LogManager.shared.log(category: .apns, message: "APNS error 429: Rate limited - wait before retrying") completion(false, errorMessage) case 500: - let errorMessage = "Internal server error at APNs. \(responseBodyMessage)" + let errorMessage = String(localized: "Internal server error at APNs. \(responseBodyMessage)") LogManager.shared.log(category: .apns, message: "APNS error 500: \(responseBodyMessage)") completion(false, errorMessage) case 503: - let errorMessage = "Service unavailable. The server is temporarily unavailable. Try again later. \(responseBodyMessage)" + let errorMessage = String(localized: "Service unavailable. The server is temporarily unavailable. Try again later. \(responseBodyMessage)") LogManager.shared.log(category: .apns, message: "APNS error 503: \(responseBodyMessage)") completion(false, errorMessage) default: - let errorMessage = "Unexpected status code: \(httpResponse.statusCode). \(responseBodyMessage)" + let errorMessage = String(localized: "Unexpected status code: \(httpResponse.statusCode). \(responseBodyMessage)") LogManager.shared.log(category: .apns, message: "APNS error \(httpResponse.statusCode): \(responseBodyMessage)") completion(false, errorMessage) } } else { - let errorMessage = "Failed to get a valid HTTP response." + let errorMessage = String(localized: "Failed to get a valid HTTP response.") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) } @@ -475,7 +475,7 @@ class LoopAPNSService { task.resume() } catch { - let errorMessage = "Failed to serialize APNS payload: \(error.localizedDescription)" + let errorMessage = String(localized: "Failed to serialize APNS payload: \(error.localizedDescription)") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) } @@ -643,7 +643,7 @@ class LoopAPNSService { func sendOverrideNotification(presetName: String, duration: TimeInterval? = nil, completion: @escaping (Bool, String?) -> Void) { let deviceToken = Storage.shared.deviceToken.value guard !deviceToken.isEmpty else { - let errorMessage = "Device token not configured" + let errorMessage = String(localized: "Device token not configured") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -651,7 +651,7 @@ class LoopAPNSService { let bundleIdentifier = Storage.shared.bundleId.value guard !bundleIdentifier.isEmpty else { - let errorMessage = "Bundle identifier not configured" + let errorMessage = String(localized: "Bundle identifier not configured") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -715,7 +715,7 @@ class LoopAPNSService { func sendCancelOverrideNotification(completion: @escaping (Bool, String?) -> Void) { let deviceToken = Storage.shared.deviceToken.value guard !deviceToken.isEmpty else { - let errorMessage = "Device token not configured" + let errorMessage = String(localized: "Device token not configured") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -723,7 +723,7 @@ class LoopAPNSService { let bundleIdentifier = Storage.shared.bundleId.value guard !bundleIdentifier.isEmpty else { - let errorMessage = "Bundle identifier not configured" + let errorMessage = String(localized: "Bundle identifier not configured") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return diff --git a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift index 6ed39e5bc..265ccf10d 100644 --- a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift +++ b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift @@ -384,7 +384,7 @@ class OverridePresetsViewModel: ObservableObject { } } catch { await MainActor.run { - self.statusMessage = "Failed to load override presets: \(error.localizedDescription)" + self.statusMessage = String(localized: "Failed to load override presets: \(error.localizedDescription)") self.alertType = .statusFailure self.showAlert = true self.isLoading = false @@ -401,14 +401,14 @@ class OverridePresetsViewModel: ObservableObject { try await sendOverrideNotification(preset: preset, duration: duration) await MainActor.run { self.isActivating = false - self.statusMessage = "\(preset.name) override activated successfully." + self.statusMessage = String(localized: "\(preset.name) override activated successfully.") self.alertType = .statusSuccess self.showAlert = true } } catch { await MainActor.run { self.isActivating = false - self.statusMessage = "Failed to activate override: \(error.localizedDescription)" + self.statusMessage = String(localized: "Failed to activate override: \(error.localizedDescription)") self.alertType = .statusFailure self.showAlert = true } @@ -424,14 +424,14 @@ class OverridePresetsViewModel: ObservableObject { try await sendCancelOverrideNotification() await MainActor.run { self.isActivating = false - self.statusMessage = "Active override cancelled successfully." + self.statusMessage = String(localized: "Active override cancelled successfully.") self.alertType = .statusSuccess self.showAlert = true } } catch { await MainActor.run { self.isActivating = false - self.statusMessage = "Failed to cancel override: \(error.localizedDescription)" + self.statusMessage = String(localized: "Failed to cancel override: \(error.localizedDescription)") self.alertType = .statusFailure self.showAlert = true } @@ -523,11 +523,11 @@ enum OverrideError: LocalizedError { var errorDescription: String? { switch self { case .nightscoutNotConfigured: - return "Nightscout URL and token not configured in settings" + return String(localized: "Nightscout URL and token not configured in settings", comment: "Override preset error") case .invalidResponse: - return "Invalid response from server" + return String(localized: "Invalid response from server", comment: "Override preset error") case let .serverError(code): - return "Server error: \(code)" + return String(localized: "Server error: \(code, specifier: "%lld")", comment: "Override preset error; %lld = HTTP status") } } } diff --git a/LoopFollow/Remote/RemoteCommandMessage.swift b/LoopFollow/Remote/RemoteCommandMessage.swift index 686734b88..1c6ecbdbb 100644 --- a/LoopFollow/Remote/RemoteCommandMessage.swift +++ b/LoopFollow/Remote/RemoteCommandMessage.swift @@ -2,5 +2,5 @@ // RemoteCommandMessage.swift enum RemoteCommandMessage { - static let sent = "Remote command sent. Wait for a notification confirming the result." + static let sent = String(localized: "Remote command sent. Wait for a notification confirming the result.") } diff --git a/LoopFollow/Remote/Settings/RemoteCommandSettings.swift b/LoopFollow/Remote/Settings/RemoteCommandSettings.swift index ddfe9203b..926b72a20 100644 --- a/LoopFollow/Remote/Settings/RemoteCommandSettings.swift +++ b/LoopFollow/Remote/Settings/RemoteCommandSettings.swift @@ -174,32 +174,32 @@ struct RemoteCommandSettings: Codable { // If current user doesn't have URL but scanned settings do, prompt to set it if !hasCurrentURL, hasScannedURL { shouldPromptForURL = true - message = "The scanned settings include a Nightscout URL. Would you like to set this as your Nightscout address?" + message = String(localized: "The scanned settings include a Nightscout URL. Would you like to set this as your Nightscout address?") } // If current user doesn't have token but scanned settings do, prompt to set it if !hasCurrentToken, hasScannedToken { shouldPromptForToken = true if !message.isEmpty { - message += "\n\nThe scanned settings also include a token. Would you like to set this as your access token?" + message += "\n\n" + String(localized: "The scanned settings also include a token. Would you like to set this as your access token?") } else { - message = "The scanned settings include a token. Would you like to set this as your access token?" + message = String(localized: "The scanned settings include a token. Would you like to set this as your access token?") } } // If both have URLs but they don't match, show warning if hasCurrentURL, hasScannedURL, currentURL != url { shouldPromptForURL = true - message = "The scanned Nightscout URL (\(url)) doesn't match your current Nightscout address (\(currentURL)). Would you like to change your Nightscout address to match the scanned settings?" + message = String(localized: "The scanned Nightscout URL (\(url)) doesn't match your current Nightscout address (\(currentURL)). Would you like to change your Nightscout address to match the scanned settings?") } // If both have tokens but they don't match, show warning if hasCurrentToken, hasScannedToken, currentToken != token { shouldPromptForToken = true if !message.isEmpty { - message += "\n\nThe scanned token doesn't match your current access token. Would you like to update your token?" + message += "\n\n" + String(localized: "The scanned token doesn't match your current access token. Would you like to update your token?") } else { - message = "The scanned token doesn't match your current access token. Would you like to update your token?" + message = String(localized: "The scanned token doesn't match your current access token. Would you like to update your token?") } } diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 6e9b2acaa..a39eccb6d 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -270,7 +270,7 @@ struct RemoteSettingsView: View { .padding(.horizontal, 6) .background(Color.green.opacity(0.1)) .cornerRadius(4) - Text("(" + (otpTimeRemaining.map { "\($0)s left" } ?? "-") + ")") + Text(verbatim: "(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") .font(.caption) .foregroundColor(.secondary) } diff --git a/LoopFollow/Remote/TRC/BolusView.swift b/LoopFollow/Remote/TRC/BolusView.swift index e41ab89ce..0074d5587 100644 --- a/LoopFollow/Remote/TRC/BolusView.swift +++ b/LoopFollow/Remote/TRC/BolusView.swift @@ -166,7 +166,7 @@ struct BolusView: View { self.alertType = .validation self.showAlert = true case .failed: - self.alertMessage = "Authentication failed" + self.alertMessage = String(localized: "Authentication failed") self.alertType = .validation self.showAlert = true case .canceled: @@ -241,7 +241,7 @@ struct BolusView: View { HStack { VStack(alignment: .leading, spacing: 4) { Text("\(InsulinFormatter.shared.string(steppedRec))U") - Text("Calculated \(mins) minute\(mins == 1 ? "" : "s") ago") + Text("Calculated \(mins) minutes ago") .font(.caption) .foregroundColor(.secondary) } @@ -281,7 +281,7 @@ struct BolusView: View { if isStale12 { return } if isStale5 { let mins = Int(ageSec / 60) - alertMessage = "This recommended bolus was calculated \(mins) minutes ago. New treatments may have occurred since then. Proceed with caution." + alertMessage = String(localized: "This recommended bolus was calculated \(mins) minutes ago. New treatments may have occurred since then. Proceed with caution.") alertType = .oldCalculationWarning showAlert = true } else { diff --git a/LoopFollow/Remote/TRC/MealView.swift b/LoopFollow/Remote/TRC/MealView.swift index 5db2ef14b..82bfa5477 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -238,31 +238,30 @@ struct MealView: View { let fatAmount = fat.doubleValue(for: HKUnit.gram()) let bolusAmount = bolusAmount.doubleValue(for: .internationalUnit()) - var message = "Are you sure you want to send the meal data" - + var message: String if let selectedTime = selectedTime { let timeFormatter = DateFormatter() timeFormatter.timeStyle = .short let timeString = timeFormatter.string(from: selectedTime) - message += " for \(timeString)?" + message = String(localized: "Are you sure you want to send the meal data for \(timeString)?") } else { - message += " now?" + message = String(localized: "Are you sure you want to send the meal data now?") } if carbsAmount > 0 { - message += String(format: "\nCarbs: %.0f g", carbsAmount) + message += "\n" + String(localized: "Carbs: \(carbsAmount, specifier: "%.0f") g") } if proteinAmount > 0 { - message += String(format: "\nProtein: %.0f g", proteinAmount) + message += "\n" + String(localized: "Protein: \(proteinAmount, specifier: "%.0f") g") } if fatAmount > 0 { - message += String(format: "\nFat: %.0f g", fatAmount) + message += "\n" + String(localized: "Fat: \(fatAmount, specifier: "%.0f") g") } if bolusAmount > 0 { - message += String(format: "\nBolus: %.2f U", bolusAmount) + message += "\n" + String(localized: "Bolus: \(bolusAmount, specifier: "%.2f") U") } return Alert( @@ -281,7 +280,7 @@ struct MealView: View { self.alertType = .validationError self.showAlert = true case .failed: - self.alertMessage = "Authentication failed" + self.alertMessage = String(localized: "Authentication failed") self.alertType = .validationError self.showAlert = true case .canceled: diff --git a/LoopFollow/Remote/TRC/PushNotificationManager.swift b/LoopFollow/Remote/TRC/PushNotificationManager.swift index aa1f661a2..fe821e110 100644 --- a/LoopFollow/Remote/TRC/PushNotificationManager.swift +++ b/LoopFollow/Remote/TRC/PushNotificationManager.swift @@ -217,28 +217,28 @@ class PushNotificationManager { if teamId.isEmpty { missingFields.append("teamId") } if !missingFields.isEmpty { - let errorMessage = "Missing required fields for command: \(missingFields.joined(separator: ", "))" + let errorMessage = String(localized: "Missing required fields for command: \(missingFields.joined(separator: ", "))") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return } if let validationErrors = validateCredentials() { - let errorMessage = "Credential validation failed: \(validationErrors.joined(separator: ", "))" + let errorMessage = String(localized: "Credential validation failed: \(validationErrors.joined(separator: ", "))") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return } guard let url = constructAPNsURL() else { - let errorMessage = "Failed to construct APNs URL" + let errorMessage = String(localized: "Failed to construct APNs URL") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return } guard let jwt = JWTManager.shared.getOrGenerateJWT(keyId: keyId, teamId: teamId, apnsKey: apnsKey) else { - let errorMessage = "Failed to generate JWT, please check that the token is correct." + let errorMessage = String(localized: "Failed to generate JWT, please check that the token is correct.") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -246,7 +246,7 @@ class PushNotificationManager { do { guard let messenger = SecureMessenger(sharedSecret: sharedSecret) else { - let errorMessage = "Failed to initialize security module. Check shared secret." + let errorMessage = String(localized: "Failed to initialize security module. Check shared secret.") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -269,7 +269,7 @@ class PushNotificationManager { let task = URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { - let errorMessage = "Failed to send push notification: \(error.localizedDescription)" + let errorMessage = String(localized: "Failed to send push notification: \(error.localizedDescription)") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) return @@ -320,7 +320,7 @@ class PushNotificationManager { task.resume() } catch { - let errorMessage = "Failed to encode or encrypt push message: \(error.localizedDescription)" + let errorMessage = String(localized: "Failed to encode or encrypt push message: \(error.localizedDescription)") LogManager.shared.log(category: .apns, message: errorMessage) completion(false, errorMessage) } diff --git a/LoopFollow/Remote/TRC/TRCCommandType.swift b/LoopFollow/Remote/TRC/TRCCommandType.swift index 520f8a43a..2c7acf3d5 100644 --- a/LoopFollow/Remote/TRC/TRCCommandType.swift +++ b/LoopFollow/Remote/TRC/TRCCommandType.swift @@ -13,12 +13,12 @@ enum TRCCommandType: String, Encodable { var displayName: String { switch self { - case .bolus: return "Bolus" - case .tempTarget: return "Temp Target" - case .cancelTempTarget: return "Cancel Temp Target" - case .meal: return "Meal" - case .startOverride: return "Start Override" - case .cancelOverride: return "Cancel Override" + case .bolus: return String(localized: "Bolus", comment: "Trio remote command") + case .tempTarget: return String(localized: "Temp Target", comment: "Trio remote command") + case .cancelTempTarget: return String(localized: "Cancel Temp Target", comment: "Trio remote command") + case .meal: return String(localized: "Meal", comment: "Trio remote command") + case .startOverride: return String(localized: "Start Override", comment: "Trio remote command") + case .cancelOverride: return String(localized: "Cancel Override", comment: "Trio remote command") } } } diff --git a/LoopFollow/Remote/TRC/TrioRemoteControlView.swift b/LoopFollow/Remote/TRC/TrioRemoteControlView.swift index 0ad641a8b..fd4e61234 100644 --- a/LoopFollow/Remote/TRC/TrioRemoteControlView.swift +++ b/LoopFollow/Remote/TRC/TrioRemoteControlView.swift @@ -33,7 +33,7 @@ struct TrioRemoteControlView: View { } struct CommandButtonView: View { - let command: String + let command: LocalizedStringKey let iconName: String let destination: Destination /// Lights the button up with a glow while the corresponding override / diff --git a/LoopFollow/Resources/Localizable.xcstrings b/LoopFollow/Resources/Localizable.xcstrings new file mode 100644 index 000000000..f6d874c44 --- /dev/null +++ b/LoopFollow/Resources/Localizable.xcstrings @@ -0,0 +1,10511 @@ +{ + "sourceLanguage" : "en", + "strings" : { + " U" : { + "shouldTranslate" : false + }, + "#WeAreNotWaitingAnymore" : { + "shouldTranslate" : false + }, + "#WeAreWaitingForData" : { + "shouldTranslate" : false + }, + "%" : { + "shouldTranslate" : false + }, + "%@" : { + "shouldTranslate" : false + }, + "%@ %@ → %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ %2$@ → %3$@" + } + } + }, + "shouldTranslate" : false + }, + "%@ %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ %2$@" + } + } + }, + "shouldTranslate" : false + }, + "%@ - %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ - %2$@" + } + } + }, + "shouldTranslate" : false + }, + "%@ gaps" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ boşluk" + } + } + } + }, + "%@ hr" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ sa" + } + } + } + }, + "%@ min" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dk" + } + } + } + }, + "%@ min ago" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dk önce" + } + } + } + }, + "%@ of %@ expected readings" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ of %2$@ expected readings" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "beklenen %2$@ okumanın %1$@ tanesi" + } + } + } + }, + "%@ override activated successfully." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ override'ı başarıyla etkinleştirildi." + } + } + } + }, + "%@ sec" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ sn" + } + } + } + }, + "%@%%" : { + "shouldTranslate" : false + }, + "%@U" : { + "shouldTranslate" : false + }, + "%@g" : { + "shouldTranslate" : false + }, + "%lld min" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dk" + } + } + } + }, + "%llds left" : { + "comment" : "OTP seconds remaining", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld sn kaldı" + } + } + } + }, + "(%@ days)" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%@ gün)" + } + } + } + }, + "+ 30m" : { + "shouldTranslate" : false + }, + "---" : { + "shouldTranslate" : false + }, + "...because the data could not be loaded" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "...çünkü veriler yüklenemedi" + } + } + } + }, + "00" : { + "shouldTranslate" : false + }, + "14d" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "14g" + } + } + } + }, + "15m" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "15dk" + } + } + } + }, + "24h" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "24sa" + } + } + } + }, + "30d" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "30g" + } + } + } + }, + "7d" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "7g" + } + } + } + }, + "90d" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "90g" + } + } + } + }, + "A network error occurred." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir ağ hatası oluştu." + } + } + } + }, + "A new version is available: %@. It is recommended to update." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni bir sürüm var: %@. Güncellemeniz önerilir." + } + } + } + }, + "A quick, straightforward setup — about 5 minutes. You can change anything later in Settings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı ve basit bir kurulum — yaklaşık 5 dakika. Her şeyi daha sonra Ayarlar'dan değiştirebilirsiniz." + } + } + } + }, + "A silent tune will play in the background, keeping the app active. May be interrupted by other apps. Allows continuous updates but consumes more battery." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arka planda sessiz bir melodi çalarak uygulamayı etkin tutar. Başka uygulamalar tarafından kesilebilir. Sürekli güncelleme sağlar ama daha fazla pil tüketir." + } + } + } + }, + "A token is required." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir belirteç gerekli." + } + } + } + }, + "API Secret" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Secret" + } + } + } + }, + "APN" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APN" + } + } + } + }, + "APNS Environment: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNS Ortamı: %@" + } + } + } + }, + "APNS Key" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNS Anahtarı" + } + } + } + }, + "APNS Key ID" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNS Anahtar Kimliği" + } + } + } + }, + "APNS warning: importing APNS settings will overwrite your existing APNS Key ID and APNS Key." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNS uyarısı: APNS ayarlarını içe aktarmak mevcut APNS Anahtar Kimliğinizin ve APNS Anahtarınızın üzerine yazar." + } + } + } + }, + "APNs credentials are missing or invalid — Live Activity updates will not work. Open Settings → APN to fix." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNs kimlik bilgileri eksik veya geçersiz — Canlı Etkinlik güncellemeleri çalışmayacak. Düzeltmek için Ayarlar → APN'ye gidin." + } + } + } + }, + "Above" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üstünde" + } + } + } + }, + "Above: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üst: %@" + } + } + } + }, + "Absorption Time" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Emilim Süresi" + } + } + } + }, + "Absorption: %g hrs" : { + "comment" : "Treatment pill detail", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Emilim: %g sa" + } + } + } + }, + "Access Denied" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erişim Reddedildi" + } + } + } + }, + "Access Restricted" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erişim Kısıtlı" + } + } + } + }, + "Access Token" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erişim Belirteci" + } + } + } + }, + "Access to Contacts is denied. Please go to Settings and enable Contacts access." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kişiler erişimi reddedildi. Lütfen Ayarlar'a gidip Kişiler erişimini açın." + } + } + } + }, + "Access to Contacts is restricted." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kişiler erişimi kısıtlı." + } + } + } + }, + "Acknowledge" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onayla" + } + } + } + }, + "Activate Override" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override'ı Etkinleştir" + } + } + } + }, + "Active" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin" + } + } + } + }, + "Active During" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin Olduğu Zaman" + } + } + } + }, + "Active Override" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin Override" + } + } + } + }, + "Active during" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin olduğu zaman" + } + } + } + }, + "Active override cancelled successfully." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin override başarıyla iptal edildi." + } + } + } + }, + "Add Alarm" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarm Ekle" + } + } + } + }, + "Add the Apple-Calendar complication to your watch or CarPlay to see BG readings. Create a separate calendar (e.g. “Follow”) — this view will **delete** events on the same calendar each time it writes new readings." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ okumalarını görmek için saatinize veya CarPlay'e Apple Takvim komplikasyonunu ekleyin. Ayrı bir takvim oluşturun (ör. “Follow”) — bu görünüm, her yeni okuma yazdığında aynı takvimdeki etkinlikleri **siler**." + } + } + } + }, + "Add the contact named '%@' to your watch face to show the current BG value in real time. Make sure to give the app full access to Contacts when prompted." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncel KŞ değerini gerçek zamanlı görmek için '%@' adlı kişiyi saat kadranınıza ekleyin. İstendiğinde uygulamaya Kişiler için tam erişim verdiğinizden emin olun." + } + } + } + }, + "Adding this alarm would exceed the QR code character limit. Please remove some alarms first." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu alarmı eklemek QR kodu karakter sınırını aşar. Lütfen önce bazı alarmları kaldırın." + } + } + } + }, + "Additional Information" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ek Bilgiler" + } + } + } + }, + "Address Empty" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adres Boş" + } + } + } + }, + "Adjust the background refresh type." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arka plan yenileme türünü ayarlayın." + } + } + } + }, + "Admin" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yönetici" + } + } + } + }, + "Advance Notice" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önceden Bildirim" + } + } + } + }, + "Advanced" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gelişmiş" + } + } + } + }, + "Advanced Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gelişmiş Ayarlar" + } + } + } + }, + "Alarm Name" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarm Adı" + } + } + } + }, + "Alarm Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarm Ayarları" + } + } + } + }, + "Alarm basics" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarm temelleri" + } + } + } + }, + "Alarm not found" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarm bulunamadı" + } + } + } + }, + "Alarms" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarmlar" + } + } + } + }, + "Alarms can behave differently during the day and at night." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarmlar gündüz ve gece farklı davranabilir." + } + } + } + }, + "Alert Sound" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uyarı Sesi" + } + } + } + }, + "Alert above" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şunun üstünde uyar" + } + } + } + }, + "Alert below" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şunun altında uyar" + } + } + } + }, + "Alert if BG is equal to or above this value." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ bu değere eşit veya üstündeyse uyar." + } + } + } + }, + "Alert if BG is equal to or below this value." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ bu değere eşit veya altındaysa uyar." + } + } + } + }, + "Alert if current IOB or total boluses reach this." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncel IOB veya toplam bolus bu değere ulaşırsa uyar." + } + } + } + }, + "Alert only if BG is equal to or above this value." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yalnızca KŞ bu değere eşit veya üstündeyse uyar." + } + } + } + }, + "Alert only if BG is equal to or below this value." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yalnızca KŞ bu değere eşit veya altındaysa uyar." + } + } + } + }, + "Alert when Carbs-on-Board is at or above this number." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB bu değere eşit veya üstündeyken uyar." + } + } + } + }, + "Alert when any reading or forecast is at or below this value." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Herhangi bir okuma veya tahmin bu değere eşit veya altındaysa uyar." + } + } + } + }, + "Alert when recommended bolus is at or above this value." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önerilen bolus bu değere eşit veya üstündeyse uyar." + } + } + } + }, + "Alerts when BG goes below a limit." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ bir sınırın altına düştüğünde uyarır." + } + } + } + }, + "Alerts when BG rises above a limit." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ bir sınırın üstüne çıktığında uyarır." + } + } + } + }, + "Alerts when Carbs-on-Board is at or above the amount you set below." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB aşağıda belirlediğiniz miktara eşit veya üstünde olduğunda uyarır." + } + } + } + }, + "Alerts when a future-dated carb entry's scheduled time arrives — a reminder to start eating. Use the max lookahead to ignore fat/protein entries that are typically scheduled further ahead." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İleri tarihli bir karbonhidrat girişinin planlanan saati geldiğinde uyarır — yemeye başlama hatırlatıcısı. Genellikle daha ileriye planlanan yağ/protein girişlerini yok saymak için maksimum ileri bakış süresini kullanın." + } + } + } + }, + "Alerts when a temp target ends." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici hedef sona erdiğinde uyarır." + } + } + } + }, + "Alerts when a temp target starts." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici hedef başladığında uyarır." + } + } + } + }, + "Alerts when an override begins." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override başladığında uyarır." + } + } + } + }, + "Alerts when an override ends." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override sona erdiğinde uyarır." + } + } + } + }, + "Alerts when carbs are logged but no bolus is delivered within the delay below. Allows small-carb / treatment exclusions and pre-bolus detection." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat kaydedilip aşağıdaki gecikme içinde bolus verilmezse uyarır. Küçük karbonhidrat / tedavi istisnalarına ve ön bolus algılamaya izin verir." + } + } + } + }, + "Alerts when glucose readings drop rapidly. For example, three straight readings each falling by at least the amount you set. Optionally limit alerts to only fire below a certain BG level." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz okumaları hızla düştüğünde uyarır. Örneğin art arda üç okumanın her biri belirlediğiniz miktar kadar düştüğünde. İsteğe bağlı olarak uyarıları yalnızca belirli bir KŞ düzeyinin altında sınırlayabilirsiniz." + } + } + } + }, + "Alerts when glucose readings rise rapidly. For example, three straight readings each climbing by at least the amount you set. Optionally limit alerts to only fire above a certain BG." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz okumaları hızla yükseldiğinde uyarır. Örneğin art arda üç okumanın her biri belirlediğiniz miktar kadar yükseldiğinde. İsteğe bağlı olarak uyarıları yalnızca belirli bir KŞ'nin üstünde sınırlayabilirsiniz." + } + } + } + }, + "Alerts when glucose stays above the limit you set below. Use Persistent if you want to ignore brief spikes." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz aşağıda belirlediğiniz sınırın üstünde kaldığında uyarır. Kısa yükselişleri yok saymak için Kalıcı seçeneğini kullanın." + } + } + } + }, + "Alerts when insulin-on-board is high, or when several boluses in quick succession exceed the limits you set." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktif insülin yüksek olduğunda ya da art arda verilen birkaç bolus belirlediğiniz sınırları aştığında uyarır." + } + } + } + }, + "Alerts when no successful loop has occurred for the time you set below." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aşağıda belirlediğiniz süre boyunca başarılı bir loop olmadığında uyarır." + } + } + } + }, + "Alerts when the pump / cannula is within the time window you choose below (relative to the 3-day change limit)." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa / kanül, aşağıda seçtiğiniz zaman penceresine girdiğinde uyarır (3 günlük değişim sınırına göre)." + } + } + } + }, + "Alerts when the recommended bolus equals or exceeds the threshold you set below." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önerilen bolus aşağıda belirlediğiniz eşiğe eşit veya üstünde olduğunda uyarır." + } + } + } + }, + "All" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tümü" + } + } + } + }, + "All Alerts Snoozed" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm Uyarılar Ertelendi" + } + } + } + }, + "All Boluses" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm Boluslar" + } + } + } + }, + "All Sounds Muted" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm Sesler Kapalı" + } + } + } + }, + "All alerts snoozed" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm uyarılar ertelendi" + } + } + } + }, + "All glucose, insulin, and carb data. Your Nightscout URL and API token. Your Dexcom credentials. Remote-command secrets and APNS keys. Time zone. Location data. Logs — these are never sent automatically; the Settings → Logs sharing flow is unchanged and only triggered by you." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm glukoz, insülin ve karbonhidrat verileri. Nightscout URL'niz ve API belirteciniz. Dexcom kimlik bilgileriniz. Uzaktan komut sırları ve APNS anahtarları. Saat dilimi. Konum verileri. Günlükler — bunlar asla otomatik gönderilmez; Ayarlar → Günlükler paylaşım akışı değişmedi ve yalnızca siz tetiklediğinizde çalışır." + } + } + } + }, + "Always" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Her Zaman" + } + } + } + }, + "Ambulatory Glucose Profile (AGP)" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ambulatuvar Glukoz Profili (AGP)" + } + } + } + }, + "An alert fires once the reservoir is at this value or lower." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rezervuar bu değere veya altına indiğinde uyarı verilir." + } + } + } + }, + "An error occurred." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir hata oluştu." + } + } + } + }, + "An unknown error occurred while checking Contacts access." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kişiler erişimi denetlenirken bilinmeyen bir hata oluştu." + } + } + } + }, + "An unknown error occurred." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilinmeyen bir hata oluştu." + } + } + } + }, + "App Expiration Warning" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama Süre Sonu Uyarısı" + } + } + } + }, + "App Settings" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama Ayarları" + } + } + } + }, + "App version, build date, whether this is a TestFlight build, the install instance number, an Apple-supplied per-vendor identifier (IDFV) that resets when all this developer's apps are removed from the device, the hardware identifier (e.g. iPhone15,2), and iOS version. Whether Nightscout and Dexcom are configured (yes/no — no URLs or usernames). Which app you're following (Loop, Trio, etc), if known. A small set of preference flags (units, appearance mode, calendar/contact integration enabled, remote-command type, background refresh method). The full JSON is visible under Diagnostics → What's sent." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama sürümü, derleme tarihi, TestFlight derlemesi olup olmadığı, kurulum örnek numarası, bu geliştiricinin tüm uygulamaları cihazdan kaldırıldığında sıfırlanan Apple tarafından sağlanan satıcı bazlı bir tanımlayıcı (IDFV), donanım tanımlayıcısı (ör. iPhone15,2) ve iOS sürümü. Nightscout ve Dexcom'un yapılandırılıp yapılandırılmadığı (evet/hayır — URL veya kullanıcı adı yok). Biliniyorsa hangi uygulamayı takip ettiğiniz (Loop, Trio vb.). Küçük bir tercih bayrağı kümesi (birimler, görünüm modu, takvim/kişi entegrasyonunun açık olup olmadığı, uzaktan komut türü, arka plan yenileme yöntemi). JSON'un tamamı Tanılama → Neler gönderiliyor altında görülebilir." + } + } + } + }, + "Appearance" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Görünüm" + } + } + } + }, + "Are you sure you want to cancel the active override?" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin override'ı iptal etmek istediğinizden emin misiniz?" + } + } + } + }, + "Are you sure you want to cancel the existing temp target?" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mevcut geçici hedefi iptal etmek istediğinizden emin misiniz?" + } + } + } + }, + "Are you sure you want to send %@ U?" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ U göndermek istediğinizden emin misiniz?" + } + } + } + }, + "Are you sure you want to send the meal data for %@?" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ için öğün verilerini göndermek istediğinizden emin misiniz?" + } + } + } + }, + "Are you sure you want to send the meal data now?" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öğün verilerini şimdi göndermek istediğinizden emin misiniz?" + } + } + } + }, + "Arrange your tabs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekmelerinizi düzenleyin" + } + } + } + }, + "At or Above" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eşit veya Üstünde" + } + } + } + }, + "At or Below" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eşit veya Altında" + } + } + } + }, + "Audio During Calls" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aramalar Sırasında Ses" + } + } + } + }, + "Authentication error. Check your certificate or authentication token. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kimlik doğrulama hatası. Sertifikanızı veya kimlik doğrulama belirtecinizi denetleyin. %@" + } + } + } + }, + "Authentication failed" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kimlik doğrulama başarısız" + } + } + } + }, + "Auto-Snooze CGM Start" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM Başlangıcında Otomatik Ertele" + } + } + } + }, + "Automatic" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otomatik" + } + } + } + }, + "Automatic Bolus" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otomatik Bolus" + } + } + } + }, + "Autosens" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autosens" + } + } + } + }, + "Auto‑Snooze CGM Start" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM Başlangıcında Otomatik Ertele" + } + } + } + }, + "Available Overrides" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanılabilir Override'lar" + } + } + } + }, + "Available Variables" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanılabilir Değişkenler" + } + } + } + }, + "Avg Bolus" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ort. Bolus" + } + } + } + }, + "Avg Carbs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ort. Karbonhidrat" + } + } + } + }, + "Avg Glucose" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ort. Glukoz" + } + } + } + }, + "BG" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ" + } + } + } + }, + "BG Check" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Ölçümü" + } + } + } + }, + "BG Limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Sınırı" + } + } + } + }, + "BG Speak is now off" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Seslendirme kapatıldı" + } + } + } + }, + "BG Speak is now on" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Seslendirme açıldı" + } + } + } + }, + "BG Update Delay" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Güncelleme Gecikmesi" + } + } + } + }, + "BG Update Delay (Sec): %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Güncelleme Gecikmesi (sn): %@" + } + } + } + }, + "Back" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri" + } + } + } + }, + "Background Color" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arka Plan Rengi" + } + } + } + }, + "Background Refresh" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arka Plan Yenileme" + } + } + } + }, + "Background Refresh Type" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arka Plan Yenileme Türü" + } + } + } + }, + "Bad request. The request was invalid or malformed. %@\n\n%@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hatalı istek. İstek geçersiz veya bozuk. %1$@\n\n%2$@" + } + } + } + }, + "Basal" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazal" + } + } + } + }, + "Basal / BG Scale" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazal / KŞ Ölçeği" + } + } + } + }, + "Battery" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pil" + } + } + } + }, + "Battery Drop" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pil Düşüşü" + } + } + } + }, + "Battery drops quickly." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pil hızla düşüyor." + } + } + } + }, + "Below" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Altında" + } + } + } + }, + "Below is the exact JSON object that LoopFollow would send to lf.bjorkert.se right now." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoopFollow'un şu anda lf.bjorkert.se adresine göndereceği JSON nesnesinin tamamı aşağıdadır." + } + } + } + }, + "Below: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alt: %@" + } + } + } + }, + "Bolus" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus" + } + } + } + }, + "Bolus Amount" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Miktarı" + } + } + } + }, + "Bolus Count" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Sayısı" + } + } + } + }, + "Bolus Filter" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Filtresi" + } + } + } + }, + "Bolus Increment" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Artışı" + } + } + } + }, + "Bolus Increment: %.3f U" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Artışı: %.3f U" + } + } + } + }, + "Bolus: %.2f U" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus: %.2f U" + } + } + } + }, + "Bolus: %g U" : { + "comment" : "Treatment pill detail", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus: %g U" + } + } + } + }, + "Boluses" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boluslar" + } + } + } + }, + "Boluses Size Limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Boyutu Sınırı" + } + } + } + }, + "Boluses at or below this size are ignored." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu boyuta eşit veya altındaki boluslar yok sayılır." + } + } + } + }, + "Build Information" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Derleme Bilgileri" + } + } + } + }, + "Built-in" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yerleşik" + } + } + } + }, + "Bundle ID: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paket Kimliği: %@" + } + } + } + }, + "Bundle identifier not configured" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paket kimliği yapılandırılmamış" + } + } + } + }, + "CAGE" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CAGE" + } + } + } + }, + "CHARGING" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ŞARJ OLUYOR" + } + } + } + }, + "COB" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + } + } + }, + "COB Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB Uyarısı" + } + } + } + }, + "CR" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CR" + } + } + } + }, + "CV" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CV" + } + } + } + }, + "Calculated %lld minutes ago" : { + "comment" : "Age of the recommended bolus calculation", + "extractionState" : "manual", + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calculated %lld minute ago" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calculated %lld minutes ago" + } + } + } + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld dakika önce hesaplandı" + } + } + } + }, + "Calendar" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Takvim" + } + } + } + }, + "Calendar Text" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Takvim Metni" + } + } + } + }, + "Calendar access denied" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Takvim erişimi reddedildi" + } + } + } + }, + "Cancel" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İptal" + } + } + } + }, + "Cancel Active Override" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin Override'ı İptal Et" + } + } + } + }, + "Cancel Override" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override'ı İptal Et" + } + } + } + }, + "Cancel Temp Target" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Hedefi İptal Et" + } + } + } + }, + "Carb entries at or below this amount will not trigger the alarm." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu miktara eşit veya altındaki karbonhidrat girişleri alarmı tetiklemez." + } + } + } + }, + "Carbs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat" + } + } + } + }, + "Carbs Amount" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat Miktarı" + } + } + } + }, + "Carbs amount (%lldg) exceeds the maximum allowed (%lldg). Please reduce the amount." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat miktarı (%1$lldg) izin verilen en yüksek değeri (%2$lldg) aşıyor. Lütfen miktarı azaltın." + } + } + } + }, + "Carbs today" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bugünkü karb." + } + } + } + }, + "Carbs without bolus." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolussuz karbonhidrat." + } + } + } + }, + "Carbs-on-Board Limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB Sınırı" + } + } + } + }, + "Carbs: %.0f g" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat: %.0f g" + } + } + } + }, + "Carbs: %g g" : { + "comment" : "Treatment pill detail", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat: %g g" + } + } + } + }, + "Category" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kategori" + } + } + } + }, + "Character Limit Reached" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karakter Sınırına Ulaşıldı" + } + } + } + }, + "Checking Nightscout profile history…" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nightscout profil geçmişi denetleniyor…" + } + } + } + }, + "Checking your account…" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hesabınız denetleniyor…" + } + } + } + }, + "Checking your connection…" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlantınız denetleniyor…" + } + } + } + }, + "Checking..." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denetleniyor..." + } + } + } + }, + "Choose Tone" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ton Seç" + } + } + } + }, + "Choose a data source" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir veri kaynağı seçin" + } + } + } + }, + "Choose how long the app should wait before alerting." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulamanın uyarmadan önce ne kadar bekleyeceğini seçin." + } + } + } + }, + "Choose how many days of notice you’d like before the build becomes unusable." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Derleme kullanılamaz hale gelmeden kaç gün önce bildirim istediğinizi seçin." + } + } + } + }, + "Close" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapat" + } + } + } + }, + "Color" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renk" + } + } + } + }, + "Color BG Text" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Metnini Renklendir" + } + } + } + }, + "Color Mode" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renk Modu" + } + } + } + }, + "Color Options" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renk Seçenekleri" + } + } + } + }, + "Coloring enabled" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renklendirme açık" + } + } + } + }, + "Command Sent" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Komut Gönderildi" + } + } + } + }, + "Cone" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koni" + } + } + } + }, + "Configure Loop APNS" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop APNS'i Yapılandır" + } + } + } + }, + "Confirm" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onayla" + } + } + } + }, + "Confirm & Import" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onayla ve İçe Aktar" + } + } + } + }, + "Confirm Bolus" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolusu Onayla" + } + } + } + }, + "Confirm Cancellation" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İptali Onayla" + } + } + } + }, + "Confirm Carbs" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidratı Onayla" + } + } + } + }, + "Confirm Command" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Komutu Onayla" + } + } + } + }, + "Confirm Insulin" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülini Onayla" + } + } + } + }, + "Confirm Meal" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öğünü Onayla" + } + } + } + }, + "Connect Dexcom Share" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dexcom Share'e bağlanın" + } + } + } + }, + "Connected" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlı" + } + } + } + }, + "Connected — read-only" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlı — salt okunur" + } + } + } + }, + "Connecting..." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlanıyor..." + } + } + } + }, + "Consecutive Drops" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Art Arda Düşüş" + } + } + } + }, + "Consecutive Rises" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Art Arda Yükseliş" + } + } + } + }, + "Consumption Time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüketim Zamanı" + } + } + } + }, + "Contact" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kişi" + } + } + } + }, + "Contact Integration" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kişi Entegrasyonu" + } + } + } + }, + "Continue" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Devam" + } + } + } + }, + "Copy JSON" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "JSON'u Kopyala" + } + } + } + }, + "Copy from another phone" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başka bir telefondan kopyalayın" + } + } + } + }, + "Could not create a token. Please try again." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belirteç oluşturulamadı. Lütfen yeniden deneyin." + } + } + } + }, + "Couldn't reach that site. Check the URL." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siteye ulaşılamadı. URL'yi denetleyin." + } + } + } + }, + "Count" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sayı" + } + } + } + }, + "Count boluses given up to this many minutes before the carb entry as valid." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat girişinden bu kadar dakika öncesine kadar verilen bolusları geçerli say." + } + } + } + }, + "Create Read-Only Token" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salt Okunur Belirteç Oluştur" + } + } + } + }, + "Create one for me" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Benim için oluştur" + } + } + } + }, + "Creating read-only token…" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salt okunur belirteç oluşturuluyor…" + } + } + } + }, + "Credential validation failed: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kimlik bilgisi doğrulaması başarısız: %@" + } + } + } + }, + "Current OTP Code" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncel OTP Kodu" + } + } + } + }, + "Current TOTP Code:" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncel TOTP Kodu:" + } + } + } + }, + "Current Target" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncel Hedef" + } + } + } + }, + "Current Token:" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncel Belirteç:" + } + } + } + }, + "Current URL:" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncel URL:" + } + } + } + }, + "Custom" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Özel" + } + } + } + }, + "Custom Range" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Özel Aralık" + } + } + } + }, + "Custom Sound" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Özel Ses" + } + } + } + }, + "Custom sounds stay on this device and aren't included in settings export." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Özel sesler bu cihazda kalır ve ayar dışa aktarımına dahil edilmez." + } + } + } + }, + "Cutoffs in %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sınırlar (%@)" + } + } + } + }, + "DB Size" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "VT Boyutu" + } + } + } + }, + "Dark" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koyu" + } + } + } + }, + "Data Availability" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veri Kullanılabilirliği" + } + } + } + }, + "Database Size" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veritabanı Boyutu" + } + } + } + }, + "Day" : { + "comment" : "Alarm schedule option: daytime only", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gündüz" + } + } + } + }, + "Day & Night" : { + "comment" : "Alarm schedule option: at any time of day", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gündüz ve Gece" + } + } + } + }, + "Day / Night" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gündüz / Gece" + } + } + } + }, + "Day / Night Schedule" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gündüz / Gece Programı" + } + } + } + }, + "Day starts" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gündüz başlangıcı" + } + } + } + }, + "Days of notice" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildirim günü" + } + } + } + }, + "Debug / Info" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hata Ayıklama / Bilgi" + } + } + } + }, + "Debug Log Level" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hata Ayıklama Günlük Düzeyi" + } + } + } + }, + "Default Snooze:" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Varsayılan Erteleme:" + } + } + } + }, + "Delay" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gecikme" + } + } + } + }, + "Delay Between Sounds" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sesler Arası Gecikme" + } + } + } + }, + "Delete" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sil" + } + } + } + }, + "Delete Alarm" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarmı Sil" + } + } + } + }, + "Delete this alarm?" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu alarm silinsin mi?" + } + } + } + }, + "Delta" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta" + } + } + } + }, + "Description" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açıklama" + } + } + } + }, + "Developer Team ID" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geliştirici Ekip Kimliği" + } + } + } + }, + "Device Data" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihaz Verileri" + } + } + } + }, + "Device Token: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihaz Belirteci: %@" + } + } + } + }, + "Device authentication is not available. " : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihaz kimlik doğrulaması kullanılamıyor. " + } + } + } + }, + "Device token not configured" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihaz belirteci yapılandırılmamış" + } + } + } + }, + "Device tokens are alternating in recent profile uploads (%@ tokens involved across %@ records). This usually means more than one app installation is uploading to the same Nightscout. Remove the app from spare or unused phones." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Device tokens are alternating in recent profile uploads (%1$@ tokens involved across %2$@ records). This usually means more than one app installation is uploading to the same Nightscout. Remove the app from spare or unused phones." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Son profil yüklemelerinde cihaz belirteçleri dönüşümlü olarak değişiyor (%2$@ kayıtta %1$@ belirteç). Bu genellikle aynı Nightscout'a birden fazla uygulama kurulumunun yükleme yaptığı anlamına gelir. Uygulamayı yedek veya kullanılmayan telefonlardan kaldırın." + } + } + } + }, + "Dexcom" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dexcom" + } + } + } + }, + "Dexcom Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dexcom Ayarları" + } + } + } + }, + "Dexcom Share" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dexcom Share" + } + } + } + }, + "Diagnostics" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tanılama" + } + } + } + }, + "Diagnostics unavailable: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tanılama kullanılamıyor: %@" + } + } + } + }, + "Disconnect" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlantıyı Kes" + } + } + } + }, + "Disconnected" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlantı yok" + } + } + } + }, + "Disconnected for %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ süredir bağlantı yok" + } + } + } + }, + "Dismiss" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapat" + } + } + } + }, + "Display" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Görüntüleme" + } + } + } + }, + "Display App Badge" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama Rozetini Göster" + } + } + } + }, + "Display Dots" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Noktaları Göster" + } + } + } + }, + "Display Lines" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çizgileri Göster" + } + } + } + }, + "Display Small Graph" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Küçük Grafiği Göster" + } + } + } + }, + "Display Stats" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İstatistikleri Göster" + } + } + } + }, + "Do you want to activate the override '%@'?" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "'%@' override'ını etkinleştirmek istiyor musunuz?" + } + } + } + }, + "Done" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitti" + } + } + } + }, + "Download Prediction" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahmini İndir" + } + } + } + }, + "Download Treatments" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tedavileri İndir" + } + } + } + }, + "Drag to reorder" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sıralamak için sürükleyin" + } + } + } + }, + "Drag to reorder. Tap a row to set its visibility and colors." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sıralamak için sürükleyin. Görünürlüğü ve renkleri ayarlamak için bir satıra dokunun." + } + } + } + }, + "Drop Amount" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşüş Miktarı" + } + } + } + }, + "Drop per reading" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Okuma başına düşüş" + } + } + } + }, + "Dropping below" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şunun altına düşerken" + } + } + } + }, + "Duration" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Süre" + } + } + } + }, + "Duration: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Süre: %@" + } + } + } + }, + "Duration: %@ minutes" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Süre: %@ dakika" + } + } + } + }, + "Dynamic (BG Range)" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dinamik (KŞ Aralığı)" + } + } + } + }, + "Dynamic mode colors text based on BG range: Green (in range), Yellow (high), Red (low)" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dinamik mod metni KŞ aralığına göre renklendirir: Yeşil (aralıkta), Sarı (yüksek), Kırmızı (düşük)" + } + } + } + }, + "Early Reminder" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erken Hatırlatma" + } + } + } + }, + "Elapsed time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçen süre" + } + } + } + }, + "Enable Contact BG Updates" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kişi KŞ Güncellemelerini Aç" + } + } + } + }, + "Enable Live Activity" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canlı Etkinliği Aç" + } + } + } + }, + "Enable Notifications" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildirimleri Aç" + } + } + } + }, + "Enable WebSocket" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "WebSocket'i Aç" + } + } + } + }, + "Enable coloring" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renklendirmeyi aç" + } + } + } + }, + "Enable high limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üst sınırı aç" + } + } + } + }, + "Enable indefinitely" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Süresiz etkinleştir" + } + } + } + }, + "Enable low limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alt sınırı aç" + } + } + } + }, + "Enable this alarm" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu alarmı aç" + } + } + } + }, + "Enabled" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açık" + } + } + } + }, + "Enact" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygula" + } + } + } + }, + "End Date" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitiş Tarihi" + } + } + } + }, + "End now" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şimdi bitir" + } + } + } + }, + "Endpoint" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uç Nokta" + } + } + } + }, + "English" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İngilizce" + } + } + } + }, + "Enter QR code URL or scan from Loop app" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR kodu URL'sini girin veya Loop uygulamasından tarayın" + } + } + } + }, + "Enter URL" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL girin" + } + } + } + }, + "Enter User" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı girin" + } + } + } + }, + "Enter Username" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı adı girin" + } + } + } + }, + "Enter or paste your full Nightscout address. If the link already includes a token, we'll fill both in for you." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tam Nightscout adresinizi girin veya yapıştırın. Bağlantı zaten bir belirteç içeriyorsa ikisini de sizin için dolduracağız." + } + } + } + }, + "Enter your site address to connect." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlanmak için site adresinizi girin." + } + } + } + }, + "Enter your username and password" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı adınızı ve parolanızı girin" + } + } + } + }, + "Environment" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ortam" + } + } + } + }, + "Error" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hata" + } + } + } + }, + "Est. A1C:" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tah. A1C:" + } + } + } + }, + "Eventual" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nihai" + } + } + } + }, + "Excellent" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mükemmel" + } + } + } + }, + "Existing Temp Target" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mevcut Geçici Hedef" + } + } + } + }, + "Expected bg delay: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beklenen KŞ gecikmesi: %@" + } + } + } + }, + "Expected: 1 CGM reading every 5 minutes" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beklenen: her 5 dakikada 1 CGM okuması" + } + } + } + }, + "Export" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dışa Aktar" + } + } + } + }, + "Export %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ Dışa Aktar" + } + } + } + }, + "Export Settings To QR Code" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayarları QR Koduna Aktar" + } + } + } + }, + "F%@ P%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "F%1$@ P%2$@" + } + } + }, + "shouldTranslate" : false + }, + "FALLING BG" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ DÜŞÜYOR" + } + } + } + }, + "Face ID is not available. Please set up a device passcode in Settings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Face ID kullanılamıyor. Lütfen Ayarlar'dan bir cihaz parolası ayarlayın." + } + } + } + }, + "Failed generating JWT token, check APNS Key ID, APNS Key and Team ID" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "JWT belirteci oluşturulamadı; APNS Anahtar Kimliği, APNS Anahtarı ve Ekip Kimliği'ni denetleyin" + } + } + } + }, + "Failed to activate override: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override etkinleştirilemedi: %@" + } + } + } + }, + "Failed to cancel override: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override iptal edilemedi: %@" + } + } + } + }, + "Failed to construct APNs URL" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNs URL'si oluşturulamadı" + } + } + } + }, + "Failed to encode or encrypt push message: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anlık bildirim mesajı kodlanamadı veya şifrelenemedi: %@" + } + } + } + }, + "Failed to generate JWT, please check that the APNS Key ID, APNS Key and Team ID are correct." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "JWT oluşturulamadı; lütfen APNS Anahtar Kimliği, APNS Anahtarı ve Ekip Kimliği'nin doğru olduğundan emin olun." + } + } + } + }, + "Failed to generate JWT, please check that the token is correct." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "JWT oluşturulamadı; lütfen belirtecin doğru olduğundan emin olun." + } + } + } + }, + "Failed to generate QR code" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR kodu oluşturulamadı" + } + } + } + }, + "Failed to get a valid HTTP response." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçerli bir HTTP yanıtı alınamadı." + } + } + } + }, + "Failed to initialize security module. Check shared secret." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güvenlik modülü başlatılamadı. Paylaşılan gizli anahtarı denetleyin." + } + } + } + }, + "Failed to load override presets: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override ön ayarları yüklenemedi: %@" + } + } + } + }, + "Failed to send push notification: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anlık bildirim gönderilemedi: %@" + } + } + } + }, + "Failed to serialize APNS payload: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNS yükü serileştirilemedi: %@" + } + } + } + }, + "Fair" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Orta" + } + } + } + }, + "Falls by" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşüş miktarı" + } + } + } + }, + "Fast Drop Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı Düşüş Uyarısı" + } + } + } + }, + "Fast Drop Delta" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı Düşüş Deltası" + } + } + } + }, + "Fast Rise Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı Yükseliş Uyarısı" + } + } + } + }, + "Fat" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yağ" + } + } + } + }, + "Fat: %.0f g" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yağ: %.0f g" + } + } + } + }, + "Features" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Özellikler" + } + } + } + }, + "Filter" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filtre" + } + } + } + }, + "Finish" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitir" + } + } + } + }, + "Finishing connection…" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlantı tamamlanıyor…" + } + } + } + }, + "Food Type" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yiyecek Türü" + } + } + } + }, + "Force portrait mode" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dikey modu zorla" + } + } + } + }, + "French" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fransızca" + } + } + } + }, + "Frequency" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sıklık" + } + } + } + }, + "From: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaynak: %@" + } + } + } + }, + "Future Carbs Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gelecek Karbonhidrat Uyarısı" + } + } + } + }, + "GMI" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + } + } + }, + "GMI:" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI:" + } + } + } + }, + "GRI (Glucose Risk Index)" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GRI (Glukoz Risk İndeksi)" + } + } + } + }, + "General" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genel" + } + } + } + }, + "Get Started" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başlayın" + } + } + } + }, + "Give each alarm a unique name—especially if you’ve added more than one of the same kind—so you can tell them apart at a glance." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Her alarma benzersiz bir ad verin — özellikle aynı türden birden fazla eklediyseniz — böylece bir bakışta ayırt edebilirsiniz." + } + } + } + }, + "Glucose" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz" + } + } + } + }, + "Glucose Unit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz Birimi" + } + } + } + }, + "Glucose must stay below the threshold for this many minutes before the alert sounds. Set 0 to alert immediately." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uyarı çalmadan önce glukozun eşiğin altında bu kadar dakika kalması gerekir. Hemen uyarmak için 0 girin." + } + } + } + }, + "Glucose value" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz değeri" + } + } + } + }, + "Glycemic Metrics" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glisemik Ölçütler" + } + } + } + }, + "Good" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İyi" + } + } + } + }, + "Grams" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gram" + } + } + } + }, + "Graph" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grafik" + } + } + } + }, + "Graph Basal" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazal Grafiği" + } + } + } + }, + "Graph Bolus" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Grafiği" + } + } + } + }, + "Graph Carbs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat Grafiği" + } + } + } + }, + "Graph Display" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grafik Görünümü" + } + } + } + }, + "Graph Other Treatments" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diğer Tedavilerin Grafiği" + } + } + } + }, + "Grid Slot - CarPlay / Watch" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izgara Yuvası - CarPlay / Watch" + } + } + } + }, + "Grid Slots" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izgara Yuvaları" + } + } + } + }, + "Grid Slots - Live Activity" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izgara Yuvaları - Canlı Etkinlik" + } + } + } + }, + "Guardrails" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güvenlik Sınırları" + } + } + } + }, + "HIGH" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "YÜKSEK" + } + } + } + }, + "Help us help you" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Size yardımcı olmamıza yardım edin" + } + } + } + }, + "Help us help you!" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Size yardımcı olmamıza yardım edin!" + } + } + } + }, + "Here's what we'll do" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şunları yapacağız" + } + } + } + }, + "Hide Information Table" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilgi Tablosunu Gizle" + } + } + } + }, + "High" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksek" + } + } + } + }, + "High BG Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksek KŞ Uyarısı" + } + } + } + }, + "High BG Limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksek KŞ Sınırı" + } + } + } + }, + "High Glucose Limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksek Glukoz Sınırı" + } + } + } + }, + "High Limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üst Sınır" + } + } + } + }, + "High carbs-on-board." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksek aktif karbonhidrat." + } + } + } + }, + "High insulin-on-board." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüksek aktif insülin." + } + } + } + }, + "History" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçmiş" + } + } + } + }, + "Home" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ana Sayfa" + } + } + } + }, + "Hours" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saat" + } + } + } + }, + "Hours of Prediction" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahmin Saati" + } + } + } + }, + "How far back to look for that drop." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu düşüş için ne kadar geriye bakılacağı." + } + } + } + }, + "How far back to look for those boluses." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu boluslar için ne kadar geriye bakılacağı." + } + } + } + }, + "How long glucose must remain above the threshold before the alarm actually fires. Set to 0 for an immediate alert." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarm çalmadan önce glukozun eşiğin üstünde ne kadar kalması gerektiği. Hemen uyarmak için 0 girin." + } + } + } + }, + "How many hours before the 3-day limit you’d like a reminder." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "3 günlük sınırdan kaç saat önce hatırlatma istediğiniz." + } + } + } + }, + "Hyperglycemia Component (%)" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiperglisemi Bileşeni (%)" + } + } + } + }, + "Hypoglycemia Component (%)" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hipoglisemi Bileşeni (%)" + } + } + } + }, + "I have a token" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belirtecim var" + } + } + } + }, + "IAGE" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAGE" + } + } + } + }, + "IOB" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + } + } + }, + "IOB Above" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB Üstünde" + } + } + } + }, + "IOB Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB Uyarısı" + } + } + } + }, + "ISF" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ISF" + } + } + } + }, + "Ignore Zero BG" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sıfır KŞ'yi Yok Say" + } + } + } + }, + "Ignore carb entries below this amount." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu miktarın altındaki karbonhidrat girişlerini yok say." + } + } + } + }, + "Ignore low BG" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşük KŞ'yi yok say" + } + } + } + }, + "Ignore small boluses" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Küçük bolusları yok say" + } + } + } + }, + "Ignore small carbs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Küçük karbonhidratları yok say" + } + } + } + }, + "Import Failed" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İçe Aktarma Başarısız" + } + } + } + }, + "Import Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayarları İçe Aktar" + } + } + } + }, + "Import Settings from QR Code" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayarları QR Kodundan İçe Aktar" + } + } + } + }, + "Import Sound…" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ses İçe Aktar…" + } + } + } + }, + "Import/Export" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İçe/Dışa Aktar" + } + } + } + }, + "Import/Export Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayarları İçe/Dışa Aktar" + } + } + } + }, + "In Menu" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menüde" + } + } + } + }, + "Inactive" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin Değil" + } + } + } + }, + "Include Delta in" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta'yı şuraya ekle" + } + } + } + }, + "Include IOB in" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB'yi şuraya ekle" + } + } + } + }, + "Include Trend in" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendi şuraya ekle" + } + } + } + }, + "Increment" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Artış" + } + } + } + }, + "Information Display" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilgi Görüntüleme" + } + } + } + }, + "Information Display Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilgi Görüntüleme Ayarları" + } + } + } + }, + "Insulin" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin" + } + } + } + }, + "Insulin Amount" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin Miktarı" + } + } + } + }, + "Insulin On Board" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktif İnsülin" + } + } + } + }, + "Insulin Totals" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin Toplamları" + } + } + } + }, + "Insulin amount (%.2fU) exceeds the maximum allowed (%.2fU). Please reduce the amount." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin miktarı (%1$.2fU) izin verilen en yüksek değeri (%2$.2fU) aşıyor. Lütfen miktarı azaltın." + } + } + } + }, + "Insulin: %@%%" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin: %@%%" + } + } + } + }, + "Internal server error at APNs. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNs'de iç sunucu hatası. %@" + } + } + } + }, + "Invalid APNs key" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz APNs anahtarı" + } + } + } + }, + "Invalid Key ID" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz Anahtar Kimliği" + } + } + } + }, + "Invalid QR code URL" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz QR kodu URL'si" + } + } + } + }, + "Invalid QR code URL. Please re-scan the QR code in settings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz QR kodu URL'si. Lütfen ayarlardan QR kodunu yeniden tarayın." + } + } + } + }, + "Invalid Token" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz Belirteç" + } + } + } + }, + "Invalid URL" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz URL" + } + } + } + }, + "Invalid input." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz giriş." + } + } + } + }, + "Invalid request: Only POST requests are supported. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz istek: Yalnızca POST istekleri destekleniyor. %@" + } + } + } + }, + "Invalid request: The :path value was incorrect. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz istek: :path değeri yanlıştı. %@" + } + } + } + }, + "Invalid response from server" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunucudan geçersiz yanıt" + } + } + } + }, + "Italian" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İtalyanca" + } + } + } + }, + "Keep Screen Active" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekranı Açık Tut" + } + } + } + }, + "Key ID must be exactly 10 uppercase letters or digits." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anahtar Kimliği tam olarak 10 büyük harf veya rakamdan oluşmalıdır." + } + } + } + }, + "LOW" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DÜŞÜK" + } + } + } + }, + "Language" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dil" + } + } + } + }, + "Last connection: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Son bağlantı: %@" + } + } + } + }, + "Let's get you connected to your data and set up a few recommended alarms. It only takes a few minutes." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sizi verilerinize bağlayalım ve önerilen birkaç alarmı kuralım. Yalnızca birkaç dakika sürer." + } + } + } + }, + "Lifetime" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanım Ömrü" + } + } + } + }, + "Light" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açık" + } + } + } + }, + "Line 1" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satır 1" + } + } + } + }, + "Line 2" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Satır 2" + } + } + } + }, + "Lines" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çizgiler" + } + } + } + }, + "Live Activity" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canlı Etkinlik" + } + } + } + }, + "Live Activity Restarted" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canlı Etkinlik Yeniden Başlatıldı" + } + } + } + }, + "Live Activity updates require APNs credentials. Configure them in Settings → APN." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canlı Etkinlik güncellemeleri APNs kimlik bilgileri gerektirir. Ayarlar → APN'den yapılandırın." + } + } + } + }, + "Load More" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha Fazla Yükle" + } + } + } + }, + "Loading data..." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veriler yükleniyor..." + } + } + } + }, + "Loading override presets..." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override ön ayarları yükleniyor..." + } + } + } + }, + "Loading stats..." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İstatistikler yükleniyor..." + } + } + } + }, + "Loading..." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yükleniyor..." + } + } + } + }, + "Logging" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlük Kaydı" + } + } + } + }, + "Logging Options" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlük Seçenekleri" + } + } + } + }, + "Look ahead this many minutes in the forecast; if any future value is at or below the threshold, you’ll be warned early. Set 0 to disable." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahminde bu kadar dakika ileriye bakılır; gelecekteki herhangi bir değer eşiğe eşit veya altındaysa erken uyarılırsınız. Kapatmak için 0 girin." + } + } + } + }, + "Lookahead" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İleri Bakış" + } + } + } + }, + "Loop APNS Configuration" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop APNS Yapılandırması" + } + } + } + }, + "Loop APNS Configuration not valid" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop APNS yapılandırması geçersiz" + } + } + } + }, + "Loop APNS Not Configured" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop APNS Yapılandırılmamış" + } + } + } + }, + "Loop APNS setup is incomplete. Please configure all required fields in settings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop APNS kurulumu eksik. Lütfen ayarlardan gerekli tüm alanları yapılandırın." + } + } + } + }, + "Loop Remote Control" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop Uzaktan Kumanda" + } + } + } + }, + "Loop hasn’t completed." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop tamamlanmadı." + } + } + } + }, + "LoopFollow APNs Credentials" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoopFollow APNs Kimlik Bilgileri" + } + } + } + }, + "LoopFollow needs a glucose data source. Pick one now — you can change or add more later in Settings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoopFollow'un bir glukoz veri kaynağına ihtiyacı var. Şimdi birini seçin — daha sonra Ayarlar'dan değiştirebilir veya ekleyebilirsiniz." + } + } + } + }, + "LoopFollow uses notifications to deliver your alarms — like low or high glucose, or a missed reading. Without them, alarms can't reach you when the app is in the background." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoopFollow alarmlarınızı — düşük veya yüksek glukoz, kaçırılan okuma gibi — bildirimlerle iletir. Bildirimler olmadan uygulama arka plandayken alarmlar size ulaşamaz." + } + } + } + }, + "LoopFollow/Helpers/Telemetry.swift on GitHub." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GitHub'da LoopFollow/Helpers/Telemetry.swift." + } + } + } + }, + "Looping app expiration" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop uygulaması süre sonu" + } + } + } + }, + "Looping-app build expiring." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop uygulaması derlemesinin süresi doluyor." + } + } + } + }, + "Low" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşük" + } + } + } + }, + "Low BG Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşük KŞ Uyarısı" + } + } + } + }, + "Low BG Limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşük KŞ Sınırı" + } + } + } + }, + "Low Battery" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşük Pil" + } + } + } + }, + "Low Limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alt Sınır" + } + } + } + }, + "Manual" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manuel" + } + } + } + }, + "Max" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maks" + } + } + } + }, + "Max %@ alarms at a time" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aynı anda en fazla %@ alarm" + } + } + } + }, + "Max Bolus" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maks Bolus" + } + } + } + }, + "Max Carbs" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maks Karbonhidrat" + } + } + } + }, + "Max Fat" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maks Yağ" + } + } + } + }, + "Max Lookahead" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maks İleri Bakış" + } + } + } + }, + "Max Protein" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maks Protein" + } + } + } + }, + "Maximum" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maksimum" + } + } + } + }, + "Maximum: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maksimum: %@" + } + } + } + }, + "Meal" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öğün" + } + } + } + }, + "Meal Data" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öğün Verileri" + } + } + } + }, + "Meal Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öğün Ayarları" + } + } + } + }, + "Meal with Bolus" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Boluslu Öğün" + } + } + } + }, + "Meal with Fat/Protein" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yağ/Proteinli Öğün" + } + } + } + }, + "Menu" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menü" + } + } + } + }, + "Metric" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ölçüt" + } + } + } + }, + "Min" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Min" + } + } + } + }, + "Min BG Scale" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Min KŞ Ölçeği" + } + } + } + }, + "Min Basal" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Min Bazal" + } + } + } + }, + "Min/Max" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Min/Maks" + } + } + } + }, + "Minimum" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minimum" + } + } + } + }, + "Minimum Carbs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minimum Karbonhidrat" + } + } + } + }, + "Minimum: %@\n" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minimum: %@\n" + } + } + } + }, + "Minutes" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dakika" + } + } + } + }, + "Minutes to wait after the carb entry before checking for a bolus." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat girişinden sonra bolus denetlemeden önce beklenecek dakika." + } + } + } + }, + "Missed Bolus Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaçırılan Bolus Uyarısı" + } + } + } + }, + "Missed Reading Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaçırılan Okuma Uyarısı" + } + } + } + }, + "Missing required fields for command: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Komut için gerekli alanlar eksik: %@" + } + } + } + }, + "Multiple devices uploading profiles" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Birden fazla cihaz profil yüklüyor" + } + } + } + }, + "Name" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ad" + } + } + } + }, + "Network Error" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağ Hatası" + } + } + } + }, + "Network error occurred" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağ hatası oluştu" + } + } + } + }, + "Network error — check your connection" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağ hatası — bağlantınızı denetleyin" + } + } + } + }, + "Network error. Check your connection and try again." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağ hatası. Bağlantınızı denetleyip yeniden deneyin." + } + } + } + }, + "Never" : { + "comment" : "Alarm schedule option: never", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hiçbir Zaman" + } + } + } + }, + "New Target: %@ %@\nDuration: %@ minutes" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "New Target: %1$@ %2$@\nDuration: %3$@ minutes" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeni Hedef: %1$@ %2$@\nSüre: %3$@ dakika" + } + } + } + }, + "Night" : { + "comment" : "Alarm schedule option: nighttime only", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gece" + } + } + } + }, + "Night starts" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gece başlangıcı" + } + } + } + }, + "Nightscout" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nightscout" + } + } + } + }, + "Nightscout Database Size" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nightscout Veritabanı Boyutu" + } + } + } + }, + "Nightscout URL" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nightscout URL'si" + } + } + } + }, + "Nightscout URL and token not configured in settings" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayarlarda Nightscout URL'si ve belirteci yapılandırılmamış" + } + } + } + }, + "Nightscout database filling up." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nightscout veritabanı doluyor." + } + } + } + }, + "No CGM data for X minutes." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "X dakikadır CGM verisi yok." + } + } + } + }, + "No Loop for…" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şu süredir Loop yok…" + } + } + } + }, + "No Results" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sonuç Yok" + } + } + } + }, + "No alarms match “%@”." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "“%@” ile eşleşen alarm yok." + } + } + } + }, + "No background refresh. Alarms and updates will not work unless the app is open in the foreground." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arka plan yenilemesi yok. Uygulama ön planda açık olmadıkça alarmlar ve güncellemeler çalışmaz." + } + } + } + }, + "No bundle identifier found in profile" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilde paket kimliği bulunamadı" + } + } + } + }, + "No data available" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veri yok" + } + } + } + }, + "No device token found in profile" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilde cihaz belirteci bulunamadı" + } + } + } + }, + "No devices found yet. They'll appear here when discovered." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Henüz cihaz bulunamadı. Bulunduklarında burada görünecekler." + } + } + } + }, + "No issues detected" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sorun saptanmadı" + } + } + } + }, + "No matches for “%@”." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "“%@” için eşleşme yok." + } + } + } + }, + "No override presets found. Configure presets in your Loop app." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override ön ayarı bulunamadı. Ön ayarları Loop uygulamanızda yapılandırın." + } + } + } + }, + "No overrides available." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanılabilir override yok." + } + } + } + }, + "No reading for" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şu süredir okuma yok" + } + } + } + }, + "No recent treatments" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yakın zamanda tedavi yok" + } + } + } + }, + "No thanks" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hayır, teşekkürler" + } + } + } + }, + "Not Looping Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop Çalışmıyor Uyarısı" + } + } + } + }, + "Not now" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şimdi değil" + } + } + } + }, + "Not set" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayarlanmamış" + } + } + } + }, + "Note: The meal will be scheduled, but the bolus is enacted immediately." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Not: Öğün planlanır ancak bolus hemen uygulanır." + } + } + } + }, + "Notice Period" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bildirim Süresi" + } + } + } + }, + "Number of Drops" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşüş Sayısı" + } + } + } + }, + "Number of days your CGM sensor lasts (e.g. 10 for Dexcom G6, 15 for G7 15-day)." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CGM sensörünüzün kaç gün dayandığı (ör. Dexcom G6 için 10, G7 15 günlük için 15)." + } + } + } + }, + "Number of drops—each meeting the rate above—required before an alert fires." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uyarı verilmeden önce gereken, her biri yukarıdaki hızı karşılayan düşüş sayısı." + } + } + } + }, + "Number of hours before the %lld-day mark that the alert will fire." : { + "comment" : "Sensor age alarm editor footer; %lld = sensor lifetime in days", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uyarının %lld günlük sınırdan kaç saat önce çalacağı." + } + } + } + }, + "Number of minutes since the last successful loop. When this time has elapsed, the alarm becomes eligible." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Son başarılı loop'tan bu yana geçen dakika. Bu süre dolduğunda alarm devreye girebilir." + } + } + } + }, + "Number of qualifying boluses needed to trigger." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tetiklemek için gereken uygun bolus sayısı." + } + } + } + }, + "Number of rises—each meeting the rate above—required before an alert fires." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uyarı verilmeden önce gereken, her biri yukarıdaki hızı karşılayan yükseliş sayısı." + } + } + } + }, + "OFF" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KAPALI" + } + } + } + }, + "OK" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamam" + } + } + } + }, + "OK (%@)" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlı (%@)" + } + } + } + }, + "ON" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "AÇIK" + } + } + } + }, + "OTP: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "OTP: %@" + } + } + } + }, + "Off" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapalı" + } + } + } + }, + "Old Calculation Warning" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eski Hesaplama Uyarısı" + } + } + } + }, + "On" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açık" + } + } + } + }, + "On your other phone, open Settings → Nightscout (or Dexcom Share) → Import/Export and export to a QR code. Then scan it here." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Diğer telefonunuzda Ayarlar → Nightscout (veya Dexcom Share) → İçe/Dışa Aktar'ı açıp bir QR koduna aktarın. Sonra burada tarayın." + } + } + } + }, + "Once a day (or after a new build), the app sends a small JSON object to https://lf.bjorkert.se. The endpoint is self-hosted by the maintainer; no third-party analytics service is involved." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günde bir kez (veya yeni bir derlemeden sonra) uygulama https://lf.bjorkert.se adresine küçük bir JSON nesnesi gönderir. Uç nokta proje sorumlusu tarafından barındırılır; üçüncü taraf bir analiz hizmeti kullanılmaz." + } + } + } + }, + "Once every 24 hours, plus once after installing a new build. The check runs in the background while the app is active or refreshing in the background." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "24 saatte bir, ayrıca yeni bir derleme kurulduktan sonra bir kez. Denetim, uygulama etkinken veya arka planda yenilenirken arka planda çalışır." + } + } + } + }, + "One-time BG limit alert." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tek seferlik KŞ sınırı uyarısı." + } + } + } + }, + "Only alert if the current BG is above this value." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yalnızca güncel KŞ bu değerin üstündeyse uyar." + } + } + } + }, + "Only track carb entries scheduled up to this many minutes in the future. Entries beyond this window are ignored." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yalnızca gelecekte bu kadar dakikaya kadar planlanan karbonhidrat girişlerini izle. Bu pencerenin ötesindeki girişler yok sayılır." + } + } + } + }, + "Opt out" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vazgeç" + } + } + } + }, + "Output Unit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çıkış Birimi" + } + } + } + }, + "Outside US" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ABD Dışı" + } + } + } + }, + "Over This Time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu Süre İçinde" + } + } + } + }, + "Override" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override" + } + } + } + }, + "Override Ended" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override Sona Erdi" + } + } + } + }, + "Override Started" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override Başladı" + } + } + } + }, + "Override System Volume" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sistem Ses Düzeyini Geçersiz Kıl" + } + } + } + }, + "Override ended." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override sona erdi." + } + } + } + }, + "Override just started." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override az önce başladı." + } + } + } + }, + "Override system volume" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sistem ses düzeyini geçersiz kıl" + } + } + } + }, + "Overrides" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override'lar" + } + } + } + }, + "Overriding the system volume lets alarms be heard even when your phone is silenced or in a Focus mode." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sistem ses düzeyini geçersiz kılmak, telefonunuz sessizdeyken veya Odaklanma modundayken bile alarmların duyulmasını sağlar." + } + } + } + }, + "PERSISTENCE" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KALICILIK" + } + } + } + }, + "PREDICTION" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TAHMİN" + } + } + } + }, + "Password" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Parola" + } + } + } + }, + "Paste a Nightscout token. If your site needs one and you only have the API secret, paste that instead — LoopFollow can create a read-only token for you." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir Nightscout belirteci yapıştırın. Siteniz belirteç gerektiriyor ve elinizde yalnızca API secret varsa onu yapıştırın — LoopFollow sizin için salt okunur bir belirteç oluşturabilir." + } + } + } + }, + "Paste the full .p8 contents — must include the BEGIN PRIVATE KEY and END PRIVATE KEY lines." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : ".p8 dosyasının tüm içeriğini yapıştırın — BEGIN PRIVATE KEY ve END PRIVATE KEY satırlarını içermelidir." + } + } + } + }, + "Payload too large. The notification payload exceeded the size limit. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yük çok büyük. Bildirim yükü boyut sınırını aştı. %@" + } + } + } + }, + "Percentage: %@%%" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüzde: %@%%" + } + } + } + }, + "Persistent" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kalıcı" + } + } + } + }, + "Persistent High" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kalıcı Yüksek" + } + } + } + }, + "Persistent Notification" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kalıcı Bildirim" + } + } + } + }, + "Persistent for" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kalıcılık süresi" + } + } + } + }, + "Phone Battery Drop" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Telefon Pili Düşüşü" + } + } + } + }, + "Phone Battery Level" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Telefon Pil Düzeyi" + } + } + } + }, + "Phone battery low." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Telefon pili düşük." + } + } + } + }, + "Pick when your day period begins and when your night period begins. Any time from your Day-starts time up until your Night-starts time will count as day; from Night-starts until the next Day-starts will count as night." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gündüz ve gece dönemlerinizin ne zaman başladığını seçin. Gündüz başlangıcından gece başlangıcına kadar olan süre gündüz, gece başlangıcından bir sonraki gündüz başlangıcına kadar olan süre gece sayılır." + } + } + } + }, + "Pick which features sit in the tab bar. You can always reach everything through the Menu." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme çubuğunda hangi özelliklerin yer alacağını seçin. Her şeye her zaman Menü'den ulaşabilirsiniz." + } + } + } + }, + "Play" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çal" + } + } + } + }, + "Play sound during calls" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aramalar sırasında ses çal" + } + } + } + }, + "Please allow access to Contacts in Settings to enable this feature." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu özelliği açmak için lütfen Ayarlar'dan Kişiler erişimine izin verin." + } + } + } + }, + "Please configure Loop APNS settings in Remote Settings to use APNS commands." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNS komutlarını kullanmak için lütfen Uzaktan Ayarlar'da Loop APNS ayarlarını yapılandırın." + } + } + } + }, + "Please enable Face ID, Touch ID, or set up a device passcode in Settings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen Ayarlar'dan Face ID veya Touch ID'yi açın ya da bir cihaz parolası ayarlayın." + } + } + } + }, + "Please enter a valid carb amount" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen geçerli bir karbonhidrat miktarı girin" + } + } + } + }, + "Please enter a valid insulin amount" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen geçerli bir insülin miktarı girin" + } + } + } + }, + "Please enter a valid site URL first." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen önce geçerli bir site URL'si girin." + } + } + } + }, + "Please enter your Nightscout URL in Settings." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen Ayarlar'da Nightscout URL'nizi girin." + } + } + } + }, + "Please select a Remote Type in Settings." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen Ayarlar'da bir Uzaktan Kumanda Türü seçin." + } + } + } + }, + "Please wait..." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lütfen bekleyin..." + } + } + } + }, + "Poor" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zayıf" + } + } + } + }, + "Pre-Bolus Time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ön Bolus Süresi" + } + } + } + }, + "Pre-bolus" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ön bolus" + } + } + } + }, + "Prediction" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahmin" + } + } + } + }, + "Prediction Style" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahmin Stili" + } + } + } + }, + "Predictive" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahmine dayalı" + } + } + } + }, + "Preset Name" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ön Ayar Adı" + } + } + } + }, + "Preset: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ön ayar: %@" + } + } + } + }, + "Presets" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ön Ayarlar" + } + } + } + }, + "Privacy" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gizlilik" + } + } + } + }, + "Proactive Low" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahmini Düşük" + } + } + } + }, + "Production" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Production" + } + } + } + }, + "Production is used for browser builders and should be switched off for Xcode builders" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Production tarayıcı derleyicileri içindir; Xcode ile derleyenler için kapatılmalıdır" + } + } + } + }, + "Profile" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + } + } + }, + "Programmed Basal" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Programlı Bazal" + } + } + } + }, + "Protein" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protein" + } + } + } + }, + "Protein: %.0f g" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protein: %.0f g" + } + } + } + }, + "Pump" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa" + } + } + } + }, + "Pump Battery" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa Pili" + } + } + } + }, + "Pump Battery Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa Pili Uyarısı" + } + } + } + }, + "Pump Battery Level" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa Pil Düzeyi" + } + } + } + }, + "Pump Change Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa Değişimi Uyarısı" + } + } + } + }, + "Pump Insulin Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa İnsülini Uyarısı" + } + } + } + }, + "Pump battery low." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa pili düşük." + } + } + } + }, + "Pump change due." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa değişim zamanı." + } + } + } + }, + "QR Code URL" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR Kodu URL'si" + } + } + } + }, + "Quick-Pick Boluses" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı Seçim Bolusları" + } + } + } + }, + "Quick-Pick Meals" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı Seçim Öğünleri" + } + } + } + }, + "RISING BG" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ YÜKSELİYOR" + } + } + } + }, + "RSSI: %@ dBm" : { + "shouldTranslate" : false + }, + "Range" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aralık" + } + } + } + }, + "Range Mode" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aralık Modu" + } + } + } + }, + "Rapid downward BG trend." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı KŞ düşüş eğilimi." + } + } + } + }, + "Rapid upward BG trend." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı KŞ yükseliş eğilimi." + } + } + } + }, + "Rate of Fall" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşüş Hızı" + } + } + } + }, + "Rate of Rise" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yükseliş Hızı" + } + } + } + }, + "Read" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Okuma" + } + } + } + }, + "Read & Write" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Okuma ve Yazma" + } + } + } + }, + "Reading Delay" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Okuma Gecikmesi" + } + } + } + }, + "Real-time Updates" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gerçek Zamanlı Güncellemeler" + } + } + } + }, + "Rec. Bolus" : { + "comment" : "Alarm type name: recommended bolus", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öner. Bolus" + } + } + } + }, + "Recommended Bolus" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önerilen Bolus" + } + } + } + }, + "Recommended bolus issued." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önerilen bolus verildi." + } + } + } + }, + "Reconnecting..." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeniden bağlanıyor..." + } + } + } + }, + "Refresh" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yenile" + } + } + } + }, + "Refreshing" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yenileniyor" + } + } + } + }, + "Reminder Time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hatırlatma Süresi" + } + } + } + }, + "Reminder when future carbs are due." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gelecek karbonhidrat zamanı geldiğinde hatırlatma." + } + } + } + }, + "Remote" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzaktan Kumanda" + } + } + } + }, + "Remote Settings" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzaktan Ayarlar" + } + } + } + }, + "Remote command sent. Wait for a notification confirming the result." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzaktan komut gönderildi. Sonucu doğrulayan bildirimi bekleyin." + } + } + } + }, + "Remote commands are currently only available for Trio." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzaktan komutlar şu anda yalnızca Trio için kullanılabilir." + } + } + } + }, + "Repeat" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yinele" + } + } + } + }, + "Requires a Dexcom G6/ONE/G7/ONE+ transmitter within Bluetooth range. Provides updates every 5 minutes and uses less battery than the silent tune method. If you have more than one device to choose from, select the one with the smallest expected bg delay." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth menzilinde bir Dexcom G6/ONE/G7/ONE+ vericisi gerektirir. Her 5 dakikada bir güncelleme sağlar ve sessiz melodi yönteminden daha az pil kullanır. Birden fazla cihaz seçeneğiniz varsa beklenen KŞ gecikmesi en küçük olanı seçin." + } + } + } + }, + "Requires a RileyLink-compatible device within Bluetooth range. Provides updates once per minute and uses less battery than the silent tune method." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth menzilinde RileyLink uyumlu bir cihaz gerektirir. Dakikada bir güncelleme sağlar ve sessiz melodi yönteminden daha az pil kullanır." + } + } + } + }, + "Requires an OmniPod DASH pod paired with this device within Bluetooth range. Provides updates once every 3 minutes and uses less battery than the silent tune method." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bluetooth menzilinde bu cihazla eşleşmiş bir OmniPod DASH pod gerektirir. Her 3 dakikada bir güncelleme sağlar ve sessiz melodi yönteminden daha az pil kullanır." + } + } + } + }, + "Reservoir Below" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rezervuar Altında" + } + } + } + }, + "Reservoir level low." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rezervuar düzeyi düşük." + } + } + } + }, + "Restart Live Activity" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canlı Etkinliği Yeniden Başlat" + } + } + } + }, + "Resume" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sürdür" + } + } + } + }, + "Review setup anyway" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kurulumu yine de gözden geçir" + } + } + } + }, + "Review the settings that will be imported" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İçe aktarılacak ayarları gözden geçirin" + } + } + } + }, + "Right slot" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sağ yuva" + } + } + } + }, + "Rises by" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yükseliş miktarı" + } + } + } + }, + "Rises in a row" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Art arda yükseliş" + } + } + } + }, + "Rising above" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şunun üstüne çıkarken" + } + } + } + }, + "Run diagnostics" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tanılamayı çalıştır" + } + } + } + }, + "Run diagnostics again" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tanılamayı yeniden çalıştır" + } + } + } + }, + "SAGE" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "SAGE" + } + } + } + }, + "SMB" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "SMB" + } + } + } + }, + "SNOOZE" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ERTELE" + } + } + } + }, + "Save" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaydet" + } + } + } + }, + "Save BG to Calendar" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ'yi Takvime Kaydet" + } + } + } + }, + "Save Preset" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ön Ayarı Kaydet" + } + } + } + }, + "Save as Preset" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ön Ayar Olarak Kaydet" + } + } + } + }, + "Scan QR Code" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR Kodu Tara" + } + } + } + }, + "Scan QR Code from Loop App" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop Uygulamasından QR Kodu Tara" + } + } + } + }, + "Scan QR Code to Import Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayarları İçe Aktarmak için QR Kodu Tara" + } + } + } + }, + "Scan a different code" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Farklı bir kod tara" + } + } + } + }, + "Scan this QR code with another LoopFollow app to import %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ içe aktarmak için bu QR kodunu başka bir LoopFollow uygulamasıyla tarayın" + } + } + } + }, + "Scanned Token:" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taranan Belirteç:" + } + } + } + }, + "Scanned URL:" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taranan URL:" + } + } + } + }, + "Scanning for %@..." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ aranıyor..." + } + } + } + }, + "Schedule" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Program" + } + } + } + }, + "Schedule for later" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sonrası için planla" + } + } + } + }, + "Search" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ara" + } + } + } + }, + "Search Log" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlükte Ara" + } + } + } + }, + "Search alarms" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarmlarda ara" + } + } + } + }, + "Secret" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gizli Anahtar" + } + } + } + }, + "Security" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güvenlik" + } + } + } + }, + "See exactly what's sent" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tam olarak neyin gönderildiğini gör" + } + } + } + }, + "Select Alarms to Export" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dışa Aktarılacak Alarmları Seç" + } + } + } + }, + "Select End Date" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bitiş Tarihi Seç" + } + } + } + }, + "Select Start Date" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başlangıç Tarihi Seç" + } + } + } + }, + "Select Time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saat Seç" + } + } + } + }, + "Select the colors for your BG values. Note: not all watch faces allow control over colors. Recommend options like Activity or Modular Duo if you want to customize colors." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ değerleriniz için renkleri seçin. Not: her saat kadranı renk denetimine izin vermez. Renkleri özelleştirmek istiyorsanız Aktivite veya Modüler Duo gibi seçenekler önerilir." + } + } + } + }, + "Selected Alarms: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seçilen Alarmlar: %@" + } + } + } + }, + "Selected Device" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Seçilen Cihaz" + } + } + } + }, + "Send" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gönder" + } + } + } + }, + "Send %@ units of insulin?" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ ünite insülin gönderilsin mi?" + } + } + } + }, + "Send %@g of carbs with %@h absorption time at %@?" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Send %1$@g of carbs with %2$@h absorption time at %3$@?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@g karbonhidrat, %2$@sa emilim süresiyle, %3$@ zamanında gönderilsin mi?" + } + } + } + }, + "Send Bolus" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Gönder" + } + } + } + }, + "Send Carbs" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat Gönder" + } + } + } + }, + "Send Insulin" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin Gönder" + } + } + } + }, + "Send Meal" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öğün Gönder" + } + } + } + }, + "Send anonymous usage stats" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anonim kullanım istatistikleri gönder" + } + } + } + }, + "Sending Bolus..." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus gönderiliyor..." + } + } + } + }, + "Sending Meal Data..." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öğün verileri gönderiliyor..." + } + } + } + }, + "Sending..." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gönderiliyor..." + } + } + } + }, + "Sends a reminder before the looping-app build you’re following reaches its TestFlight or Xcode expiry date. Works with Trio 0.4 and later." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Takip ettiğiniz loop uygulaması derlemesinin TestFlight veya Xcode süre sonu tarihinden önce hatırlatma gönderir. Trio 0.4 ve sonrasıyla çalışır." + } + } + } + }, + "Sensitivity" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duyarlılık" + } + } + } + }, + "Sensor Change Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensör Değişimi Uyarısı" + } + } + } + }, + "Sensor Lifetime" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensör Kullanım Ömrü" + } + } + } + }, + "Sensor Start" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensör Başlangıcı" + } + } + } + }, + "Sensor change due." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensör değişim zamanı." + } + } + } + }, + "Server" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunucu" + } + } + } + }, + "Server error: %lld" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunucu hatası: %lld" + } + } + } + }, + "Service unavailable. The server is temporarily unavailable. Try again later. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hizmet kullanılamıyor. Sunucu geçici olarak kullanılamıyor. Daha sonra yeniden deneyin. %@" + } + } + } + }, + "Set Up Units" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Birimleri Ayarla" + } + } + } + }, + "Set when your day and night begin, and how alarm sound behaves. You can fine-tune everything later in the Menu." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gündüz ve gecenizin ne zaman başladığını ve alarm sesinin nasıl davranacağını ayarlayın. Her şeye daha sonra Menü'den ince ayar yapabilirsiniz." + } + } + } + }, + "Settings" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayarlar" + } + } + } + }, + "Settings imported — you're connected." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ayarlar içe aktarıldı — bağlandınız." + } + } + } + }, + "Settings to Import" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İçe Aktarılacak Ayarlar" + } + } + } + }, + "Setup Dexcom Share" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dexcom Share'i Kur" + } + } + } + }, + "Setup Nightscout" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nightscout'u Kur" + } + } + } + }, + "Share" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paylaş" + } + } + } + }, + "Share Logs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlükleri Paylaş" + } + } + } + }, + "Shared Secret" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paylaşılan Gizli Anahtar" + } + } + } + }, + "Show Carb Absorption" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat Emilimini Göster" + } + } + } + }, + "Show Carb/Bolus Values" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat/Bolus Değerlerini Göster" + } + } + } + }, + "Show DIA Lines" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DIA Çizgilerini Göster" + } + } + } + }, + "Show Days Back" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gösterilecek Geçmiş Gün" + } + } + } + }, + "Show Delta" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta'yı Göster" + } + } + } + }, + "Show Display Name" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Görünen Adı Göster" + } + } + } + }, + "Show IOB" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB'yi Göster" + } + } + } + }, + "Show Midnight Lines" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gece Yarısı Çizgilerini Göster" + } + } + } + }, + "Show Prior Day Time Lines" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önceki Gün Zaman Çizgilerini Göster" + } + } + } + }, + "Show Trend" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trendi Göster" + } + } + } + }, + "Show Yesterday's BG" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dünkü KŞ'yi Göster" + } + } + } + }, + "Show in Info Display" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilgi Ekranında Göster" + } + } + } + }, + "Show −30 min Line" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "−30 dk Çizgisini Göster" + } + } + } + }, + "Show −90 min Line" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "−90 dk Çizgisini Göster" + } + } + } + }, + "Sign in with the Dexcom Share account that shares glucose data." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz verilerini paylaşan Dexcom Share hesabıyla oturum açın." + } + } + } + }, + "Site Not Found" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Site Bulunamadı" + } + } + } + }, + "Site URL" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Site URL'si" + } + } + } + }, + "Site found — it needs a token." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Site bulundu — bir belirteç gerekiyor." + } + } + } + }, + "Skip" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Atla" + } + } + } + }, + "Skip if BG is falling" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ düşüyorsa atla" + } + } + } + }, + "Skip if BG is rising" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ yükseliyorsa atla" + } + } + } + }, + "Skip while charging" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şarj olurken atla" + } + } + } + }, + "Slovak" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Slovakça" + } + } + } + }, + "Small Graph" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Küçük Grafik" + } + } + } + }, + "Small Graph Height" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Küçük Grafik Yüksekliği" + } + } + } + }, + "Snooze" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ertele" + } + } + } + }, + "Snooze & Mute Options" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erteleme ve Sessize Alma Seçenekleri" + } + } + } + }, + "Snooze Date" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erteleme Tarihi" + } + } + } + }, + "Snooze Time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erteleme Saati" + } + } + } + }, + "Snooze all · 1h" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tümünü ertele · 1sa" + } + } + } + }, + "Snooze for" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erteleme süresi" + } + } + } + }, + "Snooze until (date)" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şu tarihe kadar ertele" + } + } + } + }, + "Snooze until (time)" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şu saate kadar ertele" + } + } + } + }, + "Snoozed" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ertelendi" + } + } + } + }, + "Snoozed until %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ saatine kadar ertelendi" + } + } + } + }, + "Snoozer" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erteleyici" + } + } + } + }, + "Snoozer emoji" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erteleyici emojisi" + } + } + } + }, + "Sound" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ses" + } + } + } + }, + "Source" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaynak" + } + } + } + }, + "Speak BG" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ'yi Seslendir" + } + } + } + }, + "Start Date" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başlangıç Tarihi" + } + } + } + }, + "Start Override" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override Başlat" + } + } + } + }, + "Static" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sabit" + } + } + } + }, + "Statistics" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İstatistikler" + } + } + } + }, + "Status" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Durum" + } + } + } + }, + "Stay informed" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haberdar olun" + } + } + } + }, + "Stay silent while BG is falling. The alert only sounds when the latest reading is flat or still rising." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ düşerken sessiz kal. Uyarı yalnızca son okuma düz seyrederken veya hâlâ yükselirken çalar." + } + } + } + }, + "Stay silent while BG is rising. The alert only sounds when the latest reading is flat or still falling." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ yükselirken sessiz kal. Uyarı yalnızca son okuma düz seyrederken veya hâlâ düşerken çalar." + } + } + } + }, + "Stay silent while the phone is charging. Requires the uploader to report charging status; if it doesn't, the alert still sounds." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Telefon şarj olurken sessiz kal. Yükleyicinin şarj durumunu bildirmesi gerekir; bildirmiyorsa uyarı yine çalar." + } + } + } + }, + "Std Dev" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Std Sapma" + } + } + } + }, + "Std Dev — Standard Deviation, how much glucose swings around the average. CV — Coefficient of Variation, that swing relative to the average (Std Dev ÷ mean)." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Std Sapma — Standart Sapma, glukozun ortalama etrafında ne kadar dalgalandığı. CV — Varyasyon Katsayısı, bu dalgalanmanın ortalamaya oranı (Std Sapma ÷ ortalama)." + } + } + } + }, + "Std Dev:" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Std Sapma:" + } + } + } + }, + "Success" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başarılı" + } + } + } + }, + "Support & Community" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Destek ve Topluluk" + } + } + } + }, + "Suspend" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Askıya Al" + } + } + } + }, + "Swedish" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İsveççe" + } + } + } + }, + "System" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sistem" + } + } + } + }, + "TDD" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TDD" + } + } + } + }, + "TIR" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TIR" + } + } + } + }, + "TIR — Time in Range, the share of readings within %@.\nTITR — Time in Tight Range, within %@.\nCustom — set your own low and high." : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "TIR — Time in Range, the share of readings within %1$@.\nTITR — Time in Tight Range, within %2$@.\nCustom — set your own low and high." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TIR — Hedef Aralıkta Süre, %1$@ içindeki okumaların payı.\nTITR — Dar Hedef Aralıkta Süre, %2$@ içindeki pay.\nÖzel — kendi düşük ve yüksek değerlerinizi belirleyin." + } + } + } + }, + "TITR" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TITR" + } + } + } + }, + "TOTP Code Already Used" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TOTP Kodu Zaten Kullanıldı" + } + } + } + }, + "TOTP Code: Invalid QR code URL" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TOTP Kodu: Geçersiz QR kodu URL'si" + } + } + } + }, + "Tab %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme %@" + } + } + } + }, + "Tab 1" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme 1" + } + } + } + }, + "Tab 2" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme 2" + } + } + } + }, + "Tab 3" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme 3" + } + } + } + }, + "Tab 4" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme 4" + } + } + } + }, + "Tab 5" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme 5" + } + } + } + }, + "Tab Order" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme Sırası" + } + } + } + }, + "Tabs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekmeler" + } + } + } + }, + "Tap to load older treatments" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eski tedavileri yüklemek için dokunun" + } + } + } + }, + "Target" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef" + } + } + } + }, + "Target Range" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef Aralığı" + } + } + } + }, + "Target: %@ %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Target: %1$@ %2$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef: %1$@ %2$@" + } + } + } + }, + "Target: %@-%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Target: %1$@-%2$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef: %1$@-%2$@" + } + } + } + }, + "Team ID: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekip Kimliği: %@" + } + } + } + }, + "Temp Target" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Hedef" + } + } + } + }, + "Temp Target Ended" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Hedef Sona Erdi" + } + } + } + }, + "Temp Target Started" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Hedef Başladı" + } + } + } + }, + "Temp target ended." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici hedef sona erdi." + } + } + } + }, + "Temp target started." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici hedef başladı." + } + } + } + }, + "Temporary Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Uyarı" + } + } + } + }, + "Temporary Target" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Hedef" + } + } + } + }, + "Text Color" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Metin Rengi" + } + } + } + }, + "Thanks for sharing these logs to help us find the problem. Please describe it in as much detail as possible — what time did it happen, what did you do, and what did you expect to happen that didn't?" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sorunu bulmamıza yardımcı olmak için bu günlükleri paylaştığınız için teşekkürler. Lütfen olabildiğince ayrıntılı anlatın — ne zaman oldu, ne yaptınız ve ne olmasını bekliyordunuz da olmadı?" + } + } + } + }, + "That API secret was rejected. Check it and try again." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu API secret reddedildi. Denetleyip yeniden deneyin." + } + } + } + }, + "That's your API secret — create a read-only token" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu sizin API secret'ınız — salt okunur bir belirteç oluşturun" + } + } + } + }, + "The URL is invalid." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL geçersiz." + } + } + } + }, + "The address is empty." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adres boş." + } + } + } + }, + "The alert becomes eligible once any reading is at or above this value." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Herhangi bir okuma bu değere eşit veya üstünde olduğunda uyarı devreye girebilir." + } + } + } + }, + "The current version has a critical issue and should be updated as soon as possible." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mevcut sürümde kritik bir sorun var ve en kısa sürede güncellenmelidir." + } + } + } + }, + "The device token is no longer active for the topic. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihaz belirteci bu konu için artık etkin değil. %@" + } + } + } + }, + "The order of Fat and Protein inputs has changed." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yağ ve Protein girişlerinin sırası değişti." + } + } + } + }, + "The scanned Nightscout URL (%@) doesn't match your current Nightscout address (%@). Would you like to change your Nightscout address to match the scanned settings?" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taranan Nightscout URL'si (%1$@) mevcut Nightscout adresinizle (%2$@) eşleşmiyor. Nightscout adresinizi taranan ayarlara göre değiştirmek ister misiniz?" + } + } + } + }, + "The scanned settings also include a token. Would you like to set this as your access token?" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taranan ayarlar ayrıca bir belirteç içeriyor. Bunu erişim belirteciniz olarak ayarlamak ister misiniz?" + } + } + } + }, + "The scanned settings include a Nightscout URL. Would you like to set this as your Nightscout address?" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taranan ayarlar bir Nightscout URL'si içeriyor. Bunu Nightscout adresiniz olarak ayarlamak ister misiniz?" + } + } + } + }, + "The scanned settings include a token. Would you like to set this as your access token?" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taranan ayarlar bir belirteç içeriyor. Bunu erişim belirteciniz olarak ayarlamak ister misiniz?" + } + } + } + }, + "The scanned token doesn't match your current access token. Would you like to update your token?" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Taranan belirteç mevcut erişim belirtecinizle eşleşmiyor. Belirtecinizi güncellemek ister misiniz?" + } + } + } + }, + "The site was not found." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Site bulunamadı." + } + } + } + }, + "The token is invalid." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belirteç geçersiz." + } + } + } + }, + "The top 4 items appear in the tab bar. The Menu can always open every feature." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İlk 4 öğe sekme çubuğunda görünür. Menü her zaman tüm özellikleri açabilir." + } + } + } + }, + "The value turns yellow at or above the yellow level and red at or above the red level. In range it shows green. A visual cue only — it never triggers an alarm." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Değer, sarı düzeye eşit veya üstündeyken sarı, kırmızı düzeye eşit veya üstündeyken kırmızı olur. Aralıktayken yeşil görünür. Yalnızca görsel bir ipucudur — asla alarm tetiklemez." + } + } + } + }, + "The value turns yellow at or below the yellow level and red at or below the red level. In range it shows green. A visual cue only — it never triggers an alarm." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Değer, sarı düzeye eşit veya altındayken sarı, kırmızı düzeye eşit veya altındayken kırmızı olur. Aralıktayken yeşil görünür. Yalnızca görsel bir ipucudur — asla alarm tetiklemez." + } + } + } + }, + "These buttons show your most-used recent bolus amounts.\n\nThey're based on what you've sent before at similar times on similar days — so if you usually give 4 units before breakfast on weekdays, that button will show up on weekday mornings.\n\nTap a button to fill in the amount. Nothing is sent until you review and confirm." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu düğmeler en sık kullandığınız son bolus miktarlarını gösterir.\n\nBenzer günlerde benzer saatlerde daha önce gönderdiklerinize dayanır — örneğin hafta içi kahvaltıdan önce genellikle 4 ünite veriyorsanız, o düğme hafta içi sabahları görünür.\n\nMiktarı doldurmak için bir düğmeye dokunun. Gözden geçirip onaylayana kadar hiçbir şey gönderilmez." + } + } + } + }, + "These buttons show your most-used recent meals.\n\nThey're based on what you've sent before at similar times on similar days — so if you usually send the same breakfast on weekday mornings, it'll appear as an option.\n\nTap a button to fill in the details. Nothing is sent until you review and confirm." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu düğmeler en sık kullandığınız son öğünleri gösterir.\n\nBenzer günlerde benzer saatlerde daha önce gönderdiklerinize dayanır — örneğin hafta içi sabahları hep aynı kahvaltıyı gönderiyorsanız, seçenek olarak görünür.\n\nAyrıntıları doldurmak için bir düğmeye dokunun. Gözden geçirip onaylayana kadar hiçbir şey gönderilmez." + } + } + } + }, + "This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu TOTP kodu zaten bir komut için kullanıldı. Başka bir komut göndermeden önce lütfen sonraki kodun üretilmesini bekleyin." + } + } + } + }, + "This alert fires once when glucose crosses either of the limits you set below, and then disables itself." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu uyarı, glukoz aşağıda belirlediğiniz sınırlardan birini geçtiğinde bir kez çalar ve sonra kendini kapatır." + } + } + } + }, + "This alerts you if the phone battery drops by this much or more." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Telefon pili bu kadar veya daha fazla düşerse sizi uyarır." + } + } + } + }, + "This alerts you when the database reaches or exceeds this percentage of the limit configured on the Nightscout site." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veritabanı, Nightscout sitesinde yapılandırılan sınırın bu yüzdesine ulaştığında veya aştığında sizi uyarır." + } + } + } + }, + "This alerts you when the phone battery drops to or below this level." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Telefon pili bu düzeye veya altına indiğinde sizi uyarır." + } + } + } + }, + "This alerts you when the pump battery drops to or below this level." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa pili bu düzeye veya altına indiğinde sizi uyarır." + } + } + } + }, + "This app will expire in less than a week. Please rebuild to continue using it." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu uygulamanın süresi bir haftadan kısa sürede dolacak. Kullanmaya devam etmek için lütfen yeniden derleyin." + } + } + } + }, + "This counts only boluses larger than this size." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yalnızca bu boyuttan büyük boluslar sayılır." + } + } + } + }, + "This is how much the glucose must drop to be considered a fast drop." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı düşüş sayılması için glukozun ne kadar düşmesi gerektiği." + } + } + } + }, + "This is how much the glucose must rise to be considered a fast rise." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı yükseliş sayılması için glukozun ne kadar yükselmesi gerektiği." + } + } + } + }, + "This recommended bolus was calculated %lld minutes ago. New treatments may have occurred since then. Proceed with caution." : { + "comment" : "Alert shown before sending a stale recommended bolus; %lld = minutes", + "extractionState" : "manual", + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "This recommended bolus was calculated %lld minute ago. New treatments may have occurred since then. Proceed with caution." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "This recommended bolus was calculated %lld minutes ago. New treatments may have occurred since then. Proceed with caution." + } + } + } + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu önerilen bolus %lld dakika önce hesaplandı. O zamandan beri yeni tedaviler gerçekleşmiş olabilir. Dikkatli ilerleyin." + } + } + } + }, + "This warns you if the glucose is too low now or might be soon, based on the forecast." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz şu anda çok düşükse ya da tahmine göre yakında düşebilecekse sizi uyarır." + } + } + } + }, + "This warns you if the glucose monitor stops sending readings for too long.." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz monitörü çok uzun süre okuma göndermezse sizi uyarır." + } + } + } + }, + "This warns you if your phone’s battery drops quickly, based on the percentage and time you set." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Telefonunuzun pili belirlediğiniz yüzde ve süreye göre hızla düşerse sizi uyarır." + } + } + } + }, + "This warns you when the Nightscout database has filled the percentage you choose. Nightscout measures the used space against its own DBSIZE_MAX setting, which is 496 MiB unless the site owner changed it, so the percentage is only meaningful when that setting matches the real hosting limit." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nightscout veritabanı seçtiğiniz yüzdeye kadar dolduğunda sizi uyarır. Nightscout kullanılan alanı kendi DBSIZE_MAX ayarına göre ölçer; site sahibi değiştirmediyse bu 496 MiB'dir. Bu yüzden yüzde yalnızca bu ayar gerçek barındırma sınırıyla eşleştiğinde anlamlıdır." + } + } + } + }, + "This warns you when the insulin pump is running low on insulin." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin pompasındaki insülin azaldığında sizi uyarır." + } + } + } + }, + "This warns you when the phone's battery gets low, based on the percentage you choose." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Telefonun pili seçtiğiniz yüzdeye göre azaldığında sizi uyarır." + } + } + } + }, + "This warns you when the pump's battery gets low, based on the percentage you choose." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompanın pili seçtiğiniz yüzdeye göre azaldığında sizi uyarır." + } + } + } + }, + "This will overwrite your current settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu, mevcut ayarlarınızın üzerine yazacak" + } + } + } + }, + "Threshold" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eşik" + } + } + } + }, + "Time" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaman" + } + } + } + }, + "Time Remaining" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kalan Süre" + } + } + } + }, + "Time Window" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaman Penceresi" + } + } + } + }, + "Time Zone" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saat Dilimi" + } + } + } + }, + "Time Zone Override" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saat Dilimi Geçersiz Kılma" + } + } + } + }, + "Time in Range" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef Aralıkta Süre" + } + } + } + }, + "Time in Tight Range" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dar Hedef Aralıkta Süre" + } + } + } + }, + "Time must be within the next %lld hour" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaman en fazla %lld saat ileride olmalı" + } + } + } + }, + "Time must be within the prior %lld hours" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaman son %lld saat içinde olmalı" + } + } + } + }, + "To see your trend, delta, or IOB, include them in another contact or create separate contacts. When using 'Include', select which contact to add the value to." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trend, delta veya IOB'yi görmek için bunları başka bir kişiye ekleyin veya ayrı kişiler oluşturun. 'Ekle' seçeneğini kullanırken değerin hangi kişiye ekleneceğini seçin." + } + } + } + }, + "Token" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belirteç" + } + } + } + }, + "Token Required" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belirteç Gerekli" + } + } + } + }, + "Token created" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belirteç oluşturuldu" + } + } + } + }, + "Token created. Your site can take a few minutes to start accepting it — you can continue." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Belirteç oluşturuldu. Sitenizin kabul etmeye başlaması birkaç dakika sürebilir — devam edebilirsiniz." + } + } + } + }, + "Tone" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ton" + } + } + } + }, + "Too many requests - please wait a few minutes before trying again" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çok fazla istek - yeniden denemeden önce lütfen birkaç dakika bekleyin" + } + } + } + }, + "Too many requests. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çok fazla istek. %@" + } + } + } + }, + "Total Daily Dose" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toplam Günlük Doz" + } + } + } + }, + "Touch ID is not available. Please set up a device passcode in Settings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Touch ID kullanılamıyor. Lütfen Ayarlar'dan bir cihaz parolası ayarlayın." + } + } + } + }, + "Treatments" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tedaviler" + } + } + } + }, + "Treatments on Small Graph" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Küçük Grafikte Tedaviler" + } + } + } + }, + "Trend" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trend" + } + } + } + }, + "Trigger Level" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tetikleme Düzeyi" + } + } + } + }, + "Trio Remote Control" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trio Uzaktan Kumanda" + } + } + } + }, + "Trio Remote Control Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trio Uzaktan Kumanda Ayarları" + } + } + } + }, + "Trio Remote Control is only supported for 'Trio'" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trio Uzaktan Kumanda yalnızca 'Trio' için desteklenir" + } + } + } + }, + "Try changing Production Environment from %@ to %@ in your Loop APNS settings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop APNS ayarlarınızda Production Ortamı'nı %1$@ yerine %2$@ yapmayı deneyin." + } + } + } + }, + "Type or paste a token, or a full Nightscout URL that includes a token." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir belirteç ya da belirteç içeren tam bir Nightscout URL'si yazın veya yapıştırın." + } + } + } + }, + "U" : { + "comment" : "The short unit display string for international units of insulin", + "shouldTranslate" : false + }, + "URL" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL" + } + } + } + }, + "URL/Token Update" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL/Belirteç Güncelleme" + } + } + } + }, + "URL/Token Validation" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL/Belirteç Doğrulama" + } + } + } + }, + "US" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ABD" + } + } + } + }, + "Unauthorized - check your API secret" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yetkisiz - API secret'ınızı denetleyin" + } + } + } + }, + "Unexpected status code: %lld. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beklenmeyen durum kodu: %1$lld. %2$@" + } + } + } + }, + "Units" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ünite" + } + } + } + }, + "Units and Metrics" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Birimler ve Ölçütler" + } + } + } + }, + "Unknown" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilinmiyor" + } + } + } + }, + "Unknown Alert" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilinmeyen Uyarı" + } + } + } + }, + "Unknown Device" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilinmeyen Cihaz" + } + } + } + }, + "Unknown Error" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilinmeyen Hata" + } + } + } + }, + "Until" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şu zamana kadar" + } + } + } + }, + "Update Available" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncelleme Var" + } + } + } + }, + "Updated" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncellendi" + } + } + } + }, + "Use Anyway" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yine de Kullan" + } + } + } + }, + "Use BG Limit" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Sınırı Kullan" + } + } + } + }, + "Use the Send anonymous usage stats toggle above. Turning it off is immediate and persistent." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yukarıdaki Anonim kullanım istatistikleri gönder anahtarını kullanın. Kapatmak anında ve kalıcıdır." + } + } + } + }, + "User" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı" + } + } + } + }, + "User Information" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı Bilgileri" + } + } + } + }, + "User Name" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı Adı" + } + } + } + }, + "Username" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı Adı" + } + } + } + }, + "Username or password not accepted" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı adı veya parola kabul edilmedi" + } + } + } + }, + "Valid APNs key" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçerli APNs anahtarı" + } + } + } + }, + "Valid Key ID" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçerli Anahtar Kimliği" + } + } + } + }, + "Validation Error" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doğrulama Hatası" + } + } + } + }, + "Value outside of guardrails: %@\n" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Değer güvenlik sınırlarının dışında: %@\n" + } + } + } + }, + "Variability" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Değişkenlik" + } + } + } + }, + "View Log" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlüğü Görüntüle" + } + } + } + }, + "Volume Buttons Snooze Alarms" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ses Düğmeleri Alarmları Erteler" + } + } + } + }, + "WARNING: New treatments may have occurred since the last recommended bolus was calculated %@ ago." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "UYARI: Son önerilen bolus %@ önce hesaplandığından beri yeni tedaviler gerçekleşmiş olabilir." + } + } + } + }, + "Warn me before the sensor’s %lld-day change-over." : { + "comment" : "InfoBanner in the sensor age alarm editor; %lld = sensor lifetime in days", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensörün %lld günlük değişiminden önce beni uyar." + } + } + } + }, + "Warning" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uyarı" + } + } + } + }, + "Warning Time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uyarı Süresi" + } + } + } + }, + "Warns when glucose falls by at least %@ %@ between two readings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz iki okuma arasında en az %1$@ %2$@ düştüğünde uyarır." + } + } + } + }, + "Warns when glucose falls by at least %@ %@ on each of %lld readings in a row — about %@ %@ over roughly %lld minutes." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz art arda %3$lld okumanın her birinde en az %1$@ %2$@ düştüğünde uyarır — yaklaşık %6$lld dakikada toplam %4$@ %5$@." + } + } + } + }, + "We'll ask iOS for permission next. You can change this any time in the Settings app." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sırada iOS'tan izin isteyeceğiz. Bunu istediğiniz zaman Ayarlar uygulamasından değiştirebilirsiniz." + } + } + } + }, + "Welcome to LoopFollow" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoopFollow'a Hoş Geldiniz" + } + } + } + }, + "What is sent" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neler gönderiliyor" + } + } + } + }, + "What stays on your device" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihazınızda neler kalıyor" + } + } + } + }, + "What's sent" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neler gönderiliyor" + } + } + } + }, + "When enabled, LoopFollow maintains a live connection to your Nightscout server using WebSocket while the app is in the foreground. Data updates (new glucose readings, treatments, device status) arrive within seconds instead of waiting for the next polling cycle.\n\nThe WebSocket disconnects when LoopFollow moves to the background and reconnects when you return to the app. Polling continues to handle updates while the app is in the background.\n\nIn the foreground, polling continues at a reduced frequency as a safety net. If the WebSocket connection drops, normal polling resumes immediately." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açıkken LoopFollow, uygulama ön plandayken Nightscout sunucunuzla WebSocket üzerinden canlı bir bağlantı sürdürür. Veri güncellemeleri (yeni glukoz okumaları, tedaviler, cihaz durumu) bir sonraki yoklama döngüsünü beklemek yerine saniyeler içinde gelir.\n\nLoopFollow arka plana geçtiğinde WebSocket bağlantısı kesilir ve uygulamaya döndüğünüzde yeniden bağlanır. Uygulama arka plandayken güncellemeleri yoklama sürdürür.\n\nÖn planda, güvenlik ağı olarak yoklama daha düşük sıklıkla devam eder. WebSocket bağlantısı koparsa normal yoklama hemen sürdürülür." + } + } + } + }, + "When enabled, this alert only fires if the glucose is above the limit you set." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açıkken bu uyarı yalnızca glukoz belirlediğiniz sınırın üstündeyse çalar." + } + } + } + }, + "When enabled, this alert only fires if the glucose is below the limit you set." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açıkken bu uyarı yalnızca glukoz belirlediğiniz sınırın altındaysa çalar." + } + } + } + }, + "Yes, send anonymous stats" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evet, anonim istatistik gönder" + } + } + } + }, + "You can change this any time in Settings → General → Diagnostics." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bunu istediğiniz zaman Ayarlar → Genel → Tanılama'dan değiştirebilirsiniz." + } + } + } + }, + "You can choose to share anonymous information with the developers to help improve LoopFollow — such as app and iOS version, device type, which app you're following, and a few settings. Your health data, credentials, time zone, and logs remain on your device." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoopFollow'u geliştirmeye yardımcı olmak için geliştiricilerle anonim bilgiler paylaşmayı seçebilirsiniz — uygulama ve iOS sürümü, cihaz türü, hangi uygulamayı takip ettiğiniz ve birkaç ayar gibi. Sağlık verileriniz, kimlik bilgileriniz, saat diliminiz ve günlükleriniz cihazınızda kalır." + } + } + } + }, + "You can choose to share anonymous information with the developers to help improve LoopFollow—such as app and iOS version, device type, which app you're following, and a few settings. Your health data, credentials, time zone, and logs remain on your device." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoopFollow'u geliştirmeye yardımcı olmak için geliştiricilerle anonim bilgiler paylaşmayı seçebilirsiniz — uygulama ve iOS sürümü, cihaz türü, hangi uygulamayı takip ettiğiniz ve birkaç ayar gibi. Sağlık verileriniz, kimlik bilgileriniz, saat diliminiz ve günlükleriniz cihazınızda kalır." + } + } + } + }, + "You're all set" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Her şey hazır" + } + } + } + }, + "You're already set up. You can skip this guide, or walk through it to review your settings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kurulumunuz zaten tamam. Bu rehberi atlayabilir ya da ayarlarınızı gözden geçirmek için adım adım ilerleyebilirsiniz." + } + } + } + }, + "You're connected" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlandınız" + } + } + } + }, + "You're ready to go. Adjust everything later from the Menu — and when you have a moment, these are worth a look:" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başlamaya hazırsınız. Her şeyi daha sonra Menü'den ayarlayabilirsiniz — vaktiniz olduğunda şunlara göz atmaya değer:" + } + } + } + }, + "Your API secret is used once to create a read-only access token and is never stored." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "API secret'ınız salt okunur bir erişim belirteci oluşturmak için bir kez kullanılır ve asla saklanmaz." + } + } + } + }, + "Your data source is connected and your alarms are in place — there's nothing to set up here. You can fine-tune anything later in Settings." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veri kaynağınız bağlı ve alarmlarınız hazır — burada kurulacak bir şey yok. Her şeye daha sonra Ayarlar'dan ince ayar yapabilirsiniz." + } + } + } + }, + "Your read-only token is ready. Tap Continue to keep going — your site may take a few minutes to start accepting it." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salt okunur belirteciniz hazır. Devam etmek için Devam'a dokunun — sitenizin bunu kabul etmeye başlaması birkaç dakika sürebilir." + } + } + } + }, + "Your read-only token is set up. Tap Continue to keep going." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Salt okunur belirteciniz ayarlandı. Devam etmek için Devam'a dokunun." + } + } + } + }, + "days" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "gün" + } + } + } + }, + "eHbA1c" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "eHbA1c" + } + } + } + }, + "eHbA1c — an A1c estimate from your average glucose. GMI — Glucose Management Indicator, another A1c estimate from average glucose. % and mmol/mol (IFCC) are two scales for the result." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "eHbA1c — ortalama glukozunuzdan bir A1c tahmini. GMI — Glukoz Yönetim Göstergesi, ortalama glukozdan başka bir A1c tahmini. % ve mmol/mol (IFCC), sonucun iki farklı ölçeğidir." + } + } + } + }, + "g" : { + "comment" : "The short unit display string for grams", + "shouldTranslate" : false + }, + "hours" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "saat" + } + } + } + }, + "mg/dL" : { + "comment" : "The short unit display string for milligrams of glucose per decilter", + "shouldTranslate" : false + }, + "min" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "dk" + } + } + } + }, + "minutes" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "dakika" + } + } + } + }, + "mmol/L" : { + "comment" : "The short unit display string for millimoles of glucose per liter", + "shouldTranslate" : false + }, + "mmol/mol" : { + "shouldTranslate" : false + }, + "none" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "yok" + } + } + } + }, + "“Default Snooze” controls the default value for how long the alert stays quiet after you press Snooze. \"A snooze duration of 0 means the alarm is acknowledged (silenced), and will alert again next time the condition applies, without time limitation. \" Toggle “Snoozed” to mute this alarm right now." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "“Varsayılan Erteleme”, Ertele'ye bastıktan sonra uyarının ne kadar süre sessiz kalacağının varsayılan değerini belirler. \"0 erteleme süresi, alarmın onaylandığı (susturulduğu) ve koşul yeniden oluştuğunda süre sınırı olmadan tekrar uyaracağı anlamına gelir. \" Bu alarmı hemen susturmak için “Ertelendi” anahtarını açın." + } + } + } + }, + "“Snooze All” disables every alarm. “Mute All” silences phone sounds but still vibrates and shows iOS notifications." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "“Tümünü Ertele” her alarmı devre dışı bırakır. “Tümünü Sessize Al” telefon seslerini kapatır ama yine de titreşir ve iOS bildirimlerini gösterir." + } + } + } + }, + "••••••••" : { + "shouldTranslate" : false + }, + "− 30m" : { + "shouldTranslate" : false + }, + "⚠️ Not Looping!" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "⚠️ Loop Çalışmıyor!" + } + } + } + }, + "⚠️ Please enable at least one limit." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "⚠️ Lütfen en az bir sınır etkinleştirin." + } + } + } + }, + "🌮" : { + "shouldTranslate" : false + }, + "🍕" : { + "shouldTranslate" : false + }, + "🍭" : { + "shouldTranslate" : false + }, + "🍽️" : { + "shouldTranslate" : false + } + }, + "version" : "1.0" +} diff --git a/LoopFollow/Settings/APNSettingsView.swift b/LoopFollow/Settings/APNSettingsView.swift index 359664dbe..bb622d3e7 100644 --- a/LoopFollow/Settings/APNSettingsView.swift +++ b/LoopFollow/Settings/APNSettingsView.swift @@ -29,7 +29,7 @@ struct APNSettingsView: View { if !keyId.isEmpty { Image(systemName: keyIdValid ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") .foregroundColor(keyIdValid ? .green : .orange) - .accessibilityLabel(keyIdValid ? "Valid Key ID" : "Invalid Key ID") + .accessibilityLabel(keyIdValid ? Text("Valid Key ID") : Text("Invalid Key ID")) } } if !keyId.isEmpty, !keyIdValid { @@ -46,7 +46,7 @@ struct APNSettingsView: View { if !apnsKey.isEmpty { Image(systemName: apnsKeyValid ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") .foregroundColor(apnsKeyValid ? .green : .orange) - .accessibilityLabel(apnsKeyValid ? "Valid APNs key" : "Invalid APNs key") + .accessibilityLabel(apnsKeyValid ? Text("Valid APNs key") : Text("Invalid APNs key")) } } TogglableSecureInput( diff --git a/LoopFollow/Settings/ContactSettingsView.swift b/LoopFollow/Settings/ContactSettingsView.swift index 5ace358db..8153cfb0b 100644 --- a/LoopFollow/Settings/ContactSettingsView.swift +++ b/LoopFollow/Settings/ContactSettingsView.swift @@ -137,19 +137,19 @@ struct ContactSettingsView: View { DispatchQueue.main.async { if !granted { viewModel.contactEnabled = false - showAlert(title: "Access Denied", message: "Please allow access to Contacts in Settings to enable this feature.") + showAlert(title: String(localized: "Access Denied"), message: String(localized: "Please allow access to Contacts in Settings to enable this feature.")) } } } } else if status == .denied { viewModel.contactEnabled = false - showAlert(title: "Access Denied", message: "Access to Contacts is denied. Please go to Settings and enable Contacts access.") + showAlert(title: String(localized: "Access Denied"), message: String(localized: "Access to Contacts is denied. Please go to Settings and enable Contacts access.")) } else if status == .restricted { viewModel.contactEnabled = false - showAlert(title: "Access Restricted", message: "Access to Contacts is restricted.") + showAlert(title: String(localized: "Access Restricted"), message: String(localized: "Access to Contacts is restricted.")) } else { viewModel.contactEnabled = false - showAlert(title: "Error", message: "An unknown error occurred while checking Contacts access.") + showAlert(title: String(localized: "Error"), message: String(localized: "An unknown error occurred while checking Contacts access.")) } } diff --git a/LoopFollow/Settings/DexcomSettingsViewModel.swift b/LoopFollow/Settings/DexcomSettingsViewModel.swift index 5a929cbe6..89fb80707 100644 --- a/LoopFollow/Settings/DexcomSettingsViewModel.swift +++ b/LoopFollow/Settings/DexcomSettingsViewModel.swift @@ -51,7 +51,7 @@ class DexcomSettingsViewModel: ObservableObject { // MARK: - Verification @Published var statusKind: ConnectionStatusKind = .idle - @Published var statusMessage: String = "Enter your username and password" + @Published var statusMessage = String(localized: "Enter your username and password") /// True when a real Dexcom Share login succeeded. @Published private(set) var isVerified: Bool = false @@ -88,11 +88,11 @@ class DexcomSettingsViewModel: ObservableObject { isVerified = false if hasCredentials { statusKind = .checking - statusMessage = "Checking your account…" + statusMessage = String(localized: "Checking your account…") verifySubject.send() } else { statusKind = .idle - statusMessage = "Enter your username and password" + statusMessage = String(localized: "Enter your username and password") } } @@ -113,25 +113,25 @@ class DexcomSettingsViewModel: ObservableObject { switch error { case .loginError: self.statusKind = .error - self.statusMessage = "Username or password not accepted" + self.statusMessage = String(localized: "Username or password not accepted") self.isVerified = false self.loginRejected = true case .httpError: self.statusKind = .error - self.statusMessage = "Network error — check your connection" + self.statusMessage = String(localized: "Network error — check your connection") self.isVerified = false self.loginRejected = false default: // Login succeeded but there's no recent reading yet; the // credentials are valid, which is all we're confirming. self.statusKind = .connected - self.statusMessage = "Connected" + self.statusMessage = String(localized: "Connected") self.isVerified = true self.loginRejected = false } } else { self.statusKind = .connected - self.statusMessage = "Connected" + self.statusMessage = String(localized: "Connected") self.isVerified = true self.loginRejected = false } diff --git a/LoopFollow/Settings/ImportExport/AlarmSelectionView.swift b/LoopFollow/Settings/ImportExport/AlarmSelectionView.swift index a758ae2ac..8775cde8d 100644 --- a/LoopFollow/Settings/ImportExport/AlarmSelectionView.swift +++ b/LoopFollow/Settings/ImportExport/AlarmSelectionView.swift @@ -162,7 +162,7 @@ struct AlarmSelectionRow: View { } } - Text(alarmTypeDescription) + Text(alarm.type.displayName) .font(.caption) .foregroundColor(.secondary) @@ -190,59 +190,6 @@ struct AlarmSelectionRow: View { .buttonStyle(.plain) .disabled(isDisabled && !isSelected) } - - private var alarmTypeDescription: String { - switch alarm.type { - case .temporary: - return "Temporary Alert" - case .iob: - return "IOB Alert" - case .cob: - return "COB Alert" - case .low: - return "Low BG Alert" - case .high: - return "High BG Alert" - case .fastDrop: - return "Fast Drop Alert" - case .fastRise: - return "Fast Rise Alert" - case .missedReading: - return "Missed Reading Alert" - case .notLooping: - return "Not Looping Alert" - case .missedBolus: - return "Missed Bolus Alert" - case .futureCarbs: - return "Future Carbs Alert" - case .sensorChange: - return "Sensor Change Alert" - case .pumpChange: - return "Pump Change Alert" - case .pump: - return "Pump Insulin Alert" - case .pumpBattery: - return "Pump Battery Alert" - case .battery: - return "Low Battery" - case .batteryDrop: - return "Battery Drop" - case .recBolus: - return "Rec. Bolus" - case .overrideStart: - return "Override Started" - case .overrideEnd: - return "Override Ended" - case .tempTargetStart: - return "Temp Target Started" - case .tempTargetEnd: - return "Temp Target Ended" - case .buildExpire: - return "Looping app expiration" - case .dbSize: - return "Nightscout Database Size" - } - } } #Preview { diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 93fd0fd93..c976af833 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -42,8 +42,15 @@ struct SettingsLeaf: Hashable { let title: String let keywords: [String] - init(_ title: String, _ keywords: [String] = []) { - self.title = title + /// `title` is a catalog key; the stored title is localized so Menu search matches what the screen shows. + init(_ title: String.LocalizationValue, _ keywords: [String] = []) { + self.title = String(localized: title) + self.keywords = keywords + } + + /// For titles that are already localized (e.g. `InfoType.name`). + init(localizedTitle: String, _ keywords: [String] = []) { + title = localizedTitle self.keywords = keywords } } @@ -76,24 +83,24 @@ enum SettingsRoute: Hashable, Identifiable { /// included in `menuSections`. var title: String { switch self { - case .nightscout: return "Nightscout" - case .dexcom: return "Dexcom" - case .general: return "General" - case .graph: return "Graph" - case .infoDisplay: return "Information Display" - case .units: return "Units and Metrics" - case .tabSettings: return "Tabs" - case .backgroundRefresh: return "Background Refresh" - case .importExport: return "Import/Export" - case .apn: return "APN" + case .nightscout: return String(localized: "Nightscout", comment: "Settings screen title") + case .dexcom: return String(localized: "Dexcom", comment: "Settings screen title") + case .general: return String(localized: "General", comment: "Settings screen title") + case .graph: return String(localized: "Graph", comment: "Settings screen title") + case .infoDisplay: return String(localized: "Information Display", comment: "Settings screen title") + case .units: return String(localized: "Units and Metrics", comment: "Settings screen title") + case .tabSettings: return String(localized: "Tabs", comment: "Settings screen title") + case .backgroundRefresh: return String(localized: "Background Refresh", comment: "Settings screen title") + case .importExport: return String(localized: "Import/Export", comment: "Settings screen title") + case .apn: return String(localized: "APN", comment: "Settings screen title") #if !targetEnvironment(macCatalyst) - case .liveActivity: return "Live Activity" + case .liveActivity: return String(localized: "Live Activity", comment: "Settings screen title") #endif - case .remote: return "Remote" - case .alarmSettings: return "Alarms" - case .calendar: return "Calendar" - case .contact: return "Contact" - case .advanced: return "Advanced" + case .remote: return String(localized: "Remote", comment: "Settings screen title") + case .alarmSettings: return String(localized: "Alarms", comment: "Settings screen title") + case .calendar: return String(localized: "Calendar", comment: "Settings screen title") + case .contact: return String(localized: "Contact", comment: "Settings screen title") + case .advanced: return String(localized: "Advanced", comment: "Settings screen title") case .settings, .aggregatedStats: return "" } } @@ -189,7 +196,7 @@ enum SettingsRoute: Hashable, Identifiable { ] case .infoDisplay: return [SettingsLeaf("Hide Information Table")] - + InfoType.allCases.map { SettingsLeaf($0.name) } + + InfoType.allCases.map { SettingsLeaf(localizedTitle: $0.name) } case .units: return [ SettingsLeaf("Glucose Unit"), SettingsLeaf("Range Mode", ["tir", "titr", "time in range"]), @@ -364,7 +371,7 @@ extension UIViewController { let a = UIAlertController(title: title, message: message, preferredStyle: .alert) - a.addAction(UIAlertAction(title: "OK", style: .default)) + a.addAction(UIAlertAction(title: String(localized: "OK"), style: .default)) present(a, animated: true) } } diff --git a/LoopFollow/Settings/TabCustomizationModal.swift b/LoopFollow/Settings/TabCustomizationModal.swift index 6ac236982..096f886b7 100644 --- a/LoopFollow/Settings/TabCustomizationModal.swift +++ b/LoopFollow/Settings/TabCustomizationModal.swift @@ -18,7 +18,7 @@ enum TabCustomizationItem: Identifiable, Equatable, Hashable { var displayName: String { switch self { case let .tabItem(item): return item.displayName - case .settings: return "Menu" + case .settings: return String(localized: "Menu", comment: "Tab customization") } } diff --git a/LoopFollow/Stats/AggregatedStatsView.swift b/LoopFollow/Stats/AggregatedStatsView.swift index 0e9ad3a4d..850811e27 100644 --- a/LoopFollow/Stats/AggregatedStatsView.swift +++ b/LoopFollow/Stats/AggregatedStatsView.swift @@ -217,7 +217,7 @@ struct AggregatedStatsModalView: View { } struct StatCard: View { - let title: String + let title: LocalizedStringKey let value: String let unit: String? let color: Color @@ -313,6 +313,9 @@ struct InsulinTotalsCard: View { } struct StatsGridView: View { + private var a1cTitle: LocalizedStringKey { showGMI ? "GMI" : "Est. A1C" } + private var variabilityTitle: LocalizedStringKey { showStdDev ? "Std Deviation" : "CV" } + @ObservedObject var simpleStats: SimpleStatsViewModel @Binding var showGMI: Bool @Binding var showStdDev: Bool @@ -333,7 +336,7 @@ struct StatsGridView: View { showGMI.toggle() }) { StatCard( - title: showGMI ? "GMI" : "Est. A1C", + title: a1cTitle, value: showGMI ? formatGMI(simpleStats.avgGlucose) : formatEhbA1c(simpleStats.avgGlucose), unit: UnitSettingsStore.shared.glycemicOutputUnit.rawValue, color: .blue, @@ -353,7 +356,7 @@ struct StatsGridView: View { showStdDev.toggle() }) { StatCard( - title: showStdDev ? "Std Deviation" : "CV", + title: variabilityTitle, value: showStdDev ? formatStdDev(simpleStats.stdDeviation) : formatCV(simpleStats.coefficientOfVariation), unit: showStdDev ? UnitSettingsStore.shared.glucoseUnit.rawValue : "%", color: .orange, diff --git a/LoopFollow/Stats/DataAvailabilityCalculator.swift b/LoopFollow/Stats/DataAvailabilityCalculator.swift index 169e45edd..748818c3e 100644 --- a/LoopFollow/Stats/DataAvailabilityCalculator.swift +++ b/LoopFollow/Stats/DataAvailabilityCalculator.swift @@ -27,10 +27,10 @@ struct DataAvailabilityInfo { var description: String { switch self { - case .excellent: return "Excellent" - case .good: return "Good" - case .fair: return "Fair" - case .poor: return "Poor" + case .excellent: return String(localized: "Excellent", comment: "Data availability quality") + case .good: return String(localized: "Good", comment: "Data availability quality") + case .fair: return String(localized: "Fair", comment: "Data availability quality") + case .poor: return String(localized: "Poor", comment: "Data availability quality") } } } diff --git a/LoopFollow/Stats/DateRangePicker.swift b/LoopFollow/Stats/DateRangePicker.swift index 8b15e10d1..7c7f78e36 100644 --- a/LoopFollow/Stats/DateRangePicker.swift +++ b/LoopFollow/Stats/DateRangePicker.swift @@ -274,7 +274,7 @@ struct DateRangePicker: View { } struct QuickSelectButton: View { - let title: String + let title: LocalizedStringKey let action: () -> Void var body: some View { diff --git a/LoopFollow/Stats/TIR/TIRView.swift b/LoopFollow/Stats/TIR/TIRView.swift index 8ff66e273..a709bb006 100644 --- a/LoopFollow/Stats/TIR/TIRView.swift +++ b/LoopFollow/Stats/TIR/TIRView.swift @@ -89,11 +89,11 @@ struct TIRView: View { private var tirTitle: String { switch UnitSettingsStore.shared.timeInRangeMode { case .tir: - return "Time in Range" + return String(localized: "Time in Range", comment: "Time-in-range mode title") case .titr: - return "Time in Tight Range" + return String(localized: "Time in Tight Range", comment: "Time-in-range mode title") case .custom: - return "Custom Range" + return String(localized: "Custom Range", comment: "Time-in-range mode title") } } diff --git a/LoopFollow/Task/MinAgoTask.swift b/LoopFollow/Task/MinAgoTask.swift index 0ba964d99..71788c51f 100644 --- a/LoopFollow/Task/MinAgoTask.swift +++ b/LoopFollow/Task/MinAgoTask.swift @@ -39,7 +39,7 @@ extension MainViewController { } let formattedDuration = formatter.string(from: secondsAgo) ?? "" - let minAgoDisplayText = formattedDuration + " min ago" + let minAgoDisplayText = String(localized: "\(formattedDuration) min ago", comment: "Main screen: time since last reading, e.g. 4:35 min ago") // Update UI only if the display text has changed if minAgoDisplayText != Observable.shared.minAgoText.value { diff --git a/LoopFollow/Treatments/TreatmentsView.swift b/LoopFollow/Treatments/TreatmentsView.swift index f1b1c7595..1418d3078 100644 --- a/LoopFollow/Treatments/TreatmentsView.swift +++ b/LoopFollow/Treatments/TreatmentsView.swift @@ -882,13 +882,13 @@ enum TreatmentType: CaseIterable, Hashable { var displayName: String { switch self { - case .carb: return "Carbs" - case .bolusManual: return "Bolus" - case .bolusAutomatic: return "Automatic" - case .smb: return "SMB" - case .tempBasal: return "Basal" - case .override: return "Override" - case .tempTarget: return "Temp Target" + case .carb: return String(localized: "Carbs", comment: "Treatment type") + case .bolusManual: return String(localized: "Bolus", comment: "Treatment type") + case .bolusAutomatic: return String(localized: "Automatic", comment: "Treatment type") + case .smb: return String(localized: "SMB", comment: "Treatment type") + case .tempBasal: return String(localized: "Basal", comment: "Treatment type") + case .override: return String(localized: "Override", comment: "Treatment type") + case .tempTarget: return String(localized: "Temp Target", comment: "Treatment type") } } } @@ -904,13 +904,13 @@ enum TreatmentFilter: Hashable { func displayName(automaticLabel: String) -> String { switch self { - case .all: return "All" - case .carb: return "Carbs" - case .basal: return "Basal" - case .bolusAll: return "All Boluses" - case .bolusAutomatic: return automaticLabel - case .bolusSMB: return "SMB" - case .bolusManual: return "Manual" + case .all: return String(localized: "All", comment: "Treatment filter") + case .carb: return String(localized: "Carbs", comment: "Treatment filter") + case .basal: return String(localized: "Basal", comment: "Treatment filter") + case .bolusAll: return String(localized: "All Boluses", comment: "Treatment filter") + case .bolusAutomatic: return automaticLabel == "SMB" ? String(localized: "SMB", comment: "Treatment filter") : String(localized: "Automatic", comment: "Treatment filter") + case .bolusSMB: return String(localized: "SMB", comment: "Treatment filter") + case .bolusManual: return String(localized: "Manual", comment: "Treatment filter") } } @@ -1226,7 +1226,7 @@ class TreatmentsViewModel: ObservableObject { type: .bolusAutomatic, date: timestamp, title: String(format: "%.2f U", insulin), - subtitle: isSMB || isTrioSMBType ? "SMB" : "Automatic Bolus", + subtitle: isSMB || isTrioSMBType ? String(localized: "SMB") : String(localized: "Automatic Bolus"), icon: "arrowtriangle.down.fill", color: .blue, bgValue: actualBG diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 06c5e1f76..b277b7aaa 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -448,7 +448,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { let loadingLabel = UILabel() loadingLabel.translatesAutoresizingMaskIntoConstraints = false - loadingLabel.text = "Loading..." + loadingLabel.text = String(localized: "Loading...") loadingLabel.textAlignment = .center loadingLabel.font = UIFont.systemFont(ofSize: 17, weight: .medium) loadingLabel.textColor = UIColor.secondaryLabel @@ -559,7 +559,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { @objc func refresh() { LogManager.shared.log(category: .general, message: "Refreshing") - Observable.shared.minAgoText.value = "Refreshing" + Observable.shared.minAgoText.value = String(localized: "Refreshing") scheduleAllTasks() NightscoutSocketManager.shared.connectIfNeeded() @@ -736,24 +736,24 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { if isBlacklisted { let lastBlacklistShown = Storage.shared.lastBlacklistNotificationShown.value ?? Date.distantPast if now.timeIntervalSince(lastBlacklistShown) > 86400 { // 24 hours - self.versionAlert(message: "The current version has a critical issue and should be updated as soon as possible.") + self.versionAlert(message: String(localized: "The current version has a critical issue and should be updated as soon as possible.")) Storage.shared.lastBlacklistNotificationShown.value = now Storage.shared.lastVersionUpdateNotificationShown.value = now } } else if isNewer { let lastVersionUpdateShown = Storage.shared.lastVersionUpdateNotificationShown.value ?? Date.distantPast if now.timeIntervalSince(lastVersionUpdateShown) > 1_209_600 { // 2 weeks - self.versionAlert(message: "A new version is available: \(latestVersion ?? "Unknown"). It is recommended to update.") + self.versionAlert(message: String(localized: "A new version is available: \(latestVersion ?? String(localized: "Unknown")). It is recommended to update.")) Storage.shared.lastVersionUpdateNotificationShown.value = now } } } } - func versionAlert(title: String = "Update Available", message: String) { + func versionAlert(title: String = String(localized: "Update Available"), message: String) { DispatchQueue.main.async { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: String(localized: "OK"), style: .default, handler: nil)) self.present(alert, animated: true) } } @@ -774,8 +774,8 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { func expirationAlert() { DispatchQueue.main.async { - let alert = UIAlertController(title: "App Expiration Warning", message: "This app will expire in less than a week. Please rebuild to continue using it.", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + let alert = UIAlertController(title: String(localized: "App Expiration Warning"), message: String(localized: "This app will expire in less than a week. Please rebuild to continue using it."), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: String(localized: "OK"), style: .default, handler: nil)) self.present(alert, animated: true) } } @@ -930,7 +930,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { var minAgo = "" if deltaTime > 9 { // write old BG reading and continue pushing out end date to show last entry - minAgo = String(Int(deltaTime)) + " min" + minAgo = String(localized: "\(Int(deltaTime)) min", comment: "Minutes since last reading, e.g. 5 min") eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60)) } var basal = "~" @@ -1065,7 +1065,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { // Create Setup Nightscout button if setupNightscoutButton == nil { setupNightscoutButton = UIButton(type: .system) - setupNightscoutButton.setTitle("Setup Nightscout", for: .normal) + setupNightscoutButton.setTitle(String(localized: "Setup Nightscout"), for: .normal) setupNightscoutButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium) setupNightscoutButton.backgroundColor = UIColor.systemBlue setupNightscoutButton.setTitleColor(.white, for: .normal) @@ -1090,7 +1090,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { // Create Setup Dexcom Share button if setupDexcomButton == nil { setupDexcomButton = UIButton(type: .system) - setupDexcomButton.setTitle("Setup Dexcom Share", for: .normal) + setupDexcomButton.setTitle(String(localized: "Setup Dexcom Share"), for: .normal) setupDexcomButton.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .medium) setupDexcomButton.backgroundColor = UIColor.systemGreen setupDexcomButton.setTitleColor(.white, for: .normal) diff --git a/LoopFollow/ViewControllers/MoreMenuView.swift b/LoopFollow/ViewControllers/MoreMenuView.swift index ed1c8f360..5001b1bc4 100644 --- a/LoopFollow/ViewControllers/MoreMenuView.swift +++ b/LoopFollow/ViewControllers/MoreMenuView.swift @@ -158,7 +158,7 @@ struct MoreMenuView: View { /// never needs to be kept in sync by hand. private var searchItems: [MenuSearchItem] { var items: [MenuSearchItem] = [ - MenuSearchItem(title: "Settings", icon: "gearshape", keywords: [], kind: .settings(.settings)), + MenuSearchItem(title: String(localized: "Settings"), icon: "gearshape", keywords: [], kind: .settings(.settings)), ] let settingsSections = SettingsRoute.menuSections(nightscoutConfigured: !nightscoutURL.value.isEmpty) @@ -173,8 +173,8 @@ struct MoreMenuView: View { items.append(MenuSearchItem(title: item.displayName, icon: item.icon, keywords: [], kind: .feature(item))) } - items.append(MenuSearchItem(title: "View Log", icon: "doc.text.magnifyingglass", keywords: ["logging"], kind: .viewLog)) - items.append(MenuSearchItem(title: "Share Logs", icon: "square.and.arrow.up", keywords: ["logging"], kind: .shareLogs)) + items.append(MenuSearchItem(title: String(localized: "View Log"), icon: "doc.text.magnifyingglass", keywords: ["logging"], kind: .viewLog)) + items.append(MenuSearchItem(title: String(localized: "Share Logs"), icon: "square.and.arrow.up", keywords: ["logging"], kind: .shareLogs)) for link in MoreMenuView.supportLinks { items.append(MenuSearchItem(title: link.title, icon: link.icon, keywords: ["support", "community"], kind: .link(link.url))) diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index 27c6b584f..8c7a34354 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -91,11 +91,11 @@ extension NightscoutViewController: WKNavigationDelegate, WKUIDelegate { func webView(_: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame _: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { let alertCtrl = UIAlertController(title: nil, message: message, preferredStyle: .alert) - alertCtrl.addAction(UIAlertAction(title: "OK", style: .default) { _ in + alertCtrl.addAction(UIAlertAction(title: String(localized: "OK"), style: .default) { _ in completionHandler(true) }) - alertCtrl.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in + alertCtrl.addAction(UIAlertAction(title: String(localized: "Cancel"), style: .cancel) { _ in completionHandler(false) }) diff --git a/Scripts/localization/apply_translations.py b/Scripts/localization/apply_translations.py new file mode 100755 index 000000000..dff743b11 --- /dev/null +++ b/Scripts/localization/apply_translations.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Write translations from a TSV (keycommentvalue) into a String Catalog. + +Usage: apply_translations.py +Cells use the escapes written by missing_translations.py (\\, \t, \n). Rows with an +empty value are skipped. Keys not present in the catalog are reported and skipped. +The catalog is rewritten in Xcode's own style (2-space indent, " : " separator, sorted keys). +""" +import json +import sys + + +def unescape(s: str) -> str: + out, i = [], 0 + while i < len(s): + c = s[i] + if c == "\\" and i + 1 < len(s): + nxt = s[i + 1] + out.append({"n": "\n", "t": "\t", "\\": "\\"}.get(nxt, "\\" + nxt)) + i += 2 + else: + out.append(c) + i += 1 + return "".join(out) + + +def main() -> int: + if len(sys.argv) != 4: + print(__doc__, file=sys.stderr) + return 2 + path, lang, tsv = sys.argv[1:4] + with open(path, encoding="utf-8") as f: + catalog = json.load(f) + strings = catalog["strings"] + applied, unknown = 0, [] + with open(tsv, encoding="utf-8") as f: + for line in f.read().split("\n"): + cells = line.split("\t") + if len(cells) < 3 or not cells[2].strip(): + continue + key, value = unescape(cells[0]), unescape(cells[2]) + if key not in strings: + unknown.append(key) + continue + strings[key].setdefault("localizations", {})[lang] = {"stringUnit": {"state": "translated", "value": value}} + applied += 1 + with open(path, "w", encoding="utf-8") as f: + json.dump(catalog, f, ensure_ascii=False, indent=2, sort_keys=True, separators=(",", " : ")) + f.write("\n") + print(f"applied {applied} [{lang}] translations", file=sys.stderr) + if unknown: + print("unknown keys (not in catalog):\n " + "\n ".join(unknown), file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Scripts/localization/localized_apis.txt b/Scripts/localization/localized_apis.txt new file mode 100644 index 000000000..a256e08a0 --- /dev/null +++ b/Scripts/localization/localized_apis.txt @@ -0,0 +1,62 @@ +Text +Button +Label +Toggle +Section +NavigationLink +TextField +SecureField +Picker +Stepper +Link +DatePicker +Menu +Tab +ContentUnavailableView +LabeledContent +ProgressView +navigationTitle +alert +confirmationDialog +help +accessibilityLabel +accessibilityHint +badge +String(localized: +QuickPickSectionHeader +QuickSelectButton +InfoBanner +NavigationRow +LoadingButtonView +ErrorMessageView +OnboardingStepHeader +HKQuantityInputView +AlarmEnumMenuPicker +CommandButtonView +primaryLabel +StatCard +AlarmBGSection +AlarmBGLimitSection +AlarmStepperSection +# labels whose literal is never user-facing text +!systemImage +!icon +!iconName +!image +!imageName +!key +!keyPath +!tag +!id +!identifier +!tableName +!bundle +!comment +!unit +!format +!separator +!of +!with +BGPicker +bgPicker +SettingsLeaf diff --git a/Scripts/localization/missing_translations.py b/Scripts/localization/missing_translations.py new file mode 100755 index 000000000..a116466c3 --- /dev/null +++ b/Scripts/localization/missing_translations.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""List catalog keys that still lack a translation for a language, as TSV. + +Usage: missing_translations.py +Output columns: key, comment, (empty translation column to fill in). +Cells escape backslash, tab and newline as \\, \t and \n. Keys marked +shouldTranslate=false are skipped. +""" +import json +import sys + + +def escape(s: str) -> str: + return s.replace("\\", "\\\\").replace("\t", "\\t").replace("\n", "\\n") + + +def main() -> int: + if len(sys.argv) != 3: + print(__doc__, file=sys.stderr) + return 2 + path, lang = sys.argv[1], sys.argv[2] + with open(path, encoding="utf-8") as f: + strings = json.load(f)["strings"] + count = 0 + for key in sorted(strings): + entry = strings[key] + if entry.get("shouldTranslate") is False: + continue + unit = entry.get("localizations", {}).get(lang, {}).get("stringUnit", {}) + if unit.get("state") == "translated" and unit.get("value", "").strip(): + continue + print("\t".join([escape(key), escape(entry.get("comment", "")), ""])) + count += 1 + print(f"{count} keys missing [{lang}]", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Scripts/localization/scan_keys.py b/Scripts/localization/scan_keys.py new file mode 100755 index 000000000..0c260f814 --- /dev/null +++ b/Scripts/localization/scan_keys.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +"""Add SwiftUI/Foundation string-literal keys from the source tree to a String Catalog. + +Usage: scan_keys.py [--check] [--interpolations] [--roots LoopFollow Shared ...] +Reads Scripts/localization/localized_apis.txt: for every listed API, the first "…" literal +argument of `Api(` or `.api(` is a catalog key. Interpolated literals ("\\(") are skipped +with a warning unless --interpolations is given, which converts them to printf-style keys +heuristically (%lld for integral-looking expressions, the explicit specifier: when present, +%@ otherwise) — review those keys before translating. Keys added here carry extractionState "manual". With --check, +nothing is written and the exit code is 1 when keys are missing. +""" +import json +import pathlib +import re +import sys + +HERE = pathlib.Path(__file__).resolve().parent +INTERPOLATIONS = False +API_LIST = HERE / "localized_apis.txt" + + +def read_api_list(): + """Returns ({api_name: required_label_or_None}, ignored_labels). + An entry like `String(localized:` means: calls to String( count, but only the literal + carrying the `localized:` label is a key.""" + apis, ignored = {}, set() + for line in API_LIST.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + if line.startswith("!"): + ignored.add(line[1:]) + elif "(" in line: + name, label = line.split("(", 1) + apis[name] = label.rstrip(":") or None + else: + apis[line] = None + return apis, ignored + + +def call_literals(src, open_paren): + """Yield (label, literal, interpolated) for direct string-literal arguments of the call whose + "(" is at open_paren. Nested calls/arrays are skipped; interpolated literals are flagged.""" + i = open_paren + 1 + depth_paren, depth_bracket = 0, 0 + label = None + expect_value = True # right after "(" or "," + n = len(src) + while i < n: + c = src[i] + if c == '"': + j = i + 1 + interpolated = False + while j < n and src[j] != '"': + if src[j] == "\\": + if j + 1 < n and src[j + 1] == "(": + interpolated = True + # skip the whole \(…) expression, including nested parens and quotes + depth, j = 1, j + 2 + while j < n and depth: + if src[j] == '"': + j += 1 + while j < n and src[j] != '"': + j += 2 if src[j] == "\\" else 1 + elif src[j] == "(": + depth += 1 + elif src[j] == ")": + depth -= 1 + j += 1 + continue + j += 2 + continue + j += 1 + lit = src[i + 1:j] + if depth_paren == 0 and depth_bracket == 0 and expect_value: + yield label, lit, interpolated + expect_value = False + i = j + 1 + continue + if c == "(": + depth_paren += 1 + elif c == ")": + if depth_paren == 0: + return + depth_paren -= 1 + elif c == "[": + depth_bracket += 1 + elif c == "]": + depth_bracket -= 1 + elif c == "," and depth_paren == 0 and depth_bracket == 0: + label = None + expect_value = True + elif c == "?" and depth_paren == 0 and depth_bracket == 0: + expect_value = True # ternary then-branch + elif c == ":" and depth_paren == 0 and depth_bracket == 0 and not expect_value: + expect_value = True # ternary else-branch + elif c == ":" and depth_paren == 0 and depth_bracket == 0 and expect_value: + m = re.search(r"(\w+)\s*$", src[max(0, i - 40):i]) + label = m.group(1) if m else None + elif not c.isspace() and c not in ":" and expect_value: + # an identifier/expression as the value (e.g. `title: route.title`) is not a literal + m = re.match(r"\w+\s*:", src[i:i + 40]) + if not m: + expect_value = False + i += 1 + + +INT_HINTS = re.compile(r"^(?:Int\(|UInt\(|(?:[\w.]*\.)?\w*(?:count|Count|hours|Hours|minutes|Minutes|mins|days|Days|seconds|index|Index|statusCode|percent|Percent)\w*$)") + + +def interpolation_key(lit): + """Turn `a \\(x) b` into the printf-style key the compiler would emit: `%lld` for expressions + that look integral, the explicit `specifier:` when given, `%@` otherwise. Heuristic — review.""" + out, i, n = [], 0, len(lit) + while i < n: + if lit.startswith("\\(", i): + depth, j = 1, i + 2 + while j < n and depth: + if lit[j] == "(": + depth += 1 + elif lit[j] == ")": + depth -= 1 + j += 1 + expr = lit[i + 2:j - 1].strip() + m = re.search(r'specifier:\s*"([^"]+)"', expr) + if m: + out.append(m.group(1)) + elif INT_HINTS.search(expr): + out.append("%lld") + else: + out.append("%@") + i = j + elif lit[i] == "%": + out.append("%%") # a literal percent sign must be escaped in a printf-style key + i += 1 + else: + out.append(lit[i]) + i += 1 + return "".join(out) + + +def literal_keys(roots): + apis, ignored = read_api_list() + names = "|".join(re.escape(a) for a in apis) + call = re.compile(r"(? int: + args = sys.argv[1:] + if not args: + print(__doc__, file=sys.stderr) + return 2 + path = pathlib.Path(args[0]) + check = "--check" in args + global INTERPOLATIONS + INTERPOLATIONS = "--interpolations" in args + roots = args[args.index("--roots") + 1:] if "--roots" in args else ["LoopFollow"] + catalog = json.loads(path.read_text(encoding="utf-8")) + strings = catalog["strings"] + keys, skipped = literal_keys(roots) + spec = re.compile(r"%(?:\d+\$)?[-+0#]*\d*(?:\.\d+)?(?:ll|l|h)?[@dDiuUxXoOfeEgGcCsSpaAF]") + existing_shapes = {spec.sub("%?", k) for k in strings} + missing = [] + for k in sorted(k for k in keys if k not in strings): + if "(interpolated from:" in keys[k] and spec.sub("%?", k) in existing_shapes: + print(f"skip (a key with the same text but other specifiers already exists; heuristic type guess?): {k!r}", file=sys.stderr) + continue + missing.append(k) + for f, s in skipped: + print(f"skip (interpolated, needs compiler extraction): {f}: {s}", file=sys.stderr) + if check: + for k in missing: + print(f"missing: {keys[k]}: {k!r}") + print(f"{len(missing)} keys missing from {path}", file=sys.stderr) + return 1 if missing else 0 + for k in missing: + strings[k] = {"extractionState": "manual"} + path.write_text(json.dumps(catalog, ensure_ascii=False, indent=2, sort_keys=True, separators=(",", " : ")) + "\n", encoding="utf-8") + print(f"added {len(missing)} keys", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Tests/LocalizationCatalogTests.swift b/Tests/LocalizationCatalogTests.swift new file mode 100644 index 000000000..66d7bec33 --- /dev/null +++ b/Tests/LocalizationCatalogTests.swift @@ -0,0 +1,105 @@ +// LoopFollow +// LocalizationCatalogTests.swift + +import Foundation +@testable import LoopFollow +import Testing + +/// Guards the String Catalogs checked into the repo. Reads the JSON source files, not the +/// compiled bundle, so it runs without a device language switch. +struct LocalizationCatalogTests { + /// Repo root derived from this file's location (Tests/LocalizationCatalogTests.swift). + private static let repoRoot = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + + static let catalogs = ["LoopFollow/Resources/Localizable.xcstrings"] + static let languages = ["tr"] + + private struct Catalog { + let sourceLanguage: String + let strings: [String: [String: Any]] + } + + private static func load(_ path: String) throws -> Catalog { + let data = try Data(contentsOf: repoRoot.appendingPathComponent(path)) + let json = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any]) + let strings = try #require(json["strings"] as? [String: [String: Any]]) + let source = try #require(json["sourceLanguage"] as? String) + return Catalog(sourceLanguage: source, strings: strings) + } + + private static func unit(_ entry: [String: Any], _ language: String) -> (state: String, value: String)? { + guard let localizations = entry["localizations"] as? [String: Any], + let lang = localizations[language] as? [String: Any], + let unit = lang["stringUnit"] as? [String: Any], + let state = unit["state"] as? String, + let value = unit["value"] as? String else { return nil } + return (state, value) + } + + /// Keys marked "Don't translate" in Xcode are skipped. + private static func translatable(_ entry: [String: Any]) -> Bool { + (entry["shouldTranslate"] as? Bool) ?? true + } + + /// printf-style specifiers, e.g. %lld, %@, %.1f, %2$@. Returns (positionalIndex?, specifier). + /// The positional group only matches digits followed by "$", so a width like "%2d" is not positional. + /// The space flag is deliberately not accepted so prose such as "% and" is not read as a specifier. + private static func placeholders(in s: String) -> [(Int?, String)] { + let pattern = #"%(?:(\d+)\$)?([-+0#]*\d*(?:\.\d+)?(?:ll|l|h)?[@dDiuUxXoOfeEgGcCsSpaAF])"# + let regex = try! NSRegularExpression(pattern: pattern) + return regex.matches(in: s, range: NSRange(s.startIndex..., in: s)).compactMap { m in + guard let specRange = Range(m.range(at: 2), in: s) else { return nil } + let index = Range(m.range(at: 1), in: s).flatMap { Int(s[$0]) } + return (index, String(s[specRange])) + } + } + + @Test("every translatable key has a translated value", arguments: Self.catalogs) + func allKeysTranslated(path: String) throws { + let catalog = try Self.load(path) + #expect(catalog.sourceLanguage == "en") + var missing: [String] = [] + for (key, entry) in catalog.strings where Self.translatable(entry) { + for language in Self.languages { + guard let t = Self.unit(entry, language), t.state == "translated", + !t.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + else { missing.append("[\(language)] \(key)"); continue } + } + } + #expect(missing.isEmpty, Comment(rawValue: "Missing translations (\(missing.count)):\n" + missing.sorted().joined(separator: "\n"))) + } + + @Test("format placeholders survive translation", arguments: Self.catalogs) + func placeholdersPreserved(path: String) throws { + let catalog = try Self.load(path) + var problems: [String] = [] + for (key, entry) in catalog.strings where Self.translatable(entry) { + let source = Self.placeholders(in: key) + guard !source.isEmpty else { continue } + for language in Self.languages { + guard let t = Self.unit(entry, language) else { continue } + let target = Self.placeholders(in: t.value) + if source.map(\.1).sorted() != target.map(\.1).sorted() { + problems.append("[\(language)] \(key) -> \(t.value)") + continue + } + // Word order differs between languages, so every multi-placeholder translation must + // address its arguments explicitly (%1$@, %2$lld ...); mixing positional and + // non-positional specifiers is undefined at runtime. + if source.count > 1, target.contains(where: { $0.0 == nil }) { + problems.append("[\(language)] multi-placeholder value must use positional indexes: \(key) -> \(t.value)") + } + } + } + #expect(problems.isEmpty, Comment(rawValue: "Placeholder problems:\n" + problems.joined(separator: "\n"))) + } + + @Test("no stale keys linger in the catalog", arguments: Self.catalogs) + func noStaleKeys(path: String) throws { + let catalog = try Self.load(path) + let stale = catalog.strings.filter { ($0.value["extractionState"] as? String) == "stale" }.map(\.key) + #expect(stale.isEmpty, Comment(rawValue: "Stale keys (remove them from the catalog):\n" + stale.sorted().joined(separator: "\n"))) + } +} diff --git a/Tests/LocalizationDisplayNameTests.swift b/Tests/LocalizationDisplayNameTests.swift new file mode 100644 index 000000000..48dcbcf31 --- /dev/null +++ b/Tests/LocalizationDisplayNameTests.swift @@ -0,0 +1,122 @@ +// LoopFollow +// LocalizationDisplayNameTests.swift + +import Foundation +@testable import LoopFollow +import Testing + +struct LocalizationDisplayNameTests { + /// Persisted raw values. Changing any of these breaks decoding of stored alarms. + private static let alarmTypeRawValues: [AlarmType: String] = [ + .temporary: "Temporary Alert", + .iob: "IOB Alert", + .cob: "COB Alert", + .low: "Low BG Alert", + .high: "High BG Alert", + .fastDrop: "Fast Drop Alert", + .fastRise: "Fast Rise Alert", + .missedReading: "Missed Reading Alert", + .notLooping: "Not Looping Alert", + .missedBolus: "Missed Bolus Alert", + .futureCarbs: "Future Carbs Alert", + .sensorChange: "Sensor Change Alert", + .pumpChange: "Pump Change Alert", + .pump: "Pump Insulin Alert", + .pumpBattery: "Pump Battery Alert", + .battery: "Low Battery", + .batteryDrop: "Battery Drop", + .recBolus: "Rec. Bolus", + .overrideStart: "Override Started", + .overrideEnd: "Override Ended", + .tempTargetStart: "Temp Target Started", + .tempTargetEnd: "Temp Target Ended", + .buildExpire: "Looping app expiration", + .dbSize: "Nightscout Database Size", + ] + + @Test("AlarmType raw values are stable (they are persisted)") + func alarmTypeRawValuesAreStable() { + #expect(AlarmType.allCases.count == Self.alarmTypeRawValues.count) + for (type, raw) in Self.alarmTypeRawValues { + #expect(type.rawValue == raw) + } + } + + @Test("day/night option raw values are stable (they are persisted)") + func dayNightRawValuesAreStable() { + #expect(PlaySoundOption.allCases.map(\.rawValue) == ["always", "day", "night", "never"]) + #expect(RepeatSoundOption.allCases.map(\.rawValue) == ["always", "day", "night", "never"]) + #expect(ActiveOption.allCases.map(\.rawValue) == ["always", "day", "night"]) + } + + /// The test host runs in whatever language the simulator uses, so compare against the + /// catalog lookup for the same key instead of hardcoding English. + private func localized(_ key: String) -> String { + Bundle.main.localizedString(forKey: key, value: nil, table: nil) + } + + @Test("AlarmType.displayName resolves its raw value through the String Catalog") + func alarmTypeDisplayNameUsesCatalog() { + for type in AlarmType.allCases { + #expect(type.displayName == localized(type.rawValue)) + #expect(!type.displayName.isEmpty) + } + } + + @Test("day/night displayName resolves its English key through the String Catalog") + func dayNightDisplayNameUsesCatalog() { + #expect(PlaySoundOption.always.displayName == localized("Day & Night")) + #expect(PlaySoundOption.day.displayName == localized("Day")) + #expect(PlaySoundOption.night.displayName == localized("Night")) + #expect(PlaySoundOption.never.displayName == localized("Never")) + #expect(ActiveOption.always.displayName == localized("Day & Night")) + } + + @Test("Turkish strings are compiled into the app bundle") + func turkishBundleContainsAlarmTypeNames() throws { + let path = try #require(Bundle.main.path(forResource: "tr", ofType: "lproj"), "tr.lproj missing: check knownRegions and catalog target membership") + let bundle = try #require(Bundle(path: path)) + for type in AlarmType.allCases { + let localized = bundle.localizedString(forKey: type.rawValue, value: "MISSING", table: nil) + #expect(localized != "MISSING", Comment(rawValue: "no Turkish value for \(type.rawValue)")) + #expect(localized != type.rawValue, Comment(rawValue: "Turkish value equals English for \(type.rawValue)")) + } + #expect(bundle.localizedString(forKey: "Day & Night", value: "MISSING", table: nil) == "Gündüz ve Gece") + } + + @Test("English plural variation resolves through the compiled catalog") + func englishPluralVariationApplies() throws { + let path = try #require(Bundle.main.path(forResource: "en", ofType: "lproj")) + let bundle = try #require(Bundle(path: path)) + let one = String(localized: "Calculated \(1) minutes ago", bundle: bundle) + let many = String(localized: "Calculated \(5) minutes ago", bundle: bundle) + #expect(one == "Calculated 1 minute ago") + #expect(many == "Calculated 5 minutes ago") + } + + @Test("main screen status strings have Turkish values and keep their argument") + func minAgoFormatIsLocalized() throws { + let path = try #require(Bundle.main.path(forResource: "tr", ofType: "lproj")) + let tr = try #require(Bundle(path: path)) + for key in ["%@ min ago", "%lld min", "LOW", "HIGH", "⚠️ Not Looping!", "Refreshing", "Loading...", "Setup Nightscout", "Setup Dexcom Share", "%llds left", + "Number of hours before the %lld-day mark that the alert will fire.", "BG Check", "Sensor Start", "Update Available"] + { + let v = tr.localizedString(forKey: key, value: "MISSING", table: nil) + #expect(v != "MISSING", Comment(rawValue: "no Turkish for \(key)")) + } + let minAgo = String(format: tr.localizedString(forKey: "%lld min", value: nil, table: nil), 7) + #expect(minAgo.contains("7")) + // The main screen shows a formatted duration such as "4:35", so its key takes a string argument. + let mainScreen = String(format: tr.localizedString(forKey: "%@ min ago", value: nil, table: nil), "4:35") + #expect(mainScreen.contains("4:35")) + } + + @Test("Nightscout 'checking' state is detected in any language") + @MainActor + func nightscoutCheckingStateIsLanguageIndependent() { + let vm = NightscoutSettingsViewModel() + vm.nightscoutURL = "https://example.invalid" + vm.nightscoutStatus = String(localized: "Checking...") + #expect(vm.statusKind == .checking) + } +} diff --git a/Tests/LocalizationEnumNameTests.swift b/Tests/LocalizationEnumNameTests.swift new file mode 100644 index 000000000..0331bf2c8 --- /dev/null +++ b/Tests/LocalizationEnumNameTests.swift @@ -0,0 +1,68 @@ +// LoopFollow +// LocalizationEnumNameTests.swift + +import Foundation +@testable import LoopFollow +import Testing + +struct LocalizationEnumNameTests { + private static func turkish() throws -> Bundle { + let path = try #require(Bundle.main.path(forResource: "tr", ofType: "lproj")) + return try #require(Bundle(path: path)) + } + + private static func hasTurkish(_ bundle: Bundle, _ key: String) -> Bool { + let v = bundle.localizedString(forKey: key, value: "MISSING", table: nil) + return v != "MISSING" && !v.isEmpty + } + + @Test("persisted raw values of enums touched by this PR are unchanged") + func persistedRawValuesUnchanged() { + #expect(ContactColorMode.staticColor.rawValue == "Static") + #expect(ContactColorMode.dynamic.rawValue == "Dynamic") + #expect(TRCCommandType.cancelOverride.rawValue == "cancel_override") + #expect(TRCCommandType.tempTarget.rawValue == "temp_target") + #expect(InfoType.iob.rawValue == 0) + #expect(InfoType.dbSize.rawValue == InfoType.allCases.count - 1) + } + + @Test("every enum display name has a Turkish value in the compiled bundle") + func enumNamesHaveTurkish() throws { + let tr = try Self.turkish() + let keys = [ + "Static", "Dynamic (BG Range)", // ContactColorMode + "System", "Light", "Dark", // AppearanceMode + "Cone", "Lines", // PredictionDisplayType + "Bolus", "Temp Target", "Cancel Temp Target", "Meal", "Start Override", "Cancel Override", // TRCCommandType + "Carbs", "Automatic", "SMB", "Basal", "Override", // TreatmentType + "Excellent", "Good", "Fair", "Poor", // DataAvailability + "min", "hours", "days", "none", // AlarmType.timeUnit + "Tab 1", "Tab 2", "Tab 3", "Tab 4", "Menu", // TabPosition + "Home", "Alarms", "Remote", "Nightscout", "Snoozer", "Treatments", "Statistics", // TabItem + "Basal", "Battery", "Pump", "Pump Battery", "SAGE", "CAGE", "Rec. Bolus", "Min/Max", "Carbs today", "Autosens", "Profile", "Target", "ISF", "CR", "Updated", "TDD", "IAGE", "DB Size", // InfoType + "Custom Sound", + ] + let missing = keys.filter { !Self.hasTurkish(tr, $0) } + #expect(missing.isEmpty, Comment(rawValue: "No Turkish for: " + missing.joined(separator: ", "))) + } + + @Test("alarm blurbs resolve through the catalog") + func alarmBlurbsHaveTurkish() throws { + let tr = try Self.turkish() + for type in AlarmType.allCases { + #expect(!type.blurb.isEmpty) + } + #expect(Self.hasTurkish(tr, "Alerts when BG goes below a limit.")) + #expect(Self.hasTurkish(tr, "Alerts when BG rises above a limit.")) + } + + @Test("settings search leaves are localized") + func settingsLeafTitlesAreLocalized() throws { + let tr = try Self.turkish() + let leaf = SettingsLeaf("Access Token", ["token"]) + #expect(leaf.title == Bundle.main.localizedString(forKey: "Access Token", value: nil, table: nil)) + #expect(Self.hasTurkish(tr, "Access Token")) + #expect(Self.hasTurkish(tr, "Information Display")) + #expect(Self.hasTurkish(tr, "Background Refresh")) + } +} diff --git a/Tests/LocalizationSourceLintTests.swift b/Tests/LocalizationSourceLintTests.swift new file mode 100644 index 000000000..2e1ebe1ee --- /dev/null +++ b/Tests/LocalizationSourceLintTests.swift @@ -0,0 +1,209 @@ +// LoopFollow +// LocalizationSourceLintTests.swift + +import Foundation +@testable import LoopFollow +import Testing + +/// Source-level guards for patterns that silently defeat localization. +struct LocalizationSourceLintTests { + private static let repoRoot = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + + private static let sourceRoots = ["LoopFollow", "Shared", "LoopFollowLAExtension"] + + private static func swiftFiles() throws -> [URL] { + var files: [URL] = [] + for root in sourceRoots { + let dir = repoRoot.appendingPathComponent(root) + guard let e = FileManager.default.enumerator(at: dir, includingPropertiesForKeys: nil) else { continue } + for case let url as URL in e where url.pathExtension == "swift" { files.append(url) } + } + return files + } + + /// `Text("a " + "b")` resolves to the String overload of Text, so neither literal is ever + /// looked up in the catalog even though the extractor may still emit the first one as a key. + @Test("no Text literal is built from two string literals joined with +") + func noConcatenatedTextLiterals() throws { + let pattern = #"Text\("(?:[^"\\]|\\.)*"\s*\+\s*"# + let regex = try NSRegularExpression(pattern: pattern) + var hits: [String] = [] + for file in try Self.swiftFiles() { + let source = try String(contentsOf: file, encoding: .utf8) + for m in regex.matches(in: source, range: NSRange(source.startIndex..., in: source)) { + guard let r = Range(m.range, in: source) else { continue } + // Only flag when the next token after "+" is another string literal. + let rest = source[r.upperBound...].drop(while: { $0 == " " || $0 == "\n" || $0 == "\t" }) + guard rest.first == "\"" else { continue } + let line = source[.. label the literal must carry (nil = any direct argument), e.g. "String(localized:". + var apis: [String: String?] = [:] + var ignoredLabels: Set = [] + for raw in try String(contentsOf: listURL, encoding: .utf8).split(separator: "\n") { + let line = raw.trimmingCharacters(in: .whitespaces) + if line.isEmpty || line.hasPrefix("#") { continue } + if line.hasPrefix("!") { ignoredLabels.insert(String(line.dropFirst())); continue } + if let paren = line.firstIndex(of: "(") { + let label = String(line[line.index(after: paren)...]).trimmingSuffix(while: { $0 == ":" }) + apis[String(line[..=". + let pattern = #"((?])=(?!=)|return|title:|subtitle:|message:|actionTitle:|accessibilityLabel\()\s*[^\n?"]+\?\s*"[A-Z][^"]*"\s*:\s*"[A-Z][^"]*""# + let regex = try NSRegularExpression(pattern: pattern) + let allowed: Set = ["Alarm.swift"] // notification action titles: PR 3 + var hits: [String] = [] + for file in try Self.swiftFiles() where !allowed.contains(file.lastPathComponent) { + let source = try String(contentsOf: file, encoding: .utf8) + for m in regex.matches(in: source, range: NSRange(source.startIndex..., in: source)) { + guard let r = Range(m.range, in: source) else { continue } + let lineStart = source[.. [(String?, String, Bool)] { + var result: [(String?, String, Bool)] = [] + var i = open + 1 + var depthParen = 0, depthBracket = 0 + var label: String? + var expectValue = true + while i < chars.count { + let c = chars[i] + if c == "\"" { + var j = i + 1 + var interpolated = false + while j < chars.count, chars[j] != "\"" { + if chars[j] == "\\" { + if j + 1 < chars.count, chars[j + 1] == "(" { + interpolated = true + // Skip the whole \(…) expression, including nested parens and quotes. + var depth = 1 + j += 2 + while j < chars.count, depth > 0 { + if chars[j] == "\"" { + j += 1 + while j < chars.count, chars[j] != "\"" { j += chars[j] == "\\" ? 2 : 1 } + } else if chars[j] == "(" { depth += 1 } else if chars[j] == ")" { depth -= 1 } + j += 1 + } + continue + } + j += 2 + continue + } + j += 1 + } + let literal = String(chars[(i + 1) ..< min(j, chars.count)]) + var k = j + 1 + while k < chars.count, chars[k] == " " { k += 1 } + let concatenated = k < chars.count && chars[k] == "+" + if depthParen == 0, depthBracket == 0, expectValue, !concatenated { + result.append((label, literal, interpolated)) + } + expectValue = false + i = j + 1 + continue + } + switch c { + case "(": depthParen += 1 + case ")": + if depthParen == 0 { return result } + depthParen -= 1 + case "[": depthBracket += 1 + case "]": depthBracket -= 1 + case ",": + if depthParen == 0, depthBracket == 0 { label = nil; expectValue = true } + case "?": + if depthParen == 0, depthBracket == 0 { expectValue = true } // ternary then-branch + case ":": + if depthParen == 0, depthBracket == 0, !expectValue { expectValue = true } // ternary else-branch + else if depthParen == 0, depthBracket == 0, expectValue { + var j = i - 1 + while j >= 0, chars[j] == " " || chars[j] == "\n" { j -= 1 } + var name = "" + while j >= 0, chars[j].isLetter || chars[j].isNumber || chars[j] == "_" { name.insert(chars[j], at: name.startIndex); j -= 1 } + label = name.isEmpty ? nil : name + } + default: + if expectValue, !c.isWhitespace { + // A value that starts with an identifier is only a label if a ":" follows it. + var j = i + while j < chars.count, chars[j].isLetter || chars[j].isNumber || chars[j] == "_" { j += 1 } + var k = j + while k < chars.count, chars[k] == " " { k += 1 } + if !(j > i && k < chars.count && chars[k] == ":") { expectValue = false } + } + } + i += 1 + } + return result + } +} + +private extension String { + func trimmingSuffix(while pred: (Character) -> Bool) -> Substring { + var s = Substring(self) + while let last = s.last, pred(last) { + s = s.dropLast() + } + return s + } +}