From 8f90ebb1b4ee6ac1badc991db0e97fa4454b8ac8 Mon Sep 17 00:00:00 2001 From: Ramazan TUNCER Date: Sun, 27 Sep 2026 01:27:21 +0300 Subject: [PATCH 01/11] Add String Catalog and Turkish region Adds LoopFollow/Resources/Localizable.xcstrings (source language en) and registers tr in knownRegions. Keys come from xcodebuild -exportLocalizations plus a scan of SwiftUI LocalizedStringKey call sites (Toggle, Picker, Section, Label, navigationTitle, ...) that the extractor does not emit; those are marked extractionState "manual". No translations yet. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 345c02582e5878ba12922177d89edbf96ef836f7) --- LoopFollow.xcodeproj/project.pbxproj | 1 + LoopFollow/Resources/Localizable.xcstrings | 955 +++++++++++++++++++++ 2 files changed, 956 insertions(+) create mode 100644 LoopFollow/Resources/Localizable.xcstrings 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/Resources/Localizable.xcstrings b/LoopFollow/Resources/Localizable.xcstrings new file mode 100644 index 000000000..257d29bc2 --- /dev/null +++ b/LoopFollow/Resources/Localizable.xcstrings @@ -0,0 +1,955 @@ +{ + "sourceLanguage" : "en", + "strings" : { + " U" : {}, + "#WeAreNotWaitingAnymore" : {}, + "#WeAreWaitingForData" : {}, + "%" : {}, + "%@" : {}, + "%@ %@ → %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ %2$@ → %3$@" + } + } + } + }, + "%@ %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ %2$@" + } + } + } + }, + "%@ - %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ - %2$@" + } + } + } + }, + "%@ gaps" : {}, + "%@ hr" : {}, + "%@ min" : {}, + "%@ of %@ expected readings" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ of %2$@ expected readings" + } + } + } + }, + "%@ sec" : {}, + "%@%%" : {}, + "%@U" : {}, + "%@g" : {}, + "(" : { + "extractionState" : "manual" + }, + "(%@ days)" : {}, + "+ 30m" : {}, + "---" : {}, + "...because the data could not be loaded" : {}, + "00" : {}, + "15m" : {}, + "24h" : {}, + "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." : {}, + "API Secret" : {}, + "APN" : { + "extractionState" : "manual" + }, + "APNS Environment: %@" : {}, + "APNS Key" : {}, + "APNS Key ID" : {}, + "APNS warning: importing APNS settings will overwrite your existing APNS Key ID and APNS Key." : {}, + "APNs credentials are missing or invalid — Live Activity updates will not work. Open Settings → APN to fix." : {}, + "Above: %@" : {}, + "Absorption Time" : {}, + "Access Token" : {}, + "Activate Override" : {}, + "Active" : {}, + "Active During" : {}, + "Active Override" : {}, + "Add Alarm" : { + "extractionState" : "manual" + }, + "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." : {}, + "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." : {}, + "Adding this alarm would exceed the QR code character limit. Please remove some alarms first." : {}, + "Additional Information" : {}, + "Adjust the background refresh type." : {}, + "Advanced Settings" : {}, + "Alarm Name" : { + "extractionState" : "manual" + }, + "Alarm Settings" : {}, + "Alarm not found" : {}, + "Alarms can behave differently during the day and at night." : {}, + "Alert Sound" : {}, + "All" : {}, + "All Alerts Snoozed" : { + "extractionState" : "manual" + }, + "All Sounds Muted" : { + "extractionState" : "manual" + }, + "All alerts snoozed" : {}, + "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." : {}, + "Always" : { + "extractionState" : "manual" + }, + "Ambulatory Glucose Profile (AGP)" : {}, + "App Settings" : { + "extractionState" : "manual" + }, + "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." : {}, + "Appearance" : { + "extractionState" : "manual" + }, + "Are you sure you want to cancel the active override?" : {}, + "Are you sure you want to cancel the existing temp target?" : {}, + "Are you sure you want to send %@ U?" : {}, + "Available Overrides" : {}, + "Available Variables" : { + "extractionState" : "manual" + }, + "BG Update Delay (Sec): %@" : {}, + "Back" : { + "extractionState" : "manual" + }, + "Background Color" : { + "extractionState" : "manual" + }, + "Background Refresh Type" : { + "extractionState" : "manual" + }, + "Basal / BG Scale" : { + "extractionState" : "manual" + }, + "Below is the exact JSON object that LoopFollow would send to lf.bjorkert.se right now." : {}, + "Below: %@" : {}, + "Bolus" : { + "extractionState" : "manual" + }, + "Bolus Filter" : { + "extractionState" : "manual" + }, + "Bolus Increment" : {}, + "Bolus Increment: %.3f U" : {}, + "Build Information" : { + "extractionState" : "manual" + }, + "Built-in" : {}, + "Bundle ID: %@" : {}, + "CHARGING" : {}, + "COB" : {}, + "CV" : {}, + "Calculated %@ minute%@ ago" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Calculated %1$@ minute%2$@ ago" + } + } + } + }, + "Calendar" : { + "extractionState" : "manual" + }, + "Calendar Text" : { + "extractionState" : "manual" + }, + "Calendar access denied" : {}, + "Cancel" : {}, + "Cancel Active Override" : {}, + "Cancel Override" : {}, + "Cancel Temp Target" : {}, + "Carbs" : { + "extractionState" : "manual" + }, + "Category" : { + "extractionState" : "manual" + }, + "Character Limit Reached" : { + "extractionState" : "manual" + }, + "Checking Nightscout profile history…" : {}, + "Choose Tone" : { + "extractionState" : "manual" + }, + "Close" : { + "extractionState" : "manual" + }, + "Color" : {}, + "Color BG Text" : { + "extractionState" : "manual" + }, + "Color Mode" : { + "extractionState" : "manual" + }, + "Color Options" : {}, + "Coloring enabled" : { + "extractionState" : "manual" + }, + "Command Sent" : {}, + "Configure Loop APNS" : {}, + "Confirm" : {}, + "Confirm & Import" : { + "extractionState" : "manual" + }, + "Confirm Bolus" : {}, + "Confirm Cancellation" : {}, + "Confirm Carbs" : {}, + "Confirm Command" : {}, + "Confirm Insulin" : {}, + "Confirm Meal" : {}, + "Connected" : {}, + "Consumption Time" : { + "extractionState" : "manual" + }, + "Contact Integration" : {}, + "Continue" : {}, + "Copy JSON" : { + "extractionState" : "manual" + }, + "Create Read-Only Token" : {}, + "Create one for me" : {}, + "Creating read-only token…" : {}, + "Current OTP Code" : {}, + "Current TOTP Code:" : {}, + "Current Target" : {}, + "Current Token:" : {}, + "Current URL:" : {}, + "Custom" : {}, + "Custom sounds stay on this device and aren't included in settings export." : {}, + "Cutoffs in %@" : {}, + "Data Availability" : {}, + "Day / Night" : {}, + "Day / Night Schedule" : {}, + "Day starts" : { + "extractionState" : "manual" + }, + "Debug / Info" : {}, + "Debug Log Level" : { + "extractionState" : "manual" + }, + "Default Snooze:" : {}, + "Delay Between Sounds" : {}, + "Delete" : { + "extractionState" : "manual" + }, + "Delete Alarm" : { + "extractionState" : "manual" + }, + "Delete this alarm?" : { + "extractionState" : "manual" + }, + "Delta" : {}, + "Description" : {}, + "Developer Team ID" : {}, + "Device Data" : {}, + "Device Token: %@" : {}, + "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." + } + } + } + }, + "Dexcom Settings" : {}, + "Dexcom Share" : {}, + "Diagnostics" : { + "extractionState" : "manual" + }, + "Diagnostics unavailable: %@" : {}, + "Disconnect" : {}, + "Disconnected for %@" : {}, + "Dismiss" : { + "extractionState" : "manual" + }, + "Display" : { + "extractionState" : "manual" + }, + "Display App Badge" : { + "extractionState" : "manual" + }, + "Display Dots" : { + "extractionState" : "manual" + }, + "Display Lines" : { + "extractionState" : "manual" + }, + "Display Small Graph" : { + "extractionState" : "manual" + }, + "Display Stats" : { + "extractionState" : "manual" + }, + "Do you want to activate the override '%@'?" : {}, + "Done" : { + "extractionState" : "manual" + }, + "Download Prediction" : { + "extractionState" : "manual" + }, + "Download Treatments" : { + "extractionState" : "manual" + }, + "Drag to reorder" : {}, + "Drag to reorder. Tap a row to set its visibility and colors." : {}, + "Duration" : {}, + "Duration: %@" : {}, + "Duration: %@ minutes" : {}, + "Dynamic mode colors text based on BG range: Green (in range), Yellow (high), Red (low)" : {}, + "Enable Contact BG Updates" : { + "extractionState" : "manual" + }, + "Enable Live Activity" : { + "extractionState" : "manual" + }, + "Enable Notifications" : {}, + "Enable WebSocket" : { + "extractionState" : "manual" + }, + "Enable coloring" : { + "extractionState" : "manual" + }, + "Enable indefinitely" : { + "extractionState" : "manual" + }, + "Enable this alarm" : { + "extractionState" : "manual" + }, + "Enabled" : { + "extractionState" : "manual" + }, + "Enact" : {}, + "End Date" : {}, + "End now" : {}, + "Endpoint" : {}, + "English" : {}, + "Enter QR code URL or scan from Loop app" : { + "extractionState" : "manual" + }, + "Enter URL" : { + "extractionState" : "manual" + }, + "Enter User" : { + "extractionState" : "manual" + }, + "Enter Username" : { + "extractionState" : "manual" + }, + "Enter or paste your full Nightscout address. If the link already includes a token, we'll fill both in for you." : {}, + "Environment" : {}, + "Error" : {}, + "Eventual" : {}, + "Existing Temp Target" : {}, + "Expected bg delay: %@" : {}, + "Expected: 1 CGM reading every 5 minutes" : {}, + "Export" : { + "extractionState" : "manual" + }, + "Export %@" : {}, + "Export Settings To QR Code" : { + "extractionState" : "manual" + }, + "F%@ P%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "F%1$@ P%2$@" + } + } + } + }, + "FALLING BG" : {}, + "Failed to generate QR code" : {}, + "Features" : { + "extractionState" : "manual" + }, + "Filter" : { + "extractionState" : "manual" + }, + "Finish" : {}, + "Food Type" : {}, + "Force portrait mode" : { + "extractionState" : "manual" + }, + "French" : {}, + "Frequency" : {}, + "GMI" : {}, + "GRI (Glucose Risk Index)" : {}, + "General" : {}, + "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." : {}, + "Glucose" : { + "extractionState" : "manual" + }, + "Glucose Unit" : { + "extractionState" : "manual" + }, + "Glucose value" : {}, + "Glycemic Metrics" : {}, + "Graph Basal" : { + "extractionState" : "manual" + }, + "Graph Bolus" : { + "extractionState" : "manual" + }, + "Graph Carbs" : { + "extractionState" : "manual" + }, + "Graph Display" : { + "extractionState" : "manual" + }, + "Graph Other Treatments" : { + "extractionState" : "manual" + }, + "Grid Slot - CarPlay / Watch" : {}, + "Grid Slots - Live Activity" : {}, + "Guardrails" : {}, + "Help us help you" : {}, + "Help us help you!" : { + "extractionState" : "manual" + }, + "Hide Information Table" : {}, + "High" : { + "extractionState" : "manual" + }, + "History" : { + "extractionState" : "manual" + }, + "Home" : { + "extractionState" : "manual" + }, + "Hours" : { + "extractionState" : "manual" + }, + "Hyperglycemia Component (%)" : {}, + "Hypoglycemia Component (%)" : {}, + "I have a token" : {}, + "IOB" : {}, + "Import Settings" : {}, + "Import Settings from QR Code" : {}, + "Import Sound…" : { + "extractionState" : "manual" + }, + "Import/Export Settings" : {}, + "In Menu" : {}, + "Inactive" : {}, + "Include Delta in" : { + "extractionState" : "manual" + }, + "Include IOB in" : { + "extractionState" : "manual" + }, + "Include Trend in" : { + "extractionState" : "manual" + }, + "Increment" : {}, + "Information Display Settings" : {}, + "Insulin" : { + "extractionState" : "manual" + }, + "Insulin Totals" : {}, + "Insulin: %@%%" : {}, + "Invalid QR code URL" : {}, + "Italian" : {}, + "Keep Screen Active" : { + "extractionState" : "manual" + }, + "Key ID must be exactly 10 uppercase letters or digits." : {}, + "Language" : { + "extractionState" : "manual" + }, + "Last connection: %@" : {}, + "Line 1" : { + "extractionState" : "manual" + }, + "Line 2" : { + "extractionState" : "manual" + }, + "Live Activity" : {}, + "Live Activity updates require APNs credentials. Configure them in Settings → APN." : {}, + "Load More" : {}, + "Loading data..." : { + "extractionState" : "manual" + }, + "Loading override presets..." : {}, + "Loading stats..." : {}, + "Logging" : { + "extractionState" : "manual" + }, + "Logging Options" : {}, + "Loop APNS Configuration" : {}, + "Loop APNS Not Configured" : {}, + "LoopFollow APNs Credentials" : {}, + "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." : {}, + "LoopFollow/Helpers/Telemetry.swift on GitHub." : {}, + "Low" : { + "extractionState" : "manual" + }, + "Max" : {}, + "Max %@ alarms at a time" : {}, + "Max Bolus" : {}, + "Max Carbs" : {}, + "Max Fat" : {}, + "Max Protein" : {}, + "Maximum" : {}, + "Meal" : { + "extractionState" : "manual" + }, + "Meal Data" : {}, + "Meal Settings" : {}, + "Meal with Bolus" : { + "extractionState" : "manual" + }, + "Meal with Fat/Protein" : { + "extractionState" : "manual" + }, + "Menu" : {}, + "Metric" : { + "extractionState" : "manual" + }, + "Min" : {}, + "Minimum" : {}, + "Minutes" : { + "extractionState" : "manual" + }, + "Multiple devices uploading profiles" : {}, + "Name" : {}, + "New Target: %@ %@\nDuration: %@ minutes" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "New Target: %1$@ %2$@\nDuration: %3$@ minutes" + } + } + } + }, + "Night starts" : { + "extractionState" : "manual" + }, + "Nightscout URL" : {}, + "No Results" : {}, + "No alarms match “%@”." : {}, + "No background refresh. Alarms and updates will not work unless the app is open in the foreground." : {}, + "No data available" : {}, + "No devices found yet. They'll appear here when discovered." : {}, + "No issues detected" : {}, + "No matches for “%@”." : {}, + "No override presets found. Configure presets in your Loop app." : {}, + "No overrides available." : {}, + "No recent treatments" : {}, + "No thanks" : {}, + "Not now" : {}, + "Note: The meal will be scheduled, but the bolus is enacted immediately." : {}, + "OK" : {}, + "Off" : {}, + "Old Calculation Warning" : {}, + "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." : {}, + "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." : {}, + "Opt out" : {}, + "Output Unit" : { + "extractionState" : "manual" + }, + "Outside US" : {}, + "Override" : {}, + "Override system volume" : { + "extractionState" : "manual" + }, + "Overrides" : { + "extractionState" : "manual" + }, + "Overriding the system volume lets alarms be heard even when your phone is silenced or in a Focus mode." : {}, + "Password" : {}, + "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." : {}, + "Paste the full .p8 contents — must include the BEGIN PRIVATE KEY and END PRIVATE KEY lines." : {}, + "Percentage: %@%%" : {}, + "Persistent Notification" : { + "extractionState" : "manual" + }, + "Pick when your day period begins and when your night period begins. " : { + "extractionState" : "manual" + }, + "Play sound during calls" : { + "extractionState" : "manual" + }, + "Please configure Loop APNS settings in Remote Settings to use APNS commands." : {}, + "Please enter your Nightscout URL in Settings." : {}, + "Please select a Remote Type in Settings." : {}, + "Please wait..." : { + "extractionState" : "manual" + }, + "Prediction" : {}, + "Prediction Style" : { + "extractionState" : "manual" + }, + "Preset Name" : { + "extractionState" : "manual" + }, + "Preset: %@" : {}, + "Presets" : {}, + "Privacy" : { + "extractionState" : "manual" + }, + "Proactive Low" : { + "extractionState" : "manual" + }, + "Production" : { + "extractionState" : "manual" + }, + "Production is used for browser builders and should be switched off for Xcode builders" : {}, + "QR Code URL" : {}, + "RISING BG" : {}, + "RSSI: %@ dBm" : {}, + "Range" : {}, + "Range Mode" : { + "extractionState" : "manual" + }, + "Real-time Updates" : {}, + "Recommended Bolus" : {}, + "Reconnecting..." : {}, + "Refresh" : { + "extractionState" : "manual" + }, + "Remote" : { + "extractionState" : "manual" + }, + "Remote Settings" : { + "extractionState" : "manual" + }, + "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." : {}, + "Requires a RileyLink-compatible device within Bluetooth range. Provides updates once per minute and uses less battery than the silent tune method." : {}, + "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." : {}, + "Review setup anyway" : {}, + "Review the settings that will be imported" : {}, + "Right slot" : { + "extractionState" : "manual" + }, + "Run diagnostics" : {}, + "Run diagnostics again" : {}, + "SNOOZE" : {}, + "Save" : { + "extractionState" : "manual" + }, + "Save BG to Calendar" : { + "extractionState" : "manual" + }, + "Save Preset" : {}, + "Save as Preset" : {}, + "Scan QR Code from Loop App" : {}, + "Scan QR Code to Import Settings" : {}, + "Scan this QR code with another LoopFollow app to import %@" : {}, + "Scanned Token:" : {}, + "Scanned URL:" : {}, + "Scanning for %@..." : {}, + "Schedule" : {}, + "Schedule for later" : { + "extractionState" : "manual" + }, + "Search" : { + "extractionState" : "manual" + }, + "Search alarms" : { + "extractionState" : "manual" + }, + "Secret" : {}, + "Security" : {}, + "See exactly what's sent" : { + "extractionState" : "manual" + }, + "Select Alarms to Export" : { + "extractionState" : "manual" + }, + "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." : {}, + "Selected Alarms: %@" : {}, + "Selected Device" : {}, + "Send" : {}, + "Send %@ units of insulin?" : {}, + "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$@?" + } + } + } + }, + "Send Bolus" : {}, + "Send Carbs" : {}, + "Send Insulin" : {}, + "Send Meal" : {}, + "Send anonymous usage stats" : { + "extractionState" : "manual" + }, + "Sending Bolus..." : {}, + "Sending Meal Data..." : {}, + "Sending..." : {}, + "Sensitivity" : {}, + "Server" : { + "extractionState" : "manual" + }, + "Set Up Units" : { + "extractionState" : "manual" + }, + "Settings" : { + "extractionState" : "manual" + }, + "Settings imported — you're connected." : { + "extractionState" : "manual" + }, + "Settings to Import" : {}, + "Share" : { + "extractionState" : "manual" + }, + "Share Logs" : { + "extractionState" : "manual" + }, + "Shared Secret" : {}, + "Show Carb Absorption" : { + "extractionState" : "manual" + }, + "Show Carb/Bolus Values" : { + "extractionState" : "manual" + }, + "Show DIA Lines" : { + "extractionState" : "manual" + }, + "Show Delta" : { + "extractionState" : "manual" + }, + "Show Display Name" : { + "extractionState" : "manual" + }, + "Show IOB" : { + "extractionState" : "manual" + }, + "Show Midnight Lines" : { + "extractionState" : "manual" + }, + "Show Prior Day Time Lines" : { + "extractionState" : "manual" + }, + "Show Trend" : { + "extractionState" : "manual" + }, + "Show Yesterday's BG" : { + "extractionState" : "manual" + }, + "Show in Info Display" : { + "extractionState" : "manual" + }, + "Show −30 min Line" : { + "extractionState" : "manual" + }, + "Show −90 min Line" : { + "extractionState" : "manual" + }, + "Site URL" : {}, + "Skip" : {}, + "Skip if BG is falling" : { + "extractionState" : "manual" + }, + "Skip if BG is rising" : { + "extractionState" : "manual" + }, + "Skip while charging" : { + "extractionState" : "manual" + }, + "Slovak" : {}, + "Small Graph" : { + "extractionState" : "manual" + }, + "Snooze & Mute Options" : {}, + "Snooze Date" : { + "extractionState" : "manual" + }, + "Snooze Time" : { + "extractionState" : "manual" + }, + "Snooze all · 1h" : {}, + "Snooze for" : {}, + "Snoozed" : {}, + "Snoozed until %@" : {}, + "Snoozer emoji" : { + "extractionState" : "manual" + }, + "Sound" : {}, + "Source" : {}, + "Speak BG" : { + "extractionState" : "manual" + }, + "Start Date" : {}, + "Statistics" : {}, + "Status" : {}, + "Stay informed" : {}, + "Stay silent while BG is falling. The alert only sounds " : { + "extractionState" : "manual" + }, + "Stay silent while BG is rising. The alert only sounds " : { + "extractionState" : "manual" + }, + "Stay silent while the phone is charging. Requires the " : { + "extractionState" : "manual" + }, + "Std Dev" : {}, + "Std Dev — Standard Deviation, how much glucose swings around the average. CV — Coefficient of Variation, that swing relative to the average (Std Dev ÷ mean)." : {}, + "Success" : {}, + "Support & Community" : { + "extractionState" : "manual" + }, + "Swedish" : {}, + "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." + } + } + } + }, + "TITR" : {}, + "TOTP Code Already Used" : {}, + "TOTP Code: Invalid QR code URL" : {}, + "Tab %@" : {}, + "Tab 5" : {}, + "Tab Order" : {}, + "Tabs" : { + "extractionState" : "manual" + }, + "Tap to load older treatments" : {}, + "Target" : {}, + "Target Range" : {}, + "Target: %@ %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Target: %1$@ %2$@" + } + } + } + }, + "Target: %@-%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Target: %1$@-%2$@" + } + } + } + }, + "Team ID: %@" : {}, + "Temporary Target" : {}, + "Text Color" : { + "extractionState" : "manual" + }, + "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?" : {}, + "That's your API secret — create a read-only token" : {}, + "The order of Fat and Protein inputs has changed." : {}, + "The top 4 items appear in the tab bar. The Menu can always open every feature." : {}, + "This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command." : {}, + "This will overwrite your current settings" : {}, + "Time" : {}, + "Time Remaining" : {}, + "Time Zone" : { + "extractionState" : "manual" + }, + "Time Zone Override" : { + "extractionState" : "manual" + }, + "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." : {}, + "Token" : {}, + "Tone" : {}, + "Treatments" : {}, + "Treatments on Small Graph" : { + "extractionState" : "manual" + }, + "Trend" : {}, + "Trio Remote Control Settings" : {}, + "Trio Remote Control is only supported for 'Trio'" : {}, + "Type or paste a token, or a full Nightscout URL that includes a token." : {}, + "U" : { + "comment" : "The short unit display string for international units of insulin" + }, + "URL" : {}, + "URL/Token Update" : {}, + "URL/Token Validation" : {}, + "US" : {}, + "Units and Metrics" : { + "extractionState" : "manual" + }, + "Unknown Alert" : {}, + "Use Anyway" : {}, + "Use the Send anonymous usage stats toggle above. Turning it off is immediate and persistent." : {}, + "User" : {}, + "User Information" : {}, + "Username" : {}, + "Validation Error" : {}, + "Variability" : {}, + "View Log" : { + "extractionState" : "manual" + }, + "WARNING: New treatments may have occurred since the last recommended bolus was calculated %@ ago." : {}, + "Warning" : {}, + "We'll ask iOS for permission next. You can change this any time in the Settings app." : {}, + "Welcome to LoopFollow" : {}, + "What is sent" : {}, + "What stays on your device" : {}, + "What's sent" : { + "extractionState" : "manual" + }, + "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." : {}, + "Yes, send anonymous stats" : {}, + "You can change this any time in Settings → General → Diagnostics." : {}, + "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." : {}, + "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." : {}, + "You're all set" : {}, + "You're ready to go. Adjust everything later from the Menu — and when you have a moment, these are worth a look:" : {}, + "Your API secret is used once to create a read-only access token and is never stored." : {}, + "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." : {}, + "g" : { + "comment" : "The short unit display string for grams" + }, + "mg/dL" : { + "comment" : "The short unit display string for milligrams of glucose per decilter" + }, + "minutes" : {}, + "mmol/L" : { + "comment" : "The short unit display string for millimoles of glucose per liter" + }, + "mmol/mol" : {}, + "“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." : {}, + "“Snooze All” disables every alarm. “Mute All” silences phone sounds but still vibrates and shows iOS notifications." : {}, + "••••••••" : {}, + "− 30m" : {}, + "⚠️ Please enable at least one limit." : {}, + "🌮" : {}, + "🍕" : {}, + "🍭" : {}, + "🍽️" : {} + }, + "version" : "1.0" +} From cce8cce5347411010c37860181322c36b5e53fa6 Mon Sep 17 00:00:00 2001 From: Ramazan TUNCER Date: Sun, 27 Sep 2026 01:39:26 +0300 Subject: [PATCH 02/11] Add String Catalog coverage tests Fails until every translatable key has a Turkish value; also checks printf placeholders and stale keys. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 38286cfcb1ae0da53444786b2cc326e75318f23b) --- Tests/LocalizationCatalogTests.swift | 102 +++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Tests/LocalizationCatalogTests.swift diff --git a/Tests/LocalizationCatalogTests.swift b/Tests/LocalizationCatalogTests.swift new file mode 100644 index 000000000..cbb656648 --- /dev/null +++ b/Tests/LocalizationCatalogTests.swift @@ -0,0 +1,102 @@ +// 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. + 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 + } + let reordered = source.map(\.1) != target.map(\.1) + if source.count > 1, reordered, target.contains(where: { $0.0 == nil }) { + problems.append("[\(language)] reordered without 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"))) + } +} From d6efdc170532379e7c979a156d96ebf8f9a0aeb6 Mon Sep 17 00:00:00 2001 From: Ramazan TUNCER Date: Sun, 27 Sep 2026 01:41:09 +0300 Subject: [PATCH 03/11] Show alarm type and schedule names through localized displayName AlarmType.rawValue and the day/night option raw values are persisted, so they must stay English. Add AlarmType.displayName and make DayNightDisplayable.displayName an explicit localized switch; route the tile, editor title, list and add-sheet search, notification title and default alarm name through them. Tests pin the raw values. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit ca2276f80102049c02f5e88cc3b3829725e0c3dc) --- LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift | 3 +- LoopFollow/Alarm/AddAlarm/AlarmTile.swift | 2 +- LoopFollow/Alarm/Alarm.swift | 12 +- .../Alarm/AlarmEditing/AlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmListView.swift | 1 + LoopFollow/Alarm/AlarmType/AlarmType.swift | 33 ++++++ LoopFollow/Resources/Localizable.xcstrings | 112 ++++++++++++++++++ Tests/LocalizationDisplayNameTests.swift | 67 +++++++++++ 8 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 Tests/LocalizationDisplayNameTests.swift 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..3b9c760a9 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: 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/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/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/Resources/Localizable.xcstrings b/LoopFollow/Resources/Localizable.xcstrings index 257d29bc2..aa7f385cd 100644 --- a/LoopFollow/Resources/Localizable.xcstrings +++ b/LoopFollow/Resources/Localizable.xcstrings @@ -136,6 +136,10 @@ "Basal / BG Scale" : { "extractionState" : "manual" }, + "Battery Drop" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Below is the exact JSON object that LoopFollow would send to lf.bjorkert.se right now." : {}, "Below: %@" : {}, "Bolus" : { @@ -153,6 +157,10 @@ "Bundle ID: %@" : {}, "CHARGING" : {}, "COB" : {}, + "COB Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "CV" : {}, "Calculated %@ minute%@ ago" : { "localizations" : { @@ -235,6 +243,14 @@ "Custom sounds stay on this device and aren't included in settings export." : {}, "Cutoffs in %@" : {}, "Data Availability" : {}, + "Day" : { + "comment" : "Alarm schedule option: daytime only", + "extractionState" : "manual" + }, + "Day & Night" : { + "comment" : "Alarm schedule option: at any time of day", + "extractionState" : "manual" + }, "Day / Night" : {}, "Day / Night Schedule" : {}, "Day starts" : { @@ -380,6 +396,14 @@ }, "FALLING BG" : {}, "Failed to generate QR code" : {}, + "Fast Drop Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, + "Fast Rise Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Features" : { "extractionState" : "manual" }, @@ -393,6 +417,10 @@ }, "French" : {}, "Frequency" : {}, + "Future Carbs Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "GMI" : {}, "GRI (Glucose Risk Index)" : {}, "General" : {}, @@ -431,6 +459,10 @@ "High" : { "extractionState" : "manual" }, + "High BG Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "History" : { "extractionState" : "manual" }, @@ -444,6 +476,10 @@ "Hypoglycemia Component (%)" : {}, "I have a token" : {}, "IOB" : {}, + "IOB Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Import Settings" : {}, "Import Settings from QR Code" : {}, "Import Sound…" : { @@ -501,9 +537,21 @@ "LoopFollow APNs Credentials" : {}, "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." : {}, "LoopFollow/Helpers/Telemetry.swift on GitHub." : {}, + "Looping app expiration" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Low" : { "extractionState" : "manual" }, + "Low BG Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, + "Low Battery" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Max" : {}, "Max %@ alarms at a time" : {}, "Max Bolus" : {}, @@ -531,8 +579,20 @@ "Minutes" : { "extractionState" : "manual" }, + "Missed Bolus Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, + "Missed Reading Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Multiple devices uploading profiles" : {}, "Name" : {}, + "Never" : { + "comment" : "Alarm schedule option: never", + "extractionState" : "manual" + }, "New Target: %@ %@\nDuration: %@ minutes" : { "localizations" : { "en" : { @@ -543,9 +603,17 @@ } } }, + "Night" : { + "comment" : "Alarm schedule option: nighttime only", + "extractionState" : "manual" + }, "Night starts" : { "extractionState" : "manual" }, + "Nightscout Database Size" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Nightscout URL" : {}, "No Results" : {}, "No alarms match “%@”." : {}, @@ -558,6 +626,10 @@ "No overrides available." : {}, "No recent treatments" : {}, "No thanks" : {}, + "Not Looping Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Not now" : {}, "Note: The meal will be scheduled, but the bolus is enacted immediately." : {}, "OK" : {}, @@ -571,6 +643,14 @@ }, "Outside US" : {}, "Override" : {}, + "Override Ended" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, + "Override Started" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Override system volume" : { "extractionState" : "manual" }, @@ -616,6 +696,18 @@ "extractionState" : "manual" }, "Production is used for browser builders and should be switched off for Xcode builders" : {}, + "Pump Battery Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, + "Pump Change Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, + "Pump Insulin Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "QR Code URL" : {}, "RISING BG" : {}, "RSSI: %@ dBm" : {}, @@ -624,6 +716,10 @@ "extractionState" : "manual" }, "Real-time Updates" : {}, + "Rec. Bolus" : { + "comment" : "Alarm type name: recommended bolus", + "extractionState" : "manual" + }, "Recommended Bolus" : {}, "Reconnecting..." : {}, "Refresh" : { @@ -704,6 +800,10 @@ "Sending Meal Data..." : {}, "Sending..." : {}, "Sensitivity" : {}, + "Sensor Change Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Server" : { "extractionState" : "manual" }, @@ -861,6 +961,18 @@ } }, "Team ID: %@" : {}, + "Temp Target Ended" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, + "Temp Target Started" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, + "Temporary Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual" + }, "Temporary Target" : {}, "Text Color" : { "extractionState" : "manual" diff --git a/Tests/LocalizationDisplayNameTests.swift b/Tests/LocalizationDisplayNameTests.swift new file mode 100644 index 000000000..47fe0355c --- /dev/null +++ b/Tests/LocalizationDisplayNameTests.swift @@ -0,0 +1,67 @@ +// 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"]) + } + + @Test("AlarmType.displayName falls back to the English raw value in the test host language") + func alarmTypeDisplayNameEnglish() { + for type in AlarmType.allCases { + #expect(type.displayName == type.rawValue) + } + } + + @Test("day/night displayName keeps the English wording") + func dayNightDisplayNameEnglish() { + #expect(PlaySoundOption.always.displayName == "Day & Night") + #expect(PlaySoundOption.day.displayName == "Day") + #expect(PlaySoundOption.night.displayName == "Night") + #expect(PlaySoundOption.never.displayName == "Never") + #expect(ActiveOption.always.displayName == "Day & Night") + } +} From bd41ba68c691afbf90ac6054c5d07e141484aa39 Mon Sep 17 00:00:00 2001 From: Ramazan TUNCER Date: Sun, 27 Sep 2026 01:55:49 +0300 Subject: [PATCH 04/11] Add Turkish translations for SwiftUI strings Fills the tr locale of Localizable.xcstrings for every extracted key, marks units, symbols and product names as do-not-translate, and adds two small scripts for listing and applying translations from TSV. Two call sites are adjusted so they can be localized at all: the "minute(s) ago" plural suffix becomes a plural variation on a single "%lld minutes ago" key, and three alarm-editor footers that joined two literals with "+" (which selects the non-localized Text overload) are single literals again. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 0f417a115c1b09b3444112935a19b5ee8185b231) --- .../Editors/HighBgAlarmEditor.swift | 3 +- .../Editors/LowBgAlarmEditor.swift | 3 +- .../Editors/PhoneBatteryAlarmEditor.swift | 3 +- .../Remote/LoopAPNS/LoopAPNSBolusView.swift | 2 +- LoopFollow/Remote/TRC/BolusView.swift | 2 +- LoopFollow/Resources/Localizable.xcstrings | 6021 ++++++++++++++--- Scripts/localization/apply_translations.py | 57 + Scripts/localization/missing_translations.py | 39 + Tests/LocalizationCatalogTests.swift | 3 +- Tests/LocalizationDisplayNameTests.swift | 39 +- 10 files changed, 5351 insertions(+), 821 deletions(-) create mode 100755 Scripts/localization/apply_translations.py create mode 100755 Scripts/localization/missing_translations.py diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 1adf217aa..9f4e1c374 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -36,8 +36,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/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index a21db2239..d80953b02 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -45,8 +45,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/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/Remote/LoopAPNS/LoopAPNSBolusView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift index 09222270c..0a2a2848a 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) } diff --git a/LoopFollow/Remote/TRC/BolusView.swift b/LoopFollow/Remote/TRC/BolusView.swift index e41ab89ce..5e5795eaa 100644 --- a/LoopFollow/Remote/TRC/BolusView.swift +++ b/LoopFollow/Remote/TRC/BolusView.swift @@ -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) } diff --git a/LoopFollow/Resources/Localizable.xcstrings b/LoopFollow/Resources/Localizable.xcstrings index aa7f385cd..808e2ccda 100644 --- a/LoopFollow/Resources/Localizable.xcstrings +++ b/LoopFollow/Resources/Localizable.xcstrings @@ -1,11 +1,21 @@ { "sourceLanguage" : "en", "strings" : { - " U" : {}, - "#WeAreNotWaitingAnymore" : {}, - "#WeAreWaitingForData" : {}, - "%" : {}, - "%@" : {}, + " U" : { + "shouldTranslate" : false + }, + "#WeAreNotWaitingAnymore" : { + "shouldTranslate" : false + }, + "#WeAreWaitingForData" : { + "shouldTranslate" : false + }, + "%" : { + "shouldTranslate" : false + }, + "%@" : { + "shouldTranslate" : false + }, "%@ %@ → %@" : { "localizations" : { "en" : { @@ -14,7 +24,8 @@ "value" : "%1$@ %2$@ → %3$@" } } - } + }, + "shouldTranslate" : false }, "%@ %@" : { "localizations" : { @@ -24,7 +35,8 @@ "value" : "%1$@ %2$@" } } - } + }, + "shouldTranslate" : false }, "%@ - %@" : { "localizations" : { @@ -34,11 +46,39 @@ "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" + } + } } }, - "%@ gaps" : {}, - "%@ hr" : {}, - "%@ min" : {}, "%@ of %@ expected readings" : { "localizations" : { "en" : { @@ -46,1022 +86,5399 @@ "state" : "new", "value" : "%1$@ of %2$@ expected readings" } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "beklenen %2$@ okumanın %1$@ tanesi" + } } } }, - "%@ sec" : {}, - "%@%%" : {}, - "%@U" : {}, - "%@g" : {}, - "(" : { - "extractionState" : "manual" - }, - "(%@ days)" : {}, - "+ 30m" : {}, - "---" : {}, - "...because the data could not be loaded" : {}, - "00" : {}, - "15m" : {}, - "24h" : {}, - "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." : {}, - "API Secret" : {}, - "APN" : { - "extractionState" : "manual" - }, - "APNS Environment: %@" : {}, - "APNS Key" : {}, - "APNS Key ID" : {}, - "APNS warning: importing APNS settings will overwrite your existing APNS Key ID and APNS Key." : {}, - "APNs credentials are missing or invalid — Live Activity updates will not work. Open Settings → APN to fix." : {}, - "Above: %@" : {}, - "Absorption Time" : {}, - "Access Token" : {}, - "Activate Override" : {}, - "Active" : {}, - "Active During" : {}, - "Active Override" : {}, - "Add Alarm" : { - "extractionState" : "manual" - }, - "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." : {}, - "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." : {}, - "Adding this alarm would exceed the QR code character limit. Please remove some alarms first." : {}, - "Additional Information" : {}, - "Adjust the background refresh type." : {}, - "Advanced Settings" : {}, - "Alarm Name" : { - "extractionState" : "manual" - }, - "Alarm Settings" : {}, - "Alarm not found" : {}, - "Alarms can behave differently during the day and at night." : {}, - "Alert Sound" : {}, - "All" : {}, - "All Alerts Snoozed" : { - "extractionState" : "manual" + "%@ sec" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ sn" + } + } + } }, - "All Sounds Muted" : { - "extractionState" : "manual" + "%@%%" : { + "shouldTranslate" : false }, - "All alerts snoozed" : {}, - "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." : {}, - "Always" : { - "extractionState" : "manual" + "%@U" : { + "shouldTranslate" : false }, - "Ambulatory Glucose Profile (AGP)" : {}, - "App Settings" : { - "extractionState" : "manual" + "%@g" : { + "shouldTranslate" : false }, - "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." : {}, - "Appearance" : { - "extractionState" : "manual" + "(" : { + "extractionState" : "manual", + "shouldTranslate" : false }, - "Are you sure you want to cancel the active override?" : {}, - "Are you sure you want to cancel the existing temp target?" : {}, - "Are you sure you want to send %@ U?" : {}, - "Available Overrides" : {}, - "Available Variables" : { - "extractionState" : "manual" + "(%@ days)" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "(%@ gün)" + } + } + } }, - "BG Update Delay (Sec): %@" : {}, - "Back" : { - "extractionState" : "manual" + "+ 30m" : { + "shouldTranslate" : false }, - "Background Color" : { - "extractionState" : "manual" + "---" : { + "shouldTranslate" : false }, - "Background Refresh Type" : { - "extractionState" : "manual" + "...because the data could not be loaded" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "...çünkü veriler yüklenemedi" + } + } + } }, - "Basal / BG Scale" : { - "extractionState" : "manual" + "00" : { + "shouldTranslate" : false }, - "Battery Drop" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "15m" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "15dk" + } + } + } }, - "Below is the exact JSON object that LoopFollow would send to lf.bjorkert.se right now." : {}, - "Below: %@" : {}, - "Bolus" : { - "extractionState" : "manual" + "24h" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "24sa" + } + } + } }, - "Bolus Filter" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Bolus Increment" : {}, - "Bolus Increment: %.3f U" : {}, - "Build Information" : { - "extractionState" : "manual" + "API Secret" : { + "shouldTranslate" : false }, - "Built-in" : {}, - "Bundle ID: %@" : {}, - "CHARGING" : {}, - "COB" : {}, - "COB Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "APN" : { + "extractionState" : "manual", + "shouldTranslate" : false }, - "CV" : {}, - "Calculated %@ minute%@ ago" : { + "APNS Environment: %@" : { "localizations" : { - "en" : { + "tr" : { "stringUnit" : { - "state" : "new", - "value" : "Calculated %1$@ minute%2$@ ago" + "state" : "translated", + "value" : "APNS Ortamı: %@" } } } }, - "Calendar" : { - "extractionState" : "manual" + "APNS Key" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNS Anahtarı" + } + } + } }, - "Calendar Text" : { - "extractionState" : "manual" + "APNS Key ID" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APNS Anahtar Kimliği" + } + } + } }, - "Calendar access denied" : {}, - "Cancel" : {}, - "Cancel Active Override" : {}, - "Cancel Override" : {}, - "Cancel Temp Target" : {}, - "Carbs" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Category" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Character Limit Reached" : { - "extractionState" : "manual" + "Above: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üst: %@" + } + } + } }, - "Checking Nightscout profile history…" : {}, - "Choose Tone" : { - "extractionState" : "manual" + "Absorption Time" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Emilim Süresi" + } + } + } }, - "Close" : { - "extractionState" : "manual" + "Access Token" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erişim Belirteci" + } + } + } }, - "Color" : {}, - "Color BG Text" : { - "extractionState" : "manual" + "Activate Override" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override'ı Etkinleştir" + } + } + } }, - "Color Mode" : { - "extractionState" : "manual" + "Active" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin" + } + } + } }, - "Color Options" : {}, - "Coloring enabled" : { - "extractionState" : "manual" + "Active During" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin Olduğu Zaman" + } + } + } }, - "Command Sent" : {}, - "Configure Loop APNS" : {}, - "Confirm" : {}, - "Confirm & Import" : { - "extractionState" : "manual" - }, - "Confirm Bolus" : {}, - "Confirm Cancellation" : {}, - "Confirm Carbs" : {}, - "Confirm Command" : {}, - "Confirm Insulin" : {}, - "Confirm Meal" : {}, - "Connected" : {}, - "Consumption Time" : { - "extractionState" : "manual" + "Active Override" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin Override" + } + } + } }, - "Contact Integration" : {}, - "Continue" : {}, - "Copy JSON" : { - "extractionState" : "manual" - }, - "Create Read-Only Token" : {}, - "Create one for me" : {}, - "Creating read-only token…" : {}, - "Current OTP Code" : {}, - "Current TOTP Code:" : {}, - "Current Target" : {}, - "Current Token:" : {}, - "Current URL:" : {}, - "Custom" : {}, - "Custom sounds stay on this device and aren't included in settings export." : {}, - "Cutoffs in %@" : {}, - "Data Availability" : {}, - "Day" : { - "comment" : "Alarm schedule option: daytime only", - "extractionState" : "manual" + "Add Alarm" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarm Ekle" + } + } + } }, - "Day & Night" : { - "comment" : "Alarm schedule option: at any time of day", - "extractionState" : "manual" + "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**." + } + } + } }, - "Day / Night" : {}, - "Day / Night Schedule" : {}, - "Day starts" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Debug / Info" : {}, - "Debug Log Level" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Default Snooze:" : {}, - "Delay Between Sounds" : {}, - "Delete" : { - "extractionState" : "manual" + "Additional Information" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ek Bilgiler" + } + } + } }, - "Delete Alarm" : { - "extractionState" : "manual" + "Adjust the background refresh type." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arka plan yenileme türünü ayarlayın." + } + } + } }, - "Delete this alarm?" : { - "extractionState" : "manual" + "Advanced Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gelişmiş Ayarlar" + } + } + } }, - "Delta" : {}, - "Description" : {}, - "Developer Team ID" : {}, - "Device Data" : {}, - "Device Token: %@" : {}, - "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." : { + "Alarm Name" : { + "extractionState" : "manual", "localizations" : { - "en" : { + "tr" : { "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." + "state" : "translated", + "value" : "Alarm Adı" } } } }, - "Dexcom Settings" : {}, - "Dexcom Share" : {}, - "Diagnostics" : { - "extractionState" : "manual" + "Alarm Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarm Ayarları" + } + } + } }, - "Diagnostics unavailable: %@" : {}, - "Disconnect" : {}, - "Disconnected for %@" : {}, - "Dismiss" : { - "extractionState" : "manual" + "Alarm not found" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarm bulunamadı" + } + } + } }, - "Display" : { - "extractionState" : "manual" + "Alarms can behave differently during the day and at night." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarmlar gündüz ve gece farklı davranabilir." + } + } + } }, - "Display App Badge" : { - "extractionState" : "manual" + "Alert Sound" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uyarı Sesi" + } + } + } }, - "Display Dots" : { - "extractionState" : "manual" + "All" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tümü" + } + } + } }, - "Display Lines" : { - "extractionState" : "manual" + "All Alerts Snoozed" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm Uyarılar Ertelendi" + } + } + } }, - "Display Small Graph" : { - "extractionState" : "manual" + "All Sounds Muted" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm Sesler Kapalı" + } + } + } }, - "Display Stats" : { - "extractionState" : "manual" + "All alerts snoozed" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm uyarılar ertelendi" + } + } + } }, - "Do you want to activate the override '%@'?" : {}, - "Done" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Download Prediction" : { - "extractionState" : "manual" + "Always" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Her Zaman" + } + } + } }, - "Download Treatments" : { - "extractionState" : "manual" - }, - "Drag to reorder" : {}, - "Drag to reorder. Tap a row to set its visibility and colors." : {}, - "Duration" : {}, - "Duration: %@" : {}, - "Duration: %@ minutes" : {}, - "Dynamic mode colors text based on BG range: Green (in range), Yellow (high), Red (low)" : {}, - "Enable Contact BG Updates" : { - "extractionState" : "manual" + "Ambulatory Glucose Profile (AGP)" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ambulatuvar Glukoz Profili (AGP)" + } + } + } }, - "Enable Live Activity" : { - "extractionState" : "manual" + "App Settings" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama Ayarları" + } + } + } }, - "Enable Notifications" : {}, - "Enable WebSocket" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Enable coloring" : { - "extractionState" : "manual" + "Appearance" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Görünüm" + } + } + } }, - "Enable indefinitely" : { - "extractionState" : "manual" + "Are you sure you want to cancel the active override?" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin override'ı iptal etmek istediğinizden emin misiniz?" + } + } + } }, - "Enable this alarm" : { - "extractionState" : "manual" + "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?" + } + } + } }, - "Enabled" : { - "extractionState" : "manual" + "Are you sure you want to send %@ U?" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ U göndermek istediğinizden emin misiniz?" + } + } + } }, - "Enact" : {}, - "End Date" : {}, - "End now" : {}, - "Endpoint" : {}, - "English" : {}, - "Enter QR code URL or scan from Loop app" : { - "extractionState" : "manual" + "Available Overrides" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanılabilir Override'lar" + } + } + } }, - "Enter URL" : { - "extractionState" : "manual" + "Available Variables" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanılabilir Değişkenler" + } + } + } }, - "Enter User" : { - "extractionState" : "manual" + "BG Update Delay (Sec): %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Güncelleme Gecikmesi (sn): %@" + } + } + } }, - "Enter Username" : { - "extractionState" : "manual" - }, - "Enter or paste your full Nightscout address. If the link already includes a token, we'll fill both in for you." : {}, - "Environment" : {}, - "Error" : {}, - "Eventual" : {}, - "Existing Temp Target" : {}, - "Expected bg delay: %@" : {}, - "Expected: 1 CGM reading every 5 minutes" : {}, - "Export" : { - "extractionState" : "manual" + "Back" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geri" + } + } + } }, - "Export %@" : {}, - "Export Settings To QR Code" : { - "extractionState" : "manual" + "Background Color" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arka Plan Rengi" + } + } + } }, - "F%@ P%@" : { + "Background Refresh Type" : { + "extractionState" : "manual", "localizations" : { - "en" : { + "tr" : { "stringUnit" : { - "state" : "new", - "value" : "F%1$@ P%2$@" + "state" : "translated", + "value" : "Arka Plan Yenileme Türü" } } } }, - "FALLING BG" : {}, - "Failed to generate QR code" : {}, - "Fast Drop Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "Basal / BG Scale" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazal / KŞ Ölçeği" + } + } + } }, - "Fast Rise Alert" : { + "Battery Drop" : { "comment" : "Alarm type name", - "extractionState" : "manual" - }, - "Features" : { - "extractionState" : "manual" - }, - "Filter" : { - "extractionState" : "manual" + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pil Düşüşü" + } + } + } }, - "Finish" : {}, - "Food Type" : {}, - "Force portrait mode" : { - "extractionState" : "manual" + "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." + } + } + } }, - "French" : {}, - "Frequency" : {}, - "Future Carbs Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "Below: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alt: %@" + } + } + } }, - "GMI" : {}, - "GRI (Glucose Risk Index)" : {}, - "General" : {}, - "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." : {}, - "Glucose" : { - "extractionState" : "manual" + "Bolus" : { + "extractionState" : "manual", + "shouldTranslate" : false }, - "Glucose Unit" : { - "extractionState" : "manual" + "Bolus Filter" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Filtresi" + } + } + } }, - "Glucose value" : {}, - "Glycemic Metrics" : {}, - "Graph Basal" : { - "extractionState" : "manual" + "Bolus Increment" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Artışı" + } + } + } }, - "Graph Bolus" : { - "extractionState" : "manual" + "Bolus Increment: %.3f U" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Artışı: %.3f U" + } + } + } }, - "Graph Carbs" : { - "extractionState" : "manual" + "Build Information" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Derleme Bilgileri" + } + } + } }, - "Graph Display" : { - "extractionState" : "manual" + "Built-in" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yerleşik" + } + } + } }, - "Graph Other Treatments" : { - "extractionState" : "manual" + "Bundle ID: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paket Kimliği: %@" + } + } + } }, - "Grid Slot - CarPlay / Watch" : {}, - "Grid Slots - Live Activity" : {}, - "Guardrails" : {}, - "Help us help you" : {}, - "Help us help you!" : { - "extractionState" : "manual" + "CHARGING" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ŞARJ OLUYOR" + } + } + } }, - "Hide Information Table" : {}, - "High" : { - "extractionState" : "manual" + "COB" : { + "shouldTranslate" : false }, - "High BG Alert" : { + "COB Alert" : { "comment" : "Alarm type name", - "extractionState" : "manual" - }, - "History" : { - "extractionState" : "manual" - }, - "Home" : { - "extractionState" : "manual" + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB Uyarısı" + } + } + } }, - "Hours" : { - "extractionState" : "manual" + "CV" : { + "shouldTranslate" : false }, - "Hyperglycemia Component (%)" : {}, - "Hypoglycemia Component (%)" : {}, - "I have a token" : {}, - "IOB" : {}, - "IOB Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "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ı" + } + } + } }, - "Import Settings" : {}, - "Import Settings from QR Code" : {}, - "Import Sound…" : { - "extractionState" : "manual" + "Calendar" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Takvim" + } + } + } }, - "Import/Export Settings" : {}, - "In Menu" : {}, - "Inactive" : {}, - "Include Delta in" : { - "extractionState" : "manual" + "Calendar Text" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Takvim Metni" + } + } + } }, - "Include IOB in" : { - "extractionState" : "manual" + "Calendar access denied" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Takvim erişimi reddedildi" + } + } + } }, - "Include Trend in" : { - "extractionState" : "manual" + "Cancel" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İptal" + } + } + } }, - "Increment" : {}, - "Information Display Settings" : {}, - "Insulin" : { - "extractionState" : "manual" + "Cancel Active Override" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin Override'ı İptal Et" + } + } + } }, - "Insulin Totals" : {}, - "Insulin: %@%%" : {}, - "Invalid QR code URL" : {}, - "Italian" : {}, - "Keep Screen Active" : { - "extractionState" : "manual" + "Cancel Override" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override'ı İptal Et" + } + } + } }, - "Key ID must be exactly 10 uppercase letters or digits." : {}, - "Language" : { - "extractionState" : "manual" + "Cancel Temp Target" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Hedefi İptal Et" + } + } + } }, - "Last connection: %@" : {}, - "Line 1" : { - "extractionState" : "manual" + "Carbs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat" + } + } + } }, - "Line 2" : { - "extractionState" : "manual" + "Category" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kategori" + } + } + } }, - "Live Activity" : {}, - "Live Activity updates require APNs credentials. Configure them in Settings → APN." : {}, - "Load More" : {}, - "Loading data..." : { - "extractionState" : "manual" + "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…" + } + } + } + }, + "Choose Tone" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ton Seç" + } + } + } + }, + "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" + } + } + } + }, + "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" + } + } + } + }, + "Connected" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlı" + } + } + } + }, + "Consumption Time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüketim Zamanı" + } + } + } + }, + "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" + } + } + } + }, + "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…" + } + } + } + }, + "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 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 (%@)" + } + } + } + }, + "Data Availability" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veri Kullanılabilirliği" + } + } + } + }, + "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ı" + } + } + } + }, + "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 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" : { + "shouldTranslate" : false + }, + "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 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 Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dexcom Ayarları" + } + } + } + }, + "Dexcom Share" : { + "shouldTranslate" : false + }, + "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 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." + } + } + } + }, + "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 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)" + } + } + } + }, + "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 indefinitely" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Süresiz etkinleştir" + } + } + } + }, + "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." + } + } + } + }, + "Environment" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ortam" + } + } + } + }, + "Error" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hata" + } + } + } + }, + "Eventual" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nihai" + } + } + } + }, + "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" + } + } + } + }, + "Failed to generate QR code" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR kodu oluşturulamadı" + } + } + } + }, + "Fast Drop Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı Düşüş Uyarısı" + } + } + } + }, + "Fast Rise Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hızlı Yükseliş Uyarısı" + } + } + } + }, + "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" + } + } + } + }, + "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" + } + } + } + }, + "Future Carbs Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gelecek Karbonhidrat Uyarısı" + } + } + } + }, + "GMI" : { + "shouldTranslate" : false + }, + "GRI (Glucose Risk Index)" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GRI (Glukoz Risk İndeksi)" + } + } + } + }, + "General" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Genel" + } + } + } + }, + "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 value" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glukoz değeri" + } + } + } + }, + "Glycemic Metrics" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Glisemik Ölçütler" + } + } + } + }, + "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 - Live Activity" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izgara Yuvaları - Canlı Etkinlik" + } + } + } + }, + "Guardrails" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güvenlik Sınırları" + } + } + } + }, + "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!" + } + } + } + }, + "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ı" + } + } + } + }, + "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" + } + } + } + }, + "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" + } + } + } + }, + "IOB" : { + "shouldTranslate" : false + }, + "IOB Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB Uyarısı" + } + } + } + }, + "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 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 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 Totals" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin Toplamları" + } + } + } + }, + "Insulin: %@%%" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İnsülin: %@%%" + } + } + } + }, + "Invalid QR code URL" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçersiz QR kodu URL'si" + } + } + } + }, + "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." + } + } + } + }, + "Language" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dil" + } + } + } + }, + "Last connection: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Son bağlantı: %@" + } + } + } + }, + "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" + } + } + } + }, + "Live Activity" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canlı Etkinlik" + } + } + } + }, + "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 override presets..." : {}, - "Loading stats..." : {}, "Logging" : { - "extractionState" : "manual" - }, - "Logging Options" : {}, - "Loop APNS Configuration" : {}, - "Loop APNS Not Configured" : {}, - "LoopFollow APNs Credentials" : {}, - "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." : {}, - "LoopFollow/Helpers/Telemetry.swift on GitHub." : {}, + "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" + } + } + } + }, + "Loop APNS Configuration" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop APNS Yapılandırması" + } + } + } + }, + "Loop APNS Not Configured" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop APNS Yapılandırılmamış" + } + } + } + }, + "LoopFollow APNs Credentials" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoopFollow APNs Kimlik Bilgileri" + } + } + } + }, + "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" + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop uygulaması süre sonu" + } + } + } + }, + "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 Battery" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Düşük Pil" + } + } + } + }, + "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 Protein" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maks Protein" + } + } + } + }, + "Maximum" : { + "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" : { + "shouldTranslate" : false + }, + "Minimum" : { + "shouldTranslate" : false + }, + "Minutes" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "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ı" + } + } + } + }, + "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" + } + } + } + }, + "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: %@ %@\nSüre: %@ 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 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" + } + } + } + }, + "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 data available" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Veri yok" + } + } + } + }, + "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 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" + } + } + } + }, + "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." + } + } + } + }, + "OK" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamam" + } + } + } + }, + "Off" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kapalı" + } + } + } + }, + "Old Calculation Warning" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eski Hesaplama Uyarısı" + } + } + } + }, + "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." + } + } + } + }, + "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ışı" + } + } + } + }, + "Override" : { + "shouldTranslate" : false + }, + "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" + } + } + } + }, + "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." + } + } + } + }, + "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." + } + } + } + }, + "Percentage: %@%%" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yüzde: %@%%" + } + } + } + }, + "Persistent Notification" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kalıcı Bildirim" + } + } + } + }, + "Pick when your day period begins and when your night period begins. " : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gündüz ve gece dönemlerinizin ne zaman başladığını seçin. " + } + } + } + }, + "Play sound during calls" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aramalar sırasında ses çal" + } + } + } + }, + "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 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..." + } + } + } + }, + "Prediction" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahmin" + } + } + } + }, + "Prediction Style" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahmin Stili" + } + } + } + }, + "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" : "Öngörülü Düşük" + } + } + } + }, + "Production" : { + "extractionState" : "manual", + "shouldTranslate" : false + }, + "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" + } + } + } + }, + "Pump Battery Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa Pili Uyarısı" + } + } + } + }, + "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ı" + } + } + } + }, + "QR Code URL" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR Kodu URL'si" + } + } + } + }, + "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" + } + } + } + }, + "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" + } + } + } + }, + "Reconnecting..." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yeniden bağlanıyor..." + } + } + } + }, + "Refresh" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yenile" + } + } + } + }, + "Remote" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzaktan" + } + } + } + }, + "Remote Settings" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uzaktan Ayarlar" + } + } + } + }, + "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." + } + } + } + }, + "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" + } + } + } + }, + "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" + } + } + } + }, + "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 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 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 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 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" : "%@g karbonhidrat, %@sa emilim süresiyle, %@ 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..." + } + } + } + }, + "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ı" + } + } + } + }, + "Server" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunucu" + } + } + } + }, + "Set Up Units" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Birimleri Ayarla" + } + } + } + }, + "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" + } + } + } + }, + "Share" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paylaş" + } + } + } }, - "Low" : { - "extractionState" : "manual" + "Share Logs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlükleri Paylaş" + } + } + } }, - "Low BG Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "Shared Secret" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paylaşılan Gizli Anahtar" + } + } + } }, - "Low Battery" : { - "comment" : "Alarm type name", - "extractionState" : "manual" - }, - "Max" : {}, - "Max %@ alarms at a time" : {}, - "Max Bolus" : {}, - "Max Carbs" : {}, - "Max Fat" : {}, - "Max Protein" : {}, - "Maximum" : {}, - "Meal" : { - "extractionState" : "manual" + "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 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" + } + } + } + }, + "Site URL" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Site URL'si" + } + } + } + }, + "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" + } + } + } + }, + "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" + } + } + } + }, + "Snoozed" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ertelendi" + } + } + } + }, + "Snoozed until %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ tarihine kadar ertelendi" + } + } + } + }, + "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" + } + } + } + }, + "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." + } + } + } }, - "Meal Data" : {}, - "Meal Settings" : {}, - "Meal with Bolus" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Meal with Fat/Protein" : { - "extractionState" : "manual" + "Std Dev" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Std Sapma" + } + } + } }, - "Menu" : {}, - "Metric" : { - "extractionState" : "manual" + "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)." + } + } + } }, - "Min" : {}, - "Minimum" : {}, - "Minutes" : { - "extractionState" : "manual" + "Success" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başarılı" + } + } + } }, - "Missed Bolus Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "Support & Community" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Destek ve Topluluk" + } + } + } }, - "Missed Reading Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "Swedish" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İsveççe" + } + } + } }, - "Multiple devices uploading profiles" : {}, - "Name" : {}, - "Never" : { - "comment" : "Alarm schedule option: never", - "extractionState" : "manual" + "TIR" : { + "shouldTranslate" : false }, - "New Target: %@ %@\nDuration: %@ minutes" : { + "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" : "New Target: %1$@ %2$@\nDuration: %3$@ minutes" + "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, %@ içindeki okumaların payı.\nTITR — Dar Hedef Aralıkta Süre, %@ içindeki pay.\nÖzel — kendi düşük ve yüksek değerlerinizi belirleyin." } } } }, - "Night" : { - "comment" : "Alarm schedule option: nighttime only", - "extractionState" : "manual" - }, - "Night starts" : { - "extractionState" : "manual" - }, - "Nightscout Database Size" : { - "comment" : "Alarm type name", - "extractionState" : "manual" - }, - "Nightscout URL" : {}, - "No Results" : {}, - "No alarms match “%@”." : {}, - "No background refresh. Alarms and updates will not work unless the app is open in the foreground." : {}, - "No data available" : {}, - "No devices found yet. They'll appear here when discovered." : {}, - "No issues detected" : {}, - "No matches for “%@”." : {}, - "No override presets found. Configure presets in your Loop app." : {}, - "No overrides available." : {}, - "No recent treatments" : {}, - "No thanks" : {}, - "Not Looping Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual" - }, - "Not now" : {}, - "Note: The meal will be scheduled, but the bolus is enacted immediately." : {}, - "OK" : {}, - "Off" : {}, - "Old Calculation Warning" : {}, - "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." : {}, - "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." : {}, - "Opt out" : {}, - "Output Unit" : { - "extractionState" : "manual" - }, - "Outside US" : {}, - "Override" : {}, - "Override Ended" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "TITR" : { + "shouldTranslate" : false }, - "Override Started" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "TOTP Code Already Used" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TOTP Kodu Zaten Kullanıldı" + } + } + } }, - "Override system volume" : { - "extractionState" : "manual" + "TOTP Code: Invalid QR code URL" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TOTP Kodu: Geçersiz QR kodu URL'si" + } + } + } }, - "Overrides" : { - "extractionState" : "manual" + "Tab %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme %@" + } + } + } }, - "Overriding the system volume lets alarms be heard even when your phone is silenced or in a Focus mode." : {}, - "Password" : {}, - "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." : {}, - "Paste the full .p8 contents — must include the BEGIN PRIVATE KEY and END PRIVATE KEY lines." : {}, - "Percentage: %@%%" : {}, - "Persistent Notification" : { - "extractionState" : "manual" + "Tab 5" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme 5" + } + } + } }, - "Pick when your day period begins and when your night period begins. " : { - "extractionState" : "manual" + "Tab Order" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekme Sırası" + } + } + } }, - "Play sound during calls" : { - "extractionState" : "manual" + "Tabs" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sekmeler" + } + } + } }, - "Please configure Loop APNS settings in Remote Settings to use APNS commands." : {}, - "Please enter your Nightscout URL in Settings." : {}, - "Please select a Remote Type in Settings." : {}, - "Please wait..." : { - "extractionState" : "manual" + "Tap to load older treatments" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eski tedavileri yüklemek için dokunun" + } + } + } }, - "Prediction" : {}, - "Prediction Style" : { - "extractionState" : "manual" + "Target" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef" + } + } + } }, - "Preset Name" : { - "extractionState" : "manual" + "Target Range" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef Aralığı" + } + } + } }, - "Preset: %@" : {}, - "Presets" : {}, - "Privacy" : { - "extractionState" : "manual" + "Target: %@ %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Target: %1$@ %2$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef: %@ %@" + } + } + } }, - "Proactive Low" : { - "extractionState" : "manual" + "Target: %@-%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Target: %1$@-%2$@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hedef: %@-%@" + } + } + } }, - "Production" : { - "extractionState" : "manual" + "Team ID: %@" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ekip Kimliği: %@" + } + } + } }, - "Production is used for browser builders and should be switched off for Xcode builders" : {}, - "Pump Battery Alert" : { + "Temp Target Ended" : { "comment" : "Alarm type name", - "extractionState" : "manual" + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Hedef Sona Erdi" + } + } + } }, - "Pump Change Alert" : { + "Temp Target Started" : { "comment" : "Alarm type name", - "extractionState" : "manual" + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Hedef Başladı" + } + } + } }, - "Pump Insulin Alert" : { + "Temporary Alert" : { "comment" : "Alarm type name", - "extractionState" : "manual" + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Uyarı" + } + } + } }, - "QR Code URL" : {}, - "RISING BG" : {}, - "RSSI: %@ dBm" : {}, - "Range" : {}, - "Range Mode" : { - "extractionState" : "manual" + "Temporary Target" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geçici Hedef" + } + } + } }, - "Real-time Updates" : {}, - "Rec. Bolus" : { - "comment" : "Alarm type name: recommended bolus", - "extractionState" : "manual" + "Text Color" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Metin Rengi" + } + } + } }, - "Recommended Bolus" : {}, - "Reconnecting..." : {}, - "Refresh" : { - "extractionState" : "manual" + "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ı?" + } + } + } }, - "Remote" : { - "extractionState" : "manual" + "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" + } + } + } }, - "Remote Settings" : { - "extractionState" : "manual" + "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." + } + } + } }, - "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." : {}, - "Requires a RileyLink-compatible device within Bluetooth range. Provides updates once per minute and uses less battery than the silent tune method." : {}, - "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." : {}, - "Review setup anyway" : {}, - "Review the settings that will be imported" : {}, - "Right slot" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Run diagnostics" : {}, - "Run diagnostics again" : {}, - "SNOOZE" : {}, - "Save" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Save BG to Calendar" : { - "extractionState" : "manual" - }, - "Save Preset" : {}, - "Save as Preset" : {}, - "Scan QR Code from Loop App" : {}, - "Scan QR Code to Import Settings" : {}, - "Scan this QR code with another LoopFollow app to import %@" : {}, - "Scanned Token:" : {}, - "Scanned URL:" : {}, - "Scanning for %@..." : {}, - "Schedule" : {}, - "Schedule for later" : { - "extractionState" : "manual" + "This will overwrite your current settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu, mevcut ayarlarınızın üzerine yazacak" + } + } + } }, - "Search" : { - "extractionState" : "manual" + "Time" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaman" + } + } + } }, - "Search alarms" : { - "extractionState" : "manual" + "Time Remaining" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kalan Süre" + } + } + } + }, + "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" + } + } + } + }, + "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ç" + } + } + } }, - "Secret" : {}, - "Security" : {}, - "See exactly what's sent" : { - "extractionState" : "manual" + "Tone" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ton" + } + } + } }, - "Select Alarms to Export" : { - "extractionState" : "manual" + "Treatments" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tedaviler" + } + } + } }, - "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." : {}, - "Selected Alarms: %@" : {}, - "Selected Device" : {}, - "Send" : {}, - "Send %@ units of insulin?" : {}, - "Send %@g of carbs with %@h absorption time at %@?" : { + "Treatments on Small Graph" : { + "extractionState" : "manual", "localizations" : { - "en" : { + "tr" : { "stringUnit" : { - "state" : "new", - "value" : "Send %1$@g of carbs with %2$@h absorption time at %3$@?" + "state" : "translated", + "value" : "Küçük Grafikte Tedaviler" } } } }, - "Send Bolus" : {}, - "Send Carbs" : {}, - "Send Insulin" : {}, - "Send Meal" : {}, - "Send anonymous usage stats" : { - "extractionState" : "manual" + "Trend" : { + "shouldTranslate" : false }, - "Sending Bolus..." : {}, - "Sending Meal Data..." : {}, - "Sending..." : {}, - "Sensitivity" : {}, - "Sensor Change Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "Trio Remote Control Settings" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trio Uzaktan Kumanda Ayarları" + } + } + } }, - "Server" : { - "extractionState" : "manual" + "Trio Remote Control is only supported for 'Trio'" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trio Uzaktan Kumanda yalnızca 'Trio' için desteklenir" + } + } + } }, - "Set Up Units" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Settings" : { - "extractionState" : "manual" + "U" : { + "comment" : "The short unit display string for international units of insulin", + "shouldTranslate" : false }, - "Settings imported — you're connected." : { - "extractionState" : "manual" + "URL" : { + "shouldTranslate" : false }, - "Settings to Import" : {}, - "Share" : { - "extractionState" : "manual" + "URL/Token Update" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL/Belirteç Güncelleme" + } + } + } }, - "Share Logs" : { - "extractionState" : "manual" + "URL/Token Validation" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL/Belirteç Doğrulama" + } + } + } }, - "Shared Secret" : {}, - "Show Carb Absorption" : { - "extractionState" : "manual" + "US" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ABD" + } + } + } }, - "Show Carb/Bolus Values" : { - "extractionState" : "manual" + "Units and Metrics" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Birimler ve Ölçütler" + } + } + } }, - "Show DIA Lines" : { - "extractionState" : "manual" + "Unknown Alert" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilinmeyen Uyarı" + } + } + } }, - "Show Delta" : { - "extractionState" : "manual" + "Use Anyway" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yine de Kullan" + } + } + } }, - "Show Display Name" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Show IOB" : { - "extractionState" : "manual" + "User" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı" + } + } + } }, - "Show Midnight Lines" : { - "extractionState" : "manual" + "User Information" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı Bilgileri" + } + } + } }, - "Show Prior Day Time Lines" : { - "extractionState" : "manual" + "Username" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı Adı" + } + } + } }, - "Show Trend" : { - "extractionState" : "manual" + "Validation Error" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doğrulama Hatası" + } + } + } }, - "Show Yesterday's BG" : { - "extractionState" : "manual" + "Variability" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Değişkenlik" + } + } + } }, - "Show in Info Display" : { - "extractionState" : "manual" + "View Log" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlüğü Görüntüle" + } + } + } }, - "Show −30 min Line" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Show −90 min Line" : { - "extractionState" : "manual" + "Warning" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uyarı" + } + } + } }, - "Site URL" : {}, - "Skip" : {}, - "Skip if BG is falling" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Skip if BG is rising" : { - "extractionState" : "manual" + "Welcome to LoopFollow" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "LoopFollow'a Hoş Geldiniz" + } + } + } }, - "Skip while charging" : { - "extractionState" : "manual" + "What is sent" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neler gönderiliyor" + } + } + } }, - "Slovak" : {}, - "Small Graph" : { - "extractionState" : "manual" + "What stays on your device" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cihazınızda neler kalıyor" + } + } + } }, - "Snooze & Mute Options" : {}, - "Snooze Date" : { - "extractionState" : "manual" + "What's sent" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neler gönderiliyor" + } + } + } }, - "Snooze Time" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Snooze all · 1h" : {}, - "Snooze for" : {}, - "Snoozed" : {}, - "Snoozed until %@" : {}, - "Snoozer emoji" : { - "extractionState" : "manual" + "Yes, send anonymous stats" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Evet, anonim istatistik gönder" + } + } + } }, - "Sound" : {}, - "Source" : {}, - "Speak BG" : { - "extractionState" : "manual" + "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." + } + } + } }, - "Start Date" : {}, - "Statistics" : {}, - "Status" : {}, - "Stay informed" : {}, - "Stay silent while BG is falling. The alert only sounds " : { - "extractionState" : "manual" + "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." + } + } + } }, - "Stay silent while BG is rising. The alert only sounds " : { - "extractionState" : "manual" + "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." + } + } + } }, - "Stay silent while the phone is charging. Requires the " : { - "extractionState" : "manual" + "You're all set" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Her şey hazır" + } + } + } }, - "Std Dev" : {}, - "Std Dev — Standard Deviation, how much glucose swings around the average. CV — Coefficient of Variation, that swing relative to the average (Std Dev ÷ mean)." : {}, - "Success" : {}, - "Support & Community" : { - "extractionState" : "manual" + "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:" + } + } + } }, - "Swedish" : {}, - "TIR" : {}, - "TIR — Time in Range, the share of readings within %@.\nTITR — Time in Tight Range, within %@.\nCustom — set your own low and high." : { + "Your API secret is used once to create a read-only access token and is never stored." : { "localizations" : { - "en" : { + "tr" : { "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." + "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." } } } }, - "TITR" : {}, - "TOTP Code Already Used" : {}, - "TOTP Code: Invalid QR code URL" : {}, - "Tab %@" : {}, - "Tab 5" : {}, - "Tab Order" : {}, - "Tabs" : { - "extractionState" : "manual" + "eHbA1c" : { + "shouldTranslate" : false }, - "Tap to load older treatments" : {}, - "Target" : {}, - "Target Range" : {}, - "Target: %@ %@" : { + "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" : { - "en" : { + "tr" : { "stringUnit" : { - "state" : "new", - "value" : "Target: %1$@ %2$@" + "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." } } } }, - "Target: %@-%@" : { + "g" : { + "comment" : "The short unit display string for grams", + "shouldTranslate" : false + }, + "mg/dL" : { + "comment" : "The short unit display string for milligrams of glucose per decilter", + "shouldTranslate" : false + }, + "minutes" : { "localizations" : { - "en" : { + "tr" : { "stringUnit" : { - "state" : "new", - "value" : "Target: %1$@-%2$@" + "state" : "translated", + "value" : "dakika" } } } }, - "Team ID: %@" : {}, - "Temp Target Ended" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "mmol/L" : { + "comment" : "The short unit display string for millimoles of glucose per liter", + "shouldTranslate" : false }, - "Temp Target Started" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "mmol/mol" : { + "shouldTranslate" : false }, - "Temporary Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual" + "“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." + } + } + } }, - "Temporary Target" : {}, - "Text Color" : { - "extractionState" : "manual" - }, - "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?" : {}, - "That's your API secret — create a read-only token" : {}, - "The order of Fat and Protein inputs has changed." : {}, - "The top 4 items appear in the tab bar. The Menu can always open every feature." : {}, - "This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command." : {}, - "This will overwrite your current settings" : {}, - "Time" : {}, - "Time Remaining" : {}, - "Time Zone" : { - "extractionState" : "manual" + "“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." + } + } + } }, - "Time Zone Override" : { - "extractionState" : "manual" + "••••••••" : { + "shouldTranslate" : false }, - "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." : {}, - "Token" : {}, - "Tone" : {}, - "Treatments" : {}, - "Treatments on Small Graph" : { - "extractionState" : "manual" + "− 30m" : { + "shouldTranslate" : false }, - "Trend" : {}, - "Trio Remote Control Settings" : {}, - "Trio Remote Control is only supported for 'Trio'" : {}, - "Type or paste a token, or a full Nightscout URL that includes a token." : {}, - "U" : { - "comment" : "The short unit display string for international units of insulin" + "⚠️ Please enable at least one limit." : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "⚠️ Lütfen en az bir sınır etkinleştirin." + } + } + } }, - "URL" : {}, - "URL/Token Update" : {}, - "URL/Token Validation" : {}, - "US" : {}, - "Units and Metrics" : { - "extractionState" : "manual" - }, - "Unknown Alert" : {}, - "Use Anyway" : {}, - "Use the Send anonymous usage stats toggle above. Turning it off is immediate and persistent." : {}, - "User" : {}, - "User Information" : {}, - "Username" : {}, - "Validation Error" : {}, - "Variability" : {}, - "View Log" : { - "extractionState" : "manual" - }, - "WARNING: New treatments may have occurred since the last recommended bolus was calculated %@ ago." : {}, - "Warning" : {}, - "We'll ask iOS for permission next. You can change this any time in the Settings app." : {}, - "Welcome to LoopFollow" : {}, - "What is sent" : {}, - "What stays on your device" : {}, - "What's sent" : { - "extractionState" : "manual" - }, - "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." : {}, - "Yes, send anonymous stats" : {}, - "You can change this any time in Settings → General → Diagnostics." : {}, - "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." : {}, - "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." : {}, - "You're all set" : {}, - "You're ready to go. Adjust everything later from the Menu — and when you have a moment, these are worth a look:" : {}, - "Your API secret is used once to create a read-only access token and is never stored." : {}, - "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." : {}, - "g" : { - "comment" : "The short unit display string for grams" + "🌮" : { + "shouldTranslate" : false }, - "mg/dL" : { - "comment" : "The short unit display string for milligrams of glucose per decilter" + "🍕" : { + "shouldTranslate" : false }, - "minutes" : {}, - "mmol/L" : { - "comment" : "The short unit display string for millimoles of glucose per liter" - }, - "mmol/mol" : {}, - "“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." : {}, - "“Snooze All” disables every alarm. “Mute All” silences phone sounds but still vibrates and shows iOS notifications." : {}, - "••••••••" : {}, - "− 30m" : {}, - "⚠️ Please enable at least one limit." : {}, - "🌮" : {}, - "🍕" : {}, - "🍭" : {}, - "🍽️" : {} + "🍭" : { + "shouldTranslate" : false + }, + "🍽️" : { + "shouldTranslate" : false + } }, "version" : "1.0" } 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/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/Tests/LocalizationCatalogTests.swift b/Tests/LocalizationCatalogTests.swift index cbb656648..3f6bc8a44 100644 --- a/Tests/LocalizationCatalogTests.swift +++ b/Tests/LocalizationCatalogTests.swift @@ -45,8 +45,9 @@ struct LocalizationCatalogTests { /// 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 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 } diff --git a/Tests/LocalizationDisplayNameTests.swift b/Tests/LocalizationDisplayNameTests.swift index 47fe0355c..4b3be8f0c 100644 --- a/Tests/LocalizationDisplayNameTests.swift +++ b/Tests/LocalizationDisplayNameTests.swift @@ -49,19 +49,38 @@ struct LocalizationDisplayNameTests { #expect(ActiveOption.allCases.map(\.rawValue) == ["always", "day", "night"]) } - @Test("AlarmType.displayName falls back to the English raw value in the test host language") - func alarmTypeDisplayNameEnglish() { + /// 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 == type.rawValue) + #expect(type.displayName == localized(type.rawValue)) + #expect(!type.displayName.isEmpty) } } - @Test("day/night displayName keeps the English wording") - func dayNightDisplayNameEnglish() { - #expect(PlaySoundOption.always.displayName == "Day & Night") - #expect(PlaySoundOption.day.displayName == "Day") - #expect(PlaySoundOption.night.displayName == "Night") - #expect(PlaySoundOption.never.displayName == "Never") - #expect(ActiveOption.always.displayName == "Day & Night") + @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") } } From 25cdbffbc7aa5484605338c253d13a123087dc3b Mon Sep 17 00:00:00 2001 From: Ramazan TUNCER Date: Sun, 27 Sep 2026 02:23:31 +0300 Subject: [PATCH 05/11] Address review: merge footer literal, positional placeholders, narrower don't-translate set - AlarmSettingsView's day/night footer joined three literals with "+", which selects the non-localized Text overload; merge into one literal and translate the whole footer. Drop the dead fragment and "(" keys. - Add a source lint test that fails on Text("a" + "b") patterns. - Require positional indexes (%1$@, %2$@) in every multi-placeholder translation so argument order can be changed safely; update the five affected Turkish values. - Keep shouldTranslate=false only for units, symbols, bare specifiers and hashtags; ordinary terms that happen to match in Turkish get an explicit translation so other languages can translate them. - Verify the English plural variation at runtime from en.lproj. - Fix "Snoozed until %@" (argument is a time, not a date) and refine "Proactive Low" and the "Remote" title. Co-Authored-By: Claude Fable 5.1 --- LoopFollow/Alarm/AlarmSettingsView.swift | 4 +- LoopFollow/Resources/Localizable.xcstrings | 186 +++++++++++++++++---- Tests/LocalizationCatalogTests.swift | 8 +- Tests/LocalizationDisplayNameTests.swift | 10 ++ Tests/LocalizationSourceLintTests.swift | 46 +++++ 5 files changed, 216 insertions(+), 38 deletions(-) create mode 100644 Tests/LocalizationSourceLintTests.swift 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/Resources/Localizable.xcstrings b/LoopFollow/Resources/Localizable.xcstrings index 808e2ccda..cc797195c 100644 --- a/LoopFollow/Resources/Localizable.xcstrings +++ b/LoopFollow/Resources/Localizable.xcstrings @@ -114,10 +114,6 @@ "%@g" : { "shouldTranslate" : false }, - "(" : { - "extractionState" : "manual", - "shouldTranslate" : false - }, "(%@ days)" : { "localizations" : { "tr" : { @@ -178,11 +174,25 @@ } }, "API Secret" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Secret" + } + } + } }, "APN" : { "extractionState" : "manual", - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "APN" + } + } + } }, "APNS Environment: %@" : { "localizations" : { @@ -670,7 +680,14 @@ }, "Bolus" : { "extractionState" : "manual", - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus" + } + } + } }, "Bolus Filter" : { "extractionState" : "manual", @@ -745,7 +762,14 @@ } }, "COB" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "COB" + } + } + } }, "COB Alert" : { "comment" : "Alarm type name", @@ -760,7 +784,14 @@ } }, "CV" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CV" + } + } + } }, "Calculated %lld minutes ago" : { "comment" : "Age of the recommended bolus calculation", @@ -1385,7 +1416,14 @@ } }, "Delta" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delta" + } + } + } }, "Description" : { "localizations" : { @@ -1454,7 +1492,14 @@ } }, "Dexcom Share" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dexcom Share" + } + } + } }, "Diagnostics" : { "extractionState" : "manual", @@ -2101,7 +2146,14 @@ } }, "GMI" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI" + } + } + } }, "GRI (Glucose Risk Index)" : { "localizations" : { @@ -2378,7 +2430,14 @@ } }, "IOB" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IOB" + } + } + } }, "IOB Alert" : { "comment" : "Alarm type name", @@ -2945,10 +3004,24 @@ } }, "Min" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Min" + } + } + } }, "Minimum" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minimum" + } + } + } }, "Minutes" : { "extractionState" : "manual", @@ -3028,7 +3101,7 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yeni Hedef: %@ %@\nSüre: %@ dakika" + "value" : "Yeni Hedef: %1$@ %2$@\nSüre: %3$@ dakika" } } } @@ -3302,7 +3375,14 @@ } }, "Override" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Override" + } + } + } }, "Override Ended" : { "comment" : "Alarm type name", @@ -3411,13 +3491,13 @@ } } }, - "Pick when your day period begins and when your night period begins. " : { + "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. " + "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." } } } @@ -3543,14 +3623,21 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Öngörülü Düşük" + "value" : "Tahmini Düşük" } } } }, "Production" : { "extractionState" : "manual", - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Production" + } + } + } }, "Production is used for browser builders and should be switched off for Xcode builders" : { "localizations" : { @@ -3701,7 +3788,7 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Uzaktan" + "value" : "Uzaktan Kumanda" } } } @@ -4056,7 +4143,7 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%@g karbonhidrat, %@sa emilim süresiyle, %@ zamanında gönderilsin mi?" + "value" : "%1$@g karbonhidrat, %2$@sa emilim süresiyle, %3$@ zamanında gönderilsin mi?" } } } @@ -4534,7 +4621,7 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%@ tarihine kadar ertelendi" + "value" : "%@ saatine kadar ertelendi" } } } @@ -4706,7 +4793,14 @@ } }, "TIR" : { - "shouldTranslate" : false + "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" : { @@ -4719,13 +4813,20 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "TIR — Hedef Aralıkta Süre, %@ içindeki okumaların payı.\nTITR — Dar Hedef Aralıkta Süre, %@ içindeki pay.\nÖzel — kendi düşük ve yüksek değerlerinizi belirleyin." + "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" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TITR" + } + } + } }, "TOTP Code Already Used" : { "localizations" : { @@ -4829,7 +4930,7 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hedef: %@ %@" + "value" : "Hedef: %1$@ %2$@" } } } @@ -4845,7 +4946,7 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hedef: %@-%@" + "value" : "Hedef: %1$@-%2$@" } } } @@ -5071,7 +5172,14 @@ } }, "Trend" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trend" + } + } + } }, "Trio Remote Control Settings" : { "localizations" : { @@ -5108,7 +5216,14 @@ "shouldTranslate" : false }, "URL" : { - "shouldTranslate" : false + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL" + } + } + } }, "URL/Token Update" : { "localizations" : { @@ -5394,7 +5509,14 @@ } }, "eHbA1c" : { - "shouldTranslate" : false + "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" : { diff --git a/Tests/LocalizationCatalogTests.swift b/Tests/LocalizationCatalogTests.swift index 3f6bc8a44..66d7bec33 100644 --- a/Tests/LocalizationCatalogTests.swift +++ b/Tests/LocalizationCatalogTests.swift @@ -85,9 +85,11 @@ struct LocalizationCatalogTests { problems.append("[\(language)] \(key) -> \(t.value)") continue } - let reordered = source.map(\.1) != target.map(\.1) - if source.count > 1, reordered, target.contains(where: { $0.0 == nil }) { - problems.append("[\(language)] reordered without positional indexes: \(key) -> \(t.value)") + // 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)") } } } diff --git a/Tests/LocalizationDisplayNameTests.swift b/Tests/LocalizationDisplayNameTests.swift index 4b3be8f0c..28da45311 100644 --- a/Tests/LocalizationDisplayNameTests.swift +++ b/Tests/LocalizationDisplayNameTests.swift @@ -83,4 +83,14 @@ struct LocalizationDisplayNameTests { } #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") + } } diff --git a/Tests/LocalizationSourceLintTests.swift b/Tests/LocalizationSourceLintTests.swift new file mode 100644 index 000000000..754dd4947 --- /dev/null +++ b/Tests/LocalizationSourceLintTests.swift @@ -0,0 +1,46 @@ +// 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[.. Date: Sun, 27 Sep 2026 11:00:01 +0300 Subject: [PATCH 06/11] Add shared localized-API list, catalog key scanner and source/catalog consistency test Scripts/localization/localized_apis.txt names the SwiftUI/Foundation APIs whose first string-literal argument is a catalog key. scan_keys.py adds missing keys to the catalog (or reports them with --check) and the new scannedLiteralsExistInCatalog test enforces the same rule in the test target. Literals followed by "+" are String concatenation and are ignored by both. Co-Authored-By: Claude Fable 5.1 --- .gitignore | 3 + Scripts/localization/localized_apis.txt | 25 +++++++++ Scripts/localization/scan_keys.py | 75 +++++++++++++++++++++++++ Tests/LocalizationSourceLintTests.swift | 42 ++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 Scripts/localization/localized_apis.txt create mode 100755 Scripts/localization/scan_keys.py 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/Scripts/localization/localized_apis.txt b/Scripts/localization/localized_apis.txt new file mode 100644 index 000000000..c0928b85f --- /dev/null +++ b/Scripts/localization/localized_apis.txt @@ -0,0 +1,25 @@ +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: diff --git a/Scripts/localization/scan_keys.py b/Scripts/localization/scan_keys.py new file mode 100755 index 000000000..c671a48a5 --- /dev/null +++ b/Scripts/localization/scan_keys.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Add SwiftUI/Foundation string-literal keys from the source tree to a String Catalog. + +Usage: scan_keys.py [--check] [--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 (the compiler emits those with printf specifiers; add them from an +-exportLocalizations run). 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 +API_LIST = HERE / "localized_apis.txt" + + +def literal_keys(roots): + apis = [a.strip() for a in API_LIST.read_text().splitlines() if a.strip()] + names = [re.escape(a.rstrip("(").rstrip(":")) for a in apis] + pat = 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 + 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) + missing = sorted(k for k in keys if k not in strings) + 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/LocalizationSourceLintTests.swift b/Tests/LocalizationSourceLintTests.swift index 754dd4947..c8e188ba7 100644 --- a/Tests/LocalizationSourceLintTests.swift +++ b/Tests/LocalizationSourceLintTests.swift @@ -43,4 +43,46 @@ struct LocalizationSourceLintTests { } #expect(hits.isEmpty, Comment(rawValue: "Merge these into one literal so they localize:\n" + hits.joined(separator: "\n"))) } + + /// Same rule as Scripts/localization/scan_keys.py: every plain string literal passed as the + /// first argument of a listed API must exist as a key in the catalog. + @Test("every literal passed to a localized API is a catalog key") + func scannedLiteralsExistInCatalog() throws { + let listURL = Self.repoRoot.appendingPathComponent("Scripts/localization/localized_apis.txt") + let apis = try String(contentsOf: listURL, encoding: .utf8) + .split(separator: "\n").map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + .map { NSRegularExpression.escapedPattern(for: String($0.trimmingSuffix(while: { $0 == "(" || $0 == ":" }))) } + let pattern = #"(? Bool) -> Substring { + var s = Substring(self) + while let last = s.last, pred(last) { + s = s.dropLast() + } + return s + } } From 75242476c4c671cd1a87c29df5ce743bc89814a2 Mon Sep 17 00:00:00 2001 From: Ramazan TUNCER Date: Sun, 27 Sep 2026 11:09:12 +0300 Subject: [PATCH 07/11] Take LocalizedStringKey in reusable views that only ever receive literals QuickPickSectionHeader, QuickSelectButton, InfoBanner, NavigationRow-free helpers, LoadingButtonView, ErrorMessageView, OnboardingStepHeader, HKQuantityInputView, AlarmEnumMenuPicker, CommandButtonView, BGPicker, StatCard, the alarm editor sections (AlarmBGSection, AlarmBGLimitSection, AlarmStepperSection), the welcome primary label and the onboarding footer title now take LocalizedStringKey, so their call sites localize without change. InfoBanner texts that were split across "+" literals are single literals again; the quick-pick info texts are localized at their source. The key scanner and the matching test now collect labeled literal arguments inside each listed call (skipping systemImage/comment/icon style labels, nested calls and arrays), so these keys are covered. Adds the Turkish values for the 188 new keys. Co-Authored-By: Claude Fable 5.1 --- .../Components/AlarmAudioSection.swift | 4 +- .../Components/AlarmBGLimitSection.swift | 16 +- .../Components/AlarmBGSection.swift | 16 +- .../Components/AlarmStepperSection.swift | 22 +- .../AlarmEditing/Components/InfoBanner.swift | 2 +- .../Editors/BuildExpireAlarmEditor.swift | 3 +- .../Editors/FastRiseAlarmEditor.swift | 10 +- .../Editors/FutureCarbsAlarmEditor.swift | 7 +- .../Editors/HighBgAlarmEditor.swift | 6 +- .../AlarmEditing/Editors/IOBAlarmEditor.swift | 3 +- .../Editors/LowBgAlarmEditor.swift | 7 +- .../Editors/MissedBolusAlarmEditor.swift | 10 +- .../Editors/NotLoopingAlarmEditor.swift | 6 +- .../Editors/PumpChangeAlarmEditor.swift | 4 +- .../Editors/PumpVolumeAlarmEditor.swift | 3 +- .../Editors/RecBolusAlarmEditor.swift | 3 +- .../Editors/SensorAgeAlarmEditor.swift | 6 +- LoopFollow/Helpers/Views/BGPicker.swift | 2 +- .../Helpers/Views/ErrorMessageView.swift | 4 +- .../Helpers/Views/HKQuantityInputView.swift | 2 +- .../Helpers/Views/LoadingButtonView.swift | 4 +- .../Views/QuickPickSectionHeader.swift | 20 +- .../Onboarding/OnboardingNavFooter.swift | 2 +- .../Onboarding/OnboardingStepHeader.swift | 4 +- .../Onboarding/Steps/AlarmsStepView.swift | 2 +- .../Onboarding/Steps/UnitsStepView.swift | 2 +- .../Onboarding/Steps/WelcomeStepView.swift | 2 +- .../Remote/TRC/TrioRemoteControlView.swift | 2 +- LoopFollow/Resources/Localizable.xcstrings | 3607 +++++++++++++---- LoopFollow/Stats/AggregatedStatsView.swift | 2 +- LoopFollow/Stats/DateRangePicker.swift | 2 +- Scripts/localization/localized_apis.txt | 36 + Scripts/localization/scan_keys.py | 104 +- Tests/LocalizationSourceLintTests.swift | 104 +- 34 files changed, 3117 insertions(+), 912 deletions(-) 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/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 9f4e1c374..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, 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 d80953b02..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, 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/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/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/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..74d2e1c02 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 { 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/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 index cc797195c..7075a2ae1 100644 --- a/LoopFollow/Resources/Localizable.xcstrings +++ b/LoopFollow/Resources/Localizable.xcstrings @@ -143,6 +143,17 @@ "00" : { "shouldTranslate" : false }, + "14d" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "14g" + } + } + } + }, "15m" : { "localizations" : { "tr" : { @@ -163,6 +174,50 @@ } } }, + "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 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" : { @@ -244,6 +299,17 @@ } } }, + "Above" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Üstünde" + } + } + } + }, "Above: %@" : { "localizations" : { "tr" : { @@ -314,6 +380,17 @@ } } }, + "Active during" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin olduğu zaman" + } + } + } + }, "Add Alarm" : { "extractionState" : "manual", "localizations" : { @@ -375,6 +452,17 @@ } } }, + "Advance Notice" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önceden Bildirim" + } + } + } + }, "Advanced Settings" : { "localizations" : { "tr" : { @@ -406,3098 +494,4418 @@ } } }, - "Alarm not found" : { + "Alarm basics" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Alarm bulunamadı" + "value" : "Alarm temelleri" } } } }, - "Alarms can behave differently during the day and at night." : { + "Alarm not found" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Alarmlar gündüz ve gece farklı davranabilir." + "value" : "Alarm bulunamadı" } } } }, - "Alert Sound" : { + "Alarms can behave differently during the day and at night." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Uyarı Sesi" + "value" : "Alarmlar gündüz ve gece farklı davranabilir." } } } }, - "All" : { + "Alert Sound" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tümü" + "value" : "Uyarı Sesi" } } } }, - "All Alerts Snoozed" : { + "Alert above" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tüm Uyarılar Ertelendi" + "value" : "Şunun üstünde uyar" } } } }, - "All Sounds Muted" : { + "Alert below" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tüm Sesler Kapalı" + "value" : "Şunun altında uyar" } } } }, - "All alerts snoozed" : { + "Alert if BG is equal to or above this value." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tüm uyarılar ertelendi" + "value" : "KŞ bu değere eşit veya üstündeyse uyar." } } } }, - "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." : { + "Alert if BG is equal to or below this value." : { + "extractionState" : "manual", "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." + "value" : "KŞ bu değere eşit veya altındaysa uyar." } } } }, - "Always" : { + "Alert if current IOB or total boluses reach this." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Her Zaman" + "value" : "Güncel IOB veya toplam bolus bu değere ulaşırsa uyar." } } } }, - "Ambulatory Glucose Profile (AGP)" : { + "Alert only if BG is equal to or above this value." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ambulatuvar Glukoz Profili (AGP)" + "value" : "Yalnızca KŞ bu değere eşit veya üstündeyse uyar." } } } }, - "App Settings" : { + "Alert only if BG is equal to or below this value." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Uygulama Ayarları" + "value" : "Yalnızca KŞ bu değere eşit veya altındaysa uyar." } } } }, - "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." : { + "Alert when Carbs-on-Board is at or above this number." : { + "extractionState" : "manual", "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." + "value" : "COB bu değere eşit veya üstündeyken uyar." } } } }, - "Appearance" : { + "Alert when any reading or forecast is at or below this value." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Görünüm" + "value" : "Herhangi bir okuma veya tahmin bu değere eşit veya altındaysa uyar." } } } }, - "Are you sure you want to cancel the active override?" : { + "Alert when recommended bolus is at or above this value." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Etkin override'ı iptal etmek istediğinizden emin misiniz?" + "value" : "Önerilen bolus bu değere eşit veya üstündeyse uyar." } } } }, - "Are you sure you want to cancel the existing temp target?" : { + "Alerts when Carbs-on-Board is at or above the amount you set below." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Mevcut geçici hedefi iptal etmek istediğinizden emin misiniz?" + "value" : "COB aşağıda belirlediğiniz miktara eşit veya üstünde olduğunda uyarır." } } } }, - "Are you sure you want to send %@ U?" : { + "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" : "%@ U göndermek istediğinizden emin misiniz?" + "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." } } } }, - "Available Overrides" : { + "Alerts when a temp target ends." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kullanılabilir Override'lar" + "value" : "Geçici hedef sona erdiğinde uyarır." } } } }, - "Available Variables" : { + "Alerts when a temp target starts." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kullanılabilir Değişkenler" + "value" : "Geçici hedef başladığında uyarır." } } } }, - "BG Update Delay (Sec): %@" : { + "Alerts when an override begins." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "KŞ Güncelleme Gecikmesi (sn): %@" + "value" : "Override başladığında uyarır." } } } }, - "Back" : { + "Alerts when an override ends." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Geri" + "value" : "Override sona erdiğinde uyarır." } } } }, - "Background Color" : { + "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" : "Arka Plan Rengi" + "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." } } } }, - "Background Refresh Type" : { + "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" : "Arka Plan Yenileme Türü" + "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." } } } }, - "Basal / BG Scale" : { + "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" : "Bazal / KŞ Ölçeği" + "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." } } } }, - "Battery Drop" : { - "comment" : "Alarm type name", + "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" : "Pil Düşüşü" + "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." } } } }, - "Below is the exact JSON object that LoopFollow would send to lf.bjorkert.se right now." : { + "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" : "LoopFollow'un şu anda lf.bjorkert.se adresine göndereceği JSON nesnesinin tamamı aşağıdadır." + "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." } } } }, - "Below: %@" : { + "Alerts when no successful loop has occurred for the time you set below." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Alt: %@" + "value" : "Aşağıda belirlediğiniz süre boyunca başarılı bir loop olmadığında uyarır." } } } }, - "Bolus" : { + "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" : "Bolus" + "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)." } } } }, - "Bolus Filter" : { + "Alerts when the recommended bolus equals or exceeds the threshold you set below." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus Filtresi" + "value" : "Önerilen bolus aşağıda belirlediğiniz eşiğe eşit veya üstünde olduğunda uyarır." } } } }, - "Bolus Increment" : { + "All" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus Artışı" + "value" : "Tümü" } } } }, - "Bolus Increment: %.3f U" : { + "All Alerts Snoozed" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus Artışı: %.3f U" + "value" : "Tüm Uyarılar Ertelendi" } } } }, - "Build Information" : { + "All Sounds Muted" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Derleme Bilgileri" + "value" : "Tüm Sesler Kapalı" } } } }, - "Built-in" : { + "All alerts snoozed" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yerleşik" + "value" : "Tüm uyarılar ertelendi" } } } }, - "Bundle ID: %@" : { + "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" : "Paket Kimliği: %@" + "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." } } } }, - "CHARGING" : { + "Always" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "ŞARJ OLUYOR" + "value" : "Her Zaman" } } } }, - "COB" : { + "Ambulatory Glucose Profile (AGP)" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "COB" + "value" : "Ambulatuvar Glukoz Profili (AGP)" } } } }, - "COB Alert" : { - "comment" : "Alarm type name", + "An alert fires once the reservoir is at this value or lower." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "COB Uyarısı" + "value" : "Rezervuar bu değere veya altına indiğinde uyarı verilir." } } } }, - "CV" : { + "App Settings" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "CV" + "value" : "Uygulama Ayarları" } } } }, - "Calculated %lld minutes ago" : { - "comment" : "Age of the recommended bolus calculation", - "extractionState" : "manual", + "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" : { - "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ı" + "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." } } } }, - "Calendar" : { + "Appearance" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Takvim" + "value" : "Görünüm" } } } }, - "Calendar Text" : { - "extractionState" : "manual", + "Are you sure you want to cancel the active override?" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Takvim Metni" + "value" : "Etkin override'ı iptal etmek istediğinizden emin misiniz?" } } } }, - "Calendar access denied" : { + "Are you sure you want to cancel the existing temp target?" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Takvim erişimi reddedildi" + "value" : "Mevcut geçici hedefi iptal etmek istediğinizden emin misiniz?" } } } }, - "Cancel" : { + "Are you sure you want to send %@ U?" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İptal" + "value" : "%@ U göndermek istediğinizden emin misiniz?" } } } }, - "Cancel Active Override" : { + "Arrange your tabs" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Etkin Override'ı İptal Et" + "value" : "Sekmelerinizi düzenleyin" } } } }, - "Cancel Override" : { + "At or Above" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Override'ı İptal Et" + "value" : "Eşit veya Üstünde" } } } }, - "Cancel Temp Target" : { + "At or Below" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Geçici Hedefi İptal Et" + "value" : "Eşit veya Altında" } } } }, - "Carbs" : { + "Audio During Calls" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Karbonhidrat" + "value" : "Aramalar Sırasında Ses" } } } }, - "Category" : { + "Auto‑Snooze CGM Start" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kategori" + "value" : "CGM Başlangıcında Otomatik Ertele" } } } }, - "Character Limit Reached" : { - "extractionState" : "manual", + "Available Overrides" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Karakter Sınırına Ulaşıldı" + "value" : "Kullanılabilir Override'lar" } } } }, - "Checking Nightscout profile history…" : { + "Available Variables" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Nightscout profil geçmişi denetleniyor…" + "value" : "Kullanılabilir Değişkenler" } } } }, - "Choose Tone" : { + "Avg Bolus" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ton Seç" + "value" : "Ort. Bolus" } } } }, - "Close" : { + "Avg Carbs" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kapat" + "value" : "Ort. Karbonhidrat" } } } }, - "Color" : { + "Avg Glucose" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Renk" + "value" : "Ort. Glukoz" } } } }, - "Color BG Text" : { + "BG" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "KŞ Metnini Renklendir" + "value" : "KŞ" } } } }, - "Color Mode" : { + "BG Limit" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Renk Modu" + "value" : "KŞ Sınırı" } } } }, - "Color Options" : { + "BG Update Delay (Sec): %@" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Renk Seçenekleri" + "value" : "KŞ Güncelleme Gecikmesi (sn): %@" } } } }, - "Coloring enabled" : { + "Back" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Renklendirme açık" + "value" : "Geri" } } } }, - "Command Sent" : { + "Background Color" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Komut Gönderildi" + "value" : "Arka Plan Rengi" } } } }, - "Configure Loop APNS" : { + "Background Refresh Type" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Loop APNS'i Yapılandır" + "value" : "Arka Plan Yenileme Türü" } } } }, - "Confirm" : { + "Basal / BG Scale" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Onayla" + "value" : "Bazal / KŞ Ölçeği" } } } }, - "Confirm & Import" : { + "Battery Drop" : { + "comment" : "Alarm type name", "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Onayla ve İçe Aktar" + "value" : "Pil Düşüşü" } } } }, - "Confirm Bolus" : { + "Below" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bolusu Onayla" + "value" : "Altında" } } } }, - "Confirm Cancellation" : { + "Below is the exact JSON object that LoopFollow would send to lf.bjorkert.se right now." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İptali Onayla" + "value" : "LoopFollow'un şu anda lf.bjorkert.se adresine göndereceği JSON nesnesinin tamamı aşağıdadır." } } } }, - "Confirm Carbs" : { + "Below: %@" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Karbonhidratı Onayla" + "value" : "Alt: %@" } } } }, - "Confirm Command" : { + "Bolus" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Komutu Onayla" + "value" : "Bolus" } } } }, - "Confirm Insulin" : { + "Bolus Amount" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İnsülini Onayla" + "value" : "Bolus Miktarı" } } } }, - "Confirm Meal" : { + "Bolus Count" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Öğünü Onayla" + "value" : "Bolus Sayısı" } } } }, - "Connected" : { + "Bolus Filter" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bağlı" + "value" : "Bolus Filtresi" } } } }, - "Consumption Time" : { + "Bolus Increment" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Artışı" + } + } + } + }, + "Bolus Increment: %.3f U" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus Artışı: %.3f U" + } + } + } + }, + "Boluses" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tüketim Zamanı" + "value" : "Boluslar" } } } }, - "Contact Integration" : { + "Boluses Size Limit" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kişi Entegrasyonu" + "value" : "Bolus Boyutu Sınırı" } } } }, - "Continue" : { + "Boluses at or below this size are ignored." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Devam" + "value" : "Bu boyuta eşit veya altındaki boluslar yok sayılır." } } } }, - "Copy JSON" : { + "Build Information" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "JSON'u Kopyala" + "value" : "Derleme Bilgileri" } } } }, - "Create Read-Only Token" : { + "Built-in" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Salt Okunur Belirteç Oluştur" + "value" : "Yerleşik" } } } }, - "Create one for me" : { + "Bundle ID: %@" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Benim için oluştur" + "value" : "Paket Kimliği: %@" } } } }, - "Creating read-only token…" : { + "CHARGING" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Salt okunur belirteç oluşturuluyor…" + "value" : "ŞARJ OLUYOR" } } } }, - "Current OTP Code" : { + "COB" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Güncel OTP Kodu" + "value" : "COB" } } } }, - "Current TOTP Code:" : { + "COB Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Güncel TOTP Kodu:" + "value" : "COB Uyarısı" } } } }, - "Current Target" : { + "CV" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Güncel Hedef" + "value" : "CV" } } } }, - "Current Token:" : { + "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" : "Güncel Belirteç:" + "value" : "Takvim" } } } }, - "Current URL:" : { + "Calendar Text" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Güncel URL:" + "value" : "Takvim Metni" } } } }, - "Custom" : { + "Calendar access denied" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Özel" + "value" : "Takvim erişimi reddedildi" } } } }, - "Custom sounds stay on this device and aren't included in settings export." : { + "Cancel" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Özel sesler bu cihazda kalır ve ayar dışa aktarımına dahil edilmez." + "value" : "İptal" } } } }, - "Cutoffs in %@" : { + "Cancel Active Override" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sınırlar (%@)" + "value" : "Etkin Override'ı İptal Et" } } } }, - "Data Availability" : { + "Cancel Override" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Veri Kullanılabilirliği" + "value" : "Override'ı İptal Et" } } } }, - "Day" : { - "comment" : "Alarm schedule option: daytime only", - "extractionState" : "manual", + "Cancel Temp Target" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gündüz" + "value" : "Geçici Hedefi İptal Et" } } } }, - "Day & Night" : { - "comment" : "Alarm schedule option: at any time of day", + "Carb entries at or below this amount will not trigger the alarm." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gündüz ve Gece" + "value" : "Bu miktara eşit veya altındaki karbonhidrat girişleri alarmı tetiklemez." } } } }, - "Day / Night" : { + "Carbs" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gündüz / Gece" + "value" : "Karbonhidrat" } } } }, - "Day / Night Schedule" : { + "Carbs Amount" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gündüz / Gece Programı" + "value" : "Karbonhidrat Miktarı" } } } }, - "Day starts" : { + "Carbs-on-Board Limit" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gündüz başlangıcı" + "value" : "COB Sınırı" } } } }, - "Debug / Info" : { + "Category" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hata Ayıklama / Bilgi" + "value" : "Kategori" } } } }, - "Debug Log Level" : { + "Character Limit Reached" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hata Ayıklama Günlük Düzeyi" + "value" : "Karakter Sınırına Ulaşıldı" } } } }, - "Default Snooze:" : { + "Checking Nightscout profile history…" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Varsayılan Erteleme:" + "value" : "Nightscout profil geçmişi denetleniyor…" } } } }, - "Delay Between Sounds" : { + "Choose Tone" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sesler Arası Gecikme" + "value" : "Ton Seç" } } } }, - "Delete" : { + "Choose a data source" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sil" + "value" : "Bir veri kaynağı seçin" } } } }, - "Delete Alarm" : { + "Choose how long the app should wait before alerting." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Alarmı Sil" + "value" : "Uygulamanın uyarmadan önce ne kadar bekleyeceğini seçin." } } } }, - "Delete this alarm?" : { + "Choose how many days of notice you’d like before the build becomes unusable." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu alarm silinsin mi?" + "value" : "Derleme kullanılamaz hale gelmeden kaç gün önce bildirim istediğinizi seçin." } } } }, - "Delta" : { + "Close" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Delta" + "value" : "Kapat" } } } }, - "Description" : { + "Color" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Açıklama" + "value" : "Renk" } } } }, - "Developer Team ID" : { + "Color BG Text" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Geliştirici Ekip Kimliği" + "value" : "KŞ Metnini Renklendir" } } } }, - "Device Data" : { + "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" + } + } + } + }, + "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ı" + } + } + } + }, + "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 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" + } + } + } + }, + "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…" + } + } + } + }, + "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 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 (%@)" + } + } + } + }, + "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 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 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 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 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." + } + } + } + }, + "Environment" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ortam" + } + } + } + }, + "Error" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hata" + } + } + } + }, + "Eventual" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nihai" + } + } + } + }, + "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" + } + } + } + }, + "Failed to generate QR code" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR kodu oluşturulamadı" + } + } + } + }, + "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" : "Cihaz Verileri" + "value" : "Hızlı Düşüş Uyarısı" } } } }, - "Device Token: %@" : { + "Fast Drop Delta" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Cihaz Belirteci: %@" + "value" : "Hızlı Düşüş Deltası" } } } }, - "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." : { + "Fast Rise Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "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." + "value" : "Hızlı Yükseliş Uyarısı" } } } }, - "Dexcom Settings" : { + "Fat" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Dexcom Ayarları" + "value" : "Yağ" } } } }, - "Dexcom Share" : { + "Features" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Dexcom Share" + "value" : "Özellikler" } } } }, - "Diagnostics" : { + "Filter" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tanılama" + "value" : "Filtre" } } } }, - "Diagnostics unavailable: %@" : { + "Finish" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tanılama kullanılamıyor: %@" + "value" : "Bitir" } } } }, - "Disconnect" : { + "Food Type" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bağlantıyı Kes" + "value" : "Yiyecek Türü" } } } }, - "Disconnected for %@" : { + "Force portrait mode" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%@ süredir bağlantı yok" + "value" : "Dikey modu zorla" } } } }, - "Dismiss" : { - "extractionState" : "manual", + "French" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kapat" + "value" : "Fransızca" } } } }, - "Display" : { - "extractionState" : "manual", + "Frequency" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Görüntüleme" + "value" : "Sıklık" } } } }, - "Display App Badge" : { + "Future Carbs Alert" : { + "comment" : "Alarm type name", "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Uygulama Rozetini Göster" + "value" : "Gelecek Karbonhidrat Uyarısı" } } } }, - "Display Dots" : { - "extractionState" : "manual", + "GMI" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Noktaları Göster" + "value" : "GMI" } } } }, - "Display Lines" : { - "extractionState" : "manual", + "GRI (Glucose Risk Index)" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Çizgileri Göster" + "value" : "GRI (Glukoz Risk İndeksi)" } } } }, - "Display Small Graph" : { - "extractionState" : "manual", + "General" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Küçük Grafiği Göster" + "value" : "Genel" } } } }, - "Display Stats" : { + "Get Started" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İstatistikleri Göster" + "value" : "Başlayın" } } } }, - "Do you want to activate the override '%@'?" : { + "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" : "'%@' override'ını etkinleştirmek istiyor musunuz?" + "value" : "Her alarma benzersiz bir ad verin — özellikle aynı türden birden fazla eklediyseniz — böylece bir bakışta ayırt edebilirsiniz." } } } }, - "Done" : { + "Glucose" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bitti" + "value" : "Glukoz" } } } }, - "Download Prediction" : { + "Glucose Unit" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tahmini İndir" + "value" : "Glukoz Birimi" } } } }, - "Download Treatments" : { + "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" : "Tedavileri İndir" + "value" : "Uyarı çalmadan önce glukozun eşiğin altında bu kadar dakika kalması gerekir. Hemen uyarmak için 0 girin." } } } }, - "Drag to reorder" : { + "Glucose value" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sıralamak için sürükleyin" + "value" : "Glukoz değeri" } } } }, - "Drag to reorder. Tap a row to set its visibility and colors." : { + "Glycemic Metrics" : { "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." + "value" : "Glisemik Ölçütler" } } } }, - "Duration" : { + "Grams" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Süre" + "value" : "Gram" } } } }, - "Duration: %@" : { + "Graph Basal" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Süre: %@" + "value" : "Bazal Grafiği" } } } }, - "Duration: %@ minutes" : { + "Graph Bolus" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Süre: %@ dakika" + "value" : "Bolus Grafiği" } } } }, - "Dynamic mode colors text based on BG range: Green (in range), Yellow (high), Red (low)" : { + "Graph Carbs" : { + "extractionState" : "manual", "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)" + "value" : "Karbonhidrat Grafiği" } } } }, - "Enable Contact BG Updates" : { + "Graph Display" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kişi KŞ Güncellemelerini Aç" + "value" : "Grafik Görünümü" } } } }, - "Enable Live Activity" : { + "Graph Other Treatments" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Canlı Etkinliği Aç" + "value" : "Diğer Tedavilerin Grafiği" } } } }, - "Enable Notifications" : { + "Grid Slot - CarPlay / Watch" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bildirimleri Aç" + "value" : "Izgara Yuvası - CarPlay / Watch" } } } }, - "Enable WebSocket" : { - "extractionState" : "manual", + "Grid Slots - Live Activity" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "WebSocket'i Aç" + "value" : "Izgara Yuvaları - Canlı Etkinlik" } } } }, - "Enable coloring" : { - "extractionState" : "manual", + "Guardrails" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Renklendirmeyi aç" + "value" : "Güvenlik Sınırları" } } } }, - "Enable indefinitely" : { - "extractionState" : "manual", + "Help us help you" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Süresiz etkinleştir" + "value" : "Size yardımcı olmamıza yardım edin" } } } }, - "Enable this alarm" : { + "Help us help you!" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu alarmı aç" + "value" : "Size yardımcı olmamıza yardım edin!" } } } }, - "Enabled" : { + "Here's what we'll do" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Açık" + "value" : "Şunları yapacağız" } } } }, - "Enact" : { + "Hide Information Table" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Uygula" + "value" : "Bilgi Tablosunu Gizle" } } } }, - "End Date" : { + "High" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bitiş Tarihi" + "value" : "Yüksek" } } } }, - "End now" : { + "High BG Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Şimdi bitir" + "value" : "Yüksek KŞ Uyarısı" } } } }, - "Endpoint" : { + "High BG Limit" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Uç Nokta" + "value" : "Yüksek KŞ Sınırı" } } } }, - "English" : { + "High Glucose Limit" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İngilizce" + "value" : "Yüksek Glukoz Sınırı" } } } }, - "Enter QR code URL or scan from Loop app" : { + "High Limit" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "QR kodu URL'sini girin veya Loop uygulamasından tarayın" + "value" : "Üst Sınır" } } } }, - "Enter URL" : { + "History" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "URL girin" + "value" : "Geçmiş" } } } }, - "Enter User" : { + "Home" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kullanıcı girin" + "value" : "Ana Sayfa" } } } }, - "Enter Username" : { + "Hours" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kullanıcı adı girin" + "value" : "Saat" } } } }, - "Enter or paste your full Nightscout address. If the link already includes a token, we'll fill both in for you." : { + "How far back to look for that drop." : { + "extractionState" : "manual", "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." + "value" : "Bu düşüş için ne kadar geriye bakılacağı." } } } }, - "Environment" : { + "How far back to look for those boluses." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ortam" + "value" : "Bu boluslar için ne kadar geriye bakılacağı." } } } }, - "Error" : { + "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" : "Hata" + "value" : "Alarm çalmadan önce glukozun eşiğin üstünde ne kadar kalması gerektiği. Hemen uyarmak için 0 girin." } } } }, - "Eventual" : { + "How many hours before the 3-day limit you’d like a reminder." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Nihai" + "value" : "3 günlük sınırdan kaç saat önce hatırlatma istediğiniz." } } } }, - "Existing Temp Target" : { + "Hyperglycemia Component (%)" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Mevcut Geçici Hedef" + "value" : "Hiperglisemi Bileşeni (%)" } } } }, - "Expected bg delay: %@" : { + "Hypoglycemia Component (%)" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Beklenen KŞ gecikmesi: %@" + "value" : "Hipoglisemi Bileşeni (%)" } } } }, - "Expected: 1 CGM reading every 5 minutes" : { + "I have a token" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Beklenen: her 5 dakikada 1 CGM okuması" + "value" : "Belirtecim var" } } } }, - "Export" : { - "extractionState" : "manual", + "IOB" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Dışa Aktar" + "value" : "IOB" } } } }, - "Export %@" : { + "IOB Above" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "%@ Dışa Aktar" + "value" : "IOB Üstünde" } } } }, - "Export Settings To QR Code" : { + "IOB Alert" : { + "comment" : "Alarm type name", "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ayarları QR Koduna Aktar" + "value" : "IOB Uyarısı" } } } }, - "F%@ P%@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "F%1$@ P%2$@" - } - } - }, - "shouldTranslate" : false - }, - "FALLING BG" : { + "Ignore Zero BG" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "KŞ DÜŞÜYOR" + "value" : "Sıfır KŞ'yi Yok Say" } } } }, - "Failed to generate QR code" : { + "Ignore carb entries below this amount." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "QR kodu oluşturulamadı" + "value" : "Bu miktarın altındaki karbonhidrat girişlerini yok say." } } } }, - "Fast Drop Alert" : { - "comment" : "Alarm type name", + "Ignore low BG" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hızlı Düşüş Uyarısı" + "value" : "Düşük KŞ'yi yok say" } } } }, - "Fast Rise Alert" : { - "comment" : "Alarm type name", + "Ignore small boluses" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hızlı Yükseliş Uyarısı" + "value" : "Küçük bolusları yok say" } } } }, - "Features" : { + "Ignore small carbs" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Özellikler" + "value" : "Küçük karbonhidratları yok say" } } } }, - "Filter" : { + "Import Failed" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Filtre" + "value" : "İçe Aktarma Başarısız" } } } }, - "Finish" : { + "Import Settings" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bitir" + "value" : "Ayarları İçe Aktar" } } } }, - "Food Type" : { + "Import Settings from QR Code" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yiyecek Türü" + "value" : "Ayarları QR Kodundan İçe Aktar" } } } }, - "Force portrait mode" : { + "Import Sound…" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Dikey modu zorla" + "value" : "Ses İçe Aktar…" } } } }, - "French" : { + "Import/Export Settings" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Fransızca" + "value" : "Ayarları İçe/Dışa Aktar" } } } }, - "Frequency" : { + "In Menu" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sıklık" + "value" : "Menüde" } } } }, - "Future Carbs Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual", + "Inactive" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gelecek Karbonhidrat Uyarısı" + "value" : "Etkin Değil" } } } }, - "GMI" : { + "Include Delta in" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "GMI" + "value" : "Delta'yı şuraya ekle" } } } }, - "GRI (Glucose Risk Index)" : { + "Include IOB in" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "GRI (Glukoz Risk İndeksi)" + "value" : "IOB'yi şuraya ekle" } } } }, - "General" : { + "Include Trend in" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Genel" + "value" : "Trendi şuraya ekle" } } } }, - "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." : { + "Increment" : { "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." + "value" : "Artış" } } } }, - "Glucose" : { - "extractionState" : "manual", + "Information Display Settings" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoz" + "value" : "Bilgi Görüntüleme Ayarları" } } } }, - "Glucose Unit" : { + "Insulin" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoz Birimi" + "value" : "İnsülin" } } } }, - "Glucose value" : { + "Insulin Amount" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Glukoz değeri" + "value" : "İnsülin Miktarı" } } } }, - "Glycemic Metrics" : { + "Insulin On Board" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Glisemik Ölçütler" + "value" : "Aktif İnsülin" } } } }, - "Graph Basal" : { - "extractionState" : "manual", + "Insulin Totals" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bazal Grafiği" + "value" : "İnsülin Toplamları" } } } }, - "Graph Bolus" : { - "extractionState" : "manual", + "Insulin: %@%%" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bolus Grafiği" + "value" : "İnsülin: %@%%" } } } }, - "Graph Carbs" : { - "extractionState" : "manual", + "Invalid QR code URL" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Karbonhidrat Grafiği" + "value" : "Geçersiz QR kodu URL'si" } } } }, - "Graph Display" : { - "extractionState" : "manual", + "Italian" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Grafik Görünümü" + "value" : "İtalyanca" } } } }, - "Graph Other Treatments" : { + "Keep Screen Active" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Diğer Tedavilerin Grafiği" + "value" : "Ekranı Açık Tut" } } } }, - "Grid Slot - CarPlay / Watch" : { + "Key ID must be exactly 10 uppercase letters or digits." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Izgara Yuvası - CarPlay / Watch" + "value" : "Anahtar Kimliği tam olarak 10 büyük harf veya rakamdan oluşmalıdır." } } } }, - "Grid Slots - Live Activity" : { + "Language" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Izgara Yuvaları - Canlı Etkinlik" + "value" : "Dil" } } } }, - "Guardrails" : { + "Last connection: %@" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Güvenlik Sınırları" + "value" : "Son bağlantı: %@" } } } }, - "Help us help you" : { + "Lifetime" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Size yardımcı olmamıza yardım edin" + "value" : "Kullanım Ömrü" } } } }, - "Help us help you!" : { + "Line 1" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Size yardımcı olmamıza yardım edin!" + "value" : "Satır 1" } } } }, - "Hide Information Table" : { + "Line 2" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bilgi Tablosunu Gizle" + "value" : "Satır 2" } } } }, - "High" : { - "extractionState" : "manual", + "Live Activity" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yüksek" + "value" : "Canlı Etkinlik" } } } }, - "High BG Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual", + "Live Activity updates require APNs credentials. Configure them in Settings → APN." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yüksek KŞ Uyarısı" + "value" : "Canlı Etkinlik güncellemeleri APNs kimlik bilgileri gerektirir. Ayarlar → APN'den yapılandırın." } } } }, - "History" : { - "extractionState" : "manual", + "Load More" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Geçmiş" + "value" : "Daha Fazla Yükle" } } } }, - "Home" : { + "Loading data..." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ana Sayfa" + "value" : "Veriler yükleniyor..." } } } }, - "Hours" : { - "extractionState" : "manual", + "Loading override presets..." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Saat" + "value" : "Override ön ayarları yükleniyor..." } } } }, - "Hyperglycemia Component (%)" : { + "Loading stats..." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hiperglisemi Bileşeni (%)" + "value" : "İstatistikler yükleniyor..." } } } }, - "Hypoglycemia Component (%)" : { + "Logging" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hipoglisemi Bileşeni (%)" + "value" : "Günlük Kaydı" } } } }, - "I have a token" : { + "Logging Options" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Belirtecim var" + "value" : "Günlük Seçenekleri" } } } }, - "IOB" : { + "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" : "IOB" + "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." } } } }, - "IOB Alert" : { - "comment" : "Alarm type name", + "Lookahead" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "IOB Uyarısı" + "value" : "İleri Bakış" } } } }, - "Import Settings" : { + "Loop APNS Configuration" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ayarları İçe Aktar" + "value" : "Loop APNS Yapılandırması" } } } }, - "Import Settings from QR Code" : { + "Loop APNS Not Configured" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ayarları QR Kodundan İçe Aktar" + "value" : "Loop APNS Yapılandırılmamış" } } } }, - "Import Sound…" : { - "extractionState" : "manual", + "LoopFollow APNs Credentials" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ses İçe Aktar…" + "value" : "LoopFollow APNs Kimlik Bilgileri" } } } }, - "Import/Export Settings" : { + "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" : "Ayarları İçe/Dışa Aktar" + "value" : "LoopFollow'un bir glukoz veri kaynağına ihtiyacı var. Şimdi birini seçin — daha sonra Ayarlar'dan değiştirebilir veya ekleyebilirsiniz." } } } }, - "In Menu" : { + "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" : "Menüde" + "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." } } } }, - "Inactive" : { + "LoopFollow/Helpers/Telemetry.swift on GitHub." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Etkin Değil" + "value" : "GitHub'da LoopFollow/Helpers/Telemetry.swift." } } } }, - "Include Delta in" : { + "Looping app expiration" : { + "comment" : "Alarm type name", "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Delta'yı şuraya ekle" + "value" : "Loop uygulaması süre sonu" } } } }, - "Include IOB in" : { + "Low" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "IOB'yi şuraya ekle" + "value" : "Düşük" } } } }, - "Include Trend in" : { + "Low BG Alert" : { + "comment" : "Alarm type name", "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Trendi şuraya ekle" + "value" : "Düşük KŞ Uyarısı" } } } }, - "Increment" : { + "Low BG Limit" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Artış" + "value" : "Düşük KŞ Sınırı" } } } }, - "Information Display Settings" : { + "Low Battery" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bilgi Görüntüleme Ayarları" + "value" : "Düşük Pil" } } } }, - "Insulin" : { + "Low Limit" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İnsülin" + "value" : "Alt Sınır" } } } }, - "Insulin Totals" : { + "Max" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İnsülin Toplamları" + "value" : "Maks" } } } }, - "Insulin: %@%%" : { + "Max %@ alarms at a time" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İnsülin: %@%%" + "value" : "Aynı anda en fazla %@ alarm" } } } }, - "Invalid QR code URL" : { + "Max Bolus" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Geçersiz QR kodu URL'si" + "value" : "Maks Bolus" } } } }, - "Italian" : { + "Max Carbs" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İtalyanca" + "value" : "Maks Karbonhidrat" } } } }, - "Keep Screen Active" : { - "extractionState" : "manual", + "Max Fat" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ekranı Açık Tut" + "value" : "Maks Yağ" } } } }, - "Key ID must be exactly 10 uppercase letters or digits." : { + "Max Lookahead" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Anahtar Kimliği tam olarak 10 büyük harf veya rakamdan oluşmalıdır." + "value" : "Maks İleri Bakış" } } } }, - "Language" : { - "extractionState" : "manual", + "Max Protein" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Dil" + "value" : "Maks Protein" } } } }, - "Last connection: %@" : { + "Maximum" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Son bağlantı: %@" + "value" : "Maksimum" } } } }, - "Line 1" : { + "Meal" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Satır 1" + "value" : "Öğün" } } } }, - "Line 2" : { - "extractionState" : "manual", + "Meal Data" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Satır 2" + "value" : "Öğün Verileri" } } } }, - "Live Activity" : { + "Meal Settings" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Canlı Etkinlik" + "value" : "Öğün Ayarları" } } } }, - "Live Activity updates require APNs credentials. Configure them in Settings → APN." : { + "Meal with Bolus" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Canlı Etkinlik güncellemeleri APNs kimlik bilgileri gerektirir. Ayarlar → APN'den yapılandırın." + "value" : "Boluslu Öğün" } } } }, - "Load More" : { + "Meal with Fat/Protein" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Daha Fazla Yükle" + "value" : "Yağ/Proteinli Öğün" } } } }, - "Loading data..." : { - "extractionState" : "manual", + "Menu" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Veriler yükleniyor..." + "value" : "Menü" } } } }, - "Loading override presets..." : { + "Metric" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Override ön ayarları yükleniyor..." + "value" : "Ölçüt" } } } }, - "Loading stats..." : { + "Min" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İstatistikler yükleniyor..." + "value" : "Min" } } } }, - "Logging" : { + "Min BG Scale" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Günlük Kaydı" + "value" : "Min KŞ Ölçeği" } } } }, - "Logging Options" : { + "Minimum" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Günlük Seçenekleri" + "value" : "Minimum" } } } }, - "Loop APNS Configuration" : { + "Minimum Carbs" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Loop APNS Yapılandırması" + "value" : "Minimum Karbonhidrat" } } } }, - "Loop APNS Not Configured" : { + "Minutes" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Loop APNS Yapılandırılmamış" + "value" : "Dakika" } } } }, - "LoopFollow APNs Credentials" : { + "Minutes to wait after the carb entry before checking for a bolus." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "LoopFollow APNs Kimlik Bilgileri" + "value" : "Karbonhidrat girişinden sonra bolus denetlemeden önce beklenecek dakika." } } } }, - "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." : { + "Missed Bolus Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "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." + "value" : "Kaçırılan Bolus Uyarısı" } } } }, - "LoopFollow/Helpers/Telemetry.swift on GitHub." : { + "Missed Reading Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub'da LoopFollow/Helpers/Telemetry.swift." + "value" : "Kaçırılan Okuma Uyarısı" } } } }, - "Looping app expiration" : { - "comment" : "Alarm type name", - "extractionState" : "manual", + "Multiple devices uploading profiles" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Loop uygulaması süre sonu" + "value" : "Birden fazla cihaz profil yüklüyor" } } } }, - "Low" : { - "extractionState" : "manual", + "Name" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Düşük" + "value" : "Ad" } } } }, - "Low BG Alert" : { - "comment" : "Alarm type name", + "Never" : { + "comment" : "Alarm schedule option: never", "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Düşük KŞ Uyarısı" + "value" : "Hiçbir Zaman" } } } }, - "Low Battery" : { - "comment" : "Alarm type name", - "extractionState" : "manual", + "New Target: %@ %@\nDuration: %@ minutes" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "New Target: %1$@ %2$@\nDuration: %3$@ minutes" + } + }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Düşük Pil" + "value" : "Yeni Hedef: %1$@ %2$@\nSüre: %3$@ dakika" } } } }, - "Max" : { + "Night" : { + "comment" : "Alarm schedule option: nighttime only", + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Maks" + "value" : "Gece" } } } }, - "Max %@ alarms at a time" : { + "Night starts" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Aynı anda en fazla %@ alarm" + "value" : "Gece başlangıcı" } } } }, - "Max Bolus" : { + "Nightscout Database Size" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Maks Bolus" + "value" : "Nightscout Veritabanı Boyutu" } } } }, - "Max Carbs" : { + "Nightscout URL" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Maks Karbonhidrat" + "value" : "Nightscout URL'si" } } } }, - "Max Fat" : { + "No Loop for…" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Maks Yağ" + "value" : "Şu süredir Loop yok…" } } } }, - "Max Protein" : { + "No Results" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Maks Protein" + "value" : "Sonuç Yok" } } } }, - "Maximum" : { + "No alarms match “%@”." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Maksimum" + "value" : "“%@” ile eşleşen alarm yok." } } } }, - "Meal" : { - "extractionState" : "manual", + "No background refresh. Alarms and updates will not work unless the app is open in the foreground." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Öğün" + "value" : "Arka plan yenilemesi yok. Uygulama ön planda açık olmadıkça alarmlar ve güncellemeler çalışmaz." } } } }, - "Meal Data" : { + "No data available" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Öğün Verileri" + "value" : "Veri yok" } } } }, - "Meal Settings" : { + "No devices found yet. They'll appear here when discovered." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Öğün Ayarları" + "value" : "Henüz cihaz bulunamadı. Bulunduklarında burada görünecekler." } } } }, - "Meal with Bolus" : { - "extractionState" : "manual", + "No issues detected" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Boluslu Öğün" + "value" : "Sorun saptanmadı" } } } }, - "Meal with Fat/Protein" : { - "extractionState" : "manual", + "No matches for “%@”." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yağ/Proteinli Öğün" + "value" : "“%@” için eşleşme yok." } } } }, - "Menu" : { + "No override presets found. Configure presets in your Loop app." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Menü" + "value" : "Override ön ayarı bulunamadı. Ön ayarları Loop uygulamanızda yapılandırın." } } } }, - "Metric" : { - "extractionState" : "manual", + "No overrides available." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ölçüt" + "value" : "Kullanılabilir override yok." } } } }, - "Min" : { + "No reading for" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Min" + "value" : "Şu süredir okuma yok" } } } }, - "Minimum" : { + "No recent treatments" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Minimum" + "value" : "Yakın zamanda tedavi yok" } } } }, - "Minutes" : { - "extractionState" : "manual", + "No thanks" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Dakika" + "value" : "Hayır, teşekkürler" } } } }, - "Missed Bolus Alert" : { + "Not Looping Alert" : { "comment" : "Alarm type name", "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kaçırılan Bolus Uyarısı" + "value" : "Loop Çalışmıyor Uyarısı" } } } }, - "Missed Reading Alert" : { - "comment" : "Alarm type name", - "extractionState" : "manual", + "Not now" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kaçırılan Okuma Uyarısı" + "value" : "Şimdi değil" } } } }, - "Multiple devices uploading profiles" : { + "Note: The meal will be scheduled, but the bolus is enacted immediately." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Birden fazla cihaz profil yüklüyor" + "value" : "Not: Öğün planlanır ancak bolus hemen uygulanır." } } } }, - "Name" : { + "Notice Period" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ad" + "value" : "Bildirim Süresi" } } } }, - "Never" : { - "comment" : "Alarm schedule option: never", + "Number of Drops" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hiçbir Zaman" + "value" : "Düşüş Sayısı" } } } }, - "New Target: %@ %@\nDuration: %@ minutes" : { + "Number of days your CGM sensor lasts (e.g. 10 for Dexcom G6, 15 for G7 15-day)." : { + "extractionState" : "manual", "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" + "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)." } } } }, - "Night" : { - "comment" : "Alarm schedule option: nighttime only", + "Number of drops—each meeting the rate above—required before an alert fires." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gece" + "value" : "Uyarı verilmeden önce gereken, her biri yukarıdaki hızı karşılayan düşüş sayısı." } } } }, - "Night starts" : { + "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" : "Gece başlangıcı" + "value" : "Son başarılı loop'tan bu yana geçen dakika. Bu süre dolduğunda alarm devreye girebilir." } } } }, - "Nightscout Database Size" : { - "comment" : "Alarm type name", + "Number of qualifying boluses needed to trigger." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Nightscout Veritabanı Boyutu" + "value" : "Tetiklemek için gereken uygun bolus sayısı." } } } }, - "Nightscout URL" : { + "Number of rises—each meeting the rate above—required before an alert fires." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Nightscout URL'si" + "value" : "Uyarı verilmeden önce gereken, her biri yukarıdaki hızı karşılayan yükseliş sayısı." } } } }, - "No Results" : { + "OK" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sonuç Yok" + "value" : "Tamam" } } } }, - "No alarms match “%@”." : { + "Off" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "“%@” ile eşleşen alarm yok." + "value" : "Kapalı" } } } }, - "No background refresh. Alarms and updates will not work unless the app is open in the foreground." : { + "Old Calculation Warning" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Arka plan yenilemesi yok. Uygulama ön planda açık olmadıkça alarmlar ve güncellemeler çalışmaz." + "value" : "Eski Hesaplama Uyarısı" } } } }, - "No data available" : { + "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" : "Veri yok" + "value" : "Diğer telefonunuzda Ayarlar → Nightscout (veya Dexcom Share) → İçe/Dışa Aktar'ı açıp bir QR koduna aktarın. Sonra burada tarayın." } } } }, - "No devices found yet. They'll appear here when discovered." : { + "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" : "Henüz cihaz bulunamadı. Bulunduklarında burada görünecekler." + "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." } } } }, - "No issues detected" : { + "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" : "Sorun saptanmadı" + "value" : "24 saatte bir, ayrıca yeni bir derleme kurulduktan sonra bir kez. Denetim, uygulama etkinken veya arka planda yenilenirken arka planda çalışır." } } } }, - "No matches for “%@”." : { + "Only alert if the current BG is above this value." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "“%@” için eşleşme yok." + "value" : "Yalnızca güncel KŞ bu değerin üstündeyse uyar." } } } }, - "No override presets found. Configure presets in your Loop app." : { + "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" : "Override ön ayarı bulunamadı. Ön ayarları Loop uygulamanızda yapılandırın." + "value" : "Yalnızca gelecekte bu kadar dakikaya kadar planlanan karbonhidrat girişlerini izle. Bu pencerenin ötesindeki girişler yok sayılır." } } } }, - "No overrides available." : { + "Opt out" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kullanılabilir override yok." + "value" : "Vazgeç" } } } }, - "No recent treatments" : { + "Output Unit" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yakın zamanda tedavi yok" + "value" : "Çıkış Birimi" } } } }, - "No thanks" : { + "Outside US" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hayır, teşekkürler" + "value" : "ABD Dışı" } } } }, - "Not Looping Alert" : { - "comment" : "Alarm type name", + "Over This Time" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Loop Çalışmıyor Uyarısı" + "value" : "Bu Süre İçinde" } } } }, - "Not now" : { + "Override" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Şimdi değil" + "value" : "Override" } } } }, - "Note: The meal will be scheduled, but the bolus is enacted immediately." : { + "Override Ended" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Not: Öğün planlanır ancak bolus hemen uygulanır." + "value" : "Override Sona Erdi" } } } }, - "OK" : { + "Override Started" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tamam" + "value" : "Override Başladı" } } } }, - "Off" : { + "Override System Volume" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kapalı" + "value" : "Sistem Ses Düzeyini Geçersiz Kıl" } } } }, - "Old Calculation Warning" : { + "Override system volume" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Eski Hesaplama Uyarısı" + "value" : "Sistem ses düzeyini geçersiz kıl" } } } }, - "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." : { + "Overrides" : { + "extractionState" : "manual", "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." + "value" : "Override'lar" } } } }, - "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." : { + "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" : "24 saatte bir, ayrıca yeni bir derleme kurulduktan sonra bir kez. Denetim, uygulama etkinken veya arka planda yenilenirken arka planda çalışır." + "value" : "Sistem ses düzeyini geçersiz kılmak, telefonunuz sessizdeyken veya Odaklanma modundayken bile alarmların duyulmasını sağlar." } } } }, - "Opt out" : { + "PERSISTENCE" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Vazgeç" + "value" : "KALICILIK" } } } }, - "Output Unit" : { + "PREDICTION" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Çıkış Birimi" + "value" : "TAHMİN" } } } }, - "Outside US" : { + "Password" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "ABD Dışı" + "value" : "Parola" } } } }, - "Override" : { + "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" : "Override" + "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." } } } }, - "Override Ended" : { - "comment" : "Alarm type name", - "extractionState" : "manual", + "Paste the full .p8 contents — must include the BEGIN PRIVATE KEY and END PRIVATE KEY lines." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Override Sona Erdi" + "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." } } } }, - "Override Started" : { - "comment" : "Alarm type name", - "extractionState" : "manual", + "Percentage: %@%%" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Override Başladı" + "value" : "Yüzde: %@%%" } } } }, - "Override system volume" : { + "Persistent" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem ses düzeyini geçersiz kıl" + "value" : "Kalıcı" } } } }, - "Overrides" : { + "Persistent High" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Override'lar" + "value" : "Kalıcı Yüksek" } } } }, - "Overriding the system volume lets alarms be heard even when your phone is silenced or in a Focus mode." : { + "Persistent Notification" : { + "extractionState" : "manual", "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." + "value" : "Kalıcı Bildirim" } } } }, - "Password" : { + "Persistent for" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Parola" + "value" : "Kalıcılık süresi" } } } }, - "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." : { + "Phone Battery Drop" : { + "extractionState" : "manual", "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." + "value" : "Telefon Pili Düşüşü" } } } }, - "Paste the full .p8 contents — must include the BEGIN PRIVATE KEY and END PRIVATE KEY lines." : { + "Phone Battery Level" : { + "extractionState" : "manual", "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." + "value" : "Telefon Pil Düzeyi" } } } }, - "Percentage: %@%%" : { + "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" : "Yüzde: %@%%" + "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." } } } }, - "Persistent Notification" : { + "Pick which features sit in the tab bar. You can always reach everything through the Menu." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kalıcı Bildirim" + "value" : "Sekme çubuğunda hangi özelliklerin yer alacağını seçin. Her şeye her zaman Menü'den ulaşabilirsiniz." } } } }, - "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." : { + "Play" : { "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." + "value" : "Çal" } } } @@ -3554,6 +4962,28 @@ } } }, + "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" : { @@ -3575,6 +5005,17 @@ } } }, + "Predictive" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahmine dayalı" + } + } + } + }, "Preset Name" : { "extractionState" : "manual", "localizations" : { @@ -3649,6 +5090,28 @@ } } }, + "Programmed Basal" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Programlı Bazal" + } + } + } + }, + "Protein" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protein" + } + } + } + }, "Pump Battery Alert" : { "comment" : "Alarm type name", "extractionState" : "manual", @@ -3661,6 +5124,17 @@ } } }, + "Pump Battery Level" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pompa Pil Düzeyi" + } + } + } + }, "Pump Change Alert" : { "comment" : "Alarm type name", "extractionState" : "manual", @@ -3695,6 +5169,28 @@ } } }, + "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" : { @@ -3729,6 +5225,39 @@ } } }, + "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ı" + } + } + } + }, + "Reading Delay" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Okuma Gecikmesi" + } + } + } + }, "Real-time Updates" : { "localizations" : { "tr" : { @@ -3782,6 +5311,17 @@ } } }, + "Reminder Time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hatırlatma Süresi" + } + } + } + }, "Remote" : { "extractionState" : "manual", "localizations" : { @@ -3804,6 +5344,28 @@ } } }, + "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" : { @@ -3834,6 +5396,17 @@ } } }, + "Reservoir Below" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rezervuar Altında" + } + } + } + }, "Review setup anyway" : { "localizations" : { "tr" : { @@ -3865,6 +5438,39 @@ } } }, + "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" : { @@ -4082,6 +5688,39 @@ } } }, + "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" : { @@ -4224,7 +5863,18 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gönderiliyor..." + "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." } } } @@ -4251,6 +5901,17 @@ } } }, + "Sensor Lifetime" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensör Kullanım Ömrü" + } + } + } + }, "Server" : { "extractionState" : "manual", "localizations" : { @@ -4273,6 +5934,17 @@ } } }, + "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" : { @@ -4480,6 +6152,17 @@ } } }, + "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 URL" : { "localizations" : { "tr" : { @@ -4606,6 +6289,28 @@ } } }, + "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" : { @@ -4756,344 +6461,553 @@ "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)." + "value" : "Std Sapma — Standart Sapma, glukozun ortalama etrafında ne kadar dalgalandığı. CV — Varyasyon Katsayısı, bu dalgalanmanın ortalamaya oranı (Std Sapma ÷ ortalama)." + } + } + } + }, + "Success" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Başarılı" + } + } + } + }, + "Support & Community" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Destek ve Topluluk" + } + } + } + }, + "Swedish" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İsveççe" + } + } + } + }, + "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 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" } } } }, - "Success" : { + "Temp Target Ended" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Başarılı" + "value" : "Geçici Hedef Sona Erdi" } } } }, - "Support & Community" : { + "Temp Target Started" : { + "comment" : "Alarm type name", "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Destek ve Topluluk" + "value" : "Geçici Hedef Başladı" } } } }, - "Swedish" : { + "Temporary Alert" : { + "comment" : "Alarm type name", + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İsveççe" + "value" : "Geçici Uyarı" } } } }, - "TIR" : { + "Temporary Target" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "TIR" + "value" : "Geçici Hedef" } } } }, - "TIR — Time in Range, the share of readings within %@.\nTITR — Time in Tight Range, within %@.\nCustom — set your own low and high." : { + "Text Color" : { + "extractionState" : "manual", "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." + "value" : "Metin Rengi" } } } }, - "TITR" : { + "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" : "TITR" + "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ı?" } } } }, - "TOTP Code Already Used" : { + "That's your API secret — create a read-only token" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "TOTP Kodu Zaten Kullanıldı" + "value" : "Bu sizin API secret'ınız — salt okunur bir belirteç oluşturun" } } } }, - "TOTP Code: Invalid QR code URL" : { + "The alert becomes eligible once any reading is at or above this value." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "TOTP Kodu: Geçersiz QR kodu URL'si" + "value" : "Herhangi bir okuma bu değere eşit veya üstünde olduğunda uyarı devreye girebilir." } } } }, - "Tab %@" : { + "The order of Fat and Protein inputs has changed." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sekme %@" + "value" : "Yağ ve Protein girişlerinin sırası değişti." } } } }, - "Tab 5" : { + "The top 4 items appear in the tab bar. The Menu can always open every feature." : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sekme 5" + "value" : "İlk 4 öğe sekme çubuğunda görünür. Menü her zaman tüm özellikleri açabilir." } } } }, - "Tab Order" : { + "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" : "Sekme Sırası" + "value" : "Bu TOTP kodu zaten bir komut için kullanıldı. Başka bir komut göndermeden önce lütfen sonraki kodun üretilmesini bekleyin." } } } }, - "Tabs" : { + "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" : "Sekmeler" + "value" : "Bu uyarı, glukoz aşağıda belirlediğiniz sınırlardan birini geçtiğinde bir kez çalar ve sonra kendini kapatır." } } } }, - "Tap to load older treatments" : { + "This alerts you if the phone battery drops by this much or more." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Eski tedavileri yüklemek için dokunun" + "value" : "Telefon pili bu kadar veya daha fazla düşerse sizi uyarır." } } } }, - "Target" : { + "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" : "Hedef" + "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." } } } }, - "Target Range" : { + "This alerts you when the phone battery drops to or below this level." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hedef Aralığı" + "value" : "Telefon pili bu düzeye veya altına indiğinde sizi uyarır." } } } }, - "Target: %@ %@" : { + "This alerts you when the pump battery drops to or below this level." : { + "extractionState" : "manual", "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Target: %1$@ %2$@" - } - }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hedef: %1$@ %2$@" + "value" : "Pompa pili bu düzeye veya altına indiğinde sizi uyarır." } } } }, - "Target: %@-%@" : { + "This counts only boluses larger than this size." : { + "extractionState" : "manual", "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Target: %1$@-%2$@" - } - }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hedef: %1$@-%2$@" + "value" : "Yalnızca bu boyuttan büyük boluslar sayılır." } } } }, - "Team ID: %@" : { + "This is how much the glucose must drop to be considered a fast drop." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ekip Kimliği: %@" + "value" : "Hızlı düşüş sayılması için glukozun ne kadar düşmesi gerektiği." } } } }, - "Temp Target Ended" : { - "comment" : "Alarm type name", + "This is how much the glucose must rise to be considered a fast rise." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Geçici Hedef Sona Erdi" + "value" : "Hızlı yükseliş sayılması için glukozun ne kadar yükselmesi gerektiği." } } } }, - "Temp Target Started" : { - "comment" : "Alarm type name", + "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" : "Geçici Hedef Başladı" + "value" : "Glukoz şu anda çok düşükse ya da tahmine göre yakında düşebilecekse sizi uyarır." } } } }, - "Temporary Alert" : { - "comment" : "Alarm type name", + "This warns you if the glucose monitor stops sending readings for too long.." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Geçici Uyarı" + "value" : "Glukoz monitörü çok uzun süre okuma göndermezse sizi uyarır." } } } }, - "Temporary Target" : { + "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" : "Geçici Hedef" + "value" : "Telefonunuzun pili belirlediğiniz yüzde ve süreye göre hızla düşerse sizi uyarır." } } } }, - "Text Color" : { + "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" : "Metin Rengi" + "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." } } } }, - "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?" : { + "This warns you when the insulin pump is running low on insulin." : { + "extractionState" : "manual", "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ı?" + "value" : "İnsülin pompasındaki insülin azaldığında sizi uyarır." } } } }, - "That's your API secret — create a read-only token" : { + "This warns you when the phone's battery gets low, based on the percentage you choose." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu sizin API secret'ınız — salt okunur bir belirteç oluşturun" + "value" : "Telefonun pili seçtiğiniz yüzdeye göre azaldığında sizi uyarır." } } } }, - "The order of Fat and Protein inputs has changed." : { + "This warns you when the pump's battery gets low, based on the percentage you choose." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yağ ve Protein girişlerinin sırası değişti." + "value" : "Pompanın pili seçtiğiniz yüzdeye göre azaldığında sizi uyarır." } } } }, - "The top 4 items appear in the tab bar. The Menu can always open every feature." : { + "This will overwrite your current settings" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İlk 4 öğe sekme çubuğunda görünür. Menü her zaman tüm özellikleri açabilir." + "value" : "Bu, mevcut ayarlarınızın üzerine yazacak" } } } }, - "This TOTP code has already been used for a command. Please wait for the next code to be generated before sending another command." : { + "Threshold" : { + "extractionState" : "manual", "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." + "value" : "Eşik" } } } }, - "This will overwrite your current settings" : { + "Time" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bu, mevcut ayarlarınızın üzerine yazacak" + "value" : "Zaman" } } } }, - "Time" : { + "Time Remaining" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Zaman" + "value" : "Kalan Süre" } } } }, - "Time Remaining" : { + "Time Window" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kalan Süre" + "value" : "Zaman Penceresi" } } } @@ -5150,6 +7064,17 @@ } } }, + "Total Daily Dose" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toplam Günlük Doz" + } + } + } + }, "Treatments" : { "localizations" : { "tr" : { @@ -5181,6 +7106,17 @@ } } }, + "Trigger Level" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tetikleme Düzeyi" + } + } + } + }, "Trio Remote Control Settings" : { "localizations" : { "tr" : { @@ -5255,6 +7191,17 @@ } } }, + "Units" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ünite" + } + } + } + }, "Units and Metrics" : { "extractionState" : "manual", "localizations" : { @@ -5276,6 +7223,17 @@ } } }, + "Until" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Şu zamana kadar" + } + } + } + }, "Use Anyway" : { "localizations" : { "tr" : { @@ -5286,6 +7244,17 @@ } } }, + "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" : { @@ -5357,6 +7326,17 @@ } } }, + "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" : { @@ -5367,6 +7347,18 @@ } } }, + "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" : { @@ -5377,6 +7369,17 @@ } } }, + "Warning Time" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uyarı Süresi" + } + } + } + }, "We'll ask iOS for permission next. You can change this any time in the Settings app." : { "localizations" : { "tr" : { @@ -5438,6 +7441,28 @@ } } }, + "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" : { @@ -5508,6 +7533,28 @@ } } }, + "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." + } + } + } + }, + "days" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "gün" + } + } + } + }, "eHbA1c" : { "localizations" : { "tr" : { @@ -5532,10 +7579,32 @@ "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" : { diff --git a/LoopFollow/Stats/AggregatedStatsView.swift b/LoopFollow/Stats/AggregatedStatsView.swift index 0e9ad3a4d..21a6e117a 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 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/Scripts/localization/localized_apis.txt b/Scripts/localization/localized_apis.txt index c0928b85f..9fbf4ba02 100644 --- a/Scripts/localization/localized_apis.txt +++ b/Scripts/localization/localized_apis.txt @@ -23,3 +23,39 @@ 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 diff --git a/Scripts/localization/scan_keys.py b/Scripts/localization/scan_keys.py index c671a48a5..4e064f7fc 100755 --- a/Scripts/localization/scan_keys.py +++ b/Scripts/localization/scan_keys.py @@ -17,31 +17,97 @@ API_LIST = HERE / "localized_apis.txt" +def read_api_list(): + 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:]) + else: + apis.append(line.rstrip("(").rstrip(":")) + 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 + 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 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 + + def literal_keys(roots): - apis = [a.strip() for a in API_LIST.read_text().splitlines() if a.strip()] - names = [re.escape(a.rstrip("(").rstrip(":")) for a in apis] - pat = re.compile(r'(? = [] + 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())) } else { + apis.append(String(line.trimmingSuffix(while: { $0 == "(" || $0 == ":" }))) + } + } + let pattern = #"(? [(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 } + 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 { + 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 { From 051ea4610733388e4d179cd6dcd9d836bcaadd74 Mon Sep 17 00:00:00 2001 From: Ramazan TUNCER Date: Sun, 27 Sep 2026 11:24:29 +0300 Subject: [PATCH 08/11] Localize enum display names, info table rows, tab names and Menu search titles All displayName/name/title/label/blurb bodies go through String(localized:). SettingsLeaf localizes its title at init so Menu search matches the Turkish labels on screen (a localizedTitle: initializer covers titles that are already localized). AlarmSelectionView reuses AlarmType.displayName instead of its own copy of the 24 names. The key scanner and test now honor "Name(label:" entries, so String(localized:) literals are collected only from their localized: argument. Co-Authored-By: Claude Fable 5.1 --- LoopFollow/Alarm/Alarm.swift | 48 +- .../AlarmEditing/Components/SoundFile.swift | 2 +- .../Alarm/AlarmType/AlarmType+timeUnit.swift | 8 +- LoopFollow/Contact/ContactColorMode.swift | 4 +- LoopFollow/Helpers/AppearanceMode.swift | 6 +- .../Helpers/PredictionDisplayType.swift | 4 +- LoopFollow/Helpers/TabPosition.swift | 24 +- LoopFollow/InfoTable/InfoType.swift | 42 +- LoopFollow/Remote/TRC/TRCCommandType.swift | 12 +- LoopFollow/Resources/Localizable.xcstrings | 945 +++++++++++++++++- .../ImportExport/AlarmSelectionView.swift | 55 +- LoopFollow/Settings/SettingsMenuView.swift | 45 +- .../Settings/TabCustomizationModal.swift | 2 +- .../Stats/DataAvailabilityCalculator.swift | 8 +- LoopFollow/Treatments/TreatmentsView.swift | 28 +- LoopFollow/ViewControllers/MoreMenuView.swift | 6 +- Scripts/localization/localized_apis.txt | 1 + Scripts/localization/scan_keys.py | 16 +- Tests/LocalizationEnumNameTests.swift | 68 ++ Tests/LocalizationSourceLintTests.swift | 15 +- 20 files changed, 1155 insertions(+), 184 deletions(-) create mode 100644 Tests/LocalizationEnumNameTests.swift diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 3b9c760a9..024778605 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -454,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/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/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/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/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/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/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/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/Resources/Localizable.xcstrings b/LoopFollow/Resources/Localizable.xcstrings index 7075a2ae1..2a350d667 100644 --- a/LoopFollow/Resources/Localizable.xcstrings +++ b/LoopFollow/Resources/Localizable.xcstrings @@ -463,6 +463,17 @@ } } }, + "Advanced" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gelişmiş" + } + } + } + }, "Advanced Settings" : { "localizations" : { "tr" : { @@ -515,6 +526,17 @@ } } }, + "Alarms" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alarmlar" + } + } + } + }, "Alarms can behave differently during the day and at night." : { "localizations" : { "tr" : { @@ -645,6 +667,28 @@ } } }, + "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" : { @@ -820,6 +864,17 @@ } } }, + "All Boluses" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tüm Boluslar" + } + } + } + }, "All Sounds Muted" : { "extractionState" : "manual", "localizations" : { @@ -989,6 +1044,39 @@ } } }, + "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" + } + } + } + }, + "Autosens" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autosens" + } + } + } + }, "Auto‑Snooze CGM Start" : { "extractionState" : "manual", "localizations" : { @@ -1076,6 +1164,17 @@ } } }, + "BG Update Delay" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Güncelleme Gecikmesi" + } + } + } + }, "BG Update Delay (Sec): %@" : { "localizations" : { "tr" : { @@ -1108,6 +1207,17 @@ } } }, + "Background Refresh" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arka Plan Yenileme" + } + } + } + }, "Background Refresh Type" : { "extractionState" : "manual", "localizations" : { @@ -1119,6 +1229,17 @@ } } }, + "Basal" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bazal" + } + } + } + }, "Basal / BG Scale" : { "extractionState" : "manual", "localizations" : { @@ -1130,6 +1251,17 @@ } } }, + "Battery" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pil" + } + } + } + }, "Battery Drop" : { "comment" : "Alarm type name", "extractionState" : "manual", @@ -1142,6 +1274,17 @@ } } }, + "Battery drops quickly." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pil hızla düşüyor." + } + } + } + }, "Below" : { "extractionState" : "manual", "localizations" : { @@ -1301,6 +1444,17 @@ } } }, + "CAGE" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CAGE" + } + } + } + }, "CHARGING" : { "localizations" : { "tr" : { @@ -1333,6 +1487,17 @@ } } }, + "CR" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "CR" + } + } + } + }, "CV" : { "localizations" : { "tr" : { @@ -1478,6 +1643,28 @@ } } }, + "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" : { @@ -1639,6 +1826,17 @@ } } }, + "Cone" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koni" + } + } + } + }, "Configure Loop APNS" : { "localizations" : { "tr" : { @@ -1784,6 +1982,17 @@ } } }, + "Contact" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kişi" + } + } + } + }, "Contact Integration" : { "localizations" : { "tr" : { @@ -1938,6 +2147,17 @@ } } }, + "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" : { @@ -1958,6 +2178,28 @@ } } }, + "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" : { @@ -2196,6 +2438,17 @@ } } }, + "Dexcom" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dexcom" + } + } + } + }, "Dexcom Settings" : { "localizations" : { "tr" : { @@ -2460,6 +2713,17 @@ } } }, + "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" : { @@ -2735,6 +2999,17 @@ } } }, + "Excellent" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mükemmel" + } + } + } + }, "Existing Temp Target" : { "localizations" : { "tr" : { @@ -2828,6 +3103,17 @@ } } }, + "Fair" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Orta" + } + } + } + }, "Falls by" : { "extractionState" : "manual", "localizations" : { @@ -3074,6 +3360,17 @@ } } }, + "Good" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İyi" + } + } + } + }, "Grams" : { "extractionState" : "manual", "localizations" : { @@ -3085,6 +3382,17 @@ } } }, + "Graph" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grafik" + } + } + } + }, "Graph Basal" : { "extractionState" : "manual", "localizations" : { @@ -3150,12 +3458,23 @@ } } }, - "Grid Slots - Live Activity" : { + "Grid Slots" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Izgara Yuvaları - Canlı Etkinlik" + "value" : "Izgara Yuvaları" + } + } + } + }, + "Grid Slots - Live Activity" : { + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Izgara Yuvaları - Canlı Etkinlik" } } } @@ -3268,6 +3587,28 @@ } } }, + "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" : { @@ -3301,6 +3642,17 @@ } } }, + "Hours of Prediction" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tahmin Saati" + } + } + } + }, "How far back to look for that drop." : { "extractionState" : "manual", "localizations" : { @@ -3375,6 +3727,17 @@ } } }, + "IAGE" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "IAGE" + } + } + } + }, "IOB" : { "localizations" : { "tr" : { @@ -3408,6 +3771,17 @@ } } }, + "ISF" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "ISF" + } + } + } + }, "Ignore Zero BG" : { "extractionState" : "manual", "localizations" : { @@ -3505,6 +3879,17 @@ } } }, + "Import/Export" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İçe/Dışa Aktar" + } + } + } + }, "Import/Export Settings" : { "localizations" : { "tr" : { @@ -3578,6 +3963,17 @@ } } }, + "Information Display" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilgi Görüntüleme" + } + } + } + }, "Information Display Settings" : { "localizations" : { "tr" : { @@ -3714,6 +4110,17 @@ } } }, + "Light" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Açık" + } + } + } + }, "Line 1" : { "extractionState" : "manual", "localizations" : { @@ -3736,6 +4143,17 @@ } } }, + "Lines" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çizgiler" + } + } + } + }, "Live Activity" : { "localizations" : { "tr" : { @@ -3860,6 +4278,28 @@ } } }, + "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" : { @@ -3913,6 +4353,17 @@ } } }, + "Looping-app build expiring." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Loop uygulaması derlemesinin süresi doluyor." + } + } + } + }, "Low" : { "extractionState" : "manual", "localizations" : { @@ -3970,6 +4421,17 @@ } } }, + "Manual" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manuel" + } + } + } + }, "Max" : { "localizations" : { "tr" : { @@ -4146,6 +4608,28 @@ } } }, + "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" : { @@ -4284,6 +4768,17 @@ } } }, + "Nightscout" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nightscout" + } + } + } + }, "Nightscout Database Size" : { "comment" : "Alarm type name", "extractionState" : "manual", @@ -4306,6 +4801,28 @@ } } }, + "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" : { @@ -4608,6 +5125,17 @@ } } }, + "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" : { @@ -4717,6 +5245,28 @@ } } }, + "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" : { @@ -4877,6 +5427,17 @@ } } }, + "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" : { @@ -4962,6 +5523,17 @@ } } }, + "Poor" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zayıf" + } + } + } + }, "Pre-Bolus Time" : { "extractionState" : "manual", "localizations" : { @@ -5090,6 +5662,17 @@ } } }, + "Profile" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profil" + } + } + } + }, "Programmed Basal" : { "extractionState" : "manual", "localizations" : { @@ -5112,6 +5695,28 @@ } } }, + "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", @@ -5147,14 +5752,36 @@ } } }, - "Pump Insulin Alert" : { - "comment" : "Alarm type name", + "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 İnsülini Uyarısı" + "value" : "Pompa değişim zamanı." } } } @@ -5225,6 +5852,28 @@ } } }, + "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" : { @@ -5290,6 +5939,17 @@ } } }, + "Recommended bolus issued." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Önerilen bolus verildi." + } + } + } + }, "Reconnecting..." : { "localizations" : { "tr" : { @@ -5322,6 +5982,17 @@ } } }, + "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" : { @@ -5407,6 +6078,28 @@ } } }, + "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" + } + } + } + }, "Review setup anyway" : { "localizations" : { "tr" : { @@ -5491,6 +6184,28 @@ } } }, + "SAGE" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "SAGE" + } + } + } + }, + "SMB" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "SMB" + } + } + } + }, "SNOOZE" : { "localizations" : { "tr" : { @@ -5912,6 +6627,17 @@ } } }, + "Sensor change due." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensör değişim zamanı." + } + } + } + }, "Server" : { "extractionState" : "manual", "localizations" : { @@ -6042,6 +6768,17 @@ } } }, + "Show Days Back" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gösterilecek Geçmiş Gün" + } + } + } + }, "Show Delta" : { "extractionState" : "manual", "localizations" : { @@ -6237,6 +6974,17 @@ } } }, + "Small Graph Height" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Küçük Grafik Yüksekliği" + } + } + } + }, "Snooze & Mute Options" : { "localizations" : { "tr" : { @@ -6331,6 +7079,17 @@ } } }, + "Snoozer" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Erteleyici" + } + } + } + }, "Snoozer emoji" : { "extractionState" : "manual", "localizations" : { @@ -6383,6 +7142,28 @@ } } }, + "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" : { @@ -6497,6 +7278,28 @@ } } }, + "System" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sistem" + } + } + } + }, + "TDD" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "TDD" + } + } + } + }, "TIR" : { "localizations" : { "tr" : { @@ -6563,6 +7366,50 @@ } } }, + "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" : { @@ -6701,6 +7548,28 @@ } } }, + "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", @@ -6785,6 +7654,28 @@ } } }, + "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" : { @@ -7117,6 +8008,17 @@ } } }, + "Trio Remote Control" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Trio Uzaktan Kumanda" + } + } + } + }, "Trio Remote Control Settings" : { "localizations" : { "tr" : { @@ -7234,6 +8136,17 @@ } } }, + "Updated" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncellendi" + } + } + } + }, "Use Anyway" : { "localizations" : { "tr" : { @@ -7285,6 +8198,17 @@ } } }, + "User Name" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kullanıcı Adı" + } + } + } + }, "Username" : { "localizations" : { "tr" : { @@ -7622,6 +8546,17 @@ "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" : { 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..41b7ddc56 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"]), 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/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/Treatments/TreatmentsView.swift b/LoopFollow/Treatments/TreatmentsView.swift index f1b1c7595..787e18659 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") } } 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/Scripts/localization/localized_apis.txt b/Scripts/localization/localized_apis.txt index 9fbf4ba02..a256e08a0 100644 --- a/Scripts/localization/localized_apis.txt +++ b/Scripts/localization/localized_apis.txt @@ -59,3 +59,4 @@ AlarmStepperSection !with BGPicker bgPicker +SettingsLeaf diff --git a/Scripts/localization/scan_keys.py b/Scripts/localization/scan_keys.py index 4e064f7fc..d049dc0eb 100755 --- a/Scripts/localization/scan_keys.py +++ b/Scripts/localization/scan_keys.py @@ -18,15 +18,21 @@ def read_api_list(): - apis, ignored = [], set() + """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.append(line.rstrip("(").rstrip(":")) + apis[line] = None return apis, ignored @@ -91,16 +97,16 @@ def literal_keys(roots): for m in call.finditer(src): if m.group(1) == "Text" and src[m.start():m.start() + 14].startswith("Text(verbatim"): continue + required = apis.get(m.group(1)) for label, lit, interpolated in call_literals(src, m.end() - 1): if label in ignored or not lit.strip(): continue - after = src[src.index('"', m.end()) :] + if required is not None and label != required: + continue if interpolated: skipped.append((f.name, lit)) continue keys.setdefault(lit.replace('\\"', '"').replace("\\n", "\n"), f.name) - if label is None and m.group(1) != "String": - pass # "…" + x concatenation is never looked up: drop any key that only appears before a "+" # remove literals that are immediately followed by a "+" (String concatenation) for root in roots: 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 index b743468ba..b7fcdb7a9 100644 --- a/Tests/LocalizationSourceLintTests.swift +++ b/Tests/LocalizationSourceLintTests.swift @@ -51,16 +51,21 @@ struct LocalizationSourceLintTests { @Test("every literal passed to a localized API is a catalog key") func scannedLiteralsExistInCatalog() throws { let listURL = Self.repoRoot.appendingPathComponent("Scripts/localization/localized_apis.txt") - var apis: [String] = [] + // api name -> 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())) } else { - apis.append(String(line.trimmingSuffix(while: { $0 == "(" || $0 == ":" }))) + 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[.. Date: Sun, 27 Sep 2026 11:34:37 +0300 Subject: [PATCH 09/11] Localize string constants, ternaries and remote-command messages Every user-facing String literal assigned to a variable, chosen by a ternary or reported as a remote-command status/error goes through String(localized:); server response bodies stay interpolated arguments. The meal confirmation and the scanned-settings prompts are whole sentences instead of concatenated fragments. Adds a lint that rejects raw literal ternaries in String positions, ternary-branch support in the key scanner/test, and an opt-in heuristic (--interpolations) that turns interpolated String(localized:) literals into printf-style keys for review. Co-Authored-By: Claude Fable 5.1 --- LoopFollow/Application/AppDelegate.swift | 4 +- .../MainViewController+updateStats.swift | 4 +- .../Controllers/StatsDisplayModel.swift | 4 +- LoopFollow/Helpers/AuthService.swift | 8 +- LoopFollow/Helpers/TextFieldWithToolBar.swift | 6 +- .../NightscoutSettingsViewModel.swift | 40 +- .../Remote/LoopAPNS/LoopAPNSBolusView.swift | 14 +- .../Remote/LoopAPNS/LoopAPNSCarbsView.swift | 14 +- .../Remote/LoopAPNS/LoopAPNSService.swift | 50 +- .../Remote/LoopAPNS/OverridePresetsView.swift | 10 +- LoopFollow/Remote/RemoteCommandMessage.swift | 2 +- .../Settings/RemoteCommandSettings.swift | 14 +- LoopFollow/Remote/TRC/BolusView.swift | 4 +- LoopFollow/Remote/TRC/MealView.swift | 15 +- .../Remote/TRC/PushNotificationManager.swift | 14 +- LoopFollow/Resources/Localizable.xcstrings | 1189 ++++++++++++++++- LoopFollow/Settings/APNSettingsView.swift | 4 +- .../Settings/DexcomSettingsViewModel.swift | 14 +- LoopFollow/Stats/AggregatedStatsView.swift | 7 +- LoopFollow/Treatments/TreatmentsView.swift | 2 +- Scripts/localization/scan_keys.py | 63 +- Tests/LocalizationSourceLintTests.swift | 46 +- 22 files changed, 1397 insertions(+), 131 deletions(-) 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/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/StatsDisplayModel.swift b/LoopFollow/Controllers/StatsDisplayModel.swift index 72e60560a..16cb180a8 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 = .init(localized: "Est A1C:") @Published var stdDev: String = "" - @Published var stdDevTitle: String = "Std Dev:" + @Published var stdDevTitle: String = .init(localized: "Std Dev:") @Published var pieLow: Double = 0 @Published var pieRange: Double = 0 @Published var pieHigh: Double = 0 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/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/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 1065cc892..f2abd0e50 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -33,7 +33,7 @@ class NightscoutSettingsViewModel: ObservableObject { } } - @Published var nightscoutStatus: String = "Checking..." + @Published var nightscoutStatus: String = .init(localized: "Checking...") /// 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 +70,7 @@ class NightscoutSettingsViewModel: ObservableObject { } } - @Published var webSocketStatus: String = "Disconnected" + @Published var webSocketStatus: String = .init(localized: "Disconnected") var webSocketStatusColor: Color { switch NightscoutSocketManager.shared.connectionState { @@ -130,7 +130,7 @@ class NightscoutSettingsViewModel: ObservableObject { // "this is your API secret" verdict. provisionedTokenPending = false tokenIsVerifiedSecret = false - nightscoutStatus = "Checking..." + nightscoutStatus = String(localized: "Checking...") checkStatusWorkItem = DispatchWorkItem { self.checkStatusSubject.send() @@ -196,7 +196,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 +213,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) @@ -321,19 +321,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() @@ -356,7 +356,7 @@ class NightscoutSettingsViewModel: ObservableObject { authStatus = "Read" + (Storage.shared.nsWriteAuth.value ? " & Write" : "") } - nightscoutStatus = "OK (\(authStatus))" + nightscoutStatus = String(localized: "OK (\(authStatus))") if nightscoutURL != initialURL || nightscoutToken != initialToken { NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) @@ -400,7 +400,9 @@ class NightscoutSettingsViewModel: ObservableObject { case .idle: return "Enter your site address to connect." 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." case .pending: @@ -426,11 +428,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/Remote/LoopAPNS/LoopAPNSBolusView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift index 0a2a2848a..e1e5b37a0 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift @@ -10,7 +10,7 @@ struct LoopAPNSBolusView: View { @State private var insulinAmount = HKQuantity(unit: .internationalUnit(), doubleValue: 0.0) @State private var isLoading = false @State private var showAlert = false - @State private var alertMessage = "" + @State private var alertMessage = String(localized: "") @State private var alertType: AlertType = .success @ObservedObject private var quickPickBoluses = QuickPickBolusesManager.shared @@ -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..5b2ff7e08 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -18,7 +18,7 @@ struct LoopAPNSCarbsView: View { @State private var showAbsorptionPickerSheet = false @State private var isLoading = false @State private var showAlert = false - @State private var alertMessage = "" + @State private var alertMessage = String(localized: "") @State private var alertType: AlertType = .success @State private var otpTimeRemaining: Int? = nil @State private var showTOTPWarning = false @@ -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..ead7a7959 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift @@ -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..9254f15f8 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 } 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..17c25430d 100644 --- a/LoopFollow/Remote/Settings/RemoteCommandSettings.swift +++ b/LoopFollow/Remote/Settings/RemoteCommandSettings.swift @@ -161,7 +161,7 @@ struct RemoteCommandSettings: Codable { var shouldPromptForURL = false var shouldPromptForToken = false - var message = "" + var message = String(localized: "") // Check if current user has URL set let hasCurrentURL = !currentURL.isEmpty @@ -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/TRC/BolusView.swift b/LoopFollow/Remote/TRC/BolusView.swift index 5e5795eaa..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: @@ -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..420c31513 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -238,27 +238,26 @@ 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 { @@ -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/Resources/Localizable.xcstrings b/LoopFollow/Resources/Localizable.xcstrings index 2a350d667..9c9c11030 100644 --- a/LoopFollow/Resources/Localizable.xcstrings +++ b/LoopFollow/Resources/Localizable.xcstrings @@ -95,6 +95,17 @@ } } }, + "%@ override activated successfully." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ override'ı başarıyla etkinleştirildi." + } + } + } + }, "%@ sec" : { "localizations" : { "tr" : { @@ -340,6 +351,17 @@ } } }, + "Acknowledge" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Onayla" + } + } + } + }, "Activate Override" : { "localizations" : { "tr" : { @@ -391,6 +413,17 @@ } } }, + "Active override cancelled successfully." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Etkin override başarıyla iptal edildi." + } + } + } + }, "Add Alarm" : { "extractionState" : "manual", "localizations" : { @@ -442,6 +475,17 @@ } } }, + "Address Empty" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adres Boş" + } + } + } + }, "Adjust the background refresh type." : { "localizations" : { "tr" : { @@ -938,6 +982,17 @@ } } }, + "An error occurred." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir hata oluştu." + } + } + } + }, "App Settings" : { "extractionState" : "manual", "localizations" : { @@ -1000,6 +1055,28 @@ } } }, + "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" : { @@ -1044,6 +1121,28 @@ } } }, + "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" : { @@ -1066,6 +1165,17 @@ } } }, + "Automatic Bolus" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otomatik Bolus" + } + } + } + }, "Autosens" : { "extractionState" : "manual", "localizations" : { @@ -1164,6 +1274,28 @@ } } }, + "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" : { @@ -1229,6 +1361,17 @@ } } }, + "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" : { @@ -1444,6 +1587,17 @@ } } }, + "Bundle identifier not configured" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paket kimliği yapılandırılmamış" + } + } + } + }, "CAGE" : { "extractionState" : "manual", "localizations" : { @@ -1643,6 +1797,17 @@ } } }, + "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" : { @@ -1676,6 +1841,17 @@ } } }, + "Carbs: %.0f g" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat: %.0f g" + } + } + } + }, "Category" : { "extractionState" : "manual", "localizations" : { @@ -1708,6 +1884,39 @@ } } }, + "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" : { @@ -1949,6 +2158,17 @@ } } }, + "Connecting..." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlanıyor..." + } + } + } + }, "Consecutive Drops" : { "extractionState" : "manual", "localizations" : { @@ -2087,6 +2307,17 @@ } } }, + "Credential validation failed: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kimlik bilgisi doğrulaması başarısız: %@" + } + } + } + }, "Current OTP Code" : { "localizations" : { "tr" : { @@ -2422,6 +2653,28 @@ } } }, + "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" : { @@ -2500,6 +2753,17 @@ } } }, + "Disconnected" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlantı yok" + } + } + } + }, "Disconnected for %@" : { "localizations" : { "tr" : { @@ -2969,6 +3233,17 @@ } } }, + "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" : { @@ -2989,6 +3264,28 @@ } } }, + "Est A1C:" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tah. A1C:" + } + } + } + }, + "Est. A1C:" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tah. A1C:" + } + } + } + }, "Eventual" : { "localizations" : { "tr" : { @@ -3093,57 +3390,189 @@ } } }, - "Failed to generate QR code" : { + "Face ID is not available. Please set up a device passcode in Settings." : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "QR kodu oluşturulamadı" + "value" : "Face ID kullanılamıyor. Lütfen Ayarlar'dan bir cihaz parolası ayarlayın." } } } }, - "Fair" : { + "Failed to activate override: %@" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Orta" + "value" : "Override etkinleştirilemedi: %@" } } } }, - "Falls by" : { + "Failed to cancel override: %@" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Düşüş miktarı" + "value" : "Override iptal edilemedi: %@" } } } }, - "Fast Drop Alert" : { - "comment" : "Alarm type name", + "Failed to construct APNs URL" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hızlı Düşüş Uyarısı" + "value" : "APNs URL'si oluşturulamadı" } } } }, - "Fast Drop Delta" : { + "Failed to encode or encrypt push message: %@" : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hızlı Düşüş Deltası" + "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ı" } } } @@ -3171,6 +3600,17 @@ } } }, + "Fat: %.0f g" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yağ: %.0f g" + } + } + } + }, "Features" : { "extractionState" : "manual", "localizations" : { @@ -3203,6 +3643,17 @@ } } }, + "Finishing connection…" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlantı tamamlanıyor…" + } + } + } + }, "Food Type" : { "localizations" : { "tr" : { @@ -3266,6 +3717,17 @@ } } }, + "GMI:" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "GMI:" + } + } + } + }, "GRI (Glucose Risk Index)" : { "localizations" : { "tr" : { @@ -4027,6 +4489,17 @@ } } }, + "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" : { @@ -4037,6 +4510,37 @@ } } }, + "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" : { @@ -4047,6 +4551,72 @@ } } }, + "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ı. %@" + } + } + } + }, "Italian" : { "localizations" : { "tr" : { @@ -4099,6 +4669,17 @@ } } }, + "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" : { @@ -4164,6 +4745,17 @@ } } }, + "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" : { @@ -4268,6 +4860,17 @@ } } }, + "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" : { @@ -4278,6 +4881,17 @@ } } }, + "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" : { @@ -4513,6 +5127,17 @@ } } }, + "Maximum: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maksimum: %@" + } + } + } + }, "Meal" : { "extractionState" : "manual", "localizations" : { @@ -4651,6 +5276,17 @@ } } }, + "Minimum: %@\n" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Minimum: %@\n" + } + } + } + }, "Minutes" : { "extractionState" : "manual", "localizations" : { @@ -4697,6 +5333,17 @@ } } }, + "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" : { @@ -4717,6 +5364,28 @@ } } }, + "Network Error" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağ Hatası" + } + } + } + }, + "Network error — check your connection" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağ hatası — bağlantınızı denetleyin" + } + } + } + }, "Never" : { "comment" : "Alarm schedule option: never", "extractionState" : "manual", @@ -4977,6 +5646,17 @@ } } }, + "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" : { @@ -5064,6 +5744,17 @@ } } }, + "OFF" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KAPALI" + } + } + } + }, "OK" : { "localizations" : { "tr" : { @@ -5074,6 +5765,28 @@ } } }, + "OK (%@)" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tamam (%@)" + } + } + } + }, + "ON" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "AÇIK" + } + } + } + }, "Off" : { "localizations" : { "tr" : { @@ -5094,6 +5807,17 @@ } } }, + "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" : { @@ -5351,6 +6075,17 @@ } } }, + "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" : { @@ -5492,6 +6227,39 @@ } } }, + "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 your Nightscout URL in Settings." : { "localizations" : { "tr" : { @@ -5695,6 +6463,17 @@ } } }, + "Protein: %.0f g" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Protein: %.0f g" + } + } + } + }, "Pump" : { "extractionState" : "manual", "localizations" : { @@ -6015,6 +6794,17 @@ } } }, + "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" : { @@ -6258,6 +7048,17 @@ } } }, + "Scan QR Code" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR Kodu Tara" + } + } + } + }, "Scan QR Code from Loop App" : { "localizations" : { "tr" : { @@ -6278,6 +7079,17 @@ } } }, + "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" : { @@ -6649,6 +7461,17 @@ } } }, + "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" : { @@ -6900,6 +7723,17 @@ } } }, + "Site Not Found" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Site Bulunamadı" + } + } + } + }, "Site URL" : { "localizations" : { "tr" : { @@ -6985,6 +7819,17 @@ } } }, + "Snooze" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ertele" + } + } + } + }, "Snooze & Mute Options" : { "localizations" : { "tr" : { @@ -7227,22 +8072,33 @@ } } }, - "Std Dev" : { + "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" + "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 — Standard Deviation, how much glucose swings around the average. CV — Coefficient of Variation, that swing relative to the average (Std Dev ÷ mean)." : { + "Std Dev:" : { + "extractionState" : "manual", "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)." + "value" : "Std Sapma:" } } } @@ -7634,6 +8490,17 @@ } } }, + "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" : { @@ -7644,6 +8511,61 @@ } } }, + "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 top 4 items appear in the tab bar. The Menu can always open every feature." : { "localizations" : { "tr" : { @@ -7774,6 +8696,36 @@ } } }, + "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" : { @@ -7925,6 +8877,28 @@ } } }, + "Time must be within the next %lld hour" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saat gelecek %lld saat içinde olmalı" + } + } + } + }, + "Time must be within the prior %lld hours" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Saat geçmiş %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" : { @@ -7945,6 +8919,28 @@ } } }, + "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" + } + } + } + }, "Tone" : { "localizations" : { "tr" : { @@ -7955,6 +8951,17 @@ } } }, + "Too many requests. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Çok fazla istek. %@" + } + } + } + }, "Total Daily Dose" : { "extractionState" : "manual", "localizations" : { @@ -7966,6 +8973,17 @@ } } }, + "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" : { @@ -8039,6 +9057,17 @@ } } }, + "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" : { @@ -8093,6 +9122,17 @@ } } }, + "Unexpected status code: %lld. %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beklenmeyen durum kodu: %1$lld. %2$@" + } + } + } + }, "Units" : { "extractionState" : "manual", "localizations" : { @@ -8115,6 +9155,17 @@ } } }, + "Unknown" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilinmiyor" + } + } + } + }, "Unknown Alert" : { "localizations" : { "tr" : { @@ -8125,6 +9176,28 @@ } } }, + "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" : { @@ -8219,6 +9292,37 @@ } } }, + "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" : { @@ -8229,6 +9333,17 @@ } } }, + "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" : { @@ -8437,6 +9552,28 @@ } } }, + "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" : { @@ -8468,6 +9605,28 @@ } } }, + "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" : { 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/DexcomSettingsViewModel.swift b/LoopFollow/Settings/DexcomSettingsViewModel.swift index 5a929cbe6..0961a642f 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 = .init(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/Stats/AggregatedStatsView.swift b/LoopFollow/Stats/AggregatedStatsView.swift index 21a6e117a..850811e27 100644 --- a/LoopFollow/Stats/AggregatedStatsView.swift +++ b/LoopFollow/Stats/AggregatedStatsView.swift @@ -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/Treatments/TreatmentsView.swift b/LoopFollow/Treatments/TreatmentsView.swift index 787e18659..1418d3078 100644 --- a/LoopFollow/Treatments/TreatmentsView.swift +++ b/LoopFollow/Treatments/TreatmentsView.swift @@ -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/Scripts/localization/scan_keys.py b/Scripts/localization/scan_keys.py index d049dc0eb..79332333b 100755 --- a/Scripts/localization/scan_keys.py +++ b/Scripts/localization/scan_keys.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 """Add SwiftUI/Foundation string-literal keys from the source tree to a String Catalog. -Usage: scan_keys.py [--check] [--roots LoopFollow Shared ...] +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 (the compiler emits those with printf specifiers; add them from an --exportLocalizations run). Keys added here carry extractionState "manual". With --check, +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 @@ -14,6 +15,7 @@ import sys HERE = pathlib.Path(__file__).resolve().parent +INTERPOLATIONS = False API_LIST = HERE / "localized_apis.txt" @@ -53,6 +55,19 @@ def call_literals(src, open_paren): 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 @@ -75,6 +90,10 @@ def call_literals(src, open_paren): 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 @@ -86,6 +105,40 @@ def call_literals(src, open_paren): 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) @@ -105,6 +158,8 @@ def literal_keys(roots): continue if interpolated: skipped.append((f.name, lit)) + if INTERPOLATIONS and required == "localized": + keys.setdefault(interpolation_key(lit).replace('\\"', '"').replace("\\n", "\n"), f"{f.name} (interpolated from: {lit})") continue keys.setdefault(lit.replace('\\"', '"').replace("\\n", "\n"), f.name) # "…" + x concatenation is never looked up: drop any key that only appears before a "+" @@ -124,6 +179,8 @@ def main() -> int: 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"] diff --git a/Tests/LocalizationSourceLintTests.swift b/Tests/LocalizationSourceLintTests.swift index b7fcdb7a9..2e1ebe1ee 100644 --- a/Tests/LocalizationSourceLintTests.swift +++ b/Tests/LocalizationSourceLintTests.swift @@ -91,6 +91,32 @@ struct LocalizationSourceLintTests { #expect(missing.isEmpty, Comment(rawValue: "Run Scripts/localization/scan_keys.py and translate:\n" + missing.sorted().joined(separator: "\n"))) } + /// `cond ? "A" : "B"` assigned to a variable, returned, or passed to a String parameter never + /// reaches the catalog. (Inside Text/Button and LocalizedStringKey parameters the literals are + /// keys and are fine; those positions are not matched here.) + @Test("no user-facing ternary picks between raw string literals") + func noRawTernaryStrings() throws { + // "=" means assignment here, not "==", "!=", "<=" or ">=". + 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)] { @@ -106,7 +132,20 @@ struct LocalizationSourceLintTests { var interpolated = false while j < chars.count, chars[j] != "\"" { if chars[j] == "\\" { - if j + 1 < chars.count, chars[j + 1] == "(" { interpolated = true } + 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 } @@ -132,8 +171,11 @@ struct LocalizationSourceLintTests { 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 { + 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 = "" From a2082cc73047ff66683d518ec6071bcd4697579f Mon Sep 17 00:00:00 2001 From: Ramazan TUNCER Date: Sun, 27 Sep 2026 11:39:01 +0300 Subject: [PATCH 10/11] Localize main screen, UIKit alerts and chart pill texts Minutes-ago, LOW/HIGH, Not Looping, loading and setup-button strings, UIKit alerts, the log search placeholder, the QR scanner title, the treatment pill details and the suspend/resume chart pills go through String(localized:). The OTP countdown becomes a single interpolated key instead of concatenation. Double amounts keep their compact formatting via an explicit %g specifier. Co-Authored-By: Claude Fable 5.1 --- LoopFollow/Charts/BGChartModel.swift | 14 +- .../Controllers/Nightscout/BGData.swift | 4 +- .../Controllers/Nightscout/DeviceStatus.swift | 2 +- .../Views/SimpleQRCodeScannerView.swift | 2 +- LoopFollow/Log/SearchBar.swift | 2 +- .../Remote/LoopAPNS/LoopAPNSBolusView.swift | 2 +- .../Remote/LoopAPNS/LoopAPNSCarbsView.swift | 2 +- .../Remote/Settings/RemoteSettingsView.swift | 2 +- LoopFollow/Resources/Localizable.xcstrings | 290 ++++++++++++++++++ LoopFollow/Settings/ContactSettingsView.swift | 8 +- LoopFollow/Settings/SettingsMenuView.swift | 2 +- .../ViewControllers/MainViewController.swift | 18 +- .../NightScoutViewController.swift | 4 +- Tests/LocalizationDisplayNameTests.swift | 12 + 14 files changed, 333 insertions(+), 31 deletions(-) diff --git a/LoopFollow/Charts/BGChartModel.swift b/LoopFollow/Charts/BGChartModel.swift index 8acc2859c..88e2b3681 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: ", ") + "]" @@ -499,10 +499,10 @@ final class BGChartModel: ObservableObject { ) } 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)))") 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/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/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/Remote/LoopAPNS/LoopAPNSBolusView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift index e1e5b37a0..38c20cb8a 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift @@ -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("(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") .font(.caption) .foregroundColor(.secondary) } diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index 5b2ff7e08..0972a8748 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("(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") .font(.caption) .foregroundColor(.secondary) } diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 6e9b2acaa..1395f1413 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("(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") .font(.caption) .foregroundColor(.secondary) } diff --git a/LoopFollow/Resources/Localizable.xcstrings b/LoopFollow/Resources/Localizable.xcstrings index 9c9c11030..acac41d98 100644 --- a/LoopFollow/Resources/Localizable.xcstrings +++ b/LoopFollow/Resources/Localizable.xcstrings @@ -125,6 +125,29 @@ "%@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" : { @@ -218,6 +241,17 @@ } } }, + "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" : { @@ -341,6 +375,40 @@ } } }, + "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" : { @@ -351,6 +419,28 @@ } } }, + "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" : { @@ -993,6 +1083,28 @@ } } }, + "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." + } + } + } + }, + "App Expiration Warning" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uygulama Süre Sonu Uyarısı" + } + } + } + }, "App Settings" : { "extractionState" : "manual", "localizations" : { @@ -1523,6 +1635,18 @@ } } }, + "Bolus: %g U" : { + "comment" : "Treatment pill detail", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus: %g U" + } + } + } + }, "Boluses" : { "extractionState" : "manual", "localizations" : { @@ -1852,6 +1976,18 @@ } } }, + "Carbs: %g g" : { + "comment" : "Treatment pill detail", + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Karbonhidrat: %g g" + } + } + } + }, "Category" : { "extractionState" : "manual", "localizations" : { @@ -3695,6 +3831,17 @@ } } }, + "From: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kaynak: %@" + } + } + } + }, "Future Carbs Alert" : { "comment" : "Alarm type name", "extractionState" : "manual", @@ -3951,6 +4098,17 @@ } } }, + "HIGH" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "YÜKSEK" + } + } + } + }, "Help us help you" : { "localizations" : { "tr" : { @@ -4648,6 +4806,17 @@ } } }, + "LOW" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "DÜŞÜK" + } + } + } + }, "Language" : { "extractionState" : "manual", "localizations" : { @@ -4807,6 +4976,17 @@ } } }, + "Loading..." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yükleniyor..." + } + } + } + }, "Logging" : { "extractionState" : "manual", "localizations" : { @@ -5787,6 +5967,17 @@ } } }, + "OTP: %@" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "OTP: %@" + } + } + } + }, "Off" : { "localizations" : { "tr" : { @@ -6217,6 +6408,17 @@ } } }, + "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" : { @@ -6750,6 +6952,17 @@ } } }, + "Refreshing" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yenileniyor" + } + } + } + }, "Reminder Time" : { "extractionState" : "manual", "localizations" : { @@ -6890,6 +7103,17 @@ } } }, + "Resume" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sürdür" + } + } + } + }, "Review setup anyway" : { "localizations" : { "tr" : { @@ -7162,6 +7386,17 @@ } } }, + "Search Log" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Günlükte Ara" + } + } + } + }, "Search alarms" : { "extractionState" : "manual", "localizations" : { @@ -7526,6 +7761,28 @@ } } }, + "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" : { @@ -8124,6 +8381,17 @@ } } }, + "Suspend" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Askıya Al" + } + } + } + }, "Swedish" : { "localizations" : { "tr" : { @@ -8663,6 +8931,17 @@ } } }, + "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" : { @@ -9742,6 +10021,17 @@ "− 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" : { 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/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 41b7ddc56..c976af833 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -371,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/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 06c5e1f76..42a165b83 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() @@ -743,7 +743,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { } 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 } } @@ -753,7 +753,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { func versionAlert(title: String = "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/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/Tests/LocalizationDisplayNameTests.swift b/Tests/LocalizationDisplayNameTests.swift index 28da45311..cea68aacb 100644 --- a/Tests/LocalizationDisplayNameTests.swift +++ b/Tests/LocalizationDisplayNameTests.swift @@ -93,4 +93,16 @@ struct LocalizationDisplayNameTests { #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 ["%lld min", "LOW", "HIGH", "⚠️ Not Looping!", "Refreshing", "Loading...", "Setup Nightscout", "Setup Dexcom Share", "%llds left"] { + 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")) + } } From 29c2b02ad086c3babecd0a9ac3e5d4b2c8fd9dd8 Mon Sep 17 00:00:00 2001 From: Ramazan TUNCER Date: Sun, 27 Sep 2026 12:06:42 +0300 Subject: [PATCH 11/11] Address review: language-independent checking state, main-screen min ago, remaining English sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NightscoutSettingsViewModel compared the (now localized) status text with the English literal "Checking...", so the onboarding pill lost its checking state in Turkish; the value is a shared constant compared by identity of lookup, with a test that runs in the tr host. - The main screen's minutes-ago text lives in MinAgoTask, not the calendar title; it is now "%@ min ago" (the duration can be "4:35"). - Sensor-age footer key added; chart pill labels (Bolus, Carbs, SMB, BG Check, Sensor Start, Override, Temp Target), Nightscout friendly status and auth level, token-provisioning and Nightscout/Loop APNS/ override-preset error descriptions, TIR titles, info-row color footers, onboarding fast-drop explanation and the update alerts go through String(localized:). - Property initializers drop the explicit String type so SwiftFormat does not rewrite them to .init(localized:), which the scanner cannot see; empty String(localized: "") artifacts and the "(%@)" OTP wrapper key are gone; the meal bolus line matches its siblings; "Est. A1C:" is the single key. - scan_keys.py refuses to add a heuristic key that differs from an existing key only by specifier types. - Turkish: "OK (%@)" → "Bağlı (%@)", clearer time-window messages. Co-Authored-By: Claude Fable 5.1 --- LoopFollow/Charts/BGChartModel.swift | 14 +- .../Controllers/StatsDisplayModel.swift | 4 +- LoopFollow/Helpers/NightscoutUtils.swift | 14 +- .../InfoRowSettingsView.swift | 4 +- .../NightscoutSettingsViewModel.swift | 37 +- .../Onboarding/Steps/AlarmsStepView.swift | 4 +- .../Remote/LoopAPNS/LoopAPNSBolusView.swift | 4 +- .../Remote/LoopAPNS/LoopAPNSCarbsView.swift | 4 +- .../Remote/LoopAPNS/LoopAPNSService.swift | 20 +- .../Remote/LoopAPNS/OverridePresetsView.swift | 6 +- .../Settings/RemoteCommandSettings.swift | 2 +- .../Remote/Settings/RemoteSettingsView.swift | 2 +- LoopFollow/Remote/TRC/MealView.swift | 2 +- LoopFollow/Resources/Localizable.xcstrings | 476 +++++++++++++++++- .../Settings/DexcomSettingsViewModel.swift | 2 +- LoopFollow/Stats/TIR/TIRView.swift | 6 +- LoopFollow/Task/MinAgoTask.swift | 2 +- .../ViewControllers/MainViewController.swift | 4 +- Scripts/localization/scan_keys.py | 9 +- Tests/LocalizationDisplayNameTests.swift | 16 +- 20 files changed, 555 insertions(+), 77 deletions(-) diff --git a/LoopFollow/Charts/BGChartModel.swift b/LoopFollow/Charts/BGChartModel.swift index 88e2b3681..be549a416 100644 --- a/LoopFollow/Charts/BGChartModel.swift +++ b/LoopFollow/Charts/BGChartModel.swift @@ -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,7 +495,7 @@ 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 { @@ -505,7 +505,7 @@ final class BGChartModel: ObservableObject { 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/Controllers/StatsDisplayModel.swift b/LoopFollow/Controllers/StatsDisplayModel.swift index 16cb180a8..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 = .init(localized: "Est A1C:") + @Published var estA1CTitle = String(localized: "Est. A1C:") @Published var stdDev: String = "" - @Published var stdDevTitle: String = .init(localized: "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/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/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/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index f2abd0e50..64ab54c77 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -33,7 +33,10 @@ class NightscoutSettingsViewModel: ObservableObject { } } - @Published var nightscoutStatus: String = .init(localized: "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 = .init(localized: "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 = String(localized: "Checking...") + nightscoutStatus = Self.checkingStatus checkStatusWorkItem = DispatchWorkItem { self.checkStatusSubject.send() @@ -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") } } @@ -351,9 +354,11 @@ 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 = String(localized: "OK (\(authStatus))") @@ -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,19 +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 ? 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 } diff --git a/LoopFollow/Onboarding/Steps/AlarmsStepView.swift b/LoopFollow/Onboarding/Steps/AlarmsStepView.swift index 74d2e1c02..2c20f0643 100644 --- a/LoopFollow/Onboarding/Steps/AlarmsStepView.swift +++ b/LoopFollow/Onboarding/Steps/AlarmsStepView.swift @@ -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/Remote/LoopAPNS/LoopAPNSBolusView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift index 38c20cb8a..1f98ae3f7 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSBolusView.swift @@ -10,7 +10,7 @@ struct LoopAPNSBolusView: View { @State private var insulinAmount = HKQuantity(unit: .internationalUnit(), doubleValue: 0.0) @State private var isLoading = false @State private var showAlert = false - @State private var alertMessage = String(localized: "") + @State private var alertMessage = "" @State private var alertType: AlertType = .success @ObservedObject private var quickPickBoluses = QuickPickBolusesManager.shared @@ -166,7 +166,7 @@ struct LoopAPNSBolusView: View { .padding(.horizontal, 8) .background(Color.green.opacity(0.1)) .cornerRadius(4) - Text("(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") + Text(verbatim: "(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") .font(.caption) .foregroundColor(.secondary) } diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift index 0972a8748..7d857df2e 100644 --- a/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift +++ b/LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift @@ -18,7 +18,7 @@ struct LoopAPNSCarbsView: View { @State private var showAbsorptionPickerSheet = false @State private var isLoading = false @State private var showAlert = false - @State private var alertMessage = String(localized: "") + @State private var alertMessage = "" @State private var alertType: AlertType = .success @State private var otpTimeRemaining: Int? = nil @State private var showTOTPWarning = false @@ -290,7 +290,7 @@ struct LoopAPNSCarbsView: View { .padding(.horizontal, 8) .background(Color.green.opacity(0.1)) .cornerRadius(4) - Text("(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") + Text(verbatim: "(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") .font(.caption) .foregroundColor(.secondary) } diff --git a/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift b/LoopFollow/Remote/LoopAPNS/LoopAPNSService.swift index ead7a7959..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") } } } diff --git a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift index 9254f15f8..265ccf10d 100644 --- a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift +++ b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift @@ -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/Settings/RemoteCommandSettings.swift b/LoopFollow/Remote/Settings/RemoteCommandSettings.swift index 17c25430d..926b72a20 100644 --- a/LoopFollow/Remote/Settings/RemoteCommandSettings.swift +++ b/LoopFollow/Remote/Settings/RemoteCommandSettings.swift @@ -161,7 +161,7 @@ struct RemoteCommandSettings: Codable { var shouldPromptForURL = false var shouldPromptForToken = false - var message = String(localized: "") + var message = "" // Check if current user has URL set let hasCurrentURL = !currentURL.isEmpty diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 1395f1413..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 { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") + Text(verbatim: "(\(otpTimeRemaining.map { String(localized: "\($0)s left", comment: "OTP seconds remaining") } ?? "-"))") .font(.caption) .foregroundColor(.secondary) } diff --git a/LoopFollow/Remote/TRC/MealView.swift b/LoopFollow/Remote/TRC/MealView.swift index 420c31513..82bfa5477 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -261,7 +261,7 @@ struct MealView: View { } if bolusAmount > 0 { - message += String(format: "\nBolus: %.2f U", bolusAmount) + message += "\n" + String(localized: "Bolus: \(bolusAmount, specifier: "%.2f") U") } return Alert( diff --git a/LoopFollow/Resources/Localizable.xcstrings b/LoopFollow/Resources/Localizable.xcstrings index acac41d98..f6d874c44 100644 --- a/LoopFollow/Resources/Localizable.xcstrings +++ b/LoopFollow/Resources/Localizable.xcstrings @@ -79,6 +79,17 @@ } } }, + "%@ min ago" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ dk önce" + } + } + } + }, "%@ of %@ expected readings" : { "localizations" : { "en" : { @@ -241,6 +252,17 @@ } } }, + "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" : { @@ -273,6 +295,17 @@ } } }, + "A token is required." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bir belirteç gerekli." + } + } + } + }, "API Secret" : { "localizations" : { "tr" : { @@ -586,6 +619,17 @@ } } }, + "Admin" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Yönetici" + } + } + } + }, "Advance Notice" : { "extractionState" : "manual", "localizations" : { @@ -1094,6 +1138,17 @@ } } }, + "An unknown error occurred." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bilinmeyen bir hata oluştu." + } + } + } + }, "App Expiration Warning" : { "extractionState" : "manual", "localizations" : { @@ -1375,6 +1430,17 @@ } } }, + "BG Check" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "KŞ Ölçümü" + } + } + } + }, "BG Limit" : { "extractionState" : "manual", "localizations" : { @@ -1635,6 +1701,17 @@ } } }, + "Bolus: %.2f U" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bolus: %.2f U" + } + } + } + }, "Bolus: %g U" : { "comment" : "Treatment pill detail", "extractionState" : "manual", @@ -2294,6 +2371,17 @@ } } }, + "Connected — read-only" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bağlı — salt okunur" + } + } + } + }, "Connecting..." : { "extractionState" : "manual", "localizations" : { @@ -2391,6 +2479,28 @@ } } }, + "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" : { @@ -2514,6 +2624,17 @@ } } }, + "Custom Range" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Özel Aralık" + } + } + } + }, "Custom Sound" : { "extractionState" : "manual", "localizations" : { @@ -3369,44 +3490,44 @@ } } }, - "Enter your username and password" : { + "Enter your site address to connect." : { "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Kullanıcı adınızı ve parolanızı girin" + "value" : "Bağlanmak için site adresinizi girin." } } } }, - "Environment" : { + "Enter your username and password" : { + "extractionState" : "manual", "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ortam" + "value" : "Kullanıcı adınızı ve parolanızı girin" } } } }, - "Error" : { + "Environment" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hata" + "value" : "Ortam" } } } }, - "Est A1C:" : { - "extractionState" : "manual", + "Error" : { "localizations" : { "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tah. A1C:" + "value" : "Hata" } } } @@ -3537,6 +3658,17 @@ } } }, + "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" : { @@ -4775,6 +4907,17 @@ } } }, + "Invalid response from server" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunucudan geçersiz yanıt" + } + } + } + }, "Italian" : { "localizations" : { "tr" : { @@ -5555,6 +5698,17 @@ } } }, + "Network error occurred" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ağ hatası oluştu" + } + } + } + }, "Network error — check your connection" : { "extractionState" : "manual", "localizations" : { @@ -5566,6 +5720,17 @@ } } }, + "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", @@ -5650,6 +5815,17 @@ } } }, + "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" : { @@ -5713,6 +5889,17 @@ } } }, + "No bundle identifier found in profile" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Profilde paket kimliği bulunamadı" + } + } + } + }, "No data available" : { "localizations" : { "tr" : { @@ -5723,6 +5910,17 @@ } } }, + "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" : { @@ -5891,6 +6089,18 @@ } } }, + "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" : { @@ -5951,7 +6161,7 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tamam (%@)" + "value" : "Bağlı (%@)" } } } @@ -6462,6 +6672,17 @@ } } }, + "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" : { @@ -6877,6 +7098,28 @@ } } }, + "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" : { @@ -7674,6 +7917,17 @@ } } }, + "Sensor Start" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sensör Başlangıcı" + } + } + } + }, "Sensor change due." : { "extractionState" : "manual", "localizations" : { @@ -7696,6 +7950,17 @@ } } }, + "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" : { @@ -8001,6 +8266,17 @@ } } }, + "Site found — it needs a token." : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Site bulundu — bir belirteç gerekiyor." + } + } + } + }, "Skip" : { "localizations" : { "tr" : { @@ -8737,6 +9013,17 @@ } } }, + "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" : { @@ -8747,6 +9034,28 @@ } } }, + "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" : { @@ -8758,6 +9067,17 @@ } } }, + "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" : { @@ -8834,6 +9154,28 @@ } } }, + "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" : { @@ -8844,6 +9186,28 @@ } } }, + "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" : { @@ -9156,13 +9520,35 @@ } } }, + "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" : "Saat gelecek %lld saat içinde olmalı" + "value" : "Zaman en fazla %lld saat ileride olmalı" } } } @@ -9173,7 +9559,7 @@ "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Saat geçmiş %lld saat içinde olmalı" + "value" : "Zaman son %lld saat içinde olmalı" } } } @@ -9220,6 +9606,17 @@ } } }, + "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" : { @@ -9230,6 +9627,17 @@ } } }, + "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" : { @@ -9401,6 +9809,17 @@ } } }, + "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" : { @@ -9488,6 +9907,17 @@ } } }, + "Update Available" : { + "extractionState" : "manual", + "localizations" : { + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Güncelleme Var" + } + } + } + }, "Updated" : { "extractionState" : "manual", "localizations" : { @@ -9698,6 +10128,28 @@ } } }, + "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" : { diff --git a/LoopFollow/Settings/DexcomSettingsViewModel.swift b/LoopFollow/Settings/DexcomSettingsViewModel.swift index 0961a642f..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 = .init(localized: "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 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/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 42a165b83..b277b7aaa 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -736,7 +736,7 @@ 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 } @@ -750,7 +750,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { } } - 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: String(localized: "OK"), style: .default, handler: nil)) diff --git a/Scripts/localization/scan_keys.py b/Scripts/localization/scan_keys.py index 79332333b..0c260f814 100755 --- a/Scripts/localization/scan_keys.py +++ b/Scripts/localization/scan_keys.py @@ -185,7 +185,14 @@ def main() -> int: catalog = json.loads(path.read_text(encoding="utf-8")) strings = catalog["strings"] keys, skipped = literal_keys(roots) - missing = sorted(k for k in keys if k not in strings) + 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: diff --git a/Tests/LocalizationDisplayNameTests.swift b/Tests/LocalizationDisplayNameTests.swift index cea68aacb..48dcbcf31 100644 --- a/Tests/LocalizationDisplayNameTests.swift +++ b/Tests/LocalizationDisplayNameTests.swift @@ -98,11 +98,25 @@ struct LocalizationDisplayNameTests { func minAgoFormatIsLocalized() throws { let path = try #require(Bundle.main.path(forResource: "tr", ofType: "lproj")) let tr = try #require(Bundle(path: path)) - for key in ["%lld min", "LOW", "HIGH", "⚠️ Not Looping!", "Refreshing", "Loading...", "Setup Nightscout", "Setup Dexcom Share", "%llds left"] { + 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) } }