diff --git a/Assets/AppShortcuts.xcstrings b/Assets/AppShortcuts.xcstrings new file mode 100644 index 00000000..23e67651 --- /dev/null +++ b/Assets/AppShortcuts.xcstrings @@ -0,0 +1,422 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "Get sunrise time from ${applicationName}" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "الحصول على وقت الشروق من ${applicationName}", + "الحصول على وقت الشروق في ${applicationName}", + "عرض وقت الشروق من ${applicationName}" + ] + } + }, + "de" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Sonnenaufgangszeit von ${applicationName} abrufen", + "Sonnenaufgangszeit in ${applicationName} abrufen", + "Sonnenaufgang mit ${applicationName} anzeigen" + ] + } + }, + "en" : { + "stringSet" : { + "state" : "new", + "values" : [ + "Get sunrise time from ${applicationName}", + "Get sunrise time in ${applicationName}" + ] + } + }, + "es" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Consultar la hora del amanecer en ${applicationName}", + "Obtener la hora del amanecer con ${applicationName}", + "Ver la hora del amanecer en ${applicationName}" + ] + } + }, + "fr" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Obtenir l’heure du lever du soleil dans ${applicationName}", + "Consulter l’heure du lever du soleil dans ${applicationName}", + "Voir l’heure du lever du soleil dans ${applicationName}" + ] + } + }, + "it" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Ottieni l’ora dell’alba da ${applicationName}", + "Ottieni l’ora dell’alba in ${applicationName}", + "Consulta l’ora dell’alba in ${applicationName}", + "Mostra l’ora dell’alba in ${applicationName}" + ] + } + }, + "ja" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "${applicationName}で日の出の時刻を取得", + "${applicationName}で日の出の時刻を確認", + "${applicationName}で日の出の時刻を調べる" + ] + } + }, + "nl" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Zonsopkomsttijd ophalen uit ${applicationName}", + "Zonsopkomsttijd bekijken in ${applicationName}", + "Zonsopkomsttijd opvragen in ${applicationName}" + ] + } + }, + "pl" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Sprawdź godzinę wschodu słońca w ${applicationName}", + "Pokaż godzinę wschodu słońca w ${applicationName}", + "Pobierz godzinę wschodu słońca z ${applicationName}" + ] + } + }, + "zh-Hans" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "从${applicationName}获取日出时间", + "在${applicationName}中查看日出时间", + "用${applicationName}查询日出时间" + ] + } + } + } + }, + "Get sunset time from ${applicationName}" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "الحصول على وقت الغروب من ${applicationName}", + "الحصول على وقت الغروب في ${applicationName}", + "عرض وقت الغروب من ${applicationName}" + ] + } + }, + "de" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Sonnenuntergangszeit von ${applicationName} abrufen", + "Sonnenuntergangszeit in ${applicationName} abrufen", + "Sonnenuntergang mit ${applicationName} anzeigen" + ] + } + }, + "en" : { + "stringSet" : { + "state" : "new", + "values" : [ + "Get sunset time from ${applicationName}", + "Get sunset time in ${applicationName}" + ] + } + }, + "es" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Consultar la hora de la puesta de sol en ${applicationName}", + "Obtener la hora de la puesta de sol con ${applicationName}", + "Ver la hora del ocaso en ${applicationName}" + ] + } + }, + "fr" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Obtenir l’heure du coucher du soleil dans ${applicationName}", + "Consulter l’heure du coucher du soleil dans ${applicationName}", + "Voir l’heure du coucher du soleil dans ${applicationName}" + ] + } + }, + "it" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Ottieni l’ora del tramonto da ${applicationName}", + "Ottieni l’ora del tramonto in ${applicationName}", + "Consulta l’ora del tramonto in ${applicationName}", + "Mostra l’ora del tramonto in ${applicationName}" + ] + } + }, + "ja" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "${applicationName}で日の入りの時刻を取得", + "${applicationName}で日の入りの時刻を確認", + "${applicationName}で日の入りの時刻を調べる" + ] + } + }, + "nl" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Zonsondergangstijd ophalen uit ${applicationName}", + "Zonsondergangstijd bekijken in ${applicationName}", + "Zonsondergangstijd opvragen in ${applicationName}" + ] + } + }, + "pl" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Sprawdź godzinę zachodu słońca w ${applicationName}", + "Pokaż godzinę zachodu słońca w ${applicationName}", + "Pobierz godzinę zachodu słońca z ${applicationName}" + ] + } + }, + "zh-Hans" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "从${applicationName}获取日落时间", + "在${applicationName}中查看日落时间", + "用${applicationName}查询日落时间" + ] + } + } + } + }, + "View daylight for a location in ${applicationName}" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "عرض ضوء النهار لموقع في ${applicationName}", + "إظهار ضوء النهار لموقع في ${applicationName}", + "الاطلاع على ضوء النهار لموقع في ${applicationName}" + ] + } + }, + "de" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Tageslicht für einen Ort in ${applicationName} anzeigen", + "Tageslicht für einen Ort in ${applicationName} ansehen", + "Tageslicht eines Orts mit ${applicationName} anzeigen" + ] + } + }, + "en" : { + "stringSet" : { + "state" : "new", + "values" : [ + "View daylight for a location in ${applicationName}" + ] + } + }, + "es" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Ver la luz natural de una ubicación en ${applicationName}", + "Consultar la luz natural de un lugar en ${applicationName}", + "Mostrar la luz natural de una ubicación en ${applicationName}" + ] + } + }, + "fr" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Afficher la lumière du jour pour un lieu dans ${applicationName}", + "Consulter la lumière du jour pour un lieu dans ${applicationName}", + "Voir la lumière du jour pour un lieu dans ${applicationName}" + ] + } + }, + "it" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Visualizza la luce del giorno per una posizione in ${applicationName}", + "Mostra la luce del giorno per una posizione in ${applicationName}", + "Consulta la luce del giorno per una posizione in ${applicationName}" + ] + } + }, + "ja" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "${applicationName}で場所の日照時間を表示", + "${applicationName}で場所の日照時間を確認", + "${applicationName}で場所の日照時間を見る" + ] + } + }, + "nl" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Bekijk daglicht voor een locatie in ${applicationName}", + "Toon daglicht voor een locatie in ${applicationName}", + "Daglicht voor een locatie bekijken in ${applicationName}" + ] + } + }, + "pl" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Wyświetl światło dzienne dla lokalizacji w ${applicationName}", + "Pokaż światło dzienne dla lokalizacji w ${applicationName}", + "Zobacz światło dzienne dla lokalizacji w ${applicationName}" + ] + } + }, + "zh-Hans" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "在${applicationName}中查看某个地点的日照", + "在${applicationName}中显示某个地点的日照", + "用${applicationName}查看地点的日照" + ] + } + } + } + }, + "View remaining daylight for a location in ${applicationName}" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "عرض ضوء النهار المتبقي لموقع في ${applicationName}", + "إظهار ضوء النهار المتبقي لموقع في ${applicationName}", + "الاطلاع على ضوء النهار المتبقي لموقع في ${applicationName}" + ] + } + }, + "de" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Verbleibendes Tageslicht für einen Ort in ${applicationName} anzeigen", + "Verbleibendes Tageslicht für einen Ort in ${applicationName} ansehen", + "Restliches Tageslicht eines Orts mit ${applicationName} anzeigen" + ] + } + }, + "en" : { + "stringSet" : { + "state" : "new", + "values" : [ + "View remaining daylight for a location in ${applicationName}" + ] + } + }, + "es" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Ver la luz natural restante de una ubicación en ${applicationName}", + "Consultar la luz natural restante de un lugar en ${applicationName}", + "Mostrar la luz natural restante de una ubicación en ${applicationName}" + ] + } + }, + "fr" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Afficher la lumière du jour restante pour un lieu dans ${applicationName}", + "Consulter la lumière du jour restante pour un lieu dans ${applicationName}", + "Voir la lumière du jour restante pour un lieu dans ${applicationName}" + ] + } + }, + "it" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Visualizza la luce del giorno rimanente per una posizione in ${applicationName}", + "Mostra la luce del giorno rimanente per una posizione in ${applicationName}", + "Consulta la luce del giorno rimanente per una posizione in ${applicationName}" + ] + } + }, + "ja" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "${applicationName}で場所の残りの日照時間を表示", + "${applicationName}で場所の残りの日照時間を確認", + "${applicationName}で場所の残りの日照時間を見る" + ] + } + }, + "nl" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Bekijk resterend daglicht voor een locatie in ${applicationName}", + "Toon resterend daglicht voor een locatie in ${applicationName}", + "Resterend daglicht voor een locatie bekijken in ${applicationName}" + ] + } + }, + "pl" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "Wyświetl pozostałe światło dzienne dla lokalizacji w ${applicationName}", + "Pokaż pozostałe światło dzienne dla lokalizacji w ${applicationName}", + "Zobacz pozostałe światło dzienne dla lokalizacji w ${applicationName}" + ] + } + }, + "zh-Hans" : { + "stringSet" : { + "state" : "machine_translated", + "values" : [ + "在${applicationName}中查看某个地点的剩余日照", + "在${applicationName}中显示某个地点的剩余日照", + "用${applicationName}查看地点还剩多少日照" + ] + } + } + } + } + }, + "version" : "1.3" +} \ No newline at end of file diff --git a/Assets/InfoPlist.xcstrings b/Assets/InfoPlist.xcstrings new file mode 100644 index 00000000..3db285ac --- /dev/null +++ b/Assets/InfoPlist.xcstrings @@ -0,0 +1,270 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "CFBundleDisplayName" : { + "comment" : "Bundle display name", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Solstice" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + } + } + }, + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Solstice" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + } + } + }, + "NSLocationUsageDescription" : { + "comment" : "Privacy - Location Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يستخدم Solstice موقعك لحساب أوقات الشروق والغروب المحلية" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice verwendet deinen Standort, um die lokalen Zeiten für Sonnenauf- und -untergang zu berechnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Solstice uses your location to calculate local sunrise and sunset times" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice usa tu ubicación para calcular las horas locales de amanecer y puesta de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice utilise votre position pour calculer les heures locales de lever et de coucher du soleil." + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice usa la tua posizione per calcolare gli orari locali di alba e tramonto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeは、現地の日の出と日の入りの時刻を計算するために位置情報を使用します" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice gebruikt je locatie om de lokale tijden van zonsopkomst en zonsondergang te berekenen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice używa Twojej lokalizacji, aby obliczyć lokalne godziny wschodu i zachodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice 使用你的位置来计算当地的日出和日落时间" + } + } + } + }, + "NSLocationWhenInUseUsageDescription" : { + "comment" : "Privacy - Location When In Use Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يستخدم Solstice موقعك لحساب أوقات الشروق والغروب المحلية" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice verwendet deinen Standort, um die lokalen Zeiten für Sonnenauf- und -untergang zu berechnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Solstice uses your location to calculate local sunrise and sunset times" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice usa tu ubicación para calcular las horas locales de amanecer y puesta de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice utilise votre position pour calculer les heures locales de lever et de coucher du soleil." + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice usa la tua posizione per calcolare gli orari locali di alba e tramonto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeは、現地の日の出と日の入りの時刻を計算するために位置情報を使用します" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice gebruikt je locatie om de lokale tijden van zonsopkomst en zonsondergang te berekenen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice używa Twojej lokalizacji, aby obliczyć lokalne godziny wschodu i zachodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice 使用你的位置来计算当地的日出和日落时间" + } + } + } + } + }, + "version" : "1.3" +} \ No newline at end of file diff --git a/Data Model/Persistence.swift b/Data Model/Persistence.swift index 2332a73b..9cb39370 100644 --- a/Data Model/Persistence.swift +++ b/Data Model/Persistence.swift @@ -9,7 +9,7 @@ import CoreData class PersistenceController { static let shared = PersistenceController() - + static var preview: PersistenceController = { let result = PersistenceController(inMemory: true) let viewContext = result.container.viewContext @@ -32,28 +32,28 @@ class PersistenceController { } return result }() - + let container: NSPersistentCloudKitContainer - + init(inMemory: Bool = false) { container = NSPersistentCloudKitContainer(name: "Solstice") - + if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } else { let fileUrl = NSPersistentContainer.defaultDirectoryURL().relativePath + "/Solstice.sqlite" container.persistentStoreDescriptions.first?.url = URL(filePath: fileUrl) } - + container.persistentStoreDescriptions.first?.cloudKitContainerOptions = .init(containerIdentifier: Constants.iCloudContainerIdentifier) - + container.viewContext.automaticallyMergesChangesFromParent = true - - container.loadPersistentStores(completionHandler: { (storeDescription, error) in + + container.loadPersistentStores(completionHandler: { _, error in if let error = error as NSError? { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - + /* Typical reasons for an error here include: * The parent directory does not exist, cannot be created, or disallows writing. @@ -65,18 +65,18 @@ class PersistenceController { fatalError("Unresolved error \(error), \(error.userInfo)") } }) - + // Observe Core Data remote change notifications. NotificationCenter.default.addObserver(self, - selector: #selector(storeRemoteChange(_:)), - name: .NSPersistentStoreRemoteChange, - object: container.persistentStoreCoordinator) + selector: #selector(storeRemoteChange(_:)), + name: .NSPersistentStoreRemoteChange, + object: container.persistentStoreCoordinator) } - - @objc func storeRemoteChange(_ notification: Notification) { + + @objc func storeRemoteChange(_: Notification) { deduplicateRecords() } - + func deduplicateRecords() { container.performBackgroundTask { context in for entry in SavedLocation.defaultData { @@ -85,13 +85,13 @@ class PersistenceController { let request = SavedLocation.fetchRequest() request.predicate = NSPredicate(format: "uuid == %@", uuidString) let results = try context.fetch(request) - + for (index, result) in results.enumerated() { if index != 0 { context.delete(result) } } - + try context.save() } catch { print(error.localizedDescription) @@ -101,10 +101,10 @@ class PersistenceController { } } -extension SavedLocation { - public override func awakeFromInsert() { +public extension SavedLocation { + override func awakeFromInsert() { super.awakeFromInsert() - + if uuid == nil { uuid = UUID() } diff --git a/Data Model/SavedLocation++.swift b/Data Model/SavedLocation++.swift index 6b765d18..1091b500 100644 --- a/Data Model/SavedLocation++.swift +++ b/Data Model/SavedLocation++.swift @@ -24,13 +24,13 @@ extension SavedLocation { extension SavedLocation { static let nycUUIDString = "7AAA4D87-4402-4D0E-A35E-2D84641A71BE" - + static var defaultData: [SavedLocation.CodableRepresentation] { guard let defaultDataUrl = Bundle.main.url(forResource: "defaultData", withExtension: "json") else { print("No URL for defaultData.json") return [] } - + do { let defaultDataFileData = try Data(contentsOf: defaultDataUrl) return try JSONDecoder().decode([SavedLocation.CodableRepresentation].self, from: defaultDataFileData) diff --git a/InfoPlist.xcstrings b/InfoPlist.xcstrings new file mode 100644 index 00000000..2de625dd --- /dev/null +++ b/InfoPlist.xcstrings @@ -0,0 +1,204 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "CFBundleDisplayName" : { + "comment" : "Bundle display name", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Solstice" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + } + } + }, + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Solstice watchOS Watch App" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + } + } + }, + "NSLocationWhenInUseUsageDescription" : { + "comment" : "Privacy - Location When In Use Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يستخدم Solstice موقعك لحساب أوقات الشروق والغروب المحلية" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice verwendet deinen Standort, um die lokalen Zeiten für Sonnenauf- und -untergang zu berechnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Solstice uses your location to calculate local sunrise and sunset times" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice usa tu ubicación para calcular las horas locales de amanecer y puesta de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice utilise votre position pour calculer les heures locales de lever et de coucher du soleil." + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice usa la tua posizione per calcolare gli orari locali di alba e tramonto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeは、現地の日の出と日の入りの時刻を計算するために位置情報を使用します" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice gebruikt je locatie om de lokale tijden van zonsopkomst en zonsondergang te berekenen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice używa Twojej lokalizacji, aby obliczyć lokalne godziny wschodu i zachodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice 使用你的位置来计算当地的日出和日落时间" + } + } + } + } + }, + "version" : "1.3" +} \ No newline at end of file diff --git a/Intents/GetSunriseTime.swift b/Intents/GetSunriseTime.swift index f404c299..71cbaa9c 100644 --- a/Intents/GetSunriseTime.swift +++ b/Intents/GetSunriseTime.swift @@ -5,31 +5,31 @@ // Created by Daniel Eden on 01/03/2023. // -import Foundation import AppIntents import CoreLocation +import Foundation struct GetSunriseTime: AppIntent { static var title: LocalizedStringResource = "Get Sunrise Time" static var description = IntentDescription("Calculate the sunrise time on a given date in a given location") - + @Parameter(title: "Date") var date: Date - + @Parameter(title: "Location") var location: CLPlacemark - + static var parameterSummary: some ParameterSummary { Summary("Get the sunrise time on \(\.$date) in \(\.$location)") } - + func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let coordinate = location.location?.coordinate else { throw $location.needsValueError("What location do you want to see the sunrise for?") } - + let solar = NTSolar(for: date, coordinate: coordinate, timeZone: location.timeZone ?? .autoupdatingCurrent) - + return .result( value: solar?.sunrise, dialog: "\((solar?.sunrise ?? date).formatted(date: .omitted, time: .shortened))" diff --git a/Intents/GetSunsetTime.swift b/Intents/GetSunsetTime.swift index 5d27a793..30d83517 100644 --- a/Intents/GetSunsetTime.swift +++ b/Intents/GetSunsetTime.swift @@ -5,31 +5,31 @@ // Created by Daniel Eden on 01/03/2023. // -import Foundation import AppIntents import CoreLocation +import Foundation struct GetSunsetTime: AppIntent { static var title: LocalizedStringResource = "Get Sunset Time" static var description = IntentDescription("Calculate the sunset time on a given date in a given location") - + @Parameter(title: "Date") var date: Date - + @Parameter(title: "Location") var location: CLPlacemark - + static var parameterSummary: some ParameterSummary { Summary("Get the sunset time on \(\.$date) in \(\.$location)") } - + func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let coordinate = location.location?.coordinate else { throw $location.needsValueError("What location do you want to see the sunset for?") } - + let solar = NTSolar(for: date, coordinate: coordinate, timeZone: location.timeZone ?? .autoupdatingCurrent) - + return .result( value: solar?.sunset, dialog: "\((solar?.sunset ?? date).formatted(date: .omitted, time: .shortened))" diff --git a/Intents/LocationAppEntity.swift b/Intents/LocationAppEntity.swift index f0323882..23dd4e6c 100644 --- a/Intents/LocationAppEntity.swift +++ b/Intents/LocationAppEntity.swift @@ -5,10 +5,10 @@ // Created by Daniel Eden on 31/01/2026. // -import Foundation import AppIntents -import CoreLocation import CoreData +import CoreLocation +import Foundation import MapKit /// An AppEntity representing a location for widget configuration. @@ -126,14 +126,15 @@ struct LocationEntityQuery: EntityQuery, EntityStringQuery { let encoded = String(id.dropFirst(6)) guard let decoded = encoded.removingPercentEncoding, - let data = decoded.data(using: .utf8), - let locationData = try? JSONDecoder().decode(LocationData.self, from: data) else { + let data = decoded.data(using: .utf8), + let locationData = try? JSONDecoder().decode(LocationData.self, from: data) + else { return nil } return LocationAppEntity( id: id, - title: locationData.title ?? "Unknown", + title: locationData.title ?? String(localized: "Unknown"), subtitle: locationData.subtitle, latitude: locationData.latitude, longitude: locationData.longitude, @@ -202,13 +203,13 @@ struct LocationEntityQuery: EntityQuery, EntityStringQuery { let entity = LocationAppEntity( id: encodeTemporaryEntity( - title: item.name ?? item.placemark.locality ?? "Unknown", + title: item.name ?? item.placemark.locality ?? String(localized: "Unknown"), subtitle: item.placemark.country, latitude: location.coordinate.latitude, longitude: location.coordinate.longitude, timeZoneIdentifier: timeZoneIdentifier ), - title: item.name ?? item.placemark.locality ?? "Unknown", + title: item.name ?? item.placemark.locality ?? String(localized: "Unknown"), subtitle: [item.placemark.locality, item.placemark.administrativeArea, item.placemark.country] .compactMap { $0 } .filter { $0 != item.name } @@ -264,7 +265,8 @@ struct LocationEntityQuery: EntityQuery, EntityStringQuery { ) guard let encoded = try? JSONEncoder().encode(data), - let string = String(data: encoded, encoding: .utf8) else { + let string = String(data: encoded, encoding: .utf8) + else { return "temp:\(latitude),\(longitude)" } @@ -277,14 +279,15 @@ struct LocationEntityQuery: EntityQuery, EntityStringQuery { let encoded = String(id.dropFirst(5)) guard let decoded = encoded.removingPercentEncoding, - let data = decoded.data(using: .utf8), - let locationData = try? JSONDecoder().decode(LocationData.self, from: data) else { + let data = decoded.data(using: .utf8), + let locationData = try? JSONDecoder().decode(LocationData.self, from: data) + else { return nil } return LocationAppEntity( id: id, - title: locationData.title ?? "Unknown", + title: locationData.title ?? String(localized: "Unknown"), subtitle: locationData.subtitle, latitude: locationData.latitude, longitude: locationData.longitude, @@ -300,7 +303,7 @@ extension LocationAppEntity { /// Initialize from a SavedLocation Core Data object /// Encodes the full location data in the ID for resilient entity resolution init(from savedLocation: SavedLocation) { - let title = savedLocation.title ?? "Unknown" + let title = savedLocation.title ?? String(localized: "Unknown") let subtitle = savedLocation.subtitle // Encode full location data in ID so entity can be resolved even if Core Data lookup fails @@ -314,18 +317,19 @@ extension LocationAppEntity { ) if let encoded = try? JSONEncoder().encode(data), - let jsonString = String(data: encoded, encoding: .utf8) { - self.id = "saved:" + jsonString.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! + let jsonString = String(data: encoded, encoding: .utf8) + { + id = "saved:" + jsonString.addingPercentEncoding(withAllowedCharacters: .alphanumerics)! } else { // Fallback to UUID if encoding fails - self.id = savedLocation.uuid?.uuidString ?? UUID().uuidString + id = savedLocation.uuid?.uuidString ?? UUID().uuidString } self.title = title self.subtitle = subtitle - self.latitude = savedLocation.latitude - self.longitude = savedLocation.longitude - self.timeZoneIdentifier = savedLocation.timeZoneIdentifier - self.savedLocationUUID = savedLocation.uuid + latitude = savedLocation.latitude + longitude = savedLocation.longitude + timeZoneIdentifier = savedLocation.timeZoneIdentifier + savedLocationUUID = savedLocation.uuid } } diff --git a/Intents/SolsticeConfigurationIntent.swift b/Intents/SolsticeConfigurationIntent.swift index a3621bb3..e7542159 100644 --- a/Intents/SolsticeConfigurationIntent.swift +++ b/Intents/SolsticeConfigurationIntent.swift @@ -1,13 +1,13 @@ // -// Configuration.swift +// SolsticeConfigurationIntent.swift // Solstice // // Created by Daniel Eden on 31/01/2026. // -import Foundation import AppIntents import CoreLocation +import Foundation struct SolsticeConfigurationIntent: AppIntent, WidgetConfigurationIntent, CustomIntentMigratedAppIntent { static let intentClassName = "ConfigurationIntent" @@ -22,6 +22,7 @@ struct SolsticeConfigurationIntent: AppIntent, WidgetConfigurationIntent, Custom var selectedLocation: LocationAppEntity? // MARK: - Legacy Parameters (for migration from old ConfigurationIntent) + // These match the old ConfigurationIntent parameter names and types exactly // so the system can automatically migrate existing widget configurations. @@ -49,10 +50,11 @@ struct SolsticeConfigurationIntent: AppIntent, WidgetConfigurationIntent, Custom case .customLocation: // Custom location - convert CLPlacemark to LocationAppEntity if let placemark = location, - let coordinate = placemark.location?.coordinate { + let coordinate = placemark.location?.coordinate + { return LocationAppEntity( id: "migrated:\(coordinate.latitude),\(coordinate.longitude)", - title: placemark.locality ?? placemark.name ?? "Custom Location", + title: placemark.locality ?? placemark.name ?? String(localized: "Custom Location"), subtitle: placemark.country, latitude: coordinate.latitude, longitude: coordinate.longitude, @@ -114,19 +116,21 @@ enum LocationTypeAppEnum: Int, AppEnum { static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ .unknown: "Unknown", .currentLocation: "Current Location", - .customLocation: "Custom Location" + .customLocation: "Custom Location", ] } // MARK: - Intent Dialogs -fileprivate extension IntentDialog { +private extension IntentDialog { static var locationParameterPrompt: Self { "Which location do you want to see daylight information for?" } + static func locationParameterDisambiguationIntro(count: Int, location: LocationAppEntity) -> Self { "There are \(count) options matching '\(location.title)'." } + static func locationParameterConfirmation(location: LocationAppEntity) -> Self { "Just to confirm, you wanted '\(location.title)'?" } diff --git a/Intents/SolsticeShortcutsProvider.swift b/Intents/SolsticeShortcutsProvider.swift index e1a7a2ce..2b3d6444 100644 --- a/Intents/SolsticeShortcutsProvider.swift +++ b/Intents/SolsticeShortcutsProvider.swift @@ -5,8 +5,8 @@ // Created by Daniel Eden on 21/03/2023. // -import Foundation import AppIntents +import Foundation struct SolsticeShortcutsProvider: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { diff --git a/Intents/ViewDaylight.swift b/Intents/ViewDaylight.swift index 746ef861..f4884982 100644 --- a/Intents/ViewDaylight.swift +++ b/Intents/ViewDaylight.swift @@ -5,37 +5,37 @@ // Created by Daniel Eden on 01/03/2023. // -import Foundation import AppIntents import CoreLocation +import Foundation struct ViewDaylight: AppIntent { static var title: LocalizedStringResource = "View Daylight" static var description = IntentDescription("View how much daylight there is on a given day, based on the duration from that day’s sunrise to sunset.") - + @Parameter(title: "Date") var date: Date - + @Parameter(title: "Location") var location: CLPlacemark - + static var parameterSummary: some ParameterSummary { Summary("Get the daylight duration on \(\.$date) in \(\.$location)") } - + func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let coordinate = location.location?.coordinate else { throw $location.needsValueError("What location do you want to see the daylight for?") } - + let formatter = DateComponentsFormatter() formatter.unitsStyle = .full formatter.allowedUnits = [.hour, .minute, .second] - + let solar = NTSolar(for: date, coordinate: coordinate, timeZone: location.timeZone ?? .autoupdatingCurrent) - + let duration = (solar?.sunrise ?? .now).distance(to: solar?.sunset ?? .now) - + return .result( value: duration, dialog: "\(formatter.string(from: duration) ?? "") of daylight on \(date.formatted(date: .abbreviated, time: .omitted))" diff --git a/Intents/ViewRemainingDaylight.swift b/Intents/ViewRemainingDaylight.swift index a116e403..dd8e5bbe 100644 --- a/Intents/ViewRemainingDaylight.swift +++ b/Intents/ViewRemainingDaylight.swift @@ -5,35 +5,35 @@ // Created by Daniel Eden on 01/03/2023. // -import Foundation import AppIntents import CoreLocation +import Foundation struct ViewRemainingDaylight: AppIntent { static var title: LocalizedStringResource = "View Remaining Daylight" static var description = IntentDescription("View how much daylight is remaining today, based on the time until sunset.") - + @Parameter(title: "Location") var location: CLPlacemark - + static var parameterSummary: some ParameterSummary { Summary("Get today's remaining daylight in \(\.$location)") } - + func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { guard let coordinate = location.location?.coordinate else { throw $location.needsValueError("What location do you want to see the daylight for?") } - + let solar = NTSolar(for: .now, coordinate: coordinate, timeZone: location.timeZone ?? .autoupdatingCurrent)! - + var resultValue: TimeInterval - + let formatter = DateComponentsFormatter() formatter.unitsStyle = .full formatter.allowedUnits = [.hour, .minute, .second] - - if (solar.safeSunrise...solar.safeSunset).contains(.now) { + + if (solar.safeSunrise ... solar.safeSunset).contains(.now) { resultValue = Date().distance(to: solar.safeSunset) return .result( value: resultValue, diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 3081ac4e..6119d56e 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1,1098 +1,6109 @@ { "sourceLanguage" : "en", "strings" : { - "" : { - "shouldTranslate" : false - }, "-" : { "comment" : "Less daylight middle of sentence", + "extractionState" : "stale", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "-" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-" + } } } }, "-6mo" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-٦ أشهر" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-6 Mon." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-6 meses" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-6 mois" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "-6 mesi" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-6か月" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-6 mnd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-6 mies." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "-6个月" + } } } }, "—" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "—" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "—" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "—" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "—" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "—" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "—" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "—" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "—" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "—" + } } } }, "**Thank you so much for your support.** Feel free to leave another tip in the future if you’re feeling generous." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "**شكرًا جزيلًا على دعمك.** لا تتردد في ترك إكرامية أخرى مستقبلًا إن كنت في مزاج للكرم." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "**Vielen Dank für deine Unterstützung.** Wenn du magst, kannst du in Zukunft gerne noch ein weiteres Trinkgeld hinterlassen." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "**Muchísimas gracias por tu apoyo.** Si te sientes generoso, no dudes en dejar otra propina en el futuro." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "**Merci infiniment pour votre soutien.** N’hésitez pas à laisser un autre pourboire à l’avenir si le cœur vous en dit." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "**Grazie mille per il tuo supporto.** Sentiti libero di lasciare un altro suggerimento in futuro se ti senti generoso." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "**ご支援いただき、本当にありがとうございます。**もしよろしければ、今後またチップを送っていただけると嬉しいです。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "**Heel erg bedankt voor je steun.** Voel je vrij om in de toekomst nog een fooi te geven als je je gul voelt." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "**Bardzo dziękujemy za wsparcie.** Jeśli w przyszłości poczujesz się hojnie, możesz zostawić kolejny napiwek." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "**非常感谢你的支持。**如果日后你有兴致,欢迎再次留下打赏。" + } } } }, "%@" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@" + } } } }, "%@ (%@)" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ (%2$@)" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ (%2$@)" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ (%2$@)" } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ (%2$@)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ (%2$@)" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ (%2$@)" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@(%2$@)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ (%2$@)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ (%2$@)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@(%2$@)" + } } } }, "%@ %@" : { "comment" : "Widget heading for custom location\nWidget heading for real location", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ %2$@" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@" + } } } }, "%@ %@ daylight %@ compared to %@" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ ضوء نهار %2$@ %3$@ مقارنة بـ%4$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@ Tageslicht %3$@ im Vergleich zu %4$@" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ %2$@ daylight %3$@ compared to %4$@" } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@ de luz natural %3$@ en comparación con %4$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ de lumière du jour %2$@ %3$@ par rapport à %4$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ %2$@ di luce diurna %3$@ rispetto a %4$@ " } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%3$@は%4$@より日照時間が%1$@%2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@ daglicht %3$@ vergeleken met %4$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@ światła dziennego %3$@ w porównaniu z %4$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%3$@的日照比%4$@%2$@%1$@" + } } } }, "%@ %@ daylight %@ compared to the previous solstice" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ ضوء نهار %2$@ %3$@ مقارنة بالانقلاب الشمسي السابق" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@ Tageslicht %3$@ im Vergleich zur vorherigen Sonnenwende" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ %2$@ daylight %3$@ compared to the previous solstice" } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@ de luz natural %3$@ en comparación con el solsticio anterior" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ de lumière du jour %2$@ %3$@ par rapport au solstice précédent" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ di luce diurna %2$@ %3$@ rispetto al solstizio precedente" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%3$@は前回の至点より日照時間が%1$@%2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@ daglicht %3$@ vergeleken met de vorige zonnewende" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@ światła dziennego %3$@ w porównaniu z poprzednim przesileniem" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%3$@的日照比前一个至点%2$@%1$@" + } } } }, "%@ after" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "بعد %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ danach" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ después" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ après" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ dopo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@後" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ erna" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ później" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@之后" + } } } }, "%@ before" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ قبله" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ vorher" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ antes" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ avant" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ prima" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@前" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ ervoor" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ wcześniej" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@前" + } } } }, "%@ Daylight %@" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ ضوء النهار %2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ Tageslicht %2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ Daylight %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ Luz natural %2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ Lumière du jour %2$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ Luce diurna %2$@" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ %2$@の日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ Daglicht %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ Światło dzienne %2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ 日照 %2$@" + } } } }, "%@ in %@" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ بعد %2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ in %2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ in %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ dentro de %2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ dans %2$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ tra %2$@" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%2$@後に%1$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ over %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ za %2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%2$@后%1$@" + } } } }, "%@ of daylight" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ من ضوء النهار" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ Tageslicht" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ de luz natural" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ de lumière du jour" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ di luce diurna" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@の日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ daglicht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ światła dziennego" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@日照" + } } } }, "%@ of daylight left today" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تبقى %@ من ضوء النهار اليوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heute noch %@ Tageslicht übrig" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hoy queda %@ de luz natural" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ de lumière du jour restante aujourd’hui" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Oggi mancano %@ di luce" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日の残りの日照時間は%@です" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Vandaag nog %@ daglicht over" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dziś pozostało %@ światła dziennego" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今天还剩%@日照" + } } } }, "%@ of daylight on %@" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ من ضوء النهار في %2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ Tageslicht am %2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@ of daylight on %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ de luz natural el %2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ de lumière du jour le %2$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$@ di luce diurna il %2$@" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%2$@は%1$@の日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ daglicht op %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@ światła dziennego dnia %2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%2$@有%1$@日照" + } } } }, "%@ Solstice" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ Solstice" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@-Sonnenwende" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ Solstice" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ Solstice" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ Solstice" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ Solstice" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ Solstice" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ Solstice" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ Solstice" + } } } }, "%@, %@" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@، %2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@, %2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@, %2$@" } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@, %2$@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@, %2$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$@, %2$@" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@、%2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@, %2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@, %2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@、%2$@" + } } } }, "%@%@" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@%2$@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@%2$@" + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$@%2$@" } }, - "it" : { + "es" : { "stringUnit" : { - "state" : "translated", + "state" : "machine_translated", "value" : "%1$@%2$@" } - } - } - }, - "%lld" : { - "comment" : "Number of days in the past or future for Time Travel on Apple Watch", - "localizations" : { + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@%2$@" + } + }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "%lld" + "value" : "%1$@%2$@" } - } - } - }, - "+" : { + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@%2$@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@%2$@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@%2$@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$@%2$@" + } + } + } + }, + "%lld" : { + "comment" : "Number of days in the past or future for Time Travel on Apple Watch", + "extractionState" : "stale", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%lld" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%lld" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%lld" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%lld" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%lld" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%lld" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%lld" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%lld" + } + } + } + }, + "+" : { "comment" : "More daylight middle of sentence", + "extractionState" : "stale", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "+" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+" + } } } }, "+6mo" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+٦ أشهر" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+6 Mon." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+6 meses" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+6 mois" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "+6 mesi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+6か月" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+6 mnd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+6 mies." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "+6个月" + } + } + } + }, + "a specific time" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "وقت محدد" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "eine bestimmte Uhrzeit" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "una hora específica" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "une heure précise" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "un momento specifico" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "指定した時刻" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "een specifiek tijdstip" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "określona godzina" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "特定时间" + } + } + } + }, + "About Solstice" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عن Solstice" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Über Solstice" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Acerca de Solstice" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "À propos de Solstice" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Circa Solstice" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeについて" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Over Solstice" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "O aplikacji Solstice" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "关于 Solstice" + } + } + } + }, + "About solstices and equinoxes" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عن الانقلابات الشمسية والاعتدالات" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Über Sonnenwenden und Tagundnachtgleichen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Acerca de los solsticios y los equinoccios" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "À propos des solstices et des équinoxes" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "A proposito di solstizi ed equinozi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "至点と分点について" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Over zonnewenden en equinoxen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "O przesileniach i równonocach" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "关于至点和分点" + } + } + } + }, + "Altitude" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الارتفاع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Höhe" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Altitud" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Altitude" + } + }, "it" : { "stringUnit" : { - "state" : "translated", - "value" : "+6 mesi" + "state" : "machine_translated", + "value" : "Altitudine" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "高度" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hoogte" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wysokość" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "高度" + } + } + } + }, + "Appearance" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "المظهر" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Darstellung" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aspecto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Apparence" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apparenza" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "外観" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Weergave" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wygląd" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "外观" + } + } + } + }, + "Ascending" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تصاعدي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aufsteigend" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ascendente" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Croissant" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ascendente" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "昇順" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Oplopend" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Rosnąco" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "升序" + } + } + } + }, + "Astronomical Sunrise" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الشروق الفلكي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Astronomischer Sonnenaufgang" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Amanecer astronómico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lever du soleil astronomique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alba astronomica" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "天文薄明の日の出" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Astronomische zonsopkomst" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Świt astronomiczny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "天文日出" + } + } + } + }, + "Astronomical Sunset" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الغروب الفلكي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Astronomischer Sonnenuntergang" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ocaso astronómico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Coucher de soleil astronomique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tramonto astronomico" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "天文薄明の日の入り" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Astronomische zonsondergang" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zmierzch astronomiczny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "天文日落" + } + } + } + }, + "Astronomical Twilight" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الشفق الفلكي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Astronomische Dämmerung" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Crepúsculo astronómico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Crépuscule astronomique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crepuscolo astronomico" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "天文薄明" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Astronomische schemering" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zmierzch astronomiczny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "天文暮光" + } + } + } + }, + "at %@" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عند %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "um %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "a las %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "à %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "alle %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@に" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "om %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "o %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "在%@" + } + } + } + }, + "Calculate the sunrise time on a given date in a given location" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "حساب وقت الشروق في تاريخ محدد وموقع محدد" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Berechnet die Sonnenaufgangszeit an einem bestimmten Datum an einem bestimmten Ort" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Calcula la hora del amanecer en una fecha y una ubicación determinadas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Calcule l’heure du lever du soleil à une date et un lieu donnés." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calcola l'ora dell'alba in una determinata data in una determinata località" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "指定した場所と日付における日の出の時刻を計算します。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bereken de zonsopkomsttijd op een bepaalde datum op een bepaalde locatie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Oblicz godzinę wschodu słońca w danym dniu i danej lokalizacji" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "计算指定位置在指定日期的日出时间" + } + } + } + }, + "Calculate the sunset time on a given date in a given location" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "حساب وقت الغروب في تاريخ محدد وموقع محدد" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Berechnet die Sonnenuntergangszeit an einem bestimmten Datum an einem bestimmten Ort" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Calcula la hora de la puesta de sol en una fecha y una ubicación determinadas" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Calcule l’heure du coucher du soleil à une date et un lieu donnés." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Calcola l'ora del tramonto in una determinata data in una determinata località" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "指定した場所と日付における日の入りの時刻を計算します。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bereken de zonsondergangstijd op een bepaalde datum op een bepaalde locatie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Oblicz godzinę zachodu słońca w danym dniu i danej lokalizacji" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "计算指定位置在指定日期的日落时间" + } + } + } + }, + "Change how notifications behave when daily daylight begins to decrease. This can help with Seasonal Affective Disorder." : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يمكنك تغيير طريقة عمل الإشعارات عندما يبدأ ضوء النهار اليومي في التناقص. قد يساعد ذلك في التعامل مع الاضطراب العاطفي الموسمي (SAD)." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ändere das Verhalten von Mitteilungen, wenn das tägliche Tageslicht abzunehmen beginnt. Das kann bei saisonal-affektiver Störung (SAD) helfen." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Cambia el comportamiento de las notificaciones cuando la luz natural diaria empieza a disminuir. Esto puede ayudar con el trastorno afectivo estacional (TAE)." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Modifiez le comportement des notifications lorsque la lumière du jour commence à diminuer. Cela peut aider en cas de trouble affectif saisonnier (TAS)." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifica il comportamento delle notifiche quando la luce del giorno inizia a diminuire. Questo può aiutare con il disturbo affettivo stagionale (SAD)." + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日々の日照時間が減り始めたときの通知の動作を変更します。季節性情動障害(SAD)の緩和に役立つことがあります。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wijzig hoe meldingen zich gedragen wanneer het daglicht per dag begint af te nemen. Dit kan helpen bij een seizoensgebonden depressie (SAD)." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zmień sposób działania powiadomień, gdy ilość światła dziennego zaczyna się zmniejszać. Może to pomóc w sezonowym zaburzeniu afektywnym (SAD)." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "更改每日日照开始减少时的通知行为。这有助于缓解季节性情感障碍(SAD)。" + } + } + } + }, + "Chart appearance" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مظهر الرسم البياني" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Diagrammdarstellung" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aspecto del gráfico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Apparence du graphique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aspetto del grafico" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "チャートの外観" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Weergave van grafiek" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wygląd wykresu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "图表外观" + } + } + } + }, + "Chart theme" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "سمة الرسم البياني" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Diagrammdesign" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tema del gráfico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Thème du graphique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tema del grafico" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "チャートのテーマ" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Thema van grafiek" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Motyw wykresu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "图表主题" + } + } + } + }, + "Chart type" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "نوع الرسم البياني" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Diagrammtyp" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tipo de gráfico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Type de graphique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tipo di grafico" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "チャートの種類" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Grafiektype" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Typ wykresu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "图表类型" + } + } + } + }, + "Choose date" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "اختيار التاريخ" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Datum auswählen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Elegir fecha" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Choisir une date" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scegli la data" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日付を選択" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Datum kiezen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wybierz datę" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "选择日期" + } + } + } + }, + "Circular" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "دائري" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Kreisförmig" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Circular" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Circulaire" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Circolare" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "円形" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Cirkelvormig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Kołowy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "环形" + } + } + } + }, + "Civil Sunrise" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الشروق المدني" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bürgerlicher Sonnenaufgang" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Amanecer civil" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lever du soleil civil" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alba civile" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "常用薄明の日の出" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Civiele zonsopkomst" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Świt cywilny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "民用日出" + } + } + } + }, + "Civil Sunset" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الغروب المدني" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bürgerlicher Sonnenuntergang" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ocaso civil" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Coucher de soleil civil" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tramonto civile" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "常用薄明の日の入り" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Civiele zonsondergang" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zmierzch cywilny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "民用日落" + } + } + } + }, + "Civil Twilight" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الشفق المدني" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bürgerliche Dämmerung" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Crepúsculo civil" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Crépuscule civil" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crepuscolo civile" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "常用薄明" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Civiele schemering" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zmierzch cywilny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "民用暮光" + } + } + } + }, + "Classic" : { + "comment" : "Title for the \"Classic\" time travel mode.", + "isCommentAutoGenerated" : true, + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "كلاسيكي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Klassisch" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Clásico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Classique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Classico" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "クラシック" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Klassiek" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Klasyczny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "经典" + } + } + } + }, + "Close" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إغلاق" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Schließen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Cerrar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Fermer" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chiudere" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "閉じる" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sluiten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zamknij" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "关闭" + } + } + } + }, + "Compact" : { + "comment" : "Title of the \"Compact\" time travel appearance option.", + "isCommentAutoGenerated" : true, + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مضغوط" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Kompakt" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Compacto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Compact" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compatto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "コンパクト" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Compact" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Kompaktowy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "紧凑" + } + } + } + }, + "Configuration" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الإعداد" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Konfiguration" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Configuración" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Configuration" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Configurazione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "構成" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Configuratie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Konfiguracja" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "配置" + } + } + } + }, + "Continue with location" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "المتابعة باستخدام الموقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mit Standort fortfahren" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Continuar con la ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Continuer avec le lieu" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Continua con la posizione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "位置情報を使って続ける" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Doorgaan met locatie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Kontynuuj z lokalizacją" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "使用位置继续" + } + } + } + }, + "Current location" : { + "extractionState" : "stale", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الموقع الحالي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aktueller Ort" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ubicación actual" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lieu actuel" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posizione attuale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "現在地" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Huidige locatie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bieżąca lokalizacja" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "当前位置" + } + } + } + }, + "Current Location" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الموقع الحالي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aktueller Ort" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ubicación actual" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lieu actuel" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Posizione attuale" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "現在地" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Huidige locatie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bieżąca lokalizacja" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "当前位置" } } } }, - "a specific time" : { + "Current Location %@" : { "localizations" : { - "it" : { + "ar" : { "stringUnit" : { - "state" : "translated", - "value" : "un momento specifico" + "state" : "machine_translated", + "value" : "الموقع الحالي %@" } - } - } - }, - "About Solstice" : { - "localizations" : { - "it" : { + }, + "de" : { "stringUnit" : { - "state" : "translated", - "value" : "Circa Solstice" + "state" : "machine_translated", + "value" : "Aktueller Ort %@" } - } - } - }, - "About solstices and equinoxes" : { - "localizations" : { - "it" : { + }, + "es" : { "stringUnit" : { - "state" : "translated", - "value" : "A proposito di solstizi ed equinozi" + "state" : "machine_translated", + "value" : "Ubicación actual %@" } - } - } - }, - "Altitude" : { - - }, - "Appearance" : { - "localizations" : { - "it" : { + }, + "fr" : { "stringUnit" : { - "state" : "translated", - "value" : "Apparenza" + "state" : "machine_translated", + "value" : "Lieu actuel %@" } - } - } - }, - "Ascending" : { - "localizations" : { + }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Ascendente" + "value" : "Posizione attuale %@" } - } - } - }, - "Astronomical Sunrise" : { - "localizations" : { - "it" : { + }, + "ja" : { "stringUnit" : { - "state" : "translated", - "value" : "Alba astronomica" + "state" : "machine_translated", + "value" : "現在地 %@" } - } - } - }, - "Astronomical Sunset" : { - "localizations" : { - "it" : { + }, + "nl" : { "stringUnit" : { - "state" : "translated", - "value" : "Tramonto astronomico" + "state" : "machine_translated", + "value" : "Huidige locatie %@" } - } - } - }, - "Astronomical Twilight" : { - "localizations" : { - "it" : { + }, + "pl" : { "stringUnit" : { - "state" : "translated", - "value" : "Crepuscolo astronomico" + "state" : "machine_translated", + "value" : "Bieżąca lokalizacja %@" } - } - } - }, - "at %@" : { - "localizations" : { - "it" : { + }, + "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "alle %@" + "state" : "machine_translated", + "value" : "当前位置 %@" } } } }, - "Calculate the sunrise time on a given date in a given location" : { + "Current month" : { "localizations" : { - "it" : { + "ar" : { "stringUnit" : { - "state" : "translated", - "value" : "Calcola l'ora dell'alba in una determinata data in una determinata località" + "state" : "machine_translated", + "value" : "الشهر الحالي" } - } - } - }, - "Calculate the sunset time on a given date in a given location" : { - "localizations" : { - "it" : { + }, + "de" : { "stringUnit" : { - "state" : "translated", - "value" : "Calcola l'ora del tramonto in una determinata data in una determinata località" + "state" : "machine_translated", + "value" : "Aktueller Monat" } - } - } - }, - "Change how notifications behave when daily daylight begins to decrease. This can help with Seasonal Affective Disorder." : { - "localizations" : { - "it" : { + }, + "es" : { "stringUnit" : { - "state" : "translated", - "value" : "Modifica il comportamento delle notifiche quando la luce del giorno inizia a diminuire. Questo può aiutare con il disturbo affettivo stagionale (SAD)." + "state" : "machine_translated", + "value" : "Mes actual" } - } - } - }, - "Chart appearance" : { - "localizations" : { - "it" : { + }, + "fr" : { "stringUnit" : { - "state" : "translated", - "value" : "Aspetto del grafico" + "state" : "machine_translated", + "value" : "Mois actuel" } - } - } - }, - "Chart theme" : { - "localizations" : { + }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Tema del grafico" + "value" : "Mese corrente" } - } - } - }, - "Chart type" : { - "localizations" : { - "it" : { + }, + "ja" : { "stringUnit" : { - "state" : "translated", - "value" : "Tipo di grafico" + "state" : "machine_translated", + "value" : "今月" } - } - } - }, - "Choose date" : { - "localizations" : { - "it" : { + }, + "nl" : { "stringUnit" : { - "state" : "translated", - "value" : "Scegli la data" + "state" : "machine_translated", + "value" : "Huidige maand" } - } - } - }, - "Circular" : { - "localizations" : { - "it" : { + }, + "pl" : { "stringUnit" : { - "state" : "translated", - "value" : "Circolare" + "state" : "machine_translated", + "value" : "Bieżący miesiąc" } - } - } - }, - "Civil Sunrise" : { - "localizations" : { - "it" : { + }, + "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "Alba civile" + "state" : "machine_translated", + "value" : "当前月份" } } } }, - "Civil Sunset" : { + "Custom Location" : { "localizations" : { - "it" : { + "ar" : { "stringUnit" : { - "state" : "translated", - "value" : "Tramonto civile" + "state" : "machine_translated", + "value" : "موقع مخصص" } - } - } - }, - "Civil Twilight" : { - "localizations" : { - "it" : { + }, + "de" : { "stringUnit" : { - "state" : "translated", - "value" : "Crepuscolo civile" + "state" : "machine_translated", + "value" : "Benutzerdefinierter Ort" } - } - } - }, - "Classic" : { - "comment" : "Title for the \"Classic\" time travel mode.", - "isCommentAutoGenerated" : true, - "localizations" : { + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ubicación personalizada" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lieu personnalisé" + } + }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Classico" + "value" : "Posizione personalizzata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "カスタムの場所" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aangepaste locatie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lokalizacja niestandardowa" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "自定义位置" } } } }, - "Close" : { + "Customise notification content" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تخصيص محتوى الإشعارات" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mitteilungsinhalt anpassen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Personalizar el contenido de las notificaciones" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Personnaliser le contenu des notifications" + } + }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Chiudere" + "value" : "Personalizza il contenuto delle notifiche" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "通知内容をカスタマイズ" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Meldinginhoud aanpassen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dostosuj treść powiadomień" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "自定义通知内容" } } } }, - "Compact" : { - "comment" : "Title of the \"Compact\" time travel appearance option.", - "isCommentAutoGenerated" : true, + "Dan is prone to Seasonal Affective Disorder (SAD), a mood disorder often associated with the reduction in total daily sunlight hours during the darker months of the year. Solstice was created to help everyone look forward to brighter days." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dan معرَّض للاضطراب العاطفي الموسمي (SAD)، وهو اضطراب مزاجي غالبًا ما يرتبط بانخفاض إجمالي ساعات ضوء الشمس اليومية خلال الأشهر الأكثر ظلامًا من السنة. أُنشئ Solstice لمساعدة الجميع على التطلع إلى أيام أكثر إشراقًا." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dan neigt zur saisonal-affektiven Störung (SAD), einer Stimmungsstörung, die häufig mit der Abnahme der täglichen Sonnenlichtstunden während der dunkleren Monate des Jahres in Verbindung gebracht wird. Solstice wurde entwickelt, um allen zu helfen, sich auf hellere Tage zu freuen." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dan es propenso al trastorno afectivo estacional (TAE), un trastorno del estado de ánimo que suele asociarse con la reducción de las horas de luz solar diarias durante los meses más oscuros del año. Solstice se creó para ayudar a todo el mundo a ilusionarse con días más luminosos." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dan est sujet au trouble affectif saisonnier (TAS), un trouble de l’humeur souvent associé à la diminution du nombre total d’heures d’ensoleillement quotidien pendant les mois les plus sombres de l’année. Solstice a été créé pour aider chacun à attendre avec impatience des jours plus lumineux." + } + }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Compatto" + "value" : "Dan è incline al disturbo affettivo stagionale (SAD), un disturbo dell'umore spesso associato alla riduzione delle ore giornaliere totali di luce solare durante i mesi più bui dell'anno. Solstice è stato creato per aiutare tutti ad aspettarsi giorni più luminosi." + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Danは、季節性情動障害(SAD)になりやすい体質です。これは、一年のうち暗い時期に日々の日照時間が減少することとしばしば関連づけられる気分障害です。Solsticeは、誰もがより明るい日々を心待ちにできるように作られました。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dan is gevoelig voor een seizoensgebonden depressie (SAD), een stemmingsstoornis die vaak in verband wordt gebracht met de afname van het totale aantal uren daglicht per dag tijdens de donkere maanden van het jaar. Solstice is gemaakt om iedereen te helpen uitkijken naar lichtere dagen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dan jest podatny na sezonowe zaburzenie afektywne (SAD), zaburzenie nastroju często wiązane ze zmniejszeniem całkowitej dziennej liczby godzin nasłonecznienia w ciemniejszych miesiącach roku. Solstice powstało, aby pomóc każdemu z niecierpliwością wyczekiwać jaśniejszych dni." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dan 容易受季节性情感障碍(SAD)的影响,这是一种情绪障碍,通常与一年中较昏暗的月份里每日日照时长的减少有关。创造 Solstice 是为了帮助每个人都能期待更明亮的日子。" } } } }, - "Configuration" : { + "Date" : { "localizations" : { - "it" : { + "ar" : { "stringUnit" : { - "state" : "translated", - "value" : "Configurazione" + "state" : "machine_translated", + "value" : "التاريخ" } - } - } - }, - "Continue with location" : { - "localizations" : { - "it" : { + }, + "de" : { "stringUnit" : { - "state" : "translated", - "value" : "Continua con la posizione" + "state" : "machine_translated", + "value" : "Datum" } - } - } - }, - "Current location" : { - "localizations" : { - "it" : { + }, + "es" : { "stringUnit" : { - "state" : "translated", - "value" : "Posizione attuale" + "state" : "machine_translated", + "value" : "Fecha" } - } - } - }, - "Current Location" : { - "localizations" : { - "it" : { + }, + "fr" : { "stringUnit" : { - "state" : "translated", - "value" : "Posizione attuale" + "state" : "machine_translated", + "value" : "Date" } - } - } - }, - "Current Location %@" : { - "localizations" : { + }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Posizione attuale %@" + "value" : "Data" } - } - } - }, - "Current month" : { - "localizations" : { - "it" : { + }, + "ja" : { "stringUnit" : { - "state" : "translated", - "value" : "Mese corrente" + "state" : "machine_translated", + "value" : "日付" } - } - } - }, - "Custom Location" : { - "localizations" : { - "it" : { + }, + "nl" : { "stringUnit" : { - "state" : "translated", - "value" : "Posizione personalizzata" + "state" : "machine_translated", + "value" : "Datum" } - } - } - }, - "Customise notification content" : { - "localizations" : { - "it" : { + }, + "pl" : { "stringUnit" : { - "state" : "translated", - "value" : "Personalizza il contenuto delle notifiche" + "state" : "machine_translated", + "value" : "Data" } - } - } - }, - "Dan is prone to Seasonal Affective Disorder (SAD), a mood disorder often associated with the reduction in total daily sunlight hours during the darker months of the year. Solstice was created to help everyone look forward to brighter days." : { - "localizations" : { - "it" : { + }, + "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "Dan è incline al disturbo affettivo stagionale (SAD), un disturbo dell'umore spesso associato alla riduzione delle ore giornaliere totali di luce solare durante i mesi più bui dell'anno. Solstice è stato creato per aiutare tutti ad aspettarsi giorni più luminosi." + "state" : "machine_translated", + "value" : "日期" } } } }, - "Date" : { + "Day" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "نهار" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Día" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Jour" + } + }, "it" : { "stringUnit" : { - "state" : "translated", - "value" : "Data" + "state" : "machine_translated", + "value" : "Giorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "昼" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dzień" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "白天" } } } }, "Daylight" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ضوء النهار" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Luz natural" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lumière du jour" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Luce diurna" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Daglicht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Światło dzienne" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照" + } } } }, "Daylight duration" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مدة ضوء النهار" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslichtdauer" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Duración de la luz natural" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Durée de la lumière du jour" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Durata della luce del giorno" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Duur van daglicht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Czas trwania światła dziennego" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照时长" + } } } }, "Daylight gain/loss" : { + "extractionState" : "stale", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "فرق ضوء النهار" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslichtzunahme/-abnahme" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aumento o pérdida de luz natural" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Gain/perte de lumière du jour" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Guadagno/perdita di luce diurna" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照時間の増減" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Toename/afname daglicht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Przyrost/spadek światła dziennego" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照增减" + } } } }, "Daylight in %@" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ضوء النهار في %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht in %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Luz natural en %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lumière du jour à %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Luce del giorno a %@" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@の日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Daglicht in %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Światło dzienne w %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@的日照" + } } } }, "Daylight savings begins" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "بداية التوقيت الصيفي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Beginn der Sommerzeit" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Comienza el horario de verano" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Passage à l’heure d’été" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Inizia l'ora legale" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "夏時間の開始" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zomertijd begint" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Początek czasu letniego" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "夏令时开始" + } } } }, "Daylight savings ends" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "نهاية التوقيت الصيفي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ende der Sommerzeit" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Termina el horario de verano" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Passage à l’heure d’hiver" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "L'ora legale termina" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "夏時間の終了" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zomertijd eindigt" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Koniec czasu letniego" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "夏令时结束" + } } } }, "Daylight today" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ضوء النهار اليوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht heute" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Luz natural de hoy" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lumière du jour aujourd’hui" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Luce del giorno oggi" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日の日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Daglicht vandaag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Światło dzienne dziś" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日日照" + } } } }, "Daylight Today" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ضوء النهار اليوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht heute" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Luz natural de hoy" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lumière du jour aujourd’hui" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Luce del giorno oggi" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日の日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Daglicht vandaag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Światło dzienne dziś" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日日照" + } } } }, "Daylight tomorrow" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ضوء النهار غدًا" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht morgen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Luz natural de mañana" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lumière du jour demain" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Domani sarà la luce del giorno" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "明日の日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Daglicht morgen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Światło dzienne jutro" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "明日日照" + } } } }, "December" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ديسمبر" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dezember" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "diciembre" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Décembre" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dicembre" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "12月" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "December" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Grudzień" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "十二月" + } } } }, "December solstice" : { "comment" : "Name of December solstice", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "انقلاب ديسمبر الشمسي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dezember-Sonnenwende" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticio de diciembre" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice de décembre" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Solstizio di dicembre" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "12月の至点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonnewende in december" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Przesilenie grudniowe" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "十二月至点" + } } } }, "December Solstice Today" : { "comment" : "Notification title for December solstice", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "انقلاب ديسمبر الشمسي اليوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heute ist die Dezember-Sonnenwende" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hoy es el solsticio de diciembre" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice de décembre aujourd’hui" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Solstizio di dicembre oggi" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日は12月の至点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonnewende in december vandaag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dziś przesilenie grudniowe" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今天是十二月至点" + } } } }, "Delete Location" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "حذف الموقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ort löschen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Eliminar ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Supprimer le lieu" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina posizione" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所を削除" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locatie verwijderen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Usuń lokalizację" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "删除位置" + } } } }, "Descending" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تنازلي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Absteigend" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Descendente" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Décroissant" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Discendente" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "降順" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aflopend" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Malejąco" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "降序" + } } } }, "Edit this widget to select a new location." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يرجى تعديل هذه الودجة لتحديد موقع جديد." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bearbeite dieses Widget, um einen neuen Ort auszuwählen." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Edita este widget para seleccionar una nueva ubicación." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Modifiez ce widget pour sélectionner un nouveau lieu." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modifica questo widget per selezionare una nuova posizione." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "このウィジェットを編集して新しい場所を選択してください。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bewerk deze widget om een nieuwe locatie te selecteren." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Edytuj ten widżet, aby wybrać nową lokalizację." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "编辑此小组件以选择新位置。" + } } } }, "Enable location access or edit to select a new location." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يرجى تفعيل الوصول إلى الموقع أو التعديل لتحديد موقع جديد." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aktiviere den Standortzugriff oder bearbeite, um einen neuen Ort auszuwählen." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Activa el acceso a la ubicación o edita para seleccionar una nueva ubicación." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Activez l’accès à la localisation ou modifiez pour sélectionner un nouveau lieu." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita l'accesso alla posizione o modifica per selezionare una nuova posizione." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "位置情報へのアクセスを有効にするか、編集して新しい場所を選択してください。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Schakel locatietoegang in of bewerk om een nieuwe locatie te selecteren." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Włącz dostęp do lokalizacji lub edytuj, aby wybrać nową lokalizację." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "启用位置访问权限,或进行编辑以选择新位置。" + } } } }, "Enable location services" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تفعيل خدمات الموقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Standortdienste aktivieren" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Activar los servicios de ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Activer les services de localisation" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita i servizi di localizzazione" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "位置情報サービスを有効にする" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locatievoorzieningen inschakelen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Włącz usługi lokalizacji" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "启用位置服务" + } } } }, "Enable location services to see the daylight duration in your current location" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "فعِّل خدمات الموقع لعرض مدة ضوء النهار في موقعك الحالي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Standortdienste aktivieren, um die Tageslichtdauer an deinem aktuellen Ort zu sehen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Activa los servicios de ubicación para ver la duración de la luz natural en tu ubicación actual" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Activez les services de localisation pour voir la durée de la lumière du jour à votre position actuelle" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita i servizi di localizzazione per vedere la durata della luce diurna nella tua posizione attuale" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "現在地の日照時間を確認するには、位置情報サービスを有効にしてください。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Schakel locatievoorzieningen in om de duur van het daglicht op je huidige locatie te zien" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Włącz usługi lokalizacji, aby zobaczyć czas trwania światła dziennego w bieżącej lokalizacji" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "启用位置服务以查看当前位置的日照时长" + } } } }, "Enable notifications" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تفعيل الإشعارات" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mitteilungen aktivieren" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Activar notificaciones" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Activer les notifications" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Attivare le notifiche" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "通知を有効にする" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Meldingen inschakelen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Włącz powiadomienia" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "启用通知" + } } } }, "Equinox" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الاعتدال" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tagundnachtgleiche" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Equinoccio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Équinoxe" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Conto alla rovescia alba/tramonto" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Equinox" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Równonoc" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分点" + } } } }, "Equinox and Solstice" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الاعتدال والانقلاب الشمسي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tagundnachtgleiche und Sonnenwende" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Equinoccio y solsticio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Équinoxe et solstice" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Equinozio e Solstizio" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分点と至点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Equinox en zonnewende" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Równonoc i przesilenie" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分点与至点" + } } } }, "Evening" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "المساء" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Abend" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Noche" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Soir" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sera" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "夕方" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Avond" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wieczór" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "傍晚" + } } } }, "Event" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الحدث" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ereignis" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Evento" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Événement" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Evento" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "イベント" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Gebeurtenis" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zdarzenie" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "事件" + } } } }, "Event Time" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "وقت الحدث" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ereigniszeit" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hora del evento" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heure de l’événement" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita il viaggio nel tempo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "イベントの時刻" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijd van gebeurtenis" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Godzina zdarzenia" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "事件时间" + } } } }, "Follow the sun's journey throughout the day" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تابِع رحلة الشمس على مدار اليوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Verfolge den Weg der Sonne im Laufe des Tages" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sigue el recorrido del sol a lo largo del día" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Suivez la course du soleil tout au long de la journée" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Segui il viaggio del sole durante il giorno" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "一日を通じた太陽の軌跡をたどります。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Volg de reis van de zon gedurende de dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Śledź wędrówkę słońca w ciągu dnia" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "跟随太阳一整天的旅程" + } } } }, "For savouring the minutes you have, or looking forward to the minutes you’ll gain." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "للاستمتاع بالدقائق التي لديك، أو التطلع إلى الدقائق التي ستكسبها." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Um die Minuten zu genießen, die du hast, oder dich auf die zu freuen, die du gewinnst." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Para saborear los minutos que tienes o ilusionarte con los que vas a ganar." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pour savourer les minutes dont vous disposez, ou attendre avec impatience celles que vous gagnerez." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Per assaporare i minuti che hai, o aspettare con ansia i minuti che guadagnerai." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今ある時間を味わうために、そしてこれから増えていく時間を心待ちにするために。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Om te genieten van de minuten die je hebt, of uit te kijken naar de minuten die je erbij krijgt." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aby cieszyć się minutami, które masz, i z niecierpliwością wyczekiwać tych, które zyskasz." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "用心品味你已拥有的每一分钟,也满怀期待即将收获的每一分钟。" + } } } }, "Get sunrise time" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الحصول على وقت الشروق" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenaufgangszeit abrufen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtener la hora del amanecer" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtenir l’heure du lever du soleil" + } + }, "it" : { "stringUnit" : { - "state" : "translated", - "value" : "Ottieni l'ora dell'alba" + "state" : "translated", + "value" : "Ottieni l'ora dell'alba" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の出の時刻を取得" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonsopkomsttijd ophalen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sprawdź godzinę wschodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "获取日出时间" + } + } + } + }, + "Get Sunrise Time" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الحصول على وقت الشروق" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenaufgangszeit abrufen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtener la hora del amanecer" } - } - } - }, - "Get Sunrise Time" : { - "localizations" : { + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtenir l’heure du lever du soleil" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ottieni l'ora dell'alba" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の出の時刻を取得" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonsopkomsttijd ophalen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sprawdź godzinę wschodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "获取日出时间" + } } } }, "Get sunset time" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الحصول على وقت الغروب" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenuntergangszeit abrufen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtener la hora de la puesta de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtenir l’heure du coucher du soleil" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ottieni l'ora del tramonto" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の入りの時刻を取得" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonsondergangstijd ophalen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sprawdź godzinę zachodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "获取日落时间" + } } } }, "Get Sunset Time" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الحصول على وقت الغروب" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenuntergangszeit abrufen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtener la hora de la puesta de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtenir l’heure du coucher du soleil" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ottieni l'ora del tramonto" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の入りの時刻を取得" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonsondergangstijd ophalen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sprawdź godzinę zachodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "获取日落时间" + } } } }, "Get the daylight duration on ${date} in ${location}" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الحصول على مدة ضوء النهار بتاريخ ${date} في ${location}" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslichtdauer am ${date} in ${location} abrufen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtener la duración de la luz natural el ${date} en ${location}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtenir la durée de la lumière du jour le ${date} à ${location}" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ottieni la durata della luce diurna su ${date} a ${location}" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "${location}の${date}の日照時間を取得" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bekijk de duur van het daglicht op ${date} in ${location}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sprawdź czas trwania światła dziennego ${date} w ${location}" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "获取${location}在${date}的日照时长" + } } } }, "Get the sunrise time on ${date} in ${location}" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الحصول على وقت الشروق بتاريخ ${date} في ${location}" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenaufgangszeit am ${date} in ${location} abrufen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtener la hora del amanecer el ${date} en ${location}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtenir l’heure du lever du soleil le ${date} à ${location}" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ottieni l'ora dell'alba in ${date} a ${location}" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "${location}の${date}の日の出の時刻を取得" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bekijk de zonsopkomsttijd op ${date} in ${location}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sprawdź godzinę wschodu słońca ${date} w ${location}" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "获取${location}在${date}的日出时间" + } } } }, "Get the sunset time on ${date} in ${location}" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الحصول على وقت الغروب بتاريخ ${date} في ${location}" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenuntergangszeit am ${date} in ${location} abrufen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtener la hora de la puesta de sol el ${date} en ${location}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtenir l’heure du coucher du soleil le ${date} à ${location}" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ottieni l'orario del tramonto su ${date} a ${location}" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "${location}の${date}の日の入りの時刻を取得" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bekijk de zonsondergangstijd op ${date} in ${location}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sprawdź godzinę zachodu słońca ${date} w ${location}" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "获取${location}在${date}的日落时间" + } } } }, "Get today's remaining daylight in ${location}" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الحصول على ضوء النهار المتبقي اليوم في ${location}" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heute verbleibendes Tageslicht in ${location} abrufen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtener la luz natural restante de hoy en ${location}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Obtenir la lumière du jour restante aujourd’hui à ${location}" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ottieni la luce del giorno rimanente di oggi a ${location}" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "${location}の今日の残りの日照時間を取得" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bekijk het resterende daglicht van vandaag in ${location}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sprawdź pozostałe dziś światło dzienne w ${location}" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "获取${location}今天剩余的日照" + } } } }, "Good Afternoon" : { "comment" : "Notification title for afternoon notification", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مساء الخير" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Guten Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Buenas tardes" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bon après-midi" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Buon pomeriggio" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "こんにちは" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Goedemiddag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dzień dobry" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "下午好" + } } } }, "Good Evening" : { "comment" : "Notification title for evening notification", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "طاب مساؤك" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Guten Abend" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Buenas noches" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bonsoir" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Buona sera" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "こんばんは" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Goedenavond" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dobry wieczór" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "晚上好" + } } } }, "Good Morning" : { "comment" : "Notification title for morning notification", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "صباح الخير" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Guten Morgen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Buenos días" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bonjour" + } + }, "it" : { "stringUnit" : { - "state" : "translated", - "value" : "Buongiorno" + "state" : "translated", + "value" : "Buongiorno" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "おはようございます" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Goedemorgen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dzień dobry" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "早上好" + } + } + } + }, + "Graphical" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "رسومي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Grafisch" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Gráfico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Graphique" } - } - } - }, - "Graphical" : { - "localizations" : { + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Grafico" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "グラフィカル" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Grafisch" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Graficzny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "图形" + } } } }, "Hidden" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مخفي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ausgeblendet" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Oculto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Masqué" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascosto" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "非表示" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Verborgen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ukryty" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "隐藏" + } } } }, "Hide time travel" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إخفاء السفر عبر الزمن" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeitreise ausblenden" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ocultar el viaje en el tiempo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Masquer le voyage dans le temps" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascondi il viaggio nel tempo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "タイムトラベルを非表示" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijdreizen verbergen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ukryj podróż w czasie" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "隐藏时间旅行" + } } } }, "Imagery Source: [NASA Visible Earth Catalog](https://visibleearth.nasa.gov/collection/1484/blue-marble)" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مصدر الصور: [كتالوج NASA Visible Earth](https://visibleearth.nasa.gov/collection/1484/blue-marble)" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bildquelle: [NASA Visible Earth-Katalog](https://visibleearth.nasa.gov/collection/1484/blue-marble)" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Fuente de las imágenes: [Catálogo Visible Earth de la NASA](https://visibleearth.nasa.gov/collection/1484/blue-marble)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Source des images : [Catalogue Visible Earth de la NASA](https://visibleearth.nasa.gov/collection/1484/blue-marble)" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Fonte delle immagini: [Catalogo Visible Earth della NASA](https://visibleearth.nasa.gov/collection/1484/blue-marble)" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "画像提供: [NASA Visible Earth Catalog](https://visibleearth.nasa.gov/collection/1484/blue-marble)" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bron van afbeeldingen: [NASA Visible Earth Catalog](https://visibleearth.nasa.gov/collection/1484/blue-marble)" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Źródło zdjęć: [Katalog NASA Visible Earth](https://visibleearth.nasa.gov/collection/1484/blue-marble)" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "图像来源:[NASA Visible Earth Catalog](https://visibleearth.nasa.gov/collection/1484/blue-marble)" + } } } }, "In order for Solstice to calculate the sun’s position, it needs to access your location." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "لكي يتمكن Solstice من حساب موضع الشمس، يحتاج إلى الوصول إلى موقعك." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Damit Solstice die Position der Sonne berechnen kann, benötigt die App Zugriff auf deinen Standort." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Para que Solstice pueda calcular la posición del sol, necesita acceder a tu ubicación." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pour que Solstice puisse calculer la position du soleil, il doit accéder à votre position." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Affinché il Solstice calcoli la posizione del sole, deve accedere alla tua posizione." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeが太陽の位置を計算するには、位置情報へのアクセスが必要です。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Om de positie van de zon te kunnen berekenen, heeft Solstice toegang tot je locatie nodig." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aby Solstice mogło obliczyć położenie słońca, potrzebuje dostępu do Twojej lokalizacji." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "为了让 Solstice 计算太阳的位置,它需要访问你的位置信息。" + } } } }, "Jump to date" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الانتقال إلى التاريخ" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zum Datum springen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ir a la fecha" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aller à la date" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vai alla data" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日付へ移動" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ga naar datum" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Przejdź do daty" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "跳转到日期" + } } } }, "June" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يونيو" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Juni" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Junio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Juin" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Giugno" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "6月" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Juni" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Czerwiec" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "6月" + } } } }, "June solstice" : { "comment" : "Name of June solstice", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "انقلاب يونيو الشمسي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Juni-Sonnenwende" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticio de junio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice de juin" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Solstizio di giugno" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "6月の至点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonnewende in juni" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Przesilenie czerwcowe" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "六月至点" + } } } }, "June Solstice Today" : { "comment" : "Notification title for June solstice", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "انقلاب يونيو الشمسي اليوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heute ist die Juni-Sonnenwende" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hoy es el solsticio de junio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice de juin aujourd’hui" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Solstizio di giugno oggi" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日は6月の至点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonnewende in juni vandaag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dziś przesilenie czerwcowe" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今天是六月至点" + } } } }, "Just to confirm, you wanted '%@'?" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "للتأكيد فقط، هل قصدت \"%@\"؟" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nur zur Bestätigung: Du meintest „%@“?" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solo para confirmar, ¿querías «%@»?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Juste pour confirmer, vous vouliez « %@ » ?" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Giusto per confermare, volevi ‘%@‘?" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "確認ですが、“%@”でよろしいですか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Even ter bevestiging, je bedoelde „%@”?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tylko dla potwierdzenia: chodziło o „%@”?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "确认一下,你想要的是“%@”吗?" + } } } }, @@ -1101,304 +6112,1802 @@ }, "Leave a review" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "كتابة تقييم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bewertung schreiben" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dejar una reseña" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Laisser un avis" + } + }, "it" : { "stringUnit" : { - "state" : "translated", - "value" : "Lascia una recensione" + "state" : "translated", + "value" : "Lascia una recensione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "レビューを書く" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Een recensie achterlaten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Napisz recenzję" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "撰写评价" } } } }, "Leave a tip" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ترك إكرامية" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Trinkgeld hinterlassen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dejar una propina" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Laisser un pourboire" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Lasciare una mancia" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "チップを送る" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Een fooi geven" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zostaw napiwek" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "留下小费" + } } } }, "less" : { "comment" : "Less daylight middle of sentence", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "أقل" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "weniger" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "menos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "en moins" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "in meno" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "短く" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "minder" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "mniej" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "更少" + } } } }, "List appearance" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مظهر القائمة" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Listendarstellung" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aspecto de la lista" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Apparence de la liste" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aspetto dell'elenco" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "リストの外観" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Weergave van lijst" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wygląd listy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "列表外观" + } } } }, "List row style" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "نمط صف القائمة" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Stil der Listenzeilen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Estilo de fila de la lista" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Style des lignes de la liste" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Stile della riga dell'elenco" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "リストの行スタイル" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Stijl van lijstrij" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Styl wiersza listy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "列表行样式" + } } } }, "Loading..." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يجري التحميل…" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Laden …" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Cargando..." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Chargement..." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Caricamento..." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "読み込み中…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Laden..." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wczytywanie..." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "加载中…" + } } } }, "Local time" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "التوقيت المحلي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ortszeit" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hora local" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heure locale" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ora locale" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "現地時刻" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lokale tijd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Czas lokalny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "当地时间" + } } } }, "Location" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الموقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ort" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lieu" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ubicazione" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locatie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lokalizacja" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "位置" + } } } }, "Location Type" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "نوع الموقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ortstyp" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tipo de ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Type de lieu" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tipo di posizione" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所の種類" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locatietype" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Typ lokalizacji" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "位置类型" + } } } }, "Location update needed" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يلزم تحديث الموقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Standortaktualisierung erforderlich" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Se necesita actualizar la ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mise à jour de la position nécessaire" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento della posizione necessario" } - } - } - }, - "Locations" : { - "localizations" : { - "it" : { + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所の更新が必要です" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locatie-update vereist" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wymagana aktualizacja lokalizacji" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "需要更新位置" + } + } + } + }, + "Locations" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "المواقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Orte" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ubicaciones" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lieux" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Luoghi" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locaties" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lokalizacje" + } + }, + "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "Luoghi" + "state" : "machine_translated", + "value" : "位置" } } } }, "Locations are synced via iCloud. Delete or add new locations by using Solstice on Mac, iPhone, iPad, or Apple Vision Pro" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تجري مزامنة المواقع عبر iCloud. يمكنك حذف المواقع أو إضافة مواقع جديدة باستخدام Solstice على Mac أو iPhone أو iPad أو Apple Vision Pro" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Orte werden über iCloud synchronisiert. Orte lassen sich mit Solstice auf dem Mac, iPhone, iPad oder Apple Vision Pro löschen oder hinzufügen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Las ubicaciones se sincronizan mediante iCloud. Elimina o añade nuevas ubicaciones usando Solstice en el Mac, iPhone, iPad o Apple Vision Pro" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Les lieux sont synchronisés via iCloud. Supprimez ou ajoutez des lieux à l’aide de Solstice sur Mac, iPhone, iPad ou Apple Vision Pro" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Le posizioni vengono sincronizzate tramite iCloud. Elimina o aggiungi nuove posizioni usando Solstice su Mac, iPhone, iPad o Apple Vision Pro" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所はiCloud経由で同期されます。Mac、iPhone、iPad、またはApple Vision ProでSolsticeを使って、場所を削除したり新しく追加したりできます。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locaties worden gesynchroniseerd via iCloud. Verwijder of voeg nieuwe locaties toe met Solstice op de Mac, iPhone, iPad of Apple Vision Pro" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lokalizacje są synchronizowane przez iCloud. Usuwaj lub dodawaj nowe lokalizacje, korzystając z Solstice na Macu, iPhonie, iPadzie lub Apple Vision Pro" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "位置通过 iCloud 同步。你可以在 Mac、iPhone、iPad 或 Apple Vision Pro 上使用 Solstice 删除或添加新位置。" + } } } }, "Longest day" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "أطول يوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Längster Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "El día más largo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Jour le plus long" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La giornata più lunga" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "最も長い日" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Langste dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Najdłuższy dzień" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "白昼最长的一天" + } } } }, "March" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مارس" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "März" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Marzo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mars" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Marzo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "3月" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Maart" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Marzec" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "3月" + } } } }, "March equinox" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "اعتدال مارس" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "März-Tagundnachtgleiche" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Equinoccio de marzo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Équinoxe de mars" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Equinozio di marzo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "3月の分点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Equinox in maart" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Równonoc marcowa" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "三月分点" + } } } }, "March Equinox Today" : { "comment" : "Notification title for March equinox", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "اعتدال مارس اليوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heute ist die März-Tagundnachtgleiche" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hoy es el equinoccio de marzo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Équinoxe de mars aujourd’hui" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Equinozio di marzo oggi" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日は3月の分点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Equinox in maart vandaag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dziś równonoc marcowa" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今天是三月分点" + } } } }, "Monochrome" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "أحادي اللون" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Monochrom" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Monocromo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Monochrome" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Monocromatico" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "モノクロ" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Monochroom" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Monochromatyczny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "单色" + } } } }, "Month:" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الشهر:" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Monat:" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mes:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mois :" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mese:" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "月:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Maand:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Miesiąc:" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "月份:" + } } } }, "more" : { "comment" : "More daylight middle of sentence", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "أكثر" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "mehr" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "más" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "en plus" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "di più" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "長い" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "meer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "więcej" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "更多" + } } } }, "Morning" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الصباح" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Morgen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mañana" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Matin" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mattina" } - } - } - }, - "Nautical Sunrise" : { - "localizations" : { - "it" : { + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "朝" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ochtend" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Rano" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "上午" + } + } + } + }, + "Nautical Sunrise" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الشروق البحري" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nautischer Sonnenaufgang" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Amanecer náutico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lever du soleil nautique" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Alba nautica" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "航海薄明の日の出" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nautische zonsopkomst" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Świt nautyczny" + } + }, + "zh-Hans" : { "stringUnit" : { - "state" : "translated", - "value" : "Alba nautica" + "state" : "machine_translated", + "value" : "航海日出" } } } }, "Nautical Sunset" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الغروب البحري" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nautischer Sonnenuntergang" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ocaso náutico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Coucher de soleil nautique" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tramonto nautico" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "航海薄明の日の入り" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nautische zonsondergang" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zmierzch nautyczny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "航海日落" + } } } }, "Nautical Twilight" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الشفق البحري" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nautische Dämmerung" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Crepúsculo náutico" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Crépuscule nautique" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Crepuscolo nautico" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "航海薄明" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nautische schemering" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zmierzch nautyczny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "航海暮光" + } } } }, "Next equinox" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الاعتدال التالي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nächste Tagundnachtgleiche" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Próximo equinoccio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Prochain équinoxe" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Prossimo equinozio" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "次の分点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Volgende equinox" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Następna równonoc" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "下次分点" + } } } }, "Next solstice" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الانقلاب الشمسي التالي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nächste Sonnenwende" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Próximo solsticio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Prochain solstice" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Prossimo solstizio" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "次の至点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Volgende zonnewende" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Następne przesilenie" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "下次至点" + } + } + } + }, + "Night" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ليل" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nacht" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Noche" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nuit" + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Notte" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "夜" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nacht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Noc" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "夜晚" + } } } }, "No change" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "بلا تغيير" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Keine Änderung" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sin cambios" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aucun changement" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun cambiamento" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "変化なし" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Geen verandering" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bez zmian" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "无变化" + } } } }, "No daylight left today. The sun set %@ ago." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "لم يتبقَّ ضوء نهار اليوم. غربت الشمس منذ %@." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heute ist kein Tageslicht mehr übrig. Die Sonne ist vor %@ untergegangen." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hoy ya no queda luz natural. El sol se puso hace %@." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Il ne reste plus de lumière du jour aujourd’hui. Le soleil s’est couché il y a %@." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Non è rimasta più la luce del giorno oggi. Il sole è tramontato %@ fa." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日はもう日照がありません。太陽は%@前に沈みました。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Er is vandaag geen daglicht meer over. De zon is %@ geleden ondergegaan." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dziś nie ma już światła dziennego. Słońce zaszło %@ temu." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今天已没有日照了。太阳已在%@前落下。" + } } } }, "No location found" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "لم يتم العثور على موقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Kein Ort gefunden" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "No se encontró ninguna ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aucun lieu trouvé" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessuna posizione trovata" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所が見つかりません" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Geen locatie gevonden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nie znaleziono lokalizacji" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "未找到位置" + } } } }, "No location selected" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "لم يتم تحديد موقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Kein Ort ausgewählt" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ninguna ubicación seleccionada" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aucun lieu sélectionné" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessuna posizione selezionata" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所が選択されていません" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Geen locatie geselecteerd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nie wybrano lokalizacji" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "未选择位置" + } } } }, "No locations" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "لا توجد مواقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Keine Orte" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sin ubicaciones" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aucun lieu" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessuna località" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所がありません" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Geen locaties" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Brak lokalizacji" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "暂无位置" + } } } }, @@ -1406,17 +7915,65 @@ "comment" : "Notification fragment for length of daylight", "extractionState" : "extracted_with_value", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ من ضوء النهار اليوم." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ Tageslicht heute." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%@ of daylight today." } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ de luz natural hoy." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ de lumière du jour aujourd’hui." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ di luce oggi." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日の日照時間は%@です。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ daglicht vandaag." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ światła dziennego dziś." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今天有%@日照。" + } } } }, @@ -1424,17 +7981,65 @@ "comment" : "Notification fragment for less daylight compared to yesterday", "extractionState" : "extracted_with_value", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ من ضوء النهار أقل من الأمس." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ weniger Tageslicht als gestern." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%@ less daylight than yesterday." } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ menos de luz natural que ayer." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ de lumière du jour en moins par rapport à hier." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ in meno di luce del giorno rispetto a ieri." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "昨日より日照時間が%@短くなりました。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ minder daglicht dan gisteren." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ mniej światła dziennego niż wczoraj." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照比昨天少%@。" + } } } }, @@ -1442,34 +8047,321 @@ "comment" : "Notification fragment for more daylight compared to yesterday", "extractionState" : "extracted_with_value", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ من ضوء النهار أكثر من الأمس." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ mehr Tageslicht als gestern." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "%@ more daylight than yesterday." } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ más de luz natural que ayer." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ de lumière du jour en plus par rapport à hier." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ di luce diurna in più rispetto a ieri." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "昨日より日照時間が%@長くなりました。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ meer daglicht dan gisteren." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@ więcej światła dziennego niż wczoraj." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照比昨天多%@。" + } } } }, "notif-solstice-countdown" : { "comment" : "Notification fragment for days until next solstice", - "extractionState" : "extracted_with_value", "localizations" : { + "ar" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld أيام حتى %2$@." + } + }, + "many" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld يومًا حتى %2$@." + } + }, + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يوم واحد حتى %2$@." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld يوم حتى %2$@." + } + }, + "two" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يومان حتى %2$@." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld يوم حتى %2$@." + } + } + } + } + }, + "de" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Noch %1$lld Tag bis zur %2$@." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Noch %1$lld Tage bis zur %2$@." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Noch %1$lld Tage bis zur %2$@." + } + } + } + } + }, "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$lld days until the %2$@." + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld day until the %2$@." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld days until the %2$@." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld days until the %2$@." + } + } + } + } + }, + "es" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld día hasta el %2$@." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld días hasta el %2$@." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld días hasta el %2$@." + } + } + } + } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld jour avant le %2$@." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld jours avant le %2$@." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld jour avant le %2$@." + } + } + } } }, "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$lld giorni al %2$@." + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%1$lld giorni al %2$@." + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld giorni al %2$@." + } + }, + "zero" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "%1$lld giorni al %2$@." + } + } + } + } + }, + "ja" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%2$@まであと%1$lld日。" + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%2$@まであと%1$lld日。" + } + } + } + } + }, + "nl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld dag tot de %2$@." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld dagen tot de %2$@." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld dagen tot de %2$@." + } + } + } + } + }, + "pl" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld dni do %2$@." + } + }, + "many" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld dni do %2$@." + } + }, + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld dzień do %2$@." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld dnia do %2$@." + } + } + } + } + }, + "zh-Hans" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "距离%2$@还有%1$lld天。" + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "距离%2$@还有%1$lld天。" + } + } + } } } } @@ -1478,27 +8370,123 @@ "comment" : "Notification fragment for sunrise and sunset times", "extractionState" : "extracted_with_value", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تشرق الشمس في %1$@ وتغرب في %2$@." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Die Sonne geht um %1$@ auf und um %2$@ unter." + } + }, "en" : { "stringUnit" : { "state" : "new", "value" : "The sun rises at %1$@ and sets at %2$@." } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "El sol sale a las %1$@ y se pone a las %2$@." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Le soleil se lève à %1$@ et se couche à %2$@." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il sole sorge alle %1$@ e tramonta alle %2$@." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の出は%1$@、日の入りは%2$@です。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "De zon komt op om %1$@ en gaat onder om %2$@." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Słońce wschodzi o %1$@ i zachodzi o %2$@." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "太阳在%1$@升起,在%2$@落下。" + } } } }, "Notification Preview" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "معاينة الإشعار" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mitteilungsvorschau" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Vista previa de la notificación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aperçu de la notification" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Anteprima della notifica" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "通知プレビュー" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Voorbeeld van melding" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Podgląd powiadomienia" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "通知预览" + } } } }, @@ -1506,11 +8494,59 @@ "comment" : "A section header for settings related to notifications.", "isCommentAutoGenerated" : true, "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الإشعارات" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mitteilungen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Notificaciones" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Notifications" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Notifiche" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "通知" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Meldingen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Powiadomienia" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "通知" + } } } }, @@ -1518,11 +8554,59 @@ "comment" : "A button that takes the user one week back in time.", "isCommentAutoGenerated" : true, "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "قبل أسبوع واحد" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Eine Woche früher" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Una semana antes" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Une semaine plus tôt" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Una settimana prima" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "1週間前" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Een week eerder" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tydzień wcześniej" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "向前一周" + } } } }, @@ -1530,289 +8614,1634 @@ "comment" : "A button that advances the date by one week.", "isCommentAutoGenerated" : true, "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "بعد أسبوع واحد" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Eine Woche später" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Una semana después" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Une semaine plus tard" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Una settimana dopo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "1週間後" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Een week later" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tydzień później" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "向后一周" + } } } }, "Open Solstice to see how today's daylight has changed." : { "comment" : "Fallthrough notification content for when notification settings specify no content.", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يمكنك فتح Solstice لمعرفة كيف تغيّر ضوء النهار اليوم." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Öffne Solstice, um zu sehen, wie sich das Tageslicht heute verändert hat." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Abre Solstice para ver cómo ha cambiado la luz natural de hoy." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ouvrez Solstice pour voir comment la lumière du jour a évolué aujourd’hui." + } + }, "it" : { "stringUnit" : { - "state" : "translated", - "value" : "Apri Solstice per vedere come è cambiata la luce del giorno di oggi." + "state" : "translated", + "value" : "Apri Solstice per vedere come è cambiata la luce del giorno di oggi." + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeを開いて、今日の日照時間がどのように変化したかを確認しましょう。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Open Solstice om te zien hoe het daglicht vandaag is veranderd." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Otwórz Solstice, aby zobaczyć, jak zmieniło się dzisiejsze światło dzienne." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "打开 Solstice,看看今天的日照有何变化。" + } + } + } + }, + "Order" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الترتيب" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Reihenfolge" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Orden" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ordre" } - } - } - }, - "Order" : { - "localizations" : { + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ordinazione" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "順序" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Volgorde" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Kolejność" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "顺序" + } } } }, "Phase" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "المرحلة" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Phase" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Fase" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Phase" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Fase" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "段階" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Fase" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Faza" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "阶段" + } } } }, "Read more" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "قراءة المزيد" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mehr erfahren" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Leer más" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "En savoir plus" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Leggi di più" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "続きを読む" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Meer lezen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Czytaj więcej" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "了解更多" + } } } }, "Remaining daylight" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ضوء النهار المتبقي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Verbleibendes Tageslicht" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Luz natural restante" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lumière du jour restante" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Luce del giorno rimanente" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "残りの日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Resterend daglicht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pozostałe światło dzienne" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "剩余日照" + } } } }, "Remove daylight gain/loss" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إزالة فرق ضوء النهار" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslichtdifferenz ausblenden" + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Remove daylight loss" } }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ocultar el aumento o la pérdida de luz natural" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Masquer le gain/la perte de lumière du jour" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovere la perdita di luce diurna" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照時間の増減を非表示にする" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afname daglicht verwijderen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Usuń różnicę światła dziennego" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "隐藏日照增减" + } } } }, "Reset" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إعادة تعيين" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zurücksetzen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Restablecer" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Réinitialiser" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ripristina" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "リセット" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Herstellen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Resetuj" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "重置" + } } } }, "SAD preference" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تفضيلات الاضطراب العاطفي الموسمي (SAD)" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "SAD-Einstellung" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Preferencia de TAE" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Préférence TAS" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Preferenza SAD" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "季節性情動障害(SAD)の設定" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "SAD-voorkeur" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ustawienie SAD" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "季节性情感障碍(SAD)偏好" + } } } }, "Save Location" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "حفظ الموقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ort sichern" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Guardar ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Enregistrer le lieu" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Salva l'indirizzo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所を保存" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locatie bewaren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zapisz lokalizację" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "存储位置" + } } } }, "Search for a location or enable location services" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ابحث عن موقع أو فعِّل خدمات الموقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nach einem Ort suchen oder Standortdienste aktivieren" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Busca una ubicación o activa los servicios de ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Rechercher un lieu ou activer les services de localisation" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cerca una posizione o attiva i servizi di localizzazione" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所を検索するか、位置情報サービスを有効にしてください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zoek een locatie of schakel locatievoorzieningen in" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wyszukaj lokalizację lub włącz usługi lokalizacji" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "搜索位置或启用位置服务" + } } } }, "Search locations" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "البحث عن المواقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Orte suchen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Buscar ubicaciones" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Rechercher des lieux" + } + }, "it" : { "stringUnit" : { - "state" : "translated", - "value" : "Cerca posizione" + "state" : "translated", + "value" : "Cerca posizione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所を検索" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locaties zoeken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wyszukaj lokalizacje" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "搜索位置" + } + } + } + }, + "See the time remaining until the next sunrise/sunset" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عرض الوقت المتبقي حتى الشروق أو الغروب التالي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeigt die verbleibende Zeit bis zum nächsten Sonnenauf- bzw. -untergang an" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Consulta el tiempo restante hasta el próximo amanecer o puesta de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Voir le temps restant jusqu’au prochain lever ou coucher du soleil" } - } - } - }, - "See the time remaining until the next sunrise/sunset" : { - "localizations" : { + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza il tempo rimanente fino alla prossima alba/tramonto" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "次の日の出/日の入りまでの残り時間を確認できます。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bekijk de resterende tijd tot de volgende zonsopkomst/zonsondergang" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zobacz czas pozostały do następnego wschodu/zachodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "查看距离下次日出/日落的剩余时间" + } } } }, "See today's daylight length, how it compares to yesterday, and sunrise/sunset times." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "اطّلع على مدة ضوء النهار اليوم، وكيف تقارَن بالأمس، وأوقات الشروق والغروب." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeigt die heutige Tageslichtdauer, den Vergleich mit gestern sowie die Zeiten für Sonnenauf- und -untergang an." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Consulta la duración de la luz natural de hoy, cómo se compara con la de ayer y las horas de amanecer y puesta de sol." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Affiche la durée de lumière du jour aujourd’hui, sa comparaison avec hier, ainsi que les heures de lever et de coucher du soleil." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Guarda la lunghezza della luce del giorno di oggi, come si confronta con ieri e gli orari dell'alba/tramonto." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日の日照時間、昨日との比較、および日の出/日の入りの時刻を確認できます。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bekijk de daglengte van vandaag, hoe die zich verhoudt tot gisteren, en de tijden van zonsopkomst/zonsondergang." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zobacz dzisiejszą długość dnia, jak wypada w porównaniu z wczoraj oraz godziny wschodu/zachodu słońca." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "查看今天的日照时长、与昨天的对比,以及日出/日落时间。" + } } } }, "Select a location to view details" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "اختر موقعًا لعرض التفاصيل" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wähle einen Ort aus, um Details anzuzeigen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Selecciona una ubicación para ver los detalles" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sélectionnez un lieu pour afficher les détails" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Seleziona una posizione per visualizzare i dettagli" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "詳細を表示するには場所を選択してください" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Selecteer een locatie om details te bekijken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wybierz lokalizację, aby wyświetlić szczegóły" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "选择一个位置以查看详情" + } } } }, "Select location" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "اختيار موقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ort auswählen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Seleccionar ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sélectionner un lieu" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Seleziona la posizione" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所を選択" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locatie selecteren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wybierz lokalizację" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "选择位置" + } } } }, "Send at" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الإرسال عند" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Senden um" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Enviar a las" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Envoyer à" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Invia alle" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "送信時刻" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Verzenden om" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wyślij o" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "发送时间" + } } } }, "September" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "سبتمبر" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "September" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Septiembre" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Septembre" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Settembre" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "9月" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "September" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wrzesień" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "9月" + } } } }, "September equinox" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "اعتدال سبتمبر" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "September-Tagundnachtgleiche" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Equinoccio de septiembre" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Équinoxe de septembre" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Equinozio di settembre" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "9月の分点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Equinox in september" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Równonoc wrześniowa" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "九月分点" + } } } }, "September Equinox Today" : { "comment" : "Notification title for September equinox", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "اعتدال سبتمبر اليوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heute ist die September-Tagundnachtgleiche" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hoy es el equinoccio de septiembre" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Équinoxe de septembre aujourd’hui" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Equinozio di settembre oggi" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日は9月の分点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Equinox in september vandaag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dziś równonoc wrześniowa" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今天是九月分点" + } } } }, "Settings" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الإعدادات" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Einstellungen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ajustes" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Réglages" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Impostazioni" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "設定" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Instellingen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ustawienia" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "设置" + } } } }, "Share image" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مشاركة الصورة" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bild teilen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Compartir imagen" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Partager l’image" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Condividi immagine" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "画像を共有" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afbeelding delen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Udostępnij obraz" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分享图片" + } } } }, "Share to Instagram Story" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "المشاركة في Instagram Story" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "In Instagram Story teilen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Compartir en Instagram Story" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Partager dans Instagram Story" + } + }, "it" : { "stringUnit" : { - "state" : "translated", - "value" : "Condividi su Instagram Story" + "state" : "translated", + "value" : "Condividi su Instagram Story" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Instagram Storyに共有" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Delen in Instagram Story" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Udostępnij w Instagram Story" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分享到 Instagram Story" } } } }, "Share..." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مشاركة…" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Teilen …" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Compartir…" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Partager…" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Condividi..." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "共有…" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Delen..." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Udostępnij..." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分享…" + } } } }, "Shortest day" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "أقصر يوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Kürzester Tag" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "El día más corto" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Jour le plus court" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La giornata più corta" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "最も短い日" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Kortste dag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Najkrótszy dzień" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "白昼最短的一天" + } } } }, "Show daylight for ${location}" : { + "extractionState" : "stale", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إظهار ضوء النهار لموقع ${location}" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht für ${location} anzeigen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mostrar la luz natural de ${location}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afficher la lumière du jour pour ${location}" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra la luce del giorno per ${location}" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "${location}の日照時間を表示" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Daglicht tonen voor ${location}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pokaż światło dzienne dla ${location}" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "显示${location}的日照" + } } } }, "Show daylight for ${selectedLocation}" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إظهار ضوء النهار لموقع ${selectedLocation}" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht für ${selectedLocation} anzeigen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mostrar la luz natural de ${selectedLocation}" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afficher la lumière du jour pour ${selectedLocation}" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra la luce del giorno per ${selectedLocation}" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "${selectedLocation}の日照時間を表示" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Daglicht tonen voor ${selectedLocation}" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pokaż światło dzienne dla ${selectedLocation}" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "显示${selectedLocation}的日照" + } } } }, "Show location" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إظهار الموقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ort anzeigen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mostrar ubicación" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afficher le lieu" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra posizione" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所を表示" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locatie tonen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pokaż lokalizację" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "显示位置" + } } } }, @@ -1820,276 +10249,1772 @@ "comment" : "A label for a button that shows the time travel interface.", "isCommentAutoGenerated" : true, "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إظهار السفر عبر الزمن" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeitreise einblenden" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mostrar el viaje en el tiempo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afficher le voyage dans le temps" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra il viaggio nel tempo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "タイムトラベルを表示" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijdreizen tonen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pokaż podróż w czasie" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "显示时间旅行" + } } } }, "Simple" : { + "extractionState" : "stale", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "بسيط" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Einfach" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sencillo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Simple" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Semplice" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "シンプル" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Eenvoudig" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Prosty" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "简约" + } } } }, "Solar Chart" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الرسم البياني الشمسي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnendiagramm" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Gráfico solar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Graphique solaire" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Carta solare" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "太陽チャート" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonnegrafiek" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wykres słoneczny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "太阳图表" + } } } }, "Solar noon" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الظهيرة الشمسية" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenhöchststand" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mediodía solar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Midi solaire" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mezzogiorno solare" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "太陽の南中" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonnemiddag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Południe słoneczne" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "正午" + } } } }, "Solstice" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الانقلاب الشمسي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenwende" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Solstizio" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "至点" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonnewende" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Przesilenie" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "至点" + } } } }, "Solstice is an app made by [Dan Eden](https://daneden.me), a designer and developer based in Manchester, England." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice تطبيق من إنشاء [Dan Eden](https://daneden.me)، وهو مصمم ومطوّر يقيم في مانشستر بإنجلترا." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice ist eine App von [Dan Eden](https://daneden.me), einem Designer und Entwickler aus Manchester, England." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice es una app creada por [Dan Eden](https://daneden.me), un diseñador y desarrollador afincado en Manchester, Inglaterra." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice est une app créée par [Dan Eden](https://daneden.me), un designer et développeur basé à Manchester, en Angleterre." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Solstice è un'app realizzata da [Dan Eden](https://daneden.me), un designer e sviluppatore con sede a Manchester, in Inghilterra." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeは、イギリスのマンチェスターを拠点とするデザイナー兼開発者の[Dan Eden](https://daneden.me)によって作られたアプリです。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice is een app gemaakt door [Dan Eden](https://daneden.me), een ontwerper en ontwikkelaar gevestigd in Manchester, Engeland." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice to aplikacja stworzona przez [Dana Edena](https://daneden.me), projektanta i programistę z Manchesteru w Anglii." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice 由 [Dan Eden](https://daneden.me) 开发,他是一位常驻英国曼彻斯特的设计师兼开发者。" + } } } }, "Solstice tells you how much daylight there is today compared to yesterday." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يُطلعك Solstice على مقدار ضوء النهار اليوم مقارنةً بالأمس." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice zeigt dir, wie viel Tageslicht es heute im Vergleich zu gestern gibt." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice te muestra cuánta luz natural hay hoy en comparación con ayer." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice vous indique la quantité de lumière du jour aujourd’hui par rapport à hier." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Solstice ti dice quanta luce c'è oggi rispetto a ieri." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeは、今日の日照時間が昨日と比べてどれくらいかを教えてくれます。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice laat je zien hoeveel daglicht er vandaag is vergeleken met gisteren." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice pokazuje, ile światła dziennego jest dzisiaj w porównaniu z wczoraj." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice 会告诉你今天的日照与昨天相比有多少。" + } } } }, "Sort by" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ترتيب حسب" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sortieren nach" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ordenar por" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Trier par" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ordina per" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "並べ替え" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sorteren op" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sortuj według" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "排序方式" + } } } }, "Sort locations" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ترتيب المواقع" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Orte sortieren" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ordenar ubicaciones" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Trier les lieux" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ordina le località" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "場所を並べ替え" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Locaties sorteren" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sortuj lokalizacje" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "排序位置" + } } } }, "Submit feedback" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إرسال الملاحظات" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Feedback senden" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Enviar comentarios" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Envoyer un commentaire" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Invia feedback" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "フィードバックを送信" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Feedback verzenden" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wyślij opinię" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "提交反馈" + } } } }, "Sundial" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مزولة شمسية" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenuhr" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Reloj de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Cadran solaire" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Meridiana" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日時計" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonnewijzer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zegar słoneczny" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日晷" + } } } }, "Sunrise" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الشروق" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenaufgang" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Amanecer" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lever du soleil" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Alba" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の出" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonsopkomst" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wschód słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日出" + } } } }, "Sunrise in %@" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الشروق بعد %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenaufgang in %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Amanecer en %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lever du soleil dans %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Alba tra %@" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@後に日の出" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonsopkomst over %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wschód słońca za %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@的日出" + } } } }, "Sunrise/Sunset Countdown" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "العد التنازلي للشروق والغروب" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Countdown bis Sonnenauf-/-untergang" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Cuenta atrás para el amanecer/la puesta de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Compte à rebours du lever et du coucher du soleil" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Conto alla rovescia alba/tramonto" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の出/日の入りのカウントダウン" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aftelling zonsopkomst/zonsondergang" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Odliczanie do wschodu/zachodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日出/日落倒计时" + } } } }, "Sunrise/sunset times" : { + "extractionState" : "stale", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "أوقات الشروق والغروب" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeiten für Sonnenauf-/-untergang" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Horas de amanecer y puesta de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heures de lever et de coucher du soleil" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Orari di alba/tramonto" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の出/日の入りの時刻" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijden van zonsopkomst/zonsondergang" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Godziny wschodu/zachodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日出/日落时间" + } } } }, "Sunset" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الغروب" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenuntergang" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Puesta de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Coucher de soleil" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tramonto" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の入り" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonsondergang" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zachód słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日落" + } } } }, "Sunset in %@" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الغروب بعد %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Sonnenuntergang in %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Puesta de sol en %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Coucher de soleil dans %@" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tramonto tra %@" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の入りまであと%@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zonsondergang over %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zachód słońca za %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%@后日落" + } } } }, "Suppress notifications altogether" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إيقاف الإشعارات تمامًا" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mitteilungen ganz unterdrücken" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Silenciar las notificaciones por completo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Désactiver toutes les notifications" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina del tutto le notifiche" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "通知を完全にオフにする" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Meldingen volledig onderdrukken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Całkowicie wyłącz powiadomienia" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "完全关闭通知" + } } } }, "The configuration options for Solstice widgets" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "خيارات إعداد ودجات Solstice" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Die Konfigurationsoptionen für Solstice-Widgets" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Las opciones de configuración de los widgets de Solstice" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Les options de configuration des widgets Solstice" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "The configuration options for Solstice widgets" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeウィジェットの構成オプション" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "De configuratieopties voor Solstice-widgets" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Opcje konfiguracji widżetów Solstice" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice 小组件的配置选项" + } } } }, "The equinox and solstice define the transitions between the seasons of the astronomical calendar and are a key part of the Earth’s orbit around the Sun." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يحدد الاعتدال والانقلاب الشمسي مراحل الانتقال بين فصول التقويم الفلكي، وهما جزء أساسي من مدار الأرض حول الشمس." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Die Tagundnachtgleiche und die Sonnenwende markieren die Übergänge zwischen den Jahreszeiten des astronomischen Kalenders und sind ein zentraler Bestandteil der Umlaufbahn der Erde um die Sonne." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "El equinoccio y el solsticio marcan las transiciones entre las estaciones del calendario astronómico y son una parte fundamental de la órbita de la Tierra alrededor del Sol." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "L’équinoxe et le solstice définissent les transitions entre les saisons du calendrier astronomique et constituent un élément clé de l’orbite de la Terre autour du Soleil." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "L’equinozio e il solstizio definiscono le transizioni tra le stagioni del calendario astronomico e sono una parte fondamentale dell’orbita della Terra attorno al Sole." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分点と至点は、天文暦における季節の移り変わりを定めるもので、地球が太陽の周りを回る軌道の重要な要素です。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "De equinox en zonnewende bepalen de overgangen tussen de seizoenen van de astronomische kalender en vormen een belangrijk onderdeel van de baan van de aarde rond de zon." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Równonoc i przesilenie wyznaczają przejścia między porami roku w kalendarzu astronomicznym i stanowią kluczowy element ruchu Ziemi po orbicie wokół Słońca." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分点和至点标志着天文历法中各季节之间的过渡,是地球绕太阳运行轨道的重要组成部分。" + } } } }, "The equinox occurs twice a year, in March and September, and mark the points when the Sun crosses the equator’s path. During the equinox, day and night are around the same length all across the globe." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يحدث الاعتدال مرتين في السنة، في مارس وسبتمبر، ويشير إلى اللحظتين اللتين تعبر فيهما الشمس مسار خط الاستواء. خلال الاعتدال، يتساوى طول النهار والليل تقريبًا في جميع أنحاء العالم." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Die Tagundnachtgleiche tritt zweimal im Jahr auf, im März und im September, und markiert die Zeitpunkte, an denen die Sonne die Ebene des Äquators kreuzt. Während der Tagundnachtgleiche sind Tag und Nacht überall auf der Welt etwa gleich lang." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "El equinoccio ocurre dos veces al año, en marzo y septiembre, y marca los momentos en que el Sol cruza el plano del ecuador. Durante el equinoccio, el día y la noche tienen aproximadamente la misma duración en todo el mundo." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "L’équinoxe se produit deux fois par an, en mars et en septembre, et marque les moments où le Soleil traverse le plan de l’équateur. Pendant l’équinoxe, le jour et la nuit ont à peu près la même durée partout sur le globe." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "L’equinozio si verifica due volte l’anno, a marzo e settembre, e segna i punti in cui il Sole incrocia il percorso dell’equatore. Durante l’equinozio, il giorno e la notte hanno più o meno la stessa durata in tutto il mondo." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分点は3月と9月の年2回起こり、太陽が赤道の軌道を横切る瞬間を示します。分点の頃には、世界中で昼と夜の長さがほぼ同じになります。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "De equinox vindt twee keer per jaar plaats, in maart en september, en markeert de momenten waarop de zon de baan van de evenaar kruist. Tijdens de equinox zijn dag en nacht overal op aarde ongeveer even lang." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Równonoc przypada dwa razy w roku, w marcu i we wrześniu, i wyznacza momenty, w których Słońce przecina płaszczyznę równika. Podczas równonocy dzień i noc mają na całym świecie mniej więcej taką samą długość." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "分点每年出现两次,分别在3月和9月,标志着太阳穿越赤道轨道的时刻。分点期间,全球各地的昼夜长度都大致相同。" + } } } }, "The solstice occurs twice a year, in June and December (otherwise known as the summer and winter solstices), and mark the sun’s northernmost and southernmost excursions. During the June solstice, the Earth’s northern hemisphere is pointed towards the sun, resulting in increased sunlight and warmer temperatures; the same is true for the southern hemisphere during the December solstice." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يحدث الانقلاب الشمسي مرتين في السنة، في يونيو وديسمبر (ويُعرفان بالانقلابين الصيفي والشتوي)، ويشير إلى أقصى امتداد للشمس شمالًا وجنوبًا. خلال انقلاب يونيو، يميل نصف الأرض الشمالي نحو الشمس، مما يؤدي إلى زيادة ضوء الشمس وارتفاع درجات الحرارة؛ والأمر نفسه ينطبق على نصف الأرض الجنوبي خلال انقلاب ديسمبر." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Die Sonnenwende tritt zweimal im Jahr auf, im Juni und im Dezember (auch als Sommer- und Wintersonnenwende bekannt), und markiert die nördlichsten und südlichsten Stände der Sonne. Während der Junisonnenwende ist die nördliche Hemisphäre der Erde zur Sonne geneigt, was zu mehr Sonnenlicht und wärmeren Temperaturen führt; dasselbe gilt für die südliche Hemisphäre während der Dezembersonnenwende." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "El solsticio ocurre dos veces al año, en junio y diciembre (conocidos como los solsticios de verano e invierno), y marca las posiciones más al norte y más al sur del sol. Durante el solsticio de junio, el hemisferio norte de la Tierra apunta hacia el sol, lo que provoca más luz solar y temperaturas más cálidas; lo mismo ocurre con el hemisferio sur durante el solsticio de diciembre." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Le solstice se produit deux fois par an, en juin et en décembre (on parle alors de solstice d’été et de solstice d’hiver), et marque les positions les plus au nord et les plus au sud du Soleil. Pendant le solstice de juin, l’hémisphère nord de la Terre est orienté vers le Soleil, ce qui entraîne une augmentation de la lumière et des températures plus élevées ; il en va de même pour l’hémisphère sud pendant le solstice de décembre." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il solstizio si verifica due volte l’anno, a giugno e a dicembre (altrimenti noto come solstizio d’estate e d’inverno) e segna le escursioni più a nord e a sud del sole. Durante il solstizio di giugno, l’emisfero settentrionale della Terra è puntato verso il sole, con conseguente aumento della luce solare e temperature più calde; lo stesso vale per l'emisfero australe durante il solstizio di dicembre." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "至点は6月と12月の年2回起こり(それぞれ夏至と冬至として知られています)、太陽が最も北および最も南に達する瞬間を示します。6月の至点の頃には、地球の北半球が太陽の方を向くため、日照が増え、気温も高くなります。12月の至点の頃には、南半球で同じことが起こります。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "De zonnewende vindt twee keer per jaar plaats, in juni en december (ook bekend als de zomer- en winterzonnewende), en markeert de meest noordelijke en zuidelijke standen van de zon. Tijdens de zonnewende in juni is het noordelijk halfrond van de aarde naar de zon gericht, wat zorgt voor meer daglicht en hogere temperaturen; hetzelfde geldt voor het zuidelijk halfrond tijdens de zonnewende in december." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Przesilenie przypada dwa razy w roku, w czerwcu i w grudniu (znane jako przesilenie letnie i zimowe), i wyznacza najbardziej wysunięte na północ i na południe położenia Słońca. Podczas przesilenia czerwcowego północna półkula Ziemi jest skierowana ku Słońcu, co przynosi więcej światła słonecznego i wyższe temperatury; to samo dotyczy półkuli południowej podczas przesilenia grudniowego." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "至点每年出现两次,分别在6月和12月(也就是我们熟知的夏至和冬至),标志着太阳到达最北和最南的位置。在六月至点期间,地球的北半球朝向太阳,因而日照增多、气温升高;十二月至点期间,南半球也会出现同样的情况。" + } } } }, "There are %lld options matching '%@'." : { "localizations" : { + "ar" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "توجد %1$lld خيارات مطابقة لـ\"%2$@\"." + } + }, + "many" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يوجد %1$lld خيارًا مطابقًا لـ\"%2$@\"." + } + }, + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يوجد خيار واحد مطابق لـ\"%2$@\"." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يوجد %1$lld خيار مطابق لـ\"%2$@\"." + } + }, + "two" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يوجد خياران مطابقان لـ\"%2$@\"." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "لا توجد خيارات مطابقة لـ\"%2$@\"." + } + } + } + } + }, + "de" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Es gibt %1$lld Option, die zu „%2$@“ passt." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Es gibt %1$lld Optionen, die zu „%2$@“ passen." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Es gibt keine Optionen, die zu „%2$@“ passen." + } + } + } + } + }, "en" : { - "stringUnit" : { - "state" : "new", - "value" : "There are %1$lld options matching '%2$@'." + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "There is %1$lld option matching '%2$@'." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "There are %1$lld options matching '%2$@'." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "There are no options matching '%2$@'." + } + } + } + } + }, + "es" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hay %1$lld opción que coincide con «%2$@»." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hay %1$lld opciones que coinciden con «%2$@»." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "No hay opciones que coincidan con «%2$@»." + } + } + } + } + }, + "fr" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld option correspond à « %2$@ »." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "%1$lld options correspondent à « %2$@ »." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aucune option ne correspond à « %2$@ »." + } + } + } } }, "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ci sono %1$lld opzioni che corrispondono a ‘%2$@’" + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Ci sono %1$lld opzioni che corrispondono a ‘%2$@’" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ci sono %1$lld opzioni che corrispondono a ‘%2$@’" + } + }, + "zero" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Ci sono %1$lld opzioni che corrispondono a ‘%2$@’" + } + } + } + } + }, + "ja" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "“%2$@”に一致するオプションが%1$lld件あります。" + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "“%2$@”に一致するオプションはありません。" + } + } + } + } + }, + "nl" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Er is %1$lld optie die overeenkomt met ‘%2$@’." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Er zijn %1$lld opties die overeenkomen met ‘%2$@’." + } + }, + "zero" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "Er zijn geen opties die overeenkomen met ‘%2$@’." + } + } + } + } + }, + "pl" : { + "variations" : { + "plural" : { + "few" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Znaleziono %1$lld opcje pasujące do „%2$@”." + } + }, + "many" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Znaleziono %1$lld opcji pasujących do „%2$@”." + } + }, + "one" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Znaleziono %1$lld opcję pasującą do „%2$@”." + } + }, + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Znaleziono %1$lld opcji pasujących do „%2$@”." + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Brak opcji pasujących do „%2$@”." + } + } + } + } + }, + "zh-Hans" : { + "variations" : { + "plural" : { + "other" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "有%1$lld个与“%2$@”匹配的选项。" + } + }, + "zero" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "没有与“%2$@”匹配的选项。" + } + } + } } } } }, "Time" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الوقت" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Uhrzeit" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hora" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heure" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tempo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "時刻" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Godzina" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "时间" + } } } }, "Time Travel" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "السفر عبر الزمن" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeitreise" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Viaje en el tiempo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Voyage dans le temps" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Viaggio nel tempo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "タイムトラベル" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijdreizen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Podróż w czasie" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "时间旅行" + } } } }, "Time travel 1 week earlier" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "السفر أسبوعًا واحدًا إلى الوراء" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeitreise 1 Woche zurück" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Viajar en el tiempo 1 semana antes" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Voyager 1 semaine en arrière" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Viaggio nel tempo 1 settimana prima" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "1週間前にタイムトラベル" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijdreizen 1 week eerder" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Podróż w czasie o tydzień do tyłu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "时间旅行至1周前" + } } } }, "Time travel 1 week later" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "السفر أسبوعًا واحدًا إلى الأمام" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeitreise 1 Woche vor" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Viajar en el tiempo 1 semana después" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Voyager 1 semaine en avant" + } + }, "it" : { "stringUnit" : { - "state" : "translated", - "value" : "Viaggio nel tempo 1 settimana dopo" + "state" : "translated", + "value" : "Viaggio nel tempo 1 settimana dopo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "1週間後にタイムトラベル" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijdreizen 1 week later" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Podróż w czasie o tydzień do przodu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "时间旅行至1周后" } } } @@ -2098,52 +12023,293 @@ "comment" : "A button that allows the user to select a date to travel to in the future.", "isCommentAutoGenerated" : true, "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "السفر عبر الزمن إلى تاريخ" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeitreise zu Datum" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Viajar en el tiempo a una fecha" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Voyager jusqu’à une date" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Viaggio nel tempo fino ad oggi" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日付にタイムトラベル" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijdreizen naar datum" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Podróż w czasie do daty" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "时间旅行至指定日期" + } } } }, "Time until next solstice" : { + "extractionState" : "stale", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "الوقت حتى الانقلاب الشمسي التالي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeit bis zur nächsten Sonnenwende" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tiempo hasta el próximo solsticio" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Temps jusqu’au prochain solstice" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tempo fino al prossimo solstizio" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "次の至点までの時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijd tot volgende zonnewende" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Czas do następnego przesilenia" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "距离下次至点的时间" + } } } }, "Timezone" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "المنطقة الزمنية" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeitzone" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zona horaria" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Fuseau horaire" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Fuso orario" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "タイムゾーン" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijdzone" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Strefa czasowa" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "时区" + } } } }, "Today" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "اليوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Heute" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Hoy" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aujourd’hui" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Oggi" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Vandaag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dziś" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今天" + } } } }, "Today's Daylight" : { "comment" : "Notification title fallback", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ضوء النهار اليوم" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht heute" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "La luz natural de hoy" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lumière du jour aujourd’hui" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La luce del giorno di oggi" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日の日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Daglicht van vandaag" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dzisiejsze światło dzienne" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "今日日照" + } } } }, @@ -2151,41 +12317,233 @@ "comment" : "The label of a button that toggles the visibility of the time picker.", "isCommentAutoGenerated" : true, "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "تبديل السفر عبر الزمن" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeitreise umschalten" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Activar/desactivar el viaje en el tiempo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Activer/désactiver le voyage dans le temps" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Attiva/disattiva il viaggio nel tempo" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "タイムトラベルを切り替え" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tijdreizen in-/uitschakelen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Przełącz podróż w czasie" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "切换时间旅行" + } } } }, "Tomorrow" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "غدًا" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Morgen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Mañana" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Demain" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Domani" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "明日" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Morgen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Jutro" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "明天" + } } } }, "Total daylight" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "إجمالي ضوء النهار" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht gesamt" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Luz natural total" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Lumière du jour totale" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Luce del giorno totale" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "合計日照時間" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Totaal daglicht" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Całkowite światło dzienne" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照总时长" + } } } }, "Unknown" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "غير معروف" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Unbekannt" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Desconocido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Inconnu" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sconosciuto" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "不明" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Onbekend" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Nieznane" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "未知" + } } } }, @@ -2202,124 +12560,700 @@ }, "View daylight" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عرض ضوء النهار" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht anzeigen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ver la luz natural" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afficher la lumière du jour" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza la luce del giorno" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照時間を表示" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Daglicht bekijken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wyświetl światło dzienne" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "查看日照" + } } } }, "View Daylight" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عرض ضوء النهار" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Tageslicht anzeigen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ver la luz natural" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afficher la lumière du jour" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza la luce del giorno" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日照時間を表示" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Daglicht bekijken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wyświetl światło dzienne" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "查看日照" + } } } }, "View Earth at:" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عرض الأرض عند:" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Erde anzeigen bei:" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ver la Tierra en:" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afficher la Terre à :" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza la Terra a:" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "地球を表示する時期:" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Aarde bekijken op:" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wyświetl Ziemię w:" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "查看地球于:" + } } } }, "View how much daylight is remaining today, based on the time until sunset." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عرض مقدار ضوء النهار المتبقي اليوم، استنادًا إلى الوقت المتبقي حتى الغروب." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeigt anhand der Zeit bis zum Sonnenuntergang, wie viel Tageslicht heute noch übrig ist." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Consulta cuánta luz natural queda hoy, según el tiempo que falta hasta la puesta de sol." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Affiche la quantité de lumière du jour restante aujourd’hui, en fonction du temps restant jusqu’au coucher du soleil." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza la quantità di luce diurna rimasta oggi, in base al tempo fino al tramonto." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "日の入りまでの時間をもとに、今日の残りの日照時間を表示します。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bekijk hoeveel daglicht er vandaag nog over is, op basis van de tijd tot zonsondergang." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zobacz, ile światła dziennego pozostało dzisiaj, na podstawie czasu do zachodu słońca." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "根据距离日落的时间,查看今天还剩多少日照。" + } } } }, "View how much daylight there is on a given day, based on the duration from that day’s sunrise to sunset." : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عرض مقدار ضوء النهار في يوم محدد، استنادًا إلى المدة من شروق ذلك اليوم حتى غروبه." + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zeigt anhand der Dauer von Sonnenaufgang bis Sonnenuntergang, wie viel Tageslicht es an einem bestimmten Tag gibt." + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Consulta cuánta luz natural hay en un día determinado, según la duración desde el amanecer hasta la puesta de sol de ese día." + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Affiche la quantité de lumière du jour pour un jour donné, en fonction de la durée entre le lever et le coucher du soleil de ce jour-là." + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza quanta luce diurna c'è in un determinato giorno, in base alla durata dall'alba al tramonto di quel giorno." } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "指定した日の日の出から日の入りまでの時間をもとに、その日の日照時間を表示します。" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bekijk hoeveel daglicht er op een bepaalde dag is, op basis van de duur van de zonsopkomst tot de zonsondergang op die dag." + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Zobacz, ile światła dziennego jest danego dnia, na podstawie czasu od wschodu do zachodu słońca tego dnia." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "根据当天从日出到日落的时长,查看指定日期的日照时间。" + } } } }, "View remaining daylight" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عرض ضوء النهار المتبقي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Verbleibendes Tageslicht anzeigen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ver la luz natural restante" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afficher la lumière du jour restante" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Oggi mancano 3 minuti di luce" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "残りの日照時間を表示" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Resterend daglicht bekijken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wyświetl pozostałe światło dzienne" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "查看剩余日照" + } } } }, "View Remaining Daylight" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "عرض ضوء النهار المتبقي" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Verbleibendes Tageslicht anzeigen" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Ver la luz natural restante" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Afficher la lumière du jour restante" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza la luce del giorno rimanente" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "残りの日照時間を表示" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Resterend daglicht bekijken" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Wyświetl pozostałe światło dzienne" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "查看剩余日照" + } } } }, "Welcome to Solstice" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "مرحبًا بك في Solstice" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Willkommen bei Solstice" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Te damos la bienvenida a Solstice" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Bienvenue dans Solstice" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Benvenuti al Solstice" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeへようこそ" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Welkom bij Solstice" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Witaj w Solstice" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "欢迎使用 Solstice" + } } } }, "What location do you want to see the daylight for?" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ما الموقع الذي تريد عرض ضوء النهار له؟" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Für welchen Ort möchtest du das Tageslicht sehen?" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "¿De qué ubicación quieres ver la luz natural?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pour quel lieu souhaitez-vous voir la lumière du jour ?" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "In quale luogo vuoi vedere la luce del giorno?" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "どの場所の日照時間を表示しますか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Voor welke locatie wil je het daglicht zien?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dla której lokalizacji chcesz zobaczyć światło dzienne?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "你想查看哪个地点的日照?" + } } } }, "What location do you want to see the sunrise for?" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ما الموقع الذي تريد عرض الشروق له؟" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Für welchen Ort möchtest du den Sonnenaufgang sehen?" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "¿De qué ubicación quieres ver el amanecer?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pour quel lieu souhaitez-vous voir le lever du soleil ?" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "In quale luogo vuoi vedere l'alba?" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "どの場所の日の出を表示しますか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Voor welke locatie wil je de zonsopkomst zien?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dla której lokalizacji chcesz zobaczyć wschód słońca?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "你想查看哪个地点的日出?" + } } } }, "What location do you want to see the sunset for?" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ما الموقع الذي تريد عرض الغروب له؟" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Für welchen Ort möchtest du den Sonnenuntergang sehen?" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "¿De qué ubicación quieres ver la puesta de sol?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pour quel lieu souhaitez-vous voir le coucher du soleil ?" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "In quale luogo vuoi vedere il tramonto?" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "どの場所の日の入りを表示しますか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Voor welke locatie wil je de zonsondergang zien?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dla której lokalizacji chcesz zobaczyć zachód słońca?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "你想查看哪个地点的日落?" + } } } }, "Which location do you want to see daylight information for?" : { "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ما الموقع الذي تريد عرض معلومات ضوء النهار له؟" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Für welchen Ort möchtest du Informationen zum Tageslicht sehen?" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "¿De qué ubicación quieres ver la información sobre la luz natural?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Pour quel lieu souhaitez-vous voir les informations sur la lumière du jour ?" + } + }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Per quale luogo vuoi vedere le informazioni sulla luce del giorno?" } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "どの場所の日照情報を表示しますか?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Voor welke locatie wil je informatie over daglicht zien?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Dla której lokalizacji chcesz zobaczyć informacje o świetle dziennym?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "你想查看哪个地点的日照信息?" + } } } } }, - "version" : "1.1" + "version" : "1.3" } \ No newline at end of file diff --git a/Packages/RealityKitContent/Package.swift b/Packages/RealityKitContent/Package.swift index 1977a13f..afdee461 100644 --- a/Packages/RealityKitContent/Package.swift +++ b/Packages/RealityKitContent/Package.swift @@ -4,23 +4,25 @@ import PackageDescription let package = Package( - name: "RealityKitContent", - platforms: [.visionOS(.v1)], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "RealityKitContent", - targets: ["RealityKitContent"]), - ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "RealityKitContent", - dependencies: []), - ] + name: "RealityKitContent", + platforms: [.visionOS(.v1)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "RealityKitContent", + targets: ["RealityKitContent"] + ), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "RealityKitContent", + dependencies: [] + ), + ] ) diff --git a/Solstice.xcodeproj/project.pbxproj b/Solstice.xcodeproj/project.pbxproj index 8bc5c357..c648c8a5 100644 --- a/Solstice.xcodeproj/project.pbxproj +++ b/Solstice.xcodeproj/project.pbxproj @@ -265,6 +265,11 @@ 71CB96DE2DE9AC4700660AA9 /* Suite in Frameworks */ = {isa = PBXBuildFile; productRef = 71CB96DD2DE9AC4700660AA9 /* Suite */; }; 71DAB3D229CD862A00E30148 /* EarthNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71DAB3D129CD862A00E30148 /* EarthNode.swift */; }; 71DAB3D429CD865800E30148 /* EarthScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71DAB3D329CD865800E30148 /* EarthScene.swift */; }; + 71DC2FEA30012B2A00FDEBE0 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 71DC2FE930012B2A00FDEBE0 /* InfoPlist.xcstrings */; }; + 71DC2FED30012B2A00FDEBE0 /* AppShortcuts.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 71DC2FEB30012B2A00FDEBE0 /* AppShortcuts.xcstrings */; }; + 71DC2FEE30012B2A00FDEBE0 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 71DC2FEC30012B2A00FDEBE0 /* InfoPlist.xcstrings */; }; + 71DC2FF030012B2A00FDEBE0 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 71DC2FEF30012B2A00FDEBE0 /* InfoPlist.xcstrings */; }; + 71DC2FF130012B2A00FDEBE0 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 71DC2FEF30012B2A00FDEBE0 /* InfoPlist.xcstrings */; }; 71E3C4132EA12ABE0005C884 /* CircleWithSlice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E3C4122EA12ABE0005C884 /* CircleWithSlice.swift */; }; 71E3C4142EA12ABE0005C884 /* CircleWithSlice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E3C4122EA12ABE0005C884 /* CircleWithSlice.swift */; }; 71E3C4152EA12ABE0005C884 /* CircleWithSlice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E3C4122EA12ABE0005C884 /* CircleWithSlice.swift */; }; @@ -491,6 +496,10 @@ 71DAB3D329CD865800E30148 /* EarthScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EarthScene.swift; sourceTree = ""; }; 71DB52AB29AB8BED00D62BB7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 71DB52AD29ABD19B00D62BB7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 71DC2FE930012B2A00FDEBE0 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; + 71DC2FEB30012B2A00FDEBE0 /* AppShortcuts.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = AppShortcuts.xcstrings; sourceTree = ""; }; + 71DC2FEC30012B2A00FDEBE0 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; + 71DC2FEF30012B2A00FDEBE0 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 71E3C4122EA12ABE0005C884 /* CircleWithSlice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleWithSlice.swift; sourceTree = ""; }; 71E3C4172EA12B030005C884 /* View+backportGlassEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+backportGlassEffect.swift"; sourceTree = ""; }; 71E8DC2C29CB601800F3E1B4 /* EquinoxAndSolsticeInfoSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EquinoxAndSolsticeInfoSheet.swift; sourceTree = ""; }; @@ -576,6 +585,7 @@ 7106C86D29A276B40007A7EC /* Widget */ = { isa = PBXGroup; children = ( + 71DC2FEF30012B2A00FDEBE0 /* InfoPlist.xcstrings */, 7195120229AFBD61009D282F /* WidgetExtension.entitlements */, 7106C87729A276B40007A7EC /* Info.plist */, 7106C87229A276B40007A7EC /* Widget.swift */, @@ -592,6 +602,8 @@ 711DDC7A2EA8173800DFE99B /* Assets */ = { isa = PBXGroup; children = ( + 71DC2FEB30012B2A00FDEBE0 /* AppShortcuts.xcstrings */, + 71DC2FEC30012B2A00FDEBE0 /* InfoPlist.xcstrings */, 718FC2EA2DFB6D6D00E1A437 /* AppIcon.icon */, 711A97BC29CDBCBC00333927 /* Earth Model Assets.xcassets */, 71C635C72E9D9A8C003FA1AE /* Shared Assets.xcassets */, @@ -900,6 +912,7 @@ C1E71899E221752465BF183D /* iOS */ = { isa = PBXGroup; children = ( + 71DC2FE930012B2A00FDEBE0 /* InfoPlist.xcstrings */, 13AEC2685232EF694BB4BB09 /* Foundation.framework */, ); name = iOS; @@ -1082,6 +1095,14 @@ en, Base, it, + de, + fr, + es, + ja, + ar, + nl, + "zh-Hans", + pl, ); mainGroup = 7198467828E5895E00E866CE; packageReferences = ( @@ -1107,6 +1128,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 71DC2FF130012B2A00FDEBE0 /* InfoPlist.xcstrings in Resources */, 71A029BB2C36928400E19819 /* defaultData.json in Resources */, 71A9A5522AB82D6200C3A38C /* Localizable.xcstrings in Resources */, 7106C87629A276B40007A7EC /* Assets.xcassets in Resources */, @@ -1133,6 +1155,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 71DC2FF030012B2A00FDEBE0 /* InfoPlist.xcstrings in Resources */, 71A029BC2C36928400E19819 /* defaultData.json in Resources */, 719465552C2C1E09008408C0 /* Localizable.xcstrings in Resources */, 719465572C2C1E09008408C0 /* Assets.xcassets in Resources */, @@ -1152,6 +1175,8 @@ 71C635C82E9D9A8C003FA1AE /* Shared Assets.xcassets in Resources */, 718B136E29A9196C0001D4DC /* README.md in Resources */, 711A97BD29CDBCBC00333927 /* Earth Model Assets.xcassets in Resources */, + 71DC2FED30012B2A00FDEBE0 /* AppShortcuts.xcstrings in Resources */, + 71DC2FEE30012B2A00FDEBE0 /* InfoPlist.xcstrings in Resources */, 71A029B92C36928400E19819 /* defaultData.json in Resources */, 71A9A5502AB82D6200C3A38C /* Localizable.xcstrings in Resources */, ); @@ -1162,6 +1187,7 @@ buildActionMask = 2147483647; files = ( 71A9A5512AB82D6200C3A38C /* Localizable.xcstrings in Resources */, + 71DC2FEA30012B2A00FDEBE0 /* InfoPlist.xcstrings in Resources */, 718FC2EC2DFB6D6D00E1A437 /* AppIcon.icon in Resources */, 711DDC772EA8172200DFE99B /* Shared Assets.xcassets in Resources */, 71A029BA2C36928400E19819 /* defaultData.json in Resources */, @@ -1809,7 +1835,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Solstice uses your location to calculate local sunrise and sunset times"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 3.1.2; + MARKETING_VERSION = 3.2.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1871,7 +1897,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Solstice uses your location to calculate local sunrise and sunset times"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 3.1.2; + MARKETING_VERSION = 3.2.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; diff --git a/Solstice.xcodeproj/xcshareddata/xcodecloud/manifest.json b/Solstice.xcodeproj/xcshareddata/xcodecloud/manifest.json new file mode 100644 index 00000000..aae2a061 --- /dev/null +++ b/Solstice.xcodeproj/xcshareddata/xcodecloud/manifest.json @@ -0,0 +1,9 @@ +{ + "id" : "3ed37939-e7ea-4a91-8b55-aea3b7e3881c", + "targets" : [ + { + "id" : "7A5A18E9-064A-4FF5-BA55-4F448CEA5003", + "name" : "Solstice" + } + ] +} \ No newline at end of file diff --git a/Solstice.xcodeproj/xcshareddata/xcschemes/Solstice watchOS Watch App.xcscheme b/Solstice.xcodeproj/xcshareddata/xcschemes/Solstice watchOS Watch App.xcscheme index d0e8ac81..97e58cb6 100644 --- a/Solstice.xcodeproj/xcshareddata/xcschemes/Solstice watchOS Watch App.xcscheme +++ b/Solstice.xcodeproj/xcshareddata/xcschemes/Solstice watchOS Watch App.xcscheme @@ -60,7 +60,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "719F924229ACD1CC00C06921" BuildableName = "Solstice watchOS Watch App.app" - BlueprintName = "Solstice watchOS Watch App" ReferencedContainer = "container:Solstice.xcodeproj"> @@ -77,7 +76,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "719F924229ACD1CC00C06921" BuildableName = "Solstice watchOS Watch App.app" - BlueprintName = "Solstice watchOS Watch App" ReferencedContainer = "container:Solstice.xcodeproj"> diff --git a/Solstice/Charts/AnnualDaylightChart.swift b/Solstice/Charts/AnnualDaylightChart.swift index 130e9f50..94688419 100644 --- a/Solstice/Charts/AnnualDaylightChart.swift +++ b/Solstice/Charts/AnnualDaylightChart.swift @@ -5,8 +5,8 @@ // Created by Daniel Eden on 17/02/2023. // -import SwiftUI import Charts +import SwiftUI import TimeMachine struct AnnualDaylightChart: View { @@ -17,20 +17,51 @@ struct AnnualDaylightChart: View { .astronomical: .indigo, .nautical: .blue, .civil: .teal, - .day: .yellow + .day: .yellow, ] var dayLength: Double = 60 * 60 * 24 var body: some View { - Chart { - currentMonthIndicator - monthlyBarMarks + VStack(alignment: .leading, spacing: 8) { + Chart { + currentMonthIndicator + monthlyBarMarks + } + .chartForegroundStyleScale(kvPairs) + .chartYScale(domain: 0 ... dayLength) + .chartYAxis { yAxisMarks } + .chartLegend(.hidden) + .environment(\.timeZone, location.timeZone) + + legend + } + } + + /// Swift Charts derives its default legend from the `Plottable` raw values + /// of `NTSolar.Phase`, which are never localized. We hide it and render our + /// own from `kvPairs` so the labels honour the String Catalog. + private var legend: some View { + LazyVGrid( + columns: [GridItem(.adaptive(minimum: 160), alignment: .leading)], + alignment: .leading, + spacing: 4 + ) { + ForEach(kvPairs.map { $0.key }, id: \.self) { phase in + HStack(spacing: 6) { + Circle() + .fill(color(for: phase)) + .frame(width: 8, height: 8) + Text(phase.localizedName) + .font(.caption) + .foregroundStyle(.secondary) + } + } } - .chartForegroundStyleScale(kvPairs) - .chartYScale(domain: 0...dayLength) - .chartYAxis { yAxisMarks } - .environment(\.timeZone, location.timeZone) + } + + private func color(for phase: NTSolar.Phase) -> Color { + kvPairs.first { $0.key == phase }?.value ?? .clear } private var currentMonthIndicator: some ChartContent { @@ -55,7 +86,8 @@ struct AnnualDaylightChart: View { @ChartContentBuilder private func astronomicalBarMark(for solar: NTSolar) -> some ChartContent { if let astronomicalSunrise = solar.astronomicalSunrise, - let astronomicalSunset = solar.astronomicalSunset { + let astronomicalSunset = solar.astronomicalSunset + { let yStart: Double = max(0, solar.startOfDay.distance(to: astronomicalSunrise)) let yEnd: Double = min(dayLength, solar.startOfDay.distance(to: astronomicalSunset)) BarMark( @@ -70,7 +102,8 @@ struct AnnualDaylightChart: View { @ChartContentBuilder private func nauticalBarMark(for solar: NTSolar) -> some ChartContent { if let nauticalSunrise = solar.nauticalSunrise, - let nauticalSunset = solar.nauticalSunset { + let nauticalSunset = solar.nauticalSunset + { let yStart: Double = max(0, solar.startOfDay.distance(to: nauticalSunrise)) let yEnd: Double = min(dayLength, solar.startOfDay.distance(to: nauticalSunset)) BarMark( @@ -85,7 +118,8 @@ struct AnnualDaylightChart: View { @ChartContentBuilder private func civilBarMark(for solar: NTSolar) -> some ChartContent { if let civilSunrise = solar.civilSunrise, - let civilSunset = solar.civilSunset { + let civilSunset = solar.civilSunset + { let yStart: Double = max(0, solar.startOfDay.distance(to: civilSunrise)) let yEnd: Double = min(dayLength, solar.startOfDay.distance(to: civilSunset)) BarMark( @@ -124,7 +158,7 @@ struct AnnualDaylightChart: View { @ViewBuilder private func yAxisLabel(for value: AxisValue) -> some View { - let startOfDay: Date = Date().startOfDay(in: location.timeZone) + let startOfDay = Date().startOfDay(in: location.timeZone) if let doubleValue = value.as(Double.self) { let date: Date = startOfDay.addingTimeInterval(doubleValue) if doubleValue == 0 { @@ -139,14 +173,14 @@ struct AnnualDaylightChart: View { } extension AnnualDaylightChart { - var monthlySolars: Array { + var monthlySolars: [NTSolar] { guard let year = calendar.dateInterval(of: .year, for: timeMachine.date) else { return [] } var lastDate = calendar.date(bySetting: .day, value: 21, of: year.start) ?? year.start lastDate = calendar.date(bySetting: .hour, value: 12, of: lastDate) ?? lastDate - var dates: Array = [] + var dates: [Date] = [] while lastDate < year.end { dates.append(lastDate) @@ -154,7 +188,7 @@ extension AnnualDaylightChart { } return dates.map { date in - return NTSolar(for: date, coordinate: location.coordinate, timeZone: location.timeZone) + NTSolar(for: date, coordinate: location.coordinate, timeZone: location.timeZone) }.compactMap { $0 } } } diff --git a/Solstice/Charts/CircularSolarChart.swift b/Solstice/Charts/CircularSolarChart.swift index 11af2279..9cbd2bc1 100644 --- a/Solstice/Charts/CircularSolarChart.swift +++ b/Solstice/Charts/CircularSolarChart.swift @@ -5,10 +5,10 @@ // Created by Daniel Eden on 04/10/2025. // +import enum Accelerate.vDSP +import Suite import SwiftUI import TimeMachine -import Suite -import enum Accelerate.vDSP struct CircularSolarChart: View { @AppStorage(Preferences.detailViewChartAppearance) private var storedAppearance @@ -16,111 +16,113 @@ struct CircularSolarChart: View { @Environment(\.colorScheme) private var colorScheme @Environment(\.timeMachine) private var timeMachine #if WIDGET_EXTENSION - @Environment(\.widgetRenderingMode) private var widgetRenderingMode - @Environment(\.widgetFamily) private var widgetFamily + @Environment(\.widgetRenderingMode) private var widgetRenderingMode + @Environment(\.widgetFamily) private var widgetFamily #endif - + @State private var size = CGSize.zero var date: Date? - + var location: Location - + var appearanceOverride: DaylightChart.Appearance? - + var appearance: DaylightChart.Appearance { appearanceOverride ?? storedAppearance } - - var timeZone: TimeZone { location.timeZone } - + + var timeZone: TimeZone { + location.timeZone + } + var solar: NTSolar? { NTSolar(for: date ?? timeMachine.date, coordinate: location.coordinate, timeZone: location.timeZone) } - + var majorSunSize: Double { max(12, max(size.width, size.height) * 0.075) } - + var minorSunSize: Double { max(4, majorSunSize / 3) } - + var foregroundStyle: AnyShapeStyle { #if WIDGET_EXTENSION - if widgetRenderingMode == .fullColor { + if widgetRenderingMode == .fullColor { + switch appearance { + case .graphical: return AnyShapeStyle(.white) + case .simple: return AnyShapeStyle(.primary) + } + } else { + return AnyShapeStyle(.primary) + } + #else switch appearance { case .graphical: return AnyShapeStyle(.white) case .simple: return AnyShapeStyle(.primary) } - } else { - return AnyShapeStyle(.primary) - } - #else - switch appearance { - case .graphical: return AnyShapeStyle(.white) - case .simple: return AnyShapeStyle(.primary) - } #endif } - + var blendMode: BlendMode { #if WIDGET_EXTENSION - if widgetRenderingMode == .fullColor { + if widgetRenderingMode == .fullColor { + switch appearance { + case .graphical: return .plusLighter + case .simple: return .normal + } + } else { + return .normal + } + #else switch appearance { case .graphical: return .plusLighter case .simple: return .normal } - } else { - return .normal - } - #else - switch appearance { - case .graphical: return .plusLighter - case .simple: return .normal - } #endif } - + private var calendar: Calendar { var calendar = Calendar.current calendar.timeZone = timeZone - + return calendar } - + private var isSmallWidget: Bool { #if WIDGET_EXTENSION - return widgetFamily == .systemSmall + return widgetFamily == .systemSmall #else - return false + return false #endif } - + func angle(for date: Date) -> Angle { Helpers.angle(for: date, timeZone: timeZone) } - + @ViewBuilder var background: some View { #if WIDGET_EXTENSION - if widgetRenderingMode == .fullColor { + if widgetRenderingMode == .fullColor { + if appearance == .graphical { + solar?.view + } else { + Rectangle().fill(.regularMaterial) + } + } else { + Rectangle().fill(.primary.quinary) + } + #else if appearance == .graphical { solar?.view } else { Rectangle().fill(.regularMaterial) } - } else { - Rectangle().fill(.primary.quinary) - } - #else - if appearance == .graphical { - solar?.view - } else { - Rectangle().fill(.regularMaterial) - } #endif } - + var aboveHorizonSun: some View { Circle() .fill(foregroundStyle) @@ -129,7 +131,7 @@ struct CircularSolarChart: View { .rotationEffect(angle(for: solar?.date ?? .now)) .frame(maxWidth: .infinity, maxHeight: .infinity) } - + var belowHorizonSun: some View { background.mask { Circle() @@ -144,11 +146,11 @@ struct CircularSolarChart: View { .rotationEffect(angle(for: solar?.date ?? .now)) .frame(maxWidth: .infinity, maxHeight: .infinity) } - + var safeSunriseSunsetShape: some Shape { CircleWithSlice(startAngle: angle(for: solar?.sunrise ?? .now).degrees, endAngle: angle(for: solar?.sunset ?? .now).degrees) } - + var sundial: some View { ZStack { sundialBackground @@ -201,8 +203,9 @@ struct CircularSolarChart: View { @ViewBuilder private func phaseSlice(for key: NTSolar.Phase, phases: [NTSolar.Phase: (sunrise: Date?, sunset: Date?)]) -> some View { if let (sunrise, sunset) = phases[key], - let sunrise, - let sunset { + let sunrise, + let sunset + { CircleWithSlice(startAngle: angle(for: sunrise).degrees, endAngle: angle(for: sunset).degrees) .fill(.black.opacity(0.06)) .blendMode(.plusDarker) @@ -215,7 +218,7 @@ struct CircularSolarChart: View { let lineMaskGradient = LinearGradient( stops: [ Gradient.Stop(color: .clear, location: 0.499), - Gradient.Stop(color: .black, location: 0.5) + Gradient.Stop(color: .black, location: 0.5), ], startPoint: .leading, endPoint: .trailing @@ -281,8 +284,9 @@ struct CircularSolarChart: View { @ViewBuilder private func phaseMarker(for key: NTSolar.Phase, phases: [NTSolar.Phase: (sunrise: Date?, sunset: Date?)]) -> some View { if let (sunrise, sunset) = phases[key], - let sunrise, - let sunset { + let sunrise, + let sunset + { Circle() .frame(width: minorSunSize) .frame(maxWidth: .infinity, alignment: .trailing) @@ -298,22 +302,23 @@ struct CircularSolarChart: View { .rotationEffect(angle(for: sunset)) } } - + @ViewBuilder var labels: some View { if labelsVisibility != .hidden { if let solar { ChartLabel(text: Text(solar.safeSunrise, style: .time), - imageName: "sunrise", - angle: angle(for: solar.safeSunrise)) - + imageName: "sunrise", + angle: angle(for: solar.safeSunrise)) + ChartLabel(text: Text(solar.safeSunset, style: .time), - imageName: "sunset", - angle: angle(for: solar.safeSunset)) + imageName: "sunset", + angle: angle(for: solar.safeSunset)) } - + if let duration = solar?.daylightDuration, - let diff = solar?.compactDifferenceString { + let diff = solar?.compactDifferenceString + { VStack(spacing: 2) { HStack(spacing: 2) { if !isSmallWidget { @@ -321,7 +326,7 @@ struct CircularSolarChart: View { } Text(Duration.seconds(duration).formatted(.units(width: .narrow))) } - + Text(diff) .textScale(.secondary) .foregroundStyle(.secondary) @@ -332,63 +337,63 @@ struct CircularSolarChart: View { } } } - + var body: some View { + ZStack { ZStack { - ZStack { - sundial - - ForEach(0...23, id: \.self) { i in - RoundedRectangle(cornerRadius: majorSunSize, style: .continuous) - .fill(foregroundStyle.opacity(0.3)) - .blendMode(blendMode) - .frame(width: Double(i).remainder(dividingBy: 6) == 0 ? majorSunSize / 1.5 : minorSunSize, height: 2) - .frame(maxWidth: size.width * 0.9, alignment: .trailing) - .rotationEffect(Angle(degrees: (360 / 24) * Double(i))) - } + sundial + + ForEach(0 ... 23, id: \.self) { i in + RoundedRectangle(cornerRadius: majorSunSize, style: .continuous) + .fill(foregroundStyle.opacity(0.3)) + .blendMode(blendMode) + .frame(width: Double(i).remainder(dividingBy: 6) == 0 ? majorSunSize / 1.5 : minorSunSize, height: 2) + .frame(maxWidth: size.width * 0.9, alignment: .trailing) + .rotationEffect(Angle(degrees: (360 / 24) * Double(i))) } - #if WIDGET_EXTENSION - .if(widgetRenderingMode != .fullColor) { + } + #if WIDGET_EXTENSION + .if(widgetRenderingMode != .fullColor) { $0.reverseMask { labels .backgroundStyle(.black) } } - #endif - - labels - #if WIDGET_EXTENSION - .if(widgetRenderingMode != .fullColor) { - $0.backgroundStyle(.primary.quinary) - } - #endif - } - .environment(\.timeZone, timeZone) - .font(.footnote) - .fontWeight(.medium) - .monospacedDigit() - .frame(maxWidth: .infinity) - .aspectRatio(1, contentMode: .fit) + #endif + + labels + #if WIDGET_EXTENSION + .if(widgetRenderingMode != .fullColor) { + $0.backgroundStyle(.primary.quinary) + } + #endif + } + .environment(\.timeZone, timeZone) + .font(.footnote) + .fontWeight(.medium) + .monospacedDigit() + .frame(maxWidth: .infinity) + .aspectRatio(1, contentMode: .fit) } } -fileprivate struct ChartLabel: View { +private struct ChartLabel: View { #if WIDGET_EXTENSION - @Environment(\.widgetFamily) private var widgetFamily + @Environment(\.widgetFamily) private var widgetFamily #endif - + var text: Text var imageName: String var angle: Angle - + private var isSmallWidget: Bool { #if WIDGET_EXTENSION - return widgetFamily == .systemSmall + return widgetFamily == .systemSmall #else - return false + return false #endif } - + var body: some View { HStack(spacing: 2) { if !isSmallWidget { @@ -406,19 +411,19 @@ fileprivate struct ChartLabel: View { } } -fileprivate struct Helpers { +private enum Helpers { static func angle(for date: Date, timeZone: TimeZone = .current) -> Angle { var calendar = Calendar.current calendar.timeZone = timeZone let components = calendar.dateComponents([.hour, .minute, .second], from: date) - + let hours = Double(components.hour ?? 0) let minutes = Double(components.minute ?? 0) let seconds = Double(components.second ?? 0) - + let totalSeconds = hours * 3600 + minutes * 60 + seconds let fractionOfDay = totalSeconds / 86400 // 24 * 60 * 60 - + let degrees = fractionOfDay * 360 return .degrees(degrees + 90) // shift so midnight is at bottom } diff --git a/Solstice/Charts/DaylightChart.swift b/Solstice/Charts/DaylightChart.swift index 0c498091..5eb5e758 100644 --- a/Solstice/Charts/DaylightChart.swift +++ b/Solstice/Charts/DaylightChart.swift @@ -5,9 +5,9 @@ // Created by Daniel Eden on 12/02/2023. // -import SwiftUI import Charts import Suite +import SwiftUI import TimeMachine struct DaylightChart: View { @@ -70,7 +70,9 @@ struct DaylightChart: View { /// X-axis domain: always a full 24-hour day expressed as seconds from midnight. /// Keeping this constant means only the y-axis animates when the date changes. - var range: ClosedRange { 0...86400 } + var range: ClosedRange { + 0 ... 86400 + } var body: some View { VStack(alignment: .leading) { @@ -274,26 +276,26 @@ struct DaylightChart: View { .contentShape(Rectangle()) .if(scrubbable) { view in view -#if os(iOS) - .gesture(DragGesture() - .onChanged { value in - scrub(to: value.location, in: geo, proxy: proxy) - } - .onEnded { _ in - selectedEvent = nil - currentX = nil - }) -#elseif os(macOS) + #if os(iOS) + .gesture(DragGesture() + .onChanged { value in + scrub(to: value.location, in: geo, proxy: proxy) + } + .onEnded { _ in + selectedEvent = nil + currentX = nil + }) + #elseif os(macOS) .onContinuousHover { value in switch value { - case .active(let point): + case let .active(point): scrub(to: point, in: geo, proxy: proxy) case .ended: selectedEvent = nil currentX = nil } } -#endif + #endif } } @@ -307,11 +309,13 @@ struct DaylightChart: View { } extension DaylightChart { - var hours: Array { + var hours: [TimeInterval] { stride(from: range.lowerBound, through: range.upperBound, by: 60 * 30).map { $0 } } - var startOfDay: Date { midnight } + var startOfDay: Date { + midnight + } func pointMarkColor(for eventPhase: NTSolar.Phase) -> HierarchicalShapeStyle { switch eventPhase { @@ -329,7 +333,7 @@ extension DaylightChart { func resetSelectedEvent() { selectedEvent = solar.events.filter { $0.phase == .sunset || $0.phase == .sunrise - }.sorted(by: { a, b in + }.sorted(by: { a, _ in a.date.compare(.now) == .orderedDescending }).first } @@ -341,11 +345,11 @@ extension DaylightChart { if let yScale { return yScale } let altitudes = hours.map { yValue(for: $0) } guard let minAlt = altitudes.min(), let maxAlt = altitudes.max() else { - return -90.0...90.0 + return -90.0 ... 90.0 } let span = maxAlt - minAlt let padding = span * 0.1 - return (minAlt - padding)...(maxAlt + padding) + return (minAlt - padding) ... (maxAlt + padding) } /// The sun's actual altitude in degrees at the given offset (seconds from midnight). @@ -371,9 +375,10 @@ extension DaylightChart { currentX = proxy.value(atX: xCurrent) if let currentX, - let nearestEvent = solar.events.sorted(by: { lhs, rhs in - abs(eventOffset(for: lhs.date) - currentX) <= abs(eventOffset(for: rhs.date) - currentX) - }).first { + let nearestEvent = solar.events.sorted(by: { lhs, rhs in + abs(eventOffset(for: lhs.date) - currentX) <= abs(eventOffset(for: rhs.date) - currentX) + }).first + { selectedEvent = nearestEvent } } @@ -382,7 +387,7 @@ extension DaylightChart { extension DaylightChart { enum Appearance: String, Codable, CaseIterable { case simple = "Simple", - graphical = "Graphical" + graphical = "Graphical" var description: LocalizedStringKey { switch self { diff --git a/Solstice/ContentView.swift b/Solstice/ContentView.swift index adc9a8d5..2a0ebf70 100644 --- a/Solstice/ContentView.swift +++ b/Solstice/ContentView.swift @@ -5,9 +5,9 @@ // Created by Daniel Eden on 29/09/2022. // -import SwiftUI import CoreData import Suite +import SwiftUI import TimeMachine struct ContentView: View { @@ -17,88 +17,88 @@ struct ContentView: View { @Environment(\.managedObjectContext) private var context @Environment(\.openWindow) private var openWindow @Environment(\.horizontalSizeClass) private var horizontalSizeClass - + @SceneStorage("selectedLocation") private var selectedLocation: String? #if os(macOS) - /// Fallback storage for macOS where SceneStorage doesn't persist across app launches - @AppStorage("lastSelectedLocation") private var lastSelectedLocation: String? + /// Fallback storage for macOS where SceneStorage doesn't persist across app launches + @AppStorage("lastSelectedLocation") private var lastSelectedLocation: String? #endif - + @Environment(\.scenePhase) private var scenePhase @Environment(CurrentLocation.self) var currentLocation @Environment(LocationSearchService.self) var locationSearchService - + @State private var settingsViewOpen = false @State private var sidebarVisibility = NavigationSplitViewVisibility.doubleColumn - + @FetchRequest(sortDescriptors: []) private var locations: FetchedResults - + var body: some View { - NavigationSplitView(columnVisibility: $sidebarVisibility) { - SidebarListView(namespace: namespace) - .toolbar { - toolbarItems - } - #if os(macOS) - .frame(minWidth: 256) - #endif - } detail: { - NavigationStack { - Group { - switch selectedLocation { - case currentLocation.id: - DetailView(location: currentLocation) - case .some(let id): - if let location = locations.first(where: { $0.uuid?.uuidString == id }) { - DetailView(location: location) - .id(location) - } else { - placeholderView - } - default: + NavigationSplitView(columnVisibility: $sidebarVisibility) { + SidebarListView(namespace: namespace) + .toolbar { + toolbarItems + } + #if os(macOS) + .frame(minWidth: 256) + #endif + } detail: { + NavigationStack { + Group { + switch selectedLocation { + case currentLocation.id: + DetailView(location: currentLocation) + case let .some(id): + if let location = locations.first(where: { $0.uuid?.uuidString == id }) { + DetailView(location: location) + .id(location) + } else { placeholderView } + default: + placeholderView } } - #if os(iOS) - .navigationTransition(.zoom(sourceID: selectedLocation ?? "", in: namespace)) - #endif } - .navigationSplitViewStyle(.balanced) - .sheet(item: Bindable(locationSearchService).location) { value in - NavigationStack { - DetailView(location: value) - } - #if os(macOS) - .frame(minWidth: 600, minHeight: 400) - #endif - .timeMachineOverlay() + #if os(iOS) + .navigationTransition(.zoom(sourceID: selectedLocation ?? "", in: namespace)) + #endif + } + .navigationSplitViewStyle(.balanced) + .sheet(item: Bindable(locationSearchService).location) { value in + NavigationStack { + DetailView(location: value) } + #if os(macOS) + .frame(minWidth: 600, minHeight: 400) + #endif .timeMachineOverlay() - .onContinueUserActivity(DetailView.userActivity) { userActivity in - if let selection = userActivity.targetContentIdentifier { - selectedLocation = selection - } + } + .timeMachineOverlay() + .onContinueUserActivity(DetailView.userActivity) { userActivity in + if let selection = userActivity.targetContentIdentifier { + selectedLocation = selection } - .onContinueUserActivity(DetailView.userActivity) { userActivity in - if userActivity.targetContentIdentifier == currentLocation.id { - selectedLocation = currentLocation.id - } + } + .onContinueUserActivity(DetailView.userActivity) { userActivity in + if userActivity.targetContentIdentifier == currentLocation.id { + selectedLocation = currentLocation.id } - .resolveDeepLink(Array(locations)) - #if os(iOS) + } + .resolveDeepLink(Array(locations)) + #if os(iOS) .sheet(isPresented: $settingsViewOpen) { SettingsView() .presentationDetents(horizontalSizeClass == .regular ? [.large] : [.large, .medium]) } - #endif + #endif .deduplicateLocationRecords() .task(id: scenePhase) { switch scenePhase { #if !os(watchOS) - case .background: - await NotificationManager.scheduleNotifications(location: currentLocation.location) + case .background: + await NotificationManager.scheduleNotifications(location: currentLocation.location) #endif case .active: currentLocation.requestLocation() @@ -106,7 +106,7 @@ struct ContentView: View { return } } - #if os(macOS) + #if os(macOS) .onAppear { // Restore selection from AppStorage if SceneStorage is empty if selectedLocation == nil, let lastSelected = lastSelectedLocation { @@ -117,9 +117,9 @@ struct ContentView: View { // Sync selection to AppStorage for persistence across launches lastSelectedLocation = newValue } - #endif + #endif } - + private var placeholderView: some View { ContentUnavailableView { Label("No location selected", image: .solstice) @@ -127,7 +127,7 @@ struct ContentView: View { Text("Select a location to view details") } } - + @ToolbarContentBuilder private var toolbarItems: some ToolbarContent { ToolbarItem(placement: .primaryAction) { @@ -135,17 +135,17 @@ struct ContentView: View { Picker(selection: $itemSortDimension.animation()) { Text("Timezone") .tag(Preferences.SortingFunction.timezone) - + Text("Daylight duration") .tag(Preferences.SortingFunction.daylightDuration) } label: { Text("Sort by") } - + Picker(selection: $itemSortOrder.animation()) { Text("Ascending") .tag(SortOrder.forward) - + Text("Descending") .tag(SortOrder.reverse) } label: { @@ -156,26 +156,25 @@ struct ContentView: View { .backportCircleSymbolVariant() } } - - -#if os(visionOS) - ToolbarItem { - Button { - openWindow(id: "settings") - } label: { - Label("Settings", systemImage: "ellipsis") + + #if os(visionOS) + ToolbarItem { + Button { + openWindow(id: "settings") + } label: { + Label("Settings", systemImage: "ellipsis") + } } - } -#elseif !os(macOS) - ToolbarItem(placement: .navigation) { - Button { - settingsViewOpen = true - } label: { - Label("Settings", systemImage: "ellipsis") + #elseif !os(macOS) + ToolbarItem(placement: .navigation) { + Button { + settingsViewOpen = true + } label: { + Label("Settings", systemImage: "ellipsis") + } + .backportCircleSymbolVariant() } - .backportCircleSymbolVariant() - } -#endif + #endif } } diff --git a/Solstice/Detail View/AnnualOverview.swift b/Solstice/Detail View/AnnualOverview.swift index 84756309..e217c255 100644 --- a/Solstice/Detail View/AnnualOverview.swift +++ b/Solstice/Detail View/AnnualOverview.swift @@ -5,11 +5,11 @@ // Created by Daniel Eden on 12/03/2023. // -import SwiftUI import Suite +import SwiftUI import TimeMachine -fileprivate var solsticeAndEquinoxFormatter: RelativeDateTimeFormatter { +private var solsticeAndEquinoxFormatter: RelativeDateTimeFormatter { let formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .full formatter.dateTimeStyle = .named @@ -18,22 +18,30 @@ fileprivate var solsticeAndEquinoxFormatter: RelativeDateTimeFormatter { struct AnnualOverview: View { #if !os(watchOS) - @Environment(\.openWindow) var openWindow + @Environment(\.openWindow) var openWindow #endif @Environment(\.timeMachine) var timeMachine: TimeMachine - + @State private var isInformationSheetPresented = false - + var location: Location - + var nextGreaterThanPrevious: Bool { timeMachine.date.nextSolsticeIncreasesLight(at: location.latitude) } - - var date: Date { timeMachine.date } - var nextSolstice: Date { timeMachine.date.nextSolstice } - var nextEquinox: Date { timeMachine.date.nextEquinox } - + + var date: Date { + timeMachine.date + } + + var nextSolstice: Date { + timeMachine.date.nextSolstice + } + + var nextEquinox: Date { + timeMachine.date.nextEquinox + } + var body: some View { Section { Group { @@ -62,7 +70,7 @@ struct AnnualOverview: View { Label("Jump to date", systemImage: "clock.arrow.2.circlepath") } } - + Label { AdaptiveStack { ContentToggle { showContent in @@ -89,67 +97,68 @@ struct AnnualOverview: View { .backgroundStyle(.tint) } } - + AnnualDaylightChart(location: location) .frame(height: chartHeight) - + if let shortestDay, - let longestDay { - Label { - let duration = Duration.seconds(longestDay.daylightDuration).formatted(.units(maximumUnitCount: 2)) - - AdaptiveStack { - ContentToggle { showContent in - if showContent { - Text("\(duration) of daylight") - } else { - Text(longestDay.date, style: .date) - } + let longestDay + { + Label { + let duration = Duration.seconds(longestDay.daylightDuration).formatted(.units(maximumUnitCount: 2)) + + AdaptiveStack { + ContentToggle { showContent in + if showContent { + Text("\(duration) of daylight") + } else { + Text(longestDay.date, style: .date) } - } label: { - Text("Longest day") } - } icon: { - Image(systemName: "sun.max") + } label: { + Text("Longest day") } - .swipeActions(edge: .leading) { - Button { - withAnimation { - timeMachine.date = longestDay.date - } - } label: { - Label("Jump to date", systemImage: "clock.arrow.2.circlepath") + } icon: { + Image(systemName: "sun.max") + } + .swipeActions(edge: .leading) { + Button { + withAnimation { + timeMachine.date = longestDay.date } + } label: { + Label("Jump to date", systemImage: "clock.arrow.2.circlepath") } - - Label { - let duration = Duration.seconds(shortestDay.daylightDuration).formatted(.units(maximumUnitCount: 2)) - - AdaptiveStack { - ContentToggle { showContent in - if showContent { - Text("\(duration) of daylight") - } else { - Text(shortestDay.date, style: .date) - } + } + + Label { + let duration = Duration.seconds(shortestDay.daylightDuration).formatted(.units(maximumUnitCount: 2)) + + AdaptiveStack { + ContentToggle { showContent in + if showContent { + Text("\(duration) of daylight") + } else { + Text(shortestDay.date, style: .date) } - } label: { - Text("Shortest day") } - } icon: { - Image(systemName: "sun.min") + } label: { + Text("Shortest day") } - .swipeActions(edge: .leading) { - Button { - withAnimation { - timeMachine.date = shortestDay.date - } - } label: { - Label("Jump to date", systemImage: "clock.arrow.2.circlepath") + } icon: { + Image(systemName: "sun.min") + } + .swipeActions(edge: .leading) { + Button { + withAnimation { + timeMachine.date = shortestDay.date } + } label: { + Label("Jump to date", systemImage: "clock.arrow.2.circlepath") } + } } - + if let daylightSavingsChange = location.timeZone.nextDaylightSavingTimeTransition(after: timeMachine.date) { Label { AdaptiveStack { @@ -172,40 +181,42 @@ struct AnnualOverview: View { } extension AnnualOverview { -var decemberSolsticeSolar: NTSolar? { + var decemberSolsticeSolar: NTSolar? { let year = calendar.component(.year, from: timeMachine.date) let decemberSolstice = SolsticeCalculator.decemberSolstice(year: year) return NTSolar(for: decemberSolstice, coordinate: location.coordinate, timeZone: location.timeZone) } - + var juneSolsticeSolar: NTSolar? { let year = calendar.component(.year, from: timeMachine.date) let juneSolstice = SolsticeCalculator.juneSolstice(year: year) return NTSolar(for: juneSolstice, coordinate: location.coordinate, timeZone: location.timeZone) } - + var longestDay: NTSolar? { guard let decemberSolsticeSolar, - let juneSolsticeSolar else { + let juneSolsticeSolar + else { return nil } - + return decemberSolsticeSolar.daylightDuration > juneSolsticeSolar.daylightDuration ? decemberSolsticeSolar : juneSolsticeSolar } - + var shortestDay: NTSolar? { guard let decemberSolsticeSolar, - let juneSolsticeSolar else { + let juneSolsticeSolar + else { return nil } - + return decemberSolsticeSolar.daylightDuration < juneSolsticeSolar.daylightDuration ? decemberSolsticeSolar : juneSolsticeSolar } } #Preview { Form { - TimeMachineView() { Text("Time Machine") } + TimeMachineView { Text("Time Machine") } AnnualOverview(location: TemporaryLocation.placeholderLondon) } .withTimeMachine(.solsticeTimeMachine) diff --git a/Solstice/Detail View/DailyOverview.swift b/Solstice/Detail View/DailyOverview.swift index da7a9dbb..c005ce17 100644 --- a/Solstice/Detail View/DailyOverview.swift +++ b/Solstice/Detail View/DailyOverview.swift @@ -5,8 +5,8 @@ // Created by Daniel Eden on 12/03/2023. // -import SwiftUI import Suite +import SwiftUI import TimeMachine struct DailyOverview: View { @@ -14,41 +14,42 @@ struct DailyOverview: View { var solar: NTSolar var location: Location - + @State private var gradientSolar: NTSolar? - + @AppStorage(Preferences.detailViewChartAppearance) private var chartAppearance @AppStorage(Preferences.chartType) private var chartType - + var solarDateIsInToday: Bool { var calendar = Calendar.autoupdatingCurrent calendar.timeZone = location.timeZone return calendar.isDate(solar.date, inSameDayAs: Date()) } - + var differenceFromPreviousSolstice: TimeInterval? { guard let solar = NTSolar(for: timeMachine.date, coordinate: location.coordinate, timeZone: location.timeZone), - let previousSolsticeSolar = NTSolar(for: solar.date.previousSolstice, coordinate: location.coordinate, timeZone: location.timeZone) else { + let previousSolsticeSolar = NTSolar(for: solar.date.previousSolstice, coordinate: location.coordinate, timeZone: location.timeZone) + else { return nil } - + return previousSolsticeSolar.daylightDuration - solar.daylightDuration } - + var nextGreaterThanPrevious: Bool { timeMachine.date.nextSolsticeIncreasesLight(at: location.latitude) } - + var body: some View { Section { VStack { switch chartType { #if !os(watchOS) - case .circular: - CircularSolarChart(location: location) - .padding() - .frame(maxHeight: chartHeight) - .frame(maxWidth: .infinity) + case .circular: + CircularSolarChart(location: location) + .padding() + .frame(maxHeight: chartHeight) + .frame(maxWidth: .infinity) #endif default: daylightChartView @@ -58,9 +59,9 @@ struct DailyOverview: View { } .listRowInsets(.zero) #if os(watchOS) - .listRowBackground(Color.clear) + .listRowBackground(Color.clear) #else - .contextMenu { + .contextMenu { Picker(selection: $chartType.animation()) { ForEach(ChartType.allCases) { chartType in Label(chartType.title, image: chartType.icon) @@ -72,7 +73,7 @@ struct DailyOverview: View { Text("Chart type") } .pickerStyle(.menu) - + Picker(selection: $chartAppearance.animation()) { ForEach(DaylightChart.Appearance.allCases, id: \.self) { appearance in Label(appearance.description, systemImage: "circle.fill") @@ -82,21 +83,21 @@ struct DailyOverview: View { Text("Chart theme") } .pickerStyle(.menu) - } - .alignmentGuide(.listRowSeparatorLeading) { d in d[.leading] } - .alignmentGuide(.listRowSeparatorTrailing) { d in d[.trailing] } - #if !os(visionOS) - .listRowBackground( - solar.view - .opacity(chartType == .circular && chartAppearance == .graphical ? 0.3 : 0) - .mask { - LinearGradient(colors: [.black, .clear], startPoint: .top, endPoint: .bottom) - } - .background(Color("listRowBackgroundColor")) - ) - #endif + } + .alignmentGuide(.listRowSeparatorLeading) { d in d[.leading] } + .alignmentGuide(.listRowSeparatorTrailing) { d in d[.trailing] } + #if !os(visionOS) + .listRowBackground( + solar.view + .opacity(chartType == .circular && chartAppearance == .graphical ? 0.3 : 0) + .mask { + LinearGradient(colors: [.black, .clear], startPoint: .top, endPoint: .bottom) + } + .background(Color("listRowBackgroundColor")) + ) + #endif #endif - + Group { Label { AdaptiveStack { @@ -107,11 +108,11 @@ struct DailyOverview: View { } icon: { Image(systemName: "hourglass") } - - if solarDateIsInToday && (solar.safeSunrise...solar.safeSunset).contains(solar.date) { + + if solarDateIsInToday && (solar.safeSunrise ... solar.safeSunset).contains(solar.date) { Label { AdaptiveStack { - Text(timerInterval: solar.safeSunrise...solar.safeSunset) + Text(timerInterval: solar.safeSunrise ... solar.safeSunset) .monospacedDigit() } label: { Text("Remaining daylight") @@ -120,7 +121,7 @@ struct DailyOverview: View { Image(systemName: "timer") } } - + Label { AdaptiveStack { if let sunrise = solar.sunrise { @@ -134,7 +135,7 @@ struct DailyOverview: View { } icon: { Image(systemName: "sunrise") } - + Label { AdaptiveStack { if let solarNoon = solar.solarNoon { @@ -148,7 +149,7 @@ struct DailyOverview: View { } icon: { Image(systemName: "sun.max") } - + Label { AdaptiveStack { if let sunset = solar.sunset { @@ -167,7 +168,8 @@ struct DailyOverview: View { .materialListRowBackground() } header: { if location.timeZoneIdentifier != localTimeZone.identifier, - !(location is CurrentLocation) { + !(location is CurrentLocation) + { HStack { Text("Local time") Spacer() @@ -177,8 +179,11 @@ struct DailyOverview: View { } } footer: { if let differenceFromPreviousSolstice { + let moreOrLess = nextGreaterThanPrevious + ? String(localized: "more", comment: "More daylight middle of sentence") + : String(localized: "less", comment: "Less daylight middle of sentence") Label { - Text("\(Duration.seconds(abs(differenceFromPreviousSolstice)).formatted(.units(maximumUnitCount: 2))) \(nextGreaterThanPrevious ? "more" : "less") daylight \(timeMachine.dateLabel(context: .middleOfSentence)) compared to the previous solstice") + Text("\(Duration.seconds(abs(differenceFromPreviousSolstice)).formatted(.units(maximumUnitCount: 2))) \(moreOrLess) daylight \(timeMachine.dateLabel(context: .middleOfSentence)) compared to the previous solstice") } icon: { Image(systemName: nextGreaterThanPrevious ? "chart.line.uptrend.xyaxis" : "chart.line.downtrend.xyaxis") .contentTransition(.symbolEffect) @@ -209,7 +214,7 @@ extension DailyOverview { } .onPreferenceChange(DaylightGradientTimePreferenceKey.self) { date in self.gradientSolar = NTSolar(for: date, coordinate: solar.coordinate, timeZone: location.timeZone) - } + } #endif #if os(macOS) .padding(-12) diff --git a/Solstice/Detail View/DaylightSummaryTitle.swift b/Solstice/Detail View/DaylightSummaryTitle.swift index 5841e866..aa280515 100644 --- a/Solstice/Detail View/DaylightSummaryTitle.swift +++ b/Solstice/Detail View/DaylightSummaryTitle.swift @@ -12,87 +12,87 @@ struct DaylightSummaryTitle: View { var event: NTSolar.Event? var date: Date? var timeZone = localTimeZone - + var summaryFont: Font { #if os(watchOS) - .headline + .headline #elseif os(macOS) - .title + .title #else - .title2 + .title2 #endif } - + var summaryLineLimit: Int { #if os(watchOS) - 4 + 4 #else - 2 + 2 #endif } - - private let formatter = Date.ComponentsFormatStyle.timeDuration - - var body: some View { - VStack(alignment: .leading) { - HStack { - Text(solar.differenceString) - .contentTransition(.numericText()) - .font(summaryFont) - .fontWeight(.semibold) - .lineLimit(summaryLineLimit) - .allowsTightening(true) - .minimumScaleFactor(0.8) - Spacer(minLength: 0) - } - .opacity(event == nil ? 1 : 0) - .overlay(alignment: .leading) { - if let event { - HStack { - VStack(alignment: .leading) { - Text(event.label) - .contentTransition(.numericText(countsDown: true)) - Text(event.date, style: .time) + + private let formatter = Date.ComponentsFormatStyle.timeDuration + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(solar.differenceString) + .contentTransition(.numericText()) + .font(summaryFont) + .fontWeight(.semibold) + .lineLimit(summaryLineLimit) + .allowsTightening(true) + .minimumScaleFactor(0.8) + Spacer(minLength: 0) + } + .opacity(event == nil ? 1 : 0) + .overlay(alignment: .leading) { + if let event { + HStack { + VStack(alignment: .leading) { + Text(event.label) + .contentTransition(.numericText(countsDown: true)) + Text(event.date, style: .time) + .foregroundStyle(.secondary) + .contentTransition(.numericText()) + } + + Spacer() + + if let date { + VStack(alignment: .trailing) { + Text(date, style: .time) .foregroundStyle(.secondary) - .contentTransition(.numericText()) - } - - Spacer() - - if let date { - VStack(alignment: .trailing) { - Text(date, style: .time) - .foregroundStyle(.secondary) - - Group { - if date < solar.safeSunrise { - Text("Sunrise in \((date.. solar.safeSunset && date <= (solar.tomorrow?.safeSunrise ?? solar.endOfDay) { - Text("Sunrise in \((date..<(solar.tomorrow?.safeSunrise ?? solar.endOfDay)).formatted(formatter))") - } + + Group { + if date < solar.safeSunrise { + Text("Sunrise in \((date ..< solar.safeSunrise).formatted(formatter))") + } else if date < solar.safeSunset { + Text("Sunset in \((date ..< solar.safeSunset).formatted(formatter))") + } else if date > solar.safeSunset && date <= (solar.tomorrow?.safeSunrise ?? solar.endOfDay) { + Text("Sunrise in \((date ..< (solar.tomorrow?.safeSunrise ?? solar.endOfDay)).formatted(formatter))") } - .foregroundStyle(.tertiary) } + .foregroundStyle(.tertiary) } } - .monospacedDigit() } + .monospacedDigit() } } - #if !os(macOS) - .scenePadding() - #endif - .fontDesign(.rounded) - .fontWeight(.semibold) - .environment(\.timeZone, timeZone) - .animation(.smooth, value: event) - } + } + #if !os(macOS) + .scenePadding() + #endif + .fontDesign(.rounded) + .fontWeight(.semibold) + .environment(\.timeZone, timeZone) + .animation(.smooth, value: event) + } } struct DaylightSummaryTitle_Previews: PreviewProvider { - static var previews: some View { - DaylightSummaryTitle(solar: NTSolar(for: .now, coordinate: TemporaryLocation.placeholderLondon.coordinate, timeZone: TemporaryLocation.placeholderLondon.timeZone)!) - } + static var previews: some View { + DaylightSummaryTitle(solar: NTSolar(for: .now, coordinate: TemporaryLocation.placeholderLondon.coordinate, timeZone: TemporaryLocation.placeholderLondon.timeZone)!) + } } diff --git a/Solstice/Detail View/DetailView.swift b/Solstice/Detail View/DetailView.swift index 26a0c023..c1d60bb4 100644 --- a/Solstice/Detail View/DetailView.swift +++ b/Solstice/Detail View/DetailView.swift @@ -5,47 +5,47 @@ // Created by Daniel Eden on 29/09/2022. // -import SwiftUI import Suite +import SwiftUI import TimeMachine struct DetailView: View { static var userActivity: String { Constants.viewLocationActivityType } - + @Environment(\.managedObjectContext) var viewContext @Environment(\.dismiss) var dismiss - + var location: Location @Environment(\.timeMachine) var timeMachine: TimeMachine #if !os(watchOS) - @Environment(LocationSearchService.self) var locationSearchService + @Environment(LocationSearchService.self) var locationSearchService #endif @State private var showRemainingDaylight = false @State private var showShareSheet = false - + @AppStorage(Preferences.detailViewChartAppearance) private var chartAppearance @SceneStorage("selectedLocation") private var selectedLocation: String? - + var solar: NTSolar? { NTSolar(for: timeMachine.date, coordinate: location.coordinate, timeZone: location.timeZone) } - + var navBarTitleText: Text { guard let title = location.title else { return location is CurrentLocation ? Text("Current Location") : Text(verbatim: "Solstice") } - + return Text(title) } - + var body: some View { Form { if let solar { DailyOverview(solar: solar, location: location) } - + AnnualOverview(location: location) } .formStyle(.grouped) @@ -55,15 +55,15 @@ struct DetailView: View { } .userActivity(Self.userActivity) { userActivity in var navigationSelection: String? = nil - + if let location = location as? SavedLocation { navigationSelection = location.uuid?.uuidString } else if let location = location as? CurrentLocation { navigationSelection = location.id } - + userActivity.title = "See daylight for \(location is CurrentLocation ? "current location" : location.title ?? "location")" - + userActivity.targetContentIdentifier = navigationSelection userActivity.isEligibleForSearch = true userActivity.isEligibleForHandoff = false @@ -86,25 +86,25 @@ struct DetailView: View { } } } - + var toolbarItemPlacement: ToolbarItemPlacement { #if os(macOS) - return .automatic + return .automatic #else - return .topBarTrailing + return .topBarTrailing #endif } - + @ToolbarContentBuilder var toolbarItems: some ToolbarContent { #if !os(macOS) - ToolbarItem(placement: .topBarTrailing) { - Button("Share...", systemImage: "square.and.arrow.up") { - showShareSheet.toggle() + ToolbarItem(placement: .topBarTrailing) { + Button("Share...", systemImage: "square.and.arrow.up") { + showShareSheet.toggle() + } } - } #endif - + if let location = location as? TemporaryLocation { ToolbarItem(placement: .confirmationAction) { Button { @@ -120,17 +120,17 @@ struct DetailView: View { } } } - + #if !os(watchOS) - if locationSearchService.location != nil { - ToolbarItem(placement: .cancellationAction) { - Button { - locationSearchService.location = nil - } label: { - Text("Close") + if locationSearchService.location != nil { + ToolbarItem(placement: .cancellationAction) { + Button { + locationSearchService.location = nil + } label: { + Text("Close") + } } } - } #endif } } diff --git a/Solstice/Detail View/SolarExtremetiesOverview.swift b/Solstice/Detail View/SolarExtremetiesOverview.swift index 51b652e5..24389e9f 100644 --- a/Solstice/Detail View/SolarExtremetiesOverview.swift +++ b/Solstice/Detail View/SolarExtremetiesOverview.swift @@ -10,24 +10,25 @@ import TimeMachine struct SolarExtremetiesOverview: View { @Environment(\.timeMachine) private var timeMachine - + var location: Location - + var body: some View { if let shortestDay, - let longestDay { + let longestDay + { SolarExtremityView(solar: longestDay, extremity: .longest) SolarExtremityView(solar: shortestDay, extremity: .shortest) } } } -fileprivate struct SolarExtremityView: View { +private struct SolarExtremityView: View { @Environment(\.timeMachine) private var timeMachine - + enum Extremity { case longest, shortest - + var title: LocalizedStringKey { switch self { case .longest: @@ -36,7 +37,7 @@ fileprivate struct SolarExtremityView: View { return "Shortest day" } } - + var imageName: String { switch self { case .longest: @@ -46,10 +47,10 @@ fileprivate struct SolarExtremityView: View { } } } - -var solar: NTSolar + + var solar: NTSolar var extremity: Extremity - + var body: some View { CompatibleDisclosureGroup { Button("Time travel to date", systemImage: "clock.arrow.2.circlepath") { @@ -57,9 +58,9 @@ var solar: NTSolar timeMachine.date = solar.date } } - + let duration = Duration.seconds(solar.daylightDuration).formatted(.units(maximumUnitCount: 2)) - + Label { AdaptiveStack { Text(duration) @@ -69,7 +70,7 @@ var solar: NTSolar } icon: { Image(systemName: "hourglass") } - + if let sunrise = solar.sunrise { Label { AdaptiveStack { @@ -81,7 +82,7 @@ var solar: NTSolar Image(systemName: "sunrise") } } - + if let sunset = solar.sunset { Label { AdaptiveStack { @@ -107,28 +108,28 @@ var solar: NTSolar } } -fileprivate struct CompatibleDisclosureGroup: View { +private struct CompatibleDisclosureGroup: View { @ViewBuilder var content: Content @ViewBuilder var label: Label - + @State private var isOpen = false - + var body: some View { #if os(watchOS) - Toggle(isOn: $isOpen) { - label - } - .toggleStyle(DisclosureGroupToggleStyle()) - if isOpen { - content - .padding(.leading) - } + Toggle(isOn: $isOpen) { + label + } + .toggleStyle(DisclosureGroupToggleStyle()) + if isOpen { + content + .padding(.leading) + } #else - DisclosureGroup(isExpanded: $isOpen) { - content - } label: { - label - } + DisclosureGroup(isExpanded: $isOpen) { + content + } label: { + label + } #endif } } @@ -156,33 +157,35 @@ extension CompatibleDisclosureGroup { } extension SolarExtremetiesOverview { -var decemberSolsticeSolar: NTSolar? { + var decemberSolsticeSolar: NTSolar? { let year = calendar.component(.year, from: timeMachine.date) let decemberSolstice = SolsticeCalculator.decemberSolstice(year: year) return NTSolar(for: decemberSolstice, coordinate: location.coordinate, timeZone: location.timeZone) } - + var juneSolsticeSolar: NTSolar? { let year = calendar.component(.year, from: timeMachine.date) let juneSolstice = SolsticeCalculator.juneSolstice(year: year) return NTSolar(for: juneSolstice, coordinate: location.coordinate, timeZone: location.timeZone) } - + var longestDay: NTSolar? { guard let decemberSolsticeSolar, - let juneSolsticeSolar else { + let juneSolsticeSolar + else { return nil } - + return decemberSolsticeSolar.daylightDuration > juneSolsticeSolar.daylightDuration ? decemberSolsticeSolar : juneSolsticeSolar } - + var shortestDay: NTSolar? { guard let decemberSolsticeSolar, - let juneSolsticeSolar else { + let juneSolsticeSolar + else { return nil } - + return decemberSolsticeSolar.daylightDuration < juneSolsticeSolar.daylightDuration ? decemberSolsticeSolar : juneSolsticeSolar } } diff --git a/Solstice/Equinox and Solstice Info View/AnnualSolarEvent.swift b/Solstice/Equinox and Solstice Info View/AnnualSolarEvent.swift index 75cf26ca..c6826451 100644 --- a/Solstice/Equinox and Solstice Info View/AnnualSolarEvent.swift +++ b/Solstice/Equinox and Solstice Info View/AnnualSolarEvent.swift @@ -9,7 +9,7 @@ import SwiftUI enum AnnualSolarEvent: CaseIterable, Codable, Hashable { case marchEquinox, juneSolstice, septemberEquinox, decemberSolstice - + var description: LocalizedStringKey { switch self { case .marchEquinox: @@ -22,7 +22,7 @@ enum AnnualSolarEvent: CaseIterable, Codable, Hashable { return "December solstice" } } - + var shortMonthDescription: LocalizedStringKey { switch self { case .marchEquinox: @@ -35,7 +35,7 @@ enum AnnualSolarEvent: CaseIterable, Codable, Hashable { return "December" } } - + var shortEventDescription: LocalizedStringKey { switch self { case .marchEquinox, .septemberEquinox: @@ -44,7 +44,7 @@ enum AnnualSolarEvent: CaseIterable, Codable, Hashable { return "Solstice" } } - + var sunAngle: Double { switch self { case .marchEquinox: diff --git a/Solstice/Equinox and Solstice Info View/EarthRealityView.swift b/Solstice/Equinox and Solstice Info View/EarthRealityView.swift index ee67a2d9..de25e797 100644 --- a/Solstice/Equinox and Solstice Info View/EarthRealityView.swift +++ b/Solstice/Equinox and Solstice Info View/EarthRealityView.swift @@ -5,33 +5,33 @@ // Created by Daniel Eden on 29/06/2024. // -import SwiftUI - import RealityKit import RealityKitContent +import SwiftUI struct EarthRealityView: View { @State var selection: AnnualSolarEvent = .juneSolstice - + var body: some View { VStack { Spacer() - + SolarSystemMiniMap(event: selection) .frame(maxWidth: .infinity, alignment: .trailing) - + RealityView { content in if let earth = try? await Entity(named: "Scene", in: realityKitContentBundle) { content.add(earth) } } update: { content in guard let rootEntity = content.entities.first, - let earth = rootEntity.findEntity(named: "Earth"), - var modelComponent = earth.components[ModelComponent.self], - var shaderMaterial = modelComponent.materials.first as? ShaderGraphMaterial else { + let earth = rootEntity.findEntity(named: "Earth"), + var modelComponent = earth.components[ModelComponent.self], + var shaderMaterial = modelComponent.materials.first as? ShaderGraphMaterial + else { return } - + do { try shaderMaterial.setParameter(name: "Angle", value: .float(Float(Angle(radians: selection.sunAngle).degrees * -1))) modelComponent.materials = [shaderMaterial] @@ -40,9 +40,9 @@ struct EarthRealityView: View { print(error) } } - + Spacer() - + Picker(selection: $selection.animation()) { ForEach(AnnualSolarEvent.allCases, id: \.self) { eventType in Text(eventType.shortMonthDescription) @@ -56,5 +56,5 @@ struct EarthRealityView: View { } #Preview { - EarthRealityView() + EarthRealityView() } diff --git a/Solstice/Equinox and Solstice Info View/EarthSceneKitView.swift b/Solstice/Equinox and Solstice Info View/EarthSceneKitView.swift index 3be8a866..b9e0badc 100644 --- a/Solstice/Equinox and Solstice Info View/EarthSceneKitView.swift +++ b/Solstice/Equinox and Solstice Info View/EarthSceneKitView.swift @@ -5,52 +5,52 @@ // Created by Daniel Eden on 28/06/2024. // -import SwiftUI import SceneKit +import SwiftUI #if canImport(UIKit) -typealias NativeViewRepresentable = UIViewRepresentable + typealias NativeViewRepresentable = UIViewRepresentable #elseif canImport(AppKit) -typealias NativeViewRepresentable = NSViewRepresentable + typealias NativeViewRepresentable = NSViewRepresentable #endif #if os(iOS) -extension NSBundleResourceRequest: @unchecked @retroactive Sendable {} + extension NSBundleResourceRequest: @unchecked @retroactive Sendable {} #endif struct EarthSceneKitView: View { var height: Double = 400 @State private var selection: AnnualSolarEvent = .juneSolstice - + var rotationAmount: Double { selection.sunAngle } - + #if os(iOS) - let resourceRequest = NSBundleResourceRequest(tags: ["earth"]) + let resourceRequest = NSBundleResourceRequest(tags: ["earth"]) #endif - + @State var scene: EarthScene? - + var body: some View { ZStack(alignment: .topTrailing) { SolarSystemMiniMap(event: selection) - + VStack { if scene != nil { CustomSceneView(scene: $scene) } else { -#if os(iOS) - HStack { - Spacer() - ProgressView(value: resourceRequest.progress.fractionCompleted, total: 1.0) { - Text("Loading...") + #if os(iOS) + HStack { + Spacer() + ProgressView(value: resourceRequest.progress.fractionCompleted, total: 1.0) { + Text("Loading...") + } + .progressViewStyle(.circular) + Spacer() } - .progressViewStyle(.circular) - Spacer() - } - .frame(maxHeight: .infinity) -#endif + .frame(maxHeight: .infinity) + #endif } } .frame(idealHeight: height, maxHeight: .infinity) @@ -65,22 +65,22 @@ struct EarthSceneKitView: View { duration: 0.6, usesShortestUnitArc: true ) - + action.timingMode = .easeOut - + if let node = scene?.lightAnchorNode { node.runAction(action) } } .task { #if os(iOS) - if await !resourceRequest.conditionallyBeginAccessingResources() { - do { - try await resourceRequest.beginAccessingResources() - } catch { - print(error) + if await !resourceRequest.conditionallyBeginAccessingResources() { + do { + try await resourceRequest.beginAccessingResources() + } catch { + print(error) + } } - } #endif scene = EarthScene() } @@ -89,7 +89,7 @@ struct EarthSceneKitView: View { resourceRequest.endAccessingResources() } #endif - + Picker("View Earth at:", selection: $selection.animation()) { ForEach(AnnualSolarEvent.allCases, id: \.self) { event in Text(event.description) @@ -100,45 +100,44 @@ struct EarthSceneKitView: View { struct CustomSceneView: NativeViewRepresentable { @Binding var scene: Scene? - - func makeView(context: Context) -> SCNView { + + func makeView(context _: Context) -> SCNView { let view = SCNView() view.autoenablesDefaultLighting = false view.backgroundColor = .clear - + let node = scene?.rootNode - + node?.rotation = .init(0, 0, 90, 0) view.scene = scene - + view.pointOfView?.camera?.fieldOfView = 35 - + return view } - - func updateView(_ uiView: SCNView, context: Context) { - - } - -#if canImport(UIKit) - func makeUIView(context: Context) -> SCNView { - makeView(context: context) - } - - func updateUIView(_ uiView: SCNView, context: Context) { - updateView(uiView, context: context) - } -#elseif canImport(AppKit) - func makeNSView(context: Context) -> SCNView { - makeView(context: context) - } - - func updateNSView(_ nsView: SCNView, context: Context) { - updateView(nsView, context: context) - } -#endif + + func updateView(_: SCNView, context _: Context) {} + + #if canImport(UIKit) + func makeUIView(context: Context) -> SCNView { + makeView(context: context) + } + + func updateUIView(_ uiView: SCNView, context: Context) { + updateView(uiView, context: context) + } + + #elseif canImport(AppKit) + func makeNSView(context: Context) -> SCNView { + makeView(context: context) + } + + func updateNSView(_ nsView: SCNView, context: Context) { + updateView(nsView, context: context) + } + #endif } #Preview { - EarthSceneKitView() + EarthSceneKitView() } diff --git a/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeDescriptions.swift b/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeDescriptions.swift index 4ede0e33..05a8eb62 100644 --- a/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeDescriptions.swift +++ b/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeDescriptions.swift @@ -1,5 +1,5 @@ // -// SwiftUIView.swift +// EquinoxAndSolsticeDescriptions.swift // Solstice // // Created by Daniel Eden on 29/06/2024. @@ -8,23 +8,23 @@ import SwiftUI struct EquinoxAndSolsticeDescriptions: View { - var body: some View { - Text("The equinox and solstice define the transitions between the seasons of the astronomical calendar and are a key part of the Earth’s orbit around the Sun.") - - VStack(alignment: .leading) { - Text("Equinox") - .font(.headline) - Text("The equinox occurs twice a year, in March and September, and mark the points when the Sun crosses the equator’s path. During the equinox, day and night are around the same length all across the globe.") - } - - VStack(alignment: .leading) { - Text("Solstice") - .font(.headline) - Text("The solstice occurs twice a year, in June and December (otherwise known as the summer and winter solstices), and mark the sun’s northernmost and southernmost excursions. During the June solstice, the Earth’s northern hemisphere is pointed towards the sun, resulting in increased sunlight and warmer temperatures; the same is true for the southern hemisphere during the December solstice.") - } - } + var body: some View { + Text("The equinox and solstice define the transitions between the seasons of the astronomical calendar and are a key part of the Earth’s orbit around the Sun.") + + VStack(alignment: .leading) { + Text("Equinox") + .font(.headline) + Text("The equinox occurs twice a year, in March and September, and mark the points when the Sun crosses the equator’s path. During the equinox, day and night are around the same length all across the globe.") + } + + VStack(alignment: .leading) { + Text("Solstice") + .font(.headline) + Text("The solstice occurs twice a year, in June and December (otherwise known as the summer and winter solstices), and mark the sun’s northernmost and southernmost excursions. During the June solstice, the Earth’s northern hemisphere is pointed towards the sun, resulting in increased sunlight and warmer temperatures; the same is true for the southern hemisphere during the December solstice.") + } + } } #Preview { - EquinoxAndSolsticeDescriptions() + EquinoxAndSolsticeDescriptions() } diff --git a/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeInfoSheet.swift b/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeInfoSheet.swift index c6fb09ac..fdd7efe7 100644 --- a/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeInfoSheet.swift +++ b/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeInfoSheet.swift @@ -1,5 +1,5 @@ // -// InformationSheetView.swift +// EquinoxAndSolsticeInfoSheet.swift // Solstice // // Created by Daniel Eden on 22/03/2023. @@ -14,7 +14,7 @@ struct EquinoxAndSolsticeInfoSheet: View { Section { EarthSceneKitView(height: min(geometry.size.width, 400)) } - + Section { EquinoxAndSolsticeDescriptions() } footer: { diff --git a/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeInfoWindow.swift b/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeInfoWindow.swift index e8dcd072..ebd20aaf 100644 --- a/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeInfoWindow.swift +++ b/Solstice/Equinox and Solstice Info View/EquinoxAndSolsticeInfoWindow.swift @@ -8,23 +8,23 @@ import SwiftUI struct EquinoxAndSolsticeInfoWindow: View { - var body: some View { - HStack { - ScrollView { - VStack(alignment: .leading, spacing: 20) { - Text("About solstices and equinoxes") - .font(.largeTitle.weight(.semibold)) - EquinoxAndSolsticeDescriptions() - } - .padding() + var body: some View { + HStack { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + Text("About solstices and equinoxes") + .font(.largeTitle.weight(.semibold)) + EquinoxAndSolsticeDescriptions() } - - EarthRealityView() + .padding() } - .scenePadding() - } + + EarthRealityView() + } + .scenePadding() + } } #Preview { - EquinoxAndSolsticeInfoWindow() + EquinoxAndSolsticeInfoWindow() } diff --git a/Solstice/Equinox and Solstice Info View/SceneKit Models/EarthNode.swift b/Solstice/Equinox and Solstice Info View/SceneKit Models/EarthNode.swift index a1f15937..ea7de298 100644 --- a/Solstice/Equinox and Solstice Info View/SceneKit Models/EarthNode.swift +++ b/Solstice/Equinox and Solstice Info View/SceneKit Models/EarthNode.swift @@ -9,33 +9,33 @@ import Foundation import SceneKit import SpriteKit -public class EarthNode : SCNNode { - public override init() { +public class EarthNode: SCNNode { + override public init() { super.init() build() } - + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) build() } - + func build() { addBody() - //addClouds() + // addClouds() addPoles() addEquator() runAction(.rotate(by: .pi * 1.5, around: SCNVector3(x: 0, y: 1, z: 0), duration: 0)) beginDailyRotation() } - + func addBody() { // Earth let bodyNode = SCNNode() - + let sphere = SCNSphere(radius: 1) sphere.segmentCount = 50 - + let bodyMaterial = SCNMaterial() bodyMaterial.diffuse.contents = "Diffuse" bodyMaterial.specular.contents = "Specular" @@ -48,53 +48,53 @@ public class EarthNode : SCNNode { bodyNode.geometry = sphere addChildNode(bodyNode) } - + func addClouds() { // Clouds let atmosNode = SCNNode() let atmosSphere = SCNSphere(radius: 1.01) atmosSphere.segmentCount = 50 - + let atmosMaterial = SCNMaterial() atmosMaterial.diffuse.contents = "Clouds" atmosMaterial.transparent.contents = "Clouds" atmosSphere.firstMaterial = atmosMaterial atmosNode.geometry = atmosSphere addChildNode(atmosNode) - - let action = SCNAction.rotate(by: .pi * -2, around: SCNVector3(x:0, y:1, z:0), duration: 360) + + let action = SCNAction.rotate(by: .pi * -2, around: SCNVector3(x: 0, y: 1, z: 0), duration: 360) let repeatAction = SCNAction.repeatForever(action) atmosNode.runAction(repeatAction) } - + func addPoles() { // Poles let poleNode = SCNNode() let pole = SCNCylinder(radius: 0.005, height: 2.2) poleNode.geometry = pole - + let poleMaterial = SCNMaterial() poleMaterial.emission.contents = SKColor.red poleMaterial.diffuse.contents = SKColor.red pole.firstMaterial = poleMaterial addChildNode(poleNode) } - + func addEquator() { // Equator let equatorNode = SCNNode() let equator = SCNCylinder(radius: 1.05, height: 0.01) equatorNode.geometry = equator - + let equatorMaterial = SCNMaterial() equatorMaterial.emission.contents = SKColor.systemOrange equatorMaterial.diffuse.contents = SKColor.systemOrange equator.firstMaterial = equatorMaterial addChildNode(equatorNode) } - + func beginDailyRotation() { - let action = SCNAction.rotate(by: .pi * 2, around: SCNVector3(x:0, y:1, z:0), duration: 90) + let action = SCNAction.rotate(by: .pi * 2, around: SCNVector3(x: 0, y: 1, z: 0), duration: 90) let repeatAction = SCNAction.repeatForever(action) runAction(repeatAction) } diff --git a/Solstice/Equinox and Solstice Info View/SceneKit Models/EarthScene.swift b/Solstice/Equinox and Solstice Info View/SceneKit Models/EarthScene.swift index ef80b6c6..9bc598b5 100644 --- a/Solstice/Equinox and Solstice Info View/SceneKit Models/EarthScene.swift +++ b/Solstice/Equinox and Solstice Info View/SceneKit Models/EarthScene.swift @@ -10,25 +10,25 @@ import SpriteKit class EarthScene: SCNScene { var lightAnchorNode: SCNNode? - - public override init() { + + override init() { super.init() build() } - - public required init?(coder aDecoder: NSCoder) { + + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) build() } - + func build() { let earthNode = EarthNode() let tiltNode = SCNNode() - + tiltNode.eulerAngles = SCNVector3(-23.4 * (.pi / 180.0), .pi / 2, 0) tiltNode.addChildNode(earthNode) - self.rootNode.addChildNode(tiltNode) - + rootNode.addChildNode(tiltNode) + // Create a strong light let light = SCNLight() light.type = .omni @@ -38,15 +38,15 @@ class EarthScene: SCNScene { light.shadowSampleCount = 16 light.shadowBias = 2 light.shadowRadius = 10 - + let lightNode = SCNNode() - + lightNode.light = light lightNode.position = SCNVector3(-100, 2, 0) - - self.lightAnchorNode = SCNNode() - self.lightAnchorNode?.addChildNode(lightNode) - - self.rootNode.addChildNode(self.lightAnchorNode!) + + lightAnchorNode = SCNNode() + lightAnchorNode?.addChildNode(lightNode) + + rootNode.addChildNode(lightAnchorNode!) } } diff --git a/Solstice/Equinox and Solstice Info View/SolarSystemMiniMap.swift b/Solstice/Equinox and Solstice Info View/SolarSystemMiniMap.swift index d2bf075f..97c0f50f 100644 --- a/Solstice/Equinox and Solstice Info View/SolarSystemMiniMap.swift +++ b/Solstice/Equinox and Solstice Info View/SolarSystemMiniMap.swift @@ -9,13 +9,13 @@ import SwiftUI struct SolarSystemMiniMap: View { var event: AnnualSolarEvent - + var angle: Double { event.sunAngle } - + var size: Double = 44 - + var body: some View { HStack { VStack(alignment: .trailing, spacing: 0) { @@ -24,7 +24,7 @@ struct SolarSystemMiniMap: View { Text(event.shortEventDescription) } .font(.caption) - + ZStack { Circle() .strokeBorder(.tertiary, lineWidth: 3) @@ -35,7 +35,7 @@ struct SolarSystemMiniMap: View { .offset(x: size / 20) } .rotationEffect(Angle(radians: angle) * -1) - + Circle() .fill(.primary) .frame(width: size / 4, height: size / 4) diff --git a/Solstice/Extensions/AppStorage++.swift b/Solstice/Extensions/AppStorage++.swift index 3b9a6a7a..299da4e8 100644 --- a/Solstice/Extensions/AppStorage++.swift +++ b/Solstice/Extensions/AppStorage++.swift @@ -7,23 +7,23 @@ import SwiftUI -fileprivate let defaultNotificationDate = calendar.date(bySettingHour: 8, minute: 0, second: 0, of: Date()) ?? .now +private let defaultNotificationDate = calendar.date(bySettingHour: 8, minute: 0, second: 0, of: Date()) ?? .now -fileprivate let store = UserDefaults(suiteName: Constants.appGroupIdentifier) +private let store = UserDefaults(suiteName: Constants.appGroupIdentifier) extension AppStorage { init(_ kv: AppStorageKVPair) where Value == String { self.init(wrappedValue: kv.value, kv.key, store: store ?? .standard) } - + init(_ kv: AppStorageKVPair) where Value == Bool { self.init(wrappedValue: kv.value, kv.key, store: store ?? .standard) } - + init(_ kv: AppStorageKVPair) where Value == TimeInterval { self.init(wrappedValue: kv.value, kv.key, store: store ?? .standard) } - + init(_ kv: AppStorageKVPair) where Value: RawRepresentable, Value.RawValue == String { self.init(wrappedValue: kv.value, kv.key, store: store ?? .standard) } @@ -32,16 +32,16 @@ extension AppStorage { extension Optional: @retroactive RawRepresentable where Wrapped: Codable { public var rawValue: String { guard let data = try? JSONEncoder().encode(self), - let json = String(data: data, encoding: .utf8) + let json = String(data: data, encoding: .utf8) else { return "{}" } return json } - + public init?(rawValue: String) { guard let data = rawValue.data(using: .utf8), - let value = try? JSONDecoder().decode(Self.self, from: data) + let value = try? JSONDecoder().decode(Self.self, from: data) else { return nil } @@ -51,100 +51,106 @@ extension Optional: @retroactive RawRepresentable where Wrapped: Codable { typealias AppStorageKVPair = (key: String, value: T) -struct Preferences { +enum Preferences { typealias Value = AppStorageKVPair - + // MARK: Onboarding + static let hasCompletedOnboarding: Value = ("hasCompletedOnboarding", false) - + // MARK: Notifications + /// The user preference for whether notifications are enabled static let notificationsEnabled: Value = ("notifsEnabled", false) - + /// The user preference for whether notifications include sunrise/sunset times static let notificationsIncludeSunTimes: Value = ("notifsIncludeSunTimes", true) - + /// The user preference for whether notifications include the daylight duration static let notificationsIncludeDaylightDuration: Value = ("notifsIncludeDaylightDuration", true) - + /// The user preference for whether notifications include the change in daylight compared to yesterday static let notificationsIncludeDaylightChange: Value = ("notifsIncludeDaylightChange", true) - + /// The user preference for whether notifications include the time until the next solstice static let notificationsIncludeSolsticeCountdown: Value = ("notifsIncludeSolsticeCountdown", false) - + /// The user preference for how notifications are altered during periods of lessening daylight static let sadPreference: Value = ("sadPreverence", .none) - + static let cachedLatitude: Value = ("cachedLatitude", 0) static let cachedLongitude: Value = ("cachedLongitude", 0) - + static let customNotificationLocationUUID: Value = ("customNotificationLocationUUID", nil) - + // MARK: Scheduling - struct NotificationSettings { + + enum NotificationSettings { /// The type of notification schedule; either a specific time (specified in `notificationDate`) or relative to sunrise/sunset static let scheduleType: Value = ("notificationScheduleType", .specificTime) - + /// The date/time for notification scheduling. Only the time will be used. static let _notificationTime: Value = ("notifTime", defaultNotificationDate) - + /// The date components for notification scheduling. static let notificationDateComponents: Value = ("notifDateComponents", NotificationSettings.defaultDateComponents) static let defaultDateComponents = DateComponents(timeZone: .autoupdatingCurrent, hour: 8, minute: 0) - + /// Which solar event notifications are sent relative to static let relation: Value = ("notificationRelation", .sunrise) - + /// The offset in seconds between the notification and the chosen solar event static let relativeOffset: Value = ("notificationRelativeOffset", 30 * 60) - + /// The preset offsets for relative notification times - static let relativeOffsetDetents: Array = [ + static let relativeOffsetDetents: [TimeInterval] = [ -4 * 60 * 60, - -3 * 60 * 60, - -2 * 60 * 60, - -1 * 60 * 60, - -45 * 60, - -30 * 60, - -15 * 60, - 0, - 15 * 60, - 30 * 60, - 45 * 60, - 60 * 60, - 2 * 60 * 60, - 3 * 60 * 60, - 4 * 60 * 60 + -3 * 60 * 60, + -2 * 60 * 60, + -1 * 60 * 60, + -45 * 60, + -30 * 60, + -15 * 60, + 0, + 15 * 60, + 30 * 60, + 45 * 60, + 60 * 60, + 2 * 60 * 60, + 3 * 60 * 60, + 4 * 60 * 60, ] } - + // MARK: Appearance + enum SortingFunction: String, Codable, RawRepresentable { case timezone, daylightDuration } - + static let detailViewChartAppearance: Value = ("detailViewChartAppearance", chartAppearanceDefaultValue) static let listViewAppearance: Value = ("listViewAppearance", .graphical) - + #if !os(watchOS) - static let listViewSortDimension: Value = ("listViewOrderBy", .timezone) + static let listViewSortDimension: Value = ("listViewOrderBy", .timezone) #endif - + static let listViewSortOrder: Value = ("listViewSortOrder", .forward) static let listViewShowComplication: Value = ("listViewShowComplication", showComplicationDefaultValue) - + static let timeTravelAppearance: Value = ("timeTravelAppearance", .expanded) - + static let chartType: Value = ("chartType", .classic) static let showSolsticesInChart: Value = ("showSolsticesInChart", false) } enum TimeTravelAppearance: String, CaseIterable, RawRepresentable, Identifiable { case expanded, compact, hidden - - var id: Self { self } - + + var id: Self { + self + } + var title: LocalizedStringKey { switch self { case .expanded: return "Classic" @@ -152,39 +158,41 @@ enum TimeTravelAppearance: String, CaseIterable, RawRepresentable, Identifiable case .hidden: return "Hidden" } } - + #if !os(watchOS) - var image: ImageResource { - switch self { - case .expanded: - return .timetravelClassic - case .compact: - return .timetravelCompact - case .hidden: - return .timetravelHidden + var image: ImageResource { + switch self { + case .expanded: + return .timetravelClassic + case .compact: + return .timetravelCompact + case .hidden: + return .timetravelHidden + } } - } #endif } enum ChartType: String, CaseIterable, RawRepresentable, Identifiable { case classic, circular - + var title: LocalizedStringKey { switch self { case .classic: return "Classic" case .circular: return "Circular" } } - + var icon: ImageResource { switch self { case .classic: return .solarchartLinear case .circular: return .solarchartCircularFill } } - - var id: Self { self } + + var id: Self { + self + } } extension Preferences { @@ -192,7 +200,7 @@ extension Preferences { case none = "No change" case removeDifference = "Remove daylight gain/loss" case suppressNotifications = "Suppress notifications altogether" - + var description: LocalizedStringKey { switch self { case .none: @@ -209,7 +217,7 @@ extension Preferences { extension Preferences.NotificationSettings { enum ScheduleType: String, RawRepresentable, CaseIterable { case specificTime, sunset, sunrise - + var description: LocalizedStringKey { switch self { case .specificTime: @@ -226,31 +234,31 @@ extension Preferences.NotificationSettings { extension SortOrder: @retroactive RawRepresentable { public init?(rawValue: String) { guard let data = rawValue.data(using: .utf8), - let result = try? JSONDecoder().decode(SortOrder.self, from: data) + let result = try? JSONDecoder().decode(SortOrder.self, from: data) else { return nil } self = result } - + public var rawValue: String { guard let data = try? JSONEncoder().encode(self), - let result = String(data: data, encoding: .utf8) + let result = String(data: data, encoding: .utf8) else { return "" } return result } - + public typealias RawValue = String } -fileprivate var showComplicationDefaultValue: Bool { +private var showComplicationDefaultValue: Bool { #if os(macOS) - true + true #else - false + false #endif } -fileprivate var chartAppearanceDefaultValue: DaylightChart.Appearance = .graphical +private var chartAppearanceDefaultValue: DaylightChart.Appearance = .graphical diff --git a/Solstice/Extensions/Bundle++.swift b/Solstice/Extensions/Bundle++.swift index 58a03403..c1094abb 100644 --- a/Solstice/Extensions/Bundle++.swift +++ b/Solstice/Extensions/Bundle++.swift @@ -11,6 +11,7 @@ extension Bundle { var releaseVersionNumber: String? { return infoDictionary?["CFBundleShortVersionString"] as? String } + var buildVersionNumber: String? { return infoDictionary?["CFBundleVersion"] as? String } diff --git a/Solstice/Extensions/CLLocationCoordinate2D++.swift b/Solstice/Extensions/CLLocationCoordinate2D++.swift index 62e7f8ed..2aaeb276 100644 --- a/Solstice/Extensions/CLLocationCoordinate2D++.swift +++ b/Solstice/Extensions/CLLocationCoordinate2D++.swift @@ -1,12 +1,12 @@ // -// CLLocationCoordinate2D+RawRepresentable.swift +// CLLocationCoordinate2D++.swift // Solstice // // Created by Daniel Eden on 05/04/2023. // -import Foundation import CoreLocation +import Foundation extension CLLocationCoordinate2D { static var proxiedToTimeZone: CLLocationCoordinate2D { @@ -19,11 +19,11 @@ extension CLLocationCoordinate2D { var insideArcticCircle: Bool { latitude > 66.34 } - + var insideAntarcticCircle: Bool { latitude < -66.34 } - + var insidePolarCircle: Bool { insideArcticCircle || insideAntarcticCircle } diff --git a/Solstice/Extensions/Color+Mix.swift b/Solstice/Extensions/Color+Mix.swift index d567e338..f03e3277 100644 --- a/Solstice/Extensions/Color+Mix.swift +++ b/Solstice/Extensions/Color+Mix.swift @@ -8,13 +8,15 @@ import SwiftUI // MARK: - Platform-specific Color Type + #if canImport(UIKit) -public typealias NativeColor = UIColor + public typealias NativeColor = UIColor #elseif canImport(AppKit) -public typealias NativeColor = NSColor + public typealias NativeColor = NSColor #endif // MARK: - Color Mixing + public extension NativeColor { func mix(with target: NativeColor, amount: CGFloat) -> Self { var r1: CGFloat = 0, g1: CGFloat = 0, b1: CGFloat = 0, a1: CGFloat = 0 @@ -39,6 +41,7 @@ public extension Color { } // MARK: - Hex Conversion + extension Color { func toHex() -> String? { let uic = NativeColor(self) diff --git a/Solstice/Extensions/Date++.swift b/Solstice/Extensions/Date++.swift index ac9a9cc5..aab2e41b 100644 --- a/Solstice/Extensions/Date++.swift +++ b/Solstice/Extensions/Date++.swift @@ -11,18 +11,18 @@ extension Date { func startOfDay(in timeZone: TimeZone) -> Date { var calendar = Calendar.current calendar.timeZone = timeZone - + return calendar.startOfDay(for: self) } - + var startOfDay: Date { calendar.startOfDay(for: self) } - + var endOfDay: Date { calendar.date(bySettingHour: 23, minute: 59, second: 59, of: self) ?? self } - + var isToday: Bool { calendar.isDateInToday(self) } @@ -46,9 +46,9 @@ extension Date { /// Allows dates to be stored in AppStorage extension Date: @retroactive RawRepresentable { public var rawValue: String { - self.timeIntervalSinceReferenceDate.description + timeIntervalSinceReferenceDate.description } - + public init?(rawValue: String) { self = Date(timeIntervalSinceReferenceDate: Double(rawValue) ?? 0.0) } @@ -57,19 +57,21 @@ extension Date: @retroactive RawRepresentable { extension DateComponents: @retroactive RawRepresentable { public var rawValue: String { guard let data = try? JSONEncoder().encode(self), - let string = String(data: data, encoding: .utf8) else { + let string = String(data: data, encoding: .utf8) + else { return "{}" } - + return string } - + public init?(rawValue: String) { guard let data = rawValue.data(using: .utf8), - let decoded = try? JSONDecoder().decode(DateComponents.self, from: data) else { + let decoded = try? JSONDecoder().decode(DateComponents.self, from: data) + else { return nil } - + self = decoded } } diff --git a/Solstice/Extensions/EdgeInsets++.swift b/Solstice/Extensions/EdgeInsets++.swift index ffba6823..259fc17c 100644 --- a/Solstice/Extensions/EdgeInsets++.swift +++ b/Solstice/Extensions/EdgeInsets++.swift @@ -8,7 +8,10 @@ import SwiftUI extension EdgeInsets { - static var none: EdgeInsets { .zero } + static var none: EdgeInsets { + .zero + } + static var zero: EdgeInsets { EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) } diff --git a/Solstice/Extensions/NSImage+pngData.swift b/Solstice/Extensions/NSImage+pngData.swift index fb17d5ae..886af3b6 100644 --- a/Solstice/Extensions/NSImage+pngData.swift +++ b/Solstice/Extensions/NSImage+pngData.swift @@ -8,14 +8,14 @@ import SwiftUI #if os(macOS) -extension NSImage { - func pngData() -> Data? { - guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { - return nil + extension NSImage { + func pngData() -> Data? { + guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { + return nil + } + + let bitmapRep = NSBitmapImageRep(cgImage: cgImage) + return bitmapRep.representation(using: .png, properties: [:]) } - - let bitmapRep = NSBitmapImageRep(cgImage: cgImage) - return bitmapRep.representation(using: .png, properties: [:]) } -} #endif diff --git a/Solstice/Extensions/NTSolar++.swift b/Solstice/Extensions/NTSolar++.swift index d7d0d39f..7e7f283f 100644 --- a/Solstice/Extensions/NTSolar++.swift +++ b/Solstice/Extensions/NTSolar++.swift @@ -5,26 +5,42 @@ // Created by Daniel Eden on 03/02/2026. // -import Foundation -import SwiftUI import Charts import CoreLocation +import Foundation +import SwiftUI // MARK: - Solar Event Types + extension NTSolar { struct Event: Hashable, Identifiable { - var id: Int { hashValue } - let label: String - var date: Date + var id: Int { + hashValue + } + + let label: LocalizedStringResource + let date: Date let phase: Phase - init?(label: String, date: Date?, phase: Phase) { + init?(label: LocalizedStringResource, date: Date?, phase: Phase) { guard let date else { return nil } self.label = label self.date = date self.phase = phase } + /// An event is identified by its date and phase; `label` is derived from + /// those, so it's excluded from equality/hashing (which also avoids + /// relying on `LocalizedStringResource`'s conformances). + static func == (lhs: Event, rhs: Event) -> Bool { + lhs.date == rhs.date && lhs.phase == rhs.phase + } + + func hash(into hasher: inout Hasher) { + hasher.combine(date) + hasher.combine(phase) + } + var imageName: String { switch phase { case .sunrise: @@ -43,14 +59,29 @@ extension NTSolar { enum Phase: String, Plottable, CaseIterable { case night = "Night", - astronomical = "Astronomical Twilight", - nautical = "Nautical Twilight", - civil = "Civil Twilight", - day = "Day", - sunrise = "Sunrise", - sunset = "Sunset" + astronomical = "Astronomical Twilight", + nautical = "Nautical Twilight", + civil = "Civil Twilight", + day = "Day", + sunrise = "Sunrise", + sunset = "Sunset" static let plottablePhases: [Phase] = [.astronomical, .nautical, .civil] + + /// Localized display name for the phase. `rawValue` is the stable + /// `Plottable` identity and can't be localized directly, so this is + /// what the chart legend renders instead. + var localizedName: LocalizedStringResource { + switch self { + case .night: "Night" + case .astronomical: "Astronomical Twilight" + case .nautical: "Nautical Twilight" + case .civil: "Civil Twilight" + case .day: "Day" + case .sunrise: "Sunrise" + case .sunset: "Sunset" + } + } } var phases: [Phase: (sunrise: Date?, sunset: Date?)] { @@ -58,12 +89,13 @@ extension NTSolar { .astronomical: (astronomicalSunrise, astronomicalSunset), .nautical: (nauticalSunrise, nauticalSunset), .civil: (civilSunrise, civilSunset), - .day: (safeSunrise, safeSunset) + .day: (safeSunrise, safeSunset), ] } } // MARK: - Day Boundaries + extension NTSolar { var startOfDay: Date { calendar.startOfDay(for: date) @@ -83,6 +115,7 @@ extension NTSolar { } // MARK: - Safe Sunrise/Sunset + extension NTSolar { var fallbackSunrise: Date? { sunrise ?? civilSunrise ?? nauticalSunrise ?? astronomicalSunrise @@ -132,7 +165,7 @@ extension NTSolar { let month = calendar.component(.month, from: date) switch month { - case 1...3, 10...12: + case 1 ... 3, 10 ... 12: return coordinate.insideArcticCircle ? 0 : TimeInterval.twentyFourHours default: return coordinate.insideArcticCircle ? TimeInterval.twentyFourHours : 0 @@ -141,6 +174,7 @@ extension NTSolar { } // MARK: - Related Days + extension NTSolar { var yesterday: NTSolar? { let yesterdayDate = calendar.date(byAdding: .day, value: -1, to: date) ?? date.addingTimeInterval(60 * 60 * 24 - 1) @@ -157,9 +191,10 @@ extension NTSolar { } // MARK: - Difference Strings + extension NTSolar { var compactDifferenceString: LocalizedStringKey { - let comparator = date.isToday ? yesterday : NTSolar(for: Date(), coordinate: self.coordinate, timeZone: timeZone, calendar: calendar) + let comparator = date.isToday ? yesterday : NTSolar(for: Date(), coordinate: coordinate, timeZone: timeZone, calendar: calendar) let difference = daylightDuration - (comparator?.daylightDuration ?? 0) let differenceString = Duration.seconds(abs(difference)).formatted(.units(maximumUnitCount: 2)) @@ -174,7 +209,7 @@ extension NTSolar { formatter.dateStyle = .medium formatter.formattingContext = .middleOfSentence - let comparator = date.isToday ? yesterday : NTSolar(for: Date(), coordinate: self.coordinate, timeZone: timeZone, calendar: calendar) + let comparator = date.isToday ? yesterday : NTSolar(for: Date(), coordinate: coordinate, timeZone: timeZone, calendar: calendar) let difference = daylightDuration - (comparator?.daylightDuration ?? 0) let differenceString = Duration.seconds(abs(difference)).formatted(.units(maximumUnitCount: 2)) @@ -189,15 +224,16 @@ extension NTSolar { baseDateString = NSLocalizedString("on \(baseDateString)", comment: "Sentence fragment for nominal date") } - let comparatorDate = comparator?.date ?? self.date + let comparatorDate = comparator?.date ?? date return LocalizedStringKey("\(differenceString) \(moreOrLess) daylight \(baseDateString) compared to \(formatter.string(from: comparatorDate))") } } // MARK: - Events + extension NTSolar { - var events: Array { + var events: [Event] { [ Event(label: "Astronomical Sunrise", date: astronomicalSunrise, phase: .astronomical), Event(label: "Nautical Sunrise", date: nauticalSunrise, phase: .nautical), @@ -209,10 +245,10 @@ extension NTSolar { Event(label: "Nautical Sunset", date: nauticalSunset, phase: .nautical), Event(label: "Astronomical Sunset", date: astronomicalSunset, phase: .astronomical), ] - .compactMap { $0 } - .sorted { a, b in - a.date.compare(b.date) == .orderedAscending - } + .compactMap { $0 } + .sorted { a, b in + a.date.compare(b.date) == .orderedAscending + } } var nextSolarEvent: Event? { @@ -238,6 +274,7 @@ extension NTSolar { } // MARK: - View Helpers + extension NTSolar { var view: some View { SkyGradient(ntSolar: self) diff --git a/Solstice/Extensions/TimeInterval++.swift b/Solstice/Extensions/TimeInterval++.swift index 3c96cd7c..d1795b88 100644 --- a/Solstice/Extensions/TimeInterval++.swift +++ b/Solstice/Extensions/TimeInterval++.swift @@ -11,10 +11,10 @@ extension TimeInterval { var localizedString: String { Duration.seconds(abs(self)).formatted(.units(maximumUnitCount: 2)) } - + var abbreviatedHourString: String { Duration.seconds(self).formatted(.units(allowed: [.hours])) } - + static let twentyFourHours: TimeInterval = 60 * 60 * 24 } diff --git a/Solstice/Extensions/TimeMachine++.swift b/Solstice/Extensions/TimeMachine++.swift index e4afdb9a..40b50c30 100644 --- a/Solstice/Extensions/TimeMachine++.swift +++ b/Solstice/Extensions/TimeMachine++.swift @@ -9,15 +9,15 @@ import Foundation import TimeMachine extension TimeMachine { - static var solsticeTimeMachine = TimeMachine(incrementUnit: .day, incrementRange: -182...182) - + static var solsticeTimeMachine = TimeMachine(incrementUnit: .day, incrementRange: -182 ... 182) + func dateLabel(context: DateFormatter.Context = .standalone) -> String { let formatter = DateFormatter() formatter.formattingContext = context formatter.doesRelativeDateFormatting = true formatter.timeStyle = .none formatter.dateStyle = .medium - + return formatter.string(from: date) } } diff --git a/Solstice/Extensions/TimeZone++.swift b/Solstice/Extensions/TimeZone++.swift index c010df5f..d69c1e24 100644 --- a/Solstice/Extensions/TimeZone++.swift +++ b/Solstice/Extensions/TimeZone++.swift @@ -10,9 +10,9 @@ import Foundation extension TimeZone { func differenceFromLocalTime(for date: Date) -> TimeInterval { let currentOffset = localTimeZone.secondsFromGMT(for: date) * -1 - return TimeInterval(self.secondsFromGMT(for: date) + currentOffset) + return TimeInterval(secondsFromGMT(for: date) + currentOffset) } - + func differenceStringFromLocalTime(for date: Date) -> String { let difference = differenceFromLocalTime(for: date) let prefix = difference >= 0 ? "+" : "" diff --git a/Solstice/Extensions/View+CapsuleAppearance.swift b/Solstice/Extensions/View+CapsuleAppearance.swift index 860fd7e1..a5497520 100644 --- a/Solstice/Extensions/View+CapsuleAppearance.swift +++ b/Solstice/Extensions/View+CapsuleAppearance.swift @@ -15,7 +15,7 @@ enum CapsuleStyle { struct CapsuleModifier: ViewModifier { var appearAsCapsule: Bool = true var capsuleStyle: CapsuleStyle = .normal - + var backgroundColor: Color { switch capsuleStyle { case .normal: @@ -24,7 +24,7 @@ struct CapsuleModifier: ViewModifier { return .accentColor } } - + var foregroundColor: Color { switch capsuleStyle { case .normal: @@ -33,14 +33,14 @@ struct CapsuleModifier: ViewModifier { return .white } } - + func body(content: Content) -> some View { content .padding(4) .padding(.horizontal, 4) .background(appearAsCapsule ? backgroundColor : .clear) .foregroundStyle(appearAsCapsule ? foregroundColor : .primary) - .cornerRadius(6) + .clipShape(.rect(cornerRadius: 6)) .padding(-4) .padding(.horizontal, -4) } diff --git a/Solstice/Extensions/View+DaylightGradientTimePreferenceKey.swift b/Solstice/Extensions/View+DaylightGradientTimePreferenceKey.swift index 11289df8..f1c8726b 100644 --- a/Solstice/Extensions/View+DaylightGradientTimePreferenceKey.swift +++ b/Solstice/Extensions/View+DaylightGradientTimePreferenceKey.swift @@ -9,9 +9,9 @@ import SwiftUI struct DaylightGradientTimePreferenceKey: PreferenceKey { typealias Value = Date - - static let defaultValue: Date = Date() - + + static let defaultValue: Date = .init() + static func reduce(value: inout Date, nextValue: () -> Date) { value = nextValue() } diff --git a/Solstice/Extensions/View+Debugging.swift b/Solstice/Extensions/View+Debugging.swift index 52c931ed..52155be4 100644 --- a/Solstice/Extensions/View+Debugging.swift +++ b/Solstice/Extensions/View+Debugging.swift @@ -11,15 +11,15 @@ import SwiftUI extension ShapeStyle where Self == Color { static var random: Color { Color( - red: .random(in: 0...1), - green: .random(in: 0...1), - blue: .random(in: 0...1) + red: .random(in: 0 ... 1), + green: .random(in: 0 ... 1), + blue: .random(in: 0 ... 1) ) } } extension View { func debugWithRandomColor() -> some View { - self.background(.random) + background(.random) } } diff --git a/Solstice/Extensions/View+EllipticalEdgeMask.swift b/Solstice/Extensions/View+EllipticalEdgeMask.swift index 111c1a2d..cfc52bcb 100644 --- a/Solstice/Extensions/View+EllipticalEdgeMask.swift +++ b/Solstice/Extensions/View+EllipticalEdgeMask.swift @@ -14,7 +14,7 @@ struct EllipticalEdgeMaskModifier: ViewModifier { EllipticalGradient( gradient: Gradient(stops: [ Gradient.Stop(color: .black, location: 0.9), - Gradient.Stop(color: .clear, location: 1.0) + Gradient.Stop(color: .clear, location: 1.0), ]) ) ) @@ -23,6 +23,6 @@ struct EllipticalEdgeMaskModifier: ViewModifier { extension View { func ellipticalEdgeMask() -> some View { - self.modifier(EllipticalEdgeMaskModifier()) + modifier(EllipticalEdgeMaskModifier()) } } diff --git a/Solstice/Extensions/View+MaterialListRowBackground.swift b/Solstice/Extensions/View+MaterialListRowBackground.swift index 374b8cdf..445147b0 100644 --- a/Solstice/Extensions/View+MaterialListRowBackground.swift +++ b/Solstice/Extensions/View+MaterialListRowBackground.swift @@ -5,25 +5,25 @@ // Created by Dan Eden on 01/07/2024. // -import SwiftUI import Suite +import SwiftUI struct MateriaListRowBackground: ViewModifier { func body(content: Content) -> some View { content - #if os(watchOS) - .listRowBackground( - Color.clear.background( - .ultraThinMaterial, - in: RoundedRectangle(cornerRadius: 12) - ) + #if os(watchOS) + .listRowBackground( + Color.clear.background( + .ultraThinMaterial, + in: RoundedRectangle(cornerRadius: 12) ) - #endif + ) + #endif } } extension View { func materialListRowBackground() -> some View { - return self.modifier(MateriaListRowBackground()) + return modifier(MateriaListRowBackground()) } } diff --git a/Solstice/Extensions/View+RectangularEdgeMask.swift b/Solstice/Extensions/View+RectangularEdgeMask.swift index bde838ca..a56baa0e 100644 --- a/Solstice/Extensions/View+RectangularEdgeMask.swift +++ b/Solstice/Extensions/View+RectangularEdgeMask.swift @@ -12,7 +12,7 @@ struct RectangularEdgeMaskModifier: ViewModifier { Gradient.Stop(color: .clear, location: 0), Gradient.Stop(color: .black, location: 0.2), Gradient.Stop(color: .black, location: 0.8), - Gradient.Stop(color: .clear, location: 1.0) + Gradient.Stop(color: .clear, location: 1.0), ] func body(content: Content) -> some View { content @@ -28,6 +28,6 @@ struct RectangularEdgeMaskModifier: ViewModifier { extension View { func rectangularEdgeMask() -> some View { - self.modifier(RectangularEdgeMaskModifier()) + modifier(RectangularEdgeMaskModifier()) } } diff --git a/Solstice/Extensions/View+backportGlassEffect.swift b/Solstice/Extensions/View+backportGlassEffect.swift index 3745bd2f..ea4f35f4 100644 --- a/Solstice/Extensions/View+backportGlassEffect.swift +++ b/Solstice/Extensions/View+backportGlassEffect.swift @@ -1,5 +1,5 @@ // -// BackportGlassEffectViewModifier.swift +// View+backportGlassEffect.swift // Solstice // // Created by Dan Eden on 16/10/2025. @@ -9,31 +9,31 @@ import SwiftUI struct BackportGlassEffectViewModifier: ViewModifier { #if WIDGET_EXTENSION - @Environment(\.widgetRenderingMode) private var widgetRenderingMode + @Environment(\.widgetRenderingMode) private var widgetRenderingMode #endif - + var shape: S - + func body(content: Content) -> some View { let fallback = content.background(.regularMaterial, in: shape).shadow(color: .black.opacity(0.1), radius: 8, y: 4) - + #if WIDGET_EXTENSION - content - .modify { content in - if widgetRenderingMode == .fullColor { - fallback - } else { - content.background(.background, in: shape) + content + .modify { content in + if widgetRenderingMode == .fullColor { + fallback + } else { + content.background(.background, in: shape) + } } - } #elseif os(visionOS) - fallback - #else - if #available(iOS 26, macOS 26, watchOS 26, *) { - content.glassEffect(in: shape) - } else { fallback - } + #else + if #available(iOS 26, macOS 26, watchOS 26, *) { + content.glassEffect(in: shape) + } else { + fallback + } #endif } } diff --git a/Solstice/Extensions/View+glassButtonStyle.swift b/Solstice/Extensions/View+glassButtonStyle.swift index 2f1f8b92..9b6070f6 100644 --- a/Solstice/Extensions/View+glassButtonStyle.swift +++ b/Solstice/Extensions/View+glassButtonStyle.swift @@ -5,10 +5,10 @@ // Created by Daniel Eden on 14/08/2025. // -import SwiftUI import Suite +import SwiftUI -internal enum BackportGlassButtonStyle { +enum BackportGlassButtonStyle { case regular, prominent } @@ -17,26 +17,26 @@ extension View { func glassButtonStyle(_ style: BackportGlassButtonStyle = .regular) -> some View { switch style { case .prominent: - self.modify { content in + modify { content in if #available(iOS 26, macOS 26, watchOS 26, *) { #if !os(visionOS) - content.buttonStyle(.glassProminent) + content.buttonStyle(.glassProminent) #else - content.buttonStyle(.borderedProminent) + content.buttonStyle(.borderedProminent) #endif } else { content.buttonStyle(.borderedProminent) } } case .regular: - self.modify { content in + modify { content in if #available(iOS 26, macOS 26, watchOS 26, *) { #if !os(visionOS) - content.buttonStyle(.glass) + content.buttonStyle(.glass) #else - content - .buttonStyle(.bordered) - .backgroundStyle(.regularMaterial) + content + .buttonStyle(.bordered) + .backgroundStyle(.regularMaterial) #endif } else { content @@ -67,7 +67,7 @@ struct MaterialButtonStyle: ButtonStyle { return 16 } } - + var font: Font { switch controlSize { case .mini: @@ -84,7 +84,7 @@ struct MaterialButtonStyle: ButtonStyle { return .body } } - + func makeBody(configuration: Configuration) -> some View { configuration.label .font(font) diff --git a/Solstice/Extensions/View+timeMachineOverlayModifier.swift b/Solstice/Extensions/View+timeMachineOverlayModifier.swift index 918fff0d..9a17ce88 100644 --- a/Solstice/Extensions/View+timeMachineOverlayModifier.swift +++ b/Solstice/Extensions/View+timeMachineOverlayModifier.swift @@ -5,8 +5,8 @@ // Created by Daniel Eden on 20/05/2025. // -import SwiftUI import Suite +import SwiftUI import TimeMachine struct TimeMachineOverlayModifier: ViewModifier { @@ -14,7 +14,7 @@ struct TimeMachineOverlayModifier: ViewModifier { @Environment(\.horizontalSizeClass) private var horizontalSizeClass @AppStorage(Preferences.timeTravelAppearance) private var timeMachineAppearance @State private var size = CGSize.zero - + func body(content: Content) -> some View { content .task(id: timeMachineAppearance) { @@ -37,17 +37,17 @@ struct TimeMachineOverlayModifier: ViewModifier { } #else .modify { content in - if #available(iOS 26, macOS 26, *) { - content - .contentMargins(.bottom, size.height, for: .automatic) - .safeAreaBar(edge: .bottom) { - overlay - .readSize($size) - } - } else { - content - .backportSafeAreaBar { - overlay + if #available(iOS 26, macOS 26, *) { + content + .contentMargins(.bottom, size.height, for: .automatic) + .safeAreaBar(edge: .bottom) { + overlay + .readSize($size) + } + } else { + content + .backportSafeAreaBar { + overlay #if os(iOS) .background { if #unavailable(iOS 26) { @@ -61,13 +61,13 @@ struct TimeMachineOverlayModifier: ViewModifier { } } #endif - } + } + } } - } - .animation(.default, value: timeMachineAppearance) + .animation(.default, value: timeMachineAppearance) #endif } - + @ViewBuilder var overlay: some View { if timeMachineAppearance != .hidden { switch timeMachineAppearance { diff --git a/Solstice/Helper Views/AdaptiveStack.swift b/Solstice/Helper Views/AdaptiveStack.swift index 7a4b9fb3..816c1621 100644 --- a/Solstice/Helper Views/AdaptiveStack.swift +++ b/Solstice/Helper Views/AdaptiveStack.swift @@ -10,25 +10,25 @@ import SwiftUI struct AdaptiveStack: View { @ViewBuilder var content: () -> B @ViewBuilder var label: () -> A - - var body: some View { - ViewThatFits { - HStack { - label() - - Spacer() - - content() - .foregroundStyle(.secondary) - } - - VStack(alignment: .leading) { - label() - content() - .foregroundStyle(.secondary) - } + + var body: some View { + ViewThatFits { + HStack { + label() + + Spacer() + + content() + .foregroundStyle(.secondary) } - } + + VStack(alignment: .leading) { + label() + content() + .foregroundStyle(.secondary) + } + } + } } #Preview { diff --git a/Solstice/Helper Views/AlignedIconLabelStyle.swift b/Solstice/Helper Views/AlignedIconLabelStyle.swift index e5622d29..606d5dc3 100644 --- a/Solstice/Helper Views/AlignedIconLabelStyle.swift +++ b/Solstice/Helper Views/AlignedIconLabelStyle.swift @@ -5,28 +5,28 @@ // Created by Daniel Eden on 18/09/2025. // -import SwiftUI import Suite +import SwiftUI @Observable -fileprivate class IconWidthObserver { +private class IconWidthObserver { static let shared = IconWidthObserver() - + var iconWidth: Double = 40 - + func updateIconWidth(_ newValue: Double) { iconWidth = max(iconWidth, newValue) } } -fileprivate extension EnvironmentValues { +private extension EnvironmentValues { @Entry var iconWidth = IconWidthObserver.shared } struct AlignedIconLabelStyle: LabelStyle { @Environment(\.iconWidth) private var iconWidthObserver: IconWidthObserver @State private var iconSize: CGSize = .zero - + func makeBody(configuration: Configuration) -> some View { HStack { HStack { @@ -40,35 +40,35 @@ struct AlignedIconLabelStyle: LabelStyle { } .frame(width: iconWidthObserver.iconWidth) .padding(.leading, -8) -#if !os(watchOS) - .alignmentGuide(.listRowSeparatorLeading) { d in - CGFloat(iconWidthObserver.iconWidth) - } -#endif - + #if !os(watchOS) + .alignmentGuide(.listRowSeparatorLeading) { _ in + CGFloat(iconWidthObserver.iconWidth) + } + #endif + configuration.title } } } -fileprivate struct AlignedIconLabelStylePreviewView: View { +private struct AlignedIconLabelStylePreviewView: View { var labels: [(Label, UUID)] = [ (Label("Label", systemImage: "heart"), UUID()), (Label("Label", systemImage: "rectangle.and.pencil.and.ellipsis"), UUID()), - (Label("Label", systemImage: "arrowshape.bounce.right"), UUID()) + (Label("Label", systemImage: "arrowshape.bounce.right"), UUID()), ] - + @State var maxWidth: Double = 0 var body: some View { List { Section { - ForEach(labels, id: \.1) { (label, _) in + ForEach(labels, id: \.1) { label, _ in label } } - + Section { - ForEach(labels, id: \.1) { (label, _) in + ForEach(labels, id: \.1) { label, _ in label } } @@ -78,5 +78,5 @@ fileprivate struct AlignedIconLabelStylePreviewView: View { } #Preview { - AlignedIconLabelStylePreviewView() + AlignedIconLabelStylePreviewView() } diff --git a/Solstice/Helper Views/AsyncButton.swift b/Solstice/Helper Views/AsyncButton.swift index 30a55c8a..dd76dd41 100644 --- a/Solstice/Helper Views/AsyncButton.swift +++ b/Solstice/Helper Views/AsyncButton.swift @@ -10,17 +10,14 @@ import SwiftUI struct AsyncButton: View { let action: () async -> Void let label: Label - - @State var isRunning = false - - init( - action: @escaping () async -> Void, - @ViewBuilder label: () -> Label - ) { + + @State private var isRunning = false + + init(action: @escaping () async -> Void, @ViewBuilder label: () -> Label) { self.action = action self.label = label() } - + var body: some View { Button { isRunning = true @@ -28,7 +25,7 @@ struct AsyncButton: View { await action() isRunning = false } - } label: { + } label: { label .if(isRunning) { content in content diff --git a/Solstice/Helper Views/CircleWithSlice.swift b/Solstice/Helper Views/CircleWithSlice.swift index 63cf44cf..8b6244fe 100644 --- a/Solstice/Helper Views/CircleWithSlice.swift +++ b/Solstice/Helper Views/CircleWithSlice.swift @@ -10,7 +10,7 @@ import SwiftUI struct CircleWithSlice: Shape { var startAngle: Double // degrees var endAngle: Double // degrees - + var animatableData: AnimatablePair { get { AnimatablePair(startAngle, endAngle) } set { @@ -18,26 +18,26 @@ struct CircleWithSlice: Shape { endAngle = newValue.second } } - + func path(in rect: CGRect) -> Path { let center = CGPoint(x: rect.midX, y: rect.midY) let radius = min(rect.width, rect.height) / 2 - + var path = Path() - + // Draw the full circle path.addEllipse(in: rect) - + // Create the slice path var slice = Path() slice.move(to: center) slice.addArc(center: center, - radius: radius, - startAngle: Angle(degrees: startAngle), - endAngle: Angle(degrees: endAngle), - clockwise: false) + radius: radius, + startAngle: Angle(degrees: startAngle), + endAngle: Angle(degrees: endAngle), + clockwise: false) slice.closeSubpath() - + // Subtract the slice from the circle path.addPath(slice, transform: .identity) path.closeSubpath() diff --git a/Solstice/Helper Views/ContentToggle.swift b/Solstice/Helper Views/ContentToggle.swift index 51c0b908..4b881447 100644 --- a/Solstice/Helper Views/ContentToggle.swift +++ b/Solstice/Helper Views/ContentToggle.swift @@ -1,26 +1,31 @@ // -// ToggledContent.swift +// ContentToggle.swift // Solstice // // Created by Daniel Eden on 15/09/2023. // -import SwiftUI import Suite +import SwiftUI struct ContentToggle: View { - @State var showToggledContent = false - @ViewBuilder var content: (Bool) -> Content - + @State private var showToggledContent: Bool + let content: (Bool) -> Content + + init(showToggledContent: Bool = false, @ViewBuilder content: @escaping (Bool) -> Content) { + _showToggledContent = State(initialValue: showToggledContent) + self.content = content + } + var body: some View { HStack { content(showToggledContent) .transition(.blurReplace) } - .animation(.default, value: showToggledContent) - .onTapGesture { - showToggledContent.toggle() - } + .animation(.default, value: showToggledContent) + .onTapGesture { + showToggledContent.toggle() + } } } diff --git a/Solstice/Helper Views/LandingView.swift b/Solstice/Helper Views/LandingView.swift index 60ec21b8..66d5c901 100644 --- a/Solstice/Helper Views/LandingView.swift +++ b/Solstice/Helper Views/LandingView.swift @@ -5,21 +5,20 @@ // Created by Daniel Eden on 14/08/2025. // -import SwiftUI import Suite +import SwiftUI -fileprivate struct SizePreferenceKey: PreferenceKey { +private struct SizePreferenceKey: PreferenceKey { static let defaultValue: Double = 0 - + static func reduce(value: inout Double, nextValue: () -> Double) { value = nextValue() } } -fileprivate extension View { +private extension View { func animateIn(active: Bool, delay: TimeInterval, speed: Double = 0.6) -> some View { - self - .opacity(active ? 1 : 0) + opacity(active ? 1 : 0) .blur(radius: active ? 0 : 8) .scaleEffect(active ? 1 : 0.8, anchor: .bottom) .offset(y: active ? 0 : 8) @@ -33,18 +32,18 @@ struct LandingView: View { @Environment(CurrentLocation.self) private var currentLocation @AppStorage(Preferences.hasCompletedOnboarding) private var hasCompletedOnboarding @State private var animate = false - - @State private var contentSize: CGSize = CGSize(width: 0, height: 200) + + @State private var contentSize: CGSize = .init(width: 0, height: 200) @State private var bottomButtonSize: CGSize = .zero - + private var isWatch: Bool { #if os(watchOS) - return true + return true #else - return false + return false #endif } - + @ViewBuilder private var bottomButtons: some View { VStack { @@ -55,7 +54,7 @@ struct LandingView: View { .blendMode(.plusLighter) .padding(.bottom) .animateIn(active: animate, delay: 1) - + Button { currentLocation.requestAccess() dismiss() @@ -68,92 +67,92 @@ struct LandingView: View { .animateIn(active: animate, delay: 1.1) } .scenePadding(.horizontal) -#if os(iOS) - .background { - VariableBlurView(direction: .blurredBottomClearTop) - .ignoresSafeArea() - } -#endif - .controlSize(.extraLarge) - .readSize($bottomButtonSize) + #if os(iOS) + .background { + VariableBlurView(direction: .blurredBottomClearTop) + .ignoresSafeArea() + } + #endif + .controlSize(.extraLarge) + .readSize($bottomButtonSize) } - + private var shouldUseCompactDisplay: Bool { dynamicTypeSize > .accessibility2 } - + @State private var solar = NTSolar(for: .now, coordinate: .proxiedToTimeZone, timeZone: .autoupdatingCurrent) private let renderTime = Date.now - - var body: some View { - ZStack { - TimelineView(.animation) { context in - SkyGradient(ntSolar: solar) - .ignoresSafeArea() - .task(id: context.date) { - solar = NTSolar( - for: renderTime.addingTimeInterval(context.date.distance(to: renderTime) * 1000), - coordinate: .proxiedToTimeZone, - timeZone: .autoupdatingCurrent - ) ?? solar - } - } - - ScrollView { - VStack(alignment: .leading, spacing: 8) { - HStack(alignment: .firstTextBaseline) { - Image(.solstice) - Text("Welcome to Solstice") - } - .font(isWatch ? .headline : .largeTitle) - .fontWeight(.semibold) - .animateIn(active: animate, delay: 0.1) - .padding(.vertical) - - Text("Solstice tells you how much daylight there is today compared to yesterday.") - .animateIn(active: animate, delay: 0.4) - Text("For savouring the minutes you have, or looking forward to the minutes you’ll gain.") - .animateIn(active: animate, delay: 0.6) + + var body: some View { + ZStack { + TimelineView(.animation) { context in + SkyGradient(ntSolar: solar) + .ignoresSafeArea() + .task(id: context.date) { + solar = NTSolar( + for: renderTime.addingTimeInterval(context.date.distance(to: renderTime) * 1000), + coordinate: .proxiedToTimeZone, + timeZone: .autoupdatingCurrent + ) ?? solar } - .font(.title3) - .scenePadding() - .foregroundStyle(.white) - .blendMode(.plusLighter) - .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4) - .readSize($contentSize) - - if shouldUseCompactDisplay { - bottomButtons + } + + ScrollView { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .firstTextBaseline) { + Image(.solstice) + Text("Welcome to Solstice") } + .font(isWatch ? .headline : .largeTitle) + .fontWeight(.semibold) + .animateIn(active: animate, delay: 0.1) + .padding(.vertical) + + Text("Solstice tells you how much daylight there is today compared to yesterday.") + .animateIn(active: animate, delay: 0.4) + Text("For savouring the minutes you have, or looking forward to the minutes you’ll gain.") + .animateIn(active: animate, delay: 0.6) } - } - .task { - animate = true - } - .backportSafeAreaBar { - if !shouldUseCompactDisplay { + .font(.title3) + .scenePadding() + .foregroundStyle(.white) + .blendMode(.plusLighter) + .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4) + .readSize($contentSize) + + if shouldUseCompactDisplay { bottomButtons } } - .onDisappear { - hasCompletedOnboarding = true + } + .task { + animate = true + } + .backportSafeAreaBar { + if !shouldUseCompactDisplay { + bottomButtons } - .preference(key: SizePreferenceKey.self, value: contentSize.height + bottomButtonSize.height) - } + } + .onDisappear { + hasCompletedOnboarding = true + } + .preference(key: SizePreferenceKey.self, value: contentSize.height + bottomButtonSize.height) + } } #Preview { - LandingView() + LandingView() } -fileprivate struct WithOnboardingViewModifier: ViewModifier { +private struct WithOnboardingViewModifier: ViewModifier { @AppStorage(Preferences.hasCompletedOnboarding) private var hasCompletedOnboarding @Environment(CurrentLocation.self) private var currentLocation - + @State private var shouldPresentOnboarding = false - + @State private var sheetSize: Double = 0 - + func body(content: Content) -> some View { content .task { @@ -172,6 +171,6 @@ fileprivate struct WithOnboardingViewModifier: ViewModifier { extension View { func withAppOnboarding() -> some View { - self.modifier(WithOnboardingViewModifier()) + modifier(WithOnboardingViewModifier()) } } diff --git a/Solstice/Helper Views/ShareSolarChartView.swift b/Solstice/Helper Views/ShareSolarChartView.swift index 0ae17035..e784b06e 100644 --- a/Solstice/Helper Views/ShareSolarChartView.swift +++ b/Solstice/Helper Views/ShareSolarChartView.swift @@ -5,31 +5,37 @@ // Created by Daniel Eden on 04/03/2025. // -import SwiftUI import Suite +import SwiftUI import TimeMachine struct ShareSolarChartView: View { @AppStorage(Preferences.chartType) private var chartType - + @Environment(\.dismiss) var dismiss @Environment(\.timeMachine) var timeMachine: TimeMachine - + @State private var selectedChartType = ChartType.classic - - var solar: NTSolar - var location: Location - - @State var chartAppearance: DaylightChart.Appearance = .graphical - @State var chartRenderedAsImage: Image? - @State var imageData: Data? - - @State var showLocationName = true - + + let solar: NTSolar + let location: Location + + @State private var chartAppearance: DaylightChart.Appearance + @State private var chartRenderedAsImage: Image? + @State private var imageData: Data? + + @State private var showLocationName = true + + init(solar: NTSolar, location: Location, chartAppearance: DaylightChart.Appearance = .graphical) { + self.solar = solar + self.location = location + _chartAppearance = State(initialValue: chartAppearance) + } + var animated: Bool { chartRenderedAsImage != nil } - + @ViewBuilder var daylightChartView: some View { switch selectedChartType { @@ -52,29 +58,28 @@ struct ShareSolarChartView: View { .padding() .frame(maxWidth: .infinity) } - } - + var deps: [AnyHashable] { [showLocationName, solar.date, location, chartAppearance, selectedChartType] } - + private let igStoriesUrl: URL? = URL(string: "instagram-stories://share?source_application=me.daneden.Solstice") - + private var displayIgShareButton: Bool { guard let igStoriesUrl else { return false } - + if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { return true } - -#if os(iOS) - return UIApplication.shared.canOpenURL(igStoriesUrl) -#else - return false -#endif + + #if os(iOS) + return UIApplication.shared.canOpenURL(igStoriesUrl) + #else + return false + #endif } - + var body: some View { NavigationStack { List { @@ -124,7 +129,7 @@ struct ShareSolarChartView: View { .pickerStyle(.segmented) #endif } - + private var chartTypePicker: some View { Picker(selection: $selectedChartType) { ForEach(ChartType.allCases, id: \.self) { appearance in @@ -165,60 +170,61 @@ struct ShareSolarChartView: View { Section { shareButtons .foregroundStyle(.tint) - #if !os(watchOS) + #if !os(watchOS) .listRowSeparator(.visible) - #endif + #endif } } @ViewBuilder private var shareButtons: some View { -#if os(iOS) - instagramShareButton -#endif + #if os(iOS) + instagramShareButton + #endif standardShareLink } -#if os(iOS) - @ViewBuilder - private var instagramShareButton: some View { - let solarGradient = SkyGradient(ntSolar: solar) - if let igStoriesUrl, - let imageData, - let topColor = solarGradient.stops.first?.toHex(), - let bottomColor = solarGradient.stops.last?.toHex(), - displayIgShareButton { - Button { - let pasteboardItems: [[String: Any]] = [[ - "com.instagram.sharedSticker.stickerImage": imageData, - "com.instagram.sharedSticker.backgroundTopColor": topColor, - "com.instagram.sharedSticker.backgroundBottomColor": bottomColor - ]] - - UIPasteboard.general.setItems(pasteboardItems, options: [.expirationDate: Date().addingTimeInterval(60 * 5)]) - UIApplication.shared.open(igStoriesUrl) - } label: { - Label { - Text("Share to Instagram Story") - } icon: { - Image(.instagram) + #if os(iOS) + @ViewBuilder + private var instagramShareButton: some View { + let solarGradient = SkyGradient(ntSolar: solar) + if let igStoriesUrl, + let imageData, + let topColor = solarGradient.stops.first?.toHex(), + let bottomColor = solarGradient.stops.last?.toHex(), + displayIgShareButton + { + Button { + let pasteboardItems: [[String: Any]] = [[ + "com.instagram.sharedSticker.stickerImage": imageData, + "com.instagram.sharedSticker.backgroundTopColor": topColor, + "com.instagram.sharedSticker.backgroundBottomColor": bottomColor, + ]] + + UIPasteboard.general.setItems(pasteboardItems, options: [.expirationDate: Date().addingTimeInterval(60 * 5)]) + UIApplication.shared.open(igStoriesUrl) + } label: { + Label { + Text("Share to Instagram Story") + } icon: { + Image(.instagram) + } } } } - } -#endif + #endif @ViewBuilder private var standardShareLink: some View { if let chartRenderedAsImage { - let title: String = location.title ?? "Current Location" + let title: String = location.title ?? String(localized: "Current Location") ShareLink( item: chartRenderedAsImage, preview: SharePreview("Daylight in \(title)", image: chartRenderedAsImage) ) } } - + func buildChartRenderedAsImage() -> Image? { @ViewBuilder var footer: some View { @@ -234,14 +240,14 @@ struct ShareSolarChartView: View { } } .font(.headline) - + let duration = solar.daylightDuration.localizedString Text("\(duration) of daylight") .foregroundStyle(.secondary) } - + Spacer() - + VStack(alignment: .trailing) { Label("\(solar.safeSunrise, style: .time)", systemImage: "sunrise") Label("\(solar.safeSunset, style: .time)", systemImage: "sunset") @@ -253,8 +259,8 @@ struct ShareSolarChartView: View { let duration = solar.daylightDuration.localizedString Text("\(duration) of daylight") .font(.headline) - - Label("\(solar.safeSunrise...solar.safeSunset)", systemImage: "sun.max") + + Label("\(solar.safeSunrise ... solar.safeSunset)", systemImage: "sun.max") .foregroundStyle(.secondary) } } @@ -265,47 +271,49 @@ struct ShareSolarChartView: View { HStack { Label("Solstice", image: "solstice") .font(.headline) - + Spacer() - + Text(Date(), style: .date) .foregroundStyle(.secondary) } - .scenePadding() - + .scenePadding() + daylightChartView - + footer .scenePadding() } - .background(in: .rect(cornerRadius: 20, style: .continuous)) - .frame(width: 420, height: 525) - + .background(in: .rect(cornerRadius: 20, style: .continuous)) + .frame(width: 420, height: 525) + let imageRenderer = ImageRenderer(content: view) imageRenderer.scale = 3 imageRenderer.isOpaque = false - -#if os(macOS) - guard let image = imageRenderer.nsImage, - let data = image.pngData(), - let nsImage = NSImage(data: data) else { - return nil - } - - imageData = data - - return Image(nsImage: nsImage) -#else - guard let image = imageRenderer.uiImage, - let data = image.pngData(), - let uiImage = UIImage(data: data) else { - return nil - } - - imageData = data - - return Image(uiImage: uiImage) -#endif + + #if os(macOS) + guard let image = imageRenderer.nsImage, + let data = image.pngData(), + let nsImage = NSImage(data: data) + else { + return nil + } + + imageData = data + + return Image(nsImage: nsImage) + #else + guard let image = imageRenderer.uiImage, + let data = image.pngData(), + let uiImage = UIImage(data: data) + else { + return nil + } + + imageData = data + + return Image(uiImage: uiImage) + #endif } } diff --git a/Solstice/Helper Views/SkyGradient.swift b/Solstice/Helper Views/SkyGradient.swift index fe5c066d..b576ff5c 100644 --- a/Solstice/Helper Views/SkyGradient.swift +++ b/Solstice/Helper Views/SkyGradient.swift @@ -5,9 +5,9 @@ // Created by Daniel Eden on 21/02/2023. // +import CoreLocation import Foundation import SwiftUI -import CoreLocation /* Date construction snippet for reference: @@ -15,11 +15,11 @@ import CoreLocation let date = DateComponents(calendar: Calendar(identifier: .gregorian), timeZone: TimeZone(identifier: "America/New_York"), year: 2021, month: 12, day: 25, hour: 10, minute: 0).date -*/ + */ struct SkyGradient: View, ShapeStyle { @Environment(\.timeZone) var timeZone - + var ntSolar: NTSolar? = nil private var effectiveSolar: NTSolar? { @@ -28,59 +28,59 @@ struct SkyGradient: View, ShapeStyle { } return NTSolar(for: .now, coordinate: .proxiedToTimeZone, timeZone: timeZone) } - + static let dawn = [ Color(red: 0.388, green: 0.435, blue: 0.643), - Color(red: 0.91, green: 0.796, blue: 0.753) + Color(red: 0.91, green: 0.796, blue: 0.753), ] - + static let morning = [ Color(red: 0.11, green: 0.573, blue: 0.824), - Color(red: 0.749, green: 0.788, blue: 0.896) + Color(red: 0.749, green: 0.788, blue: 0.896), ] - + static let noon = [ Color(red: 0.184, green: 0.6, blue: 0.9), - Color(red: 0.4, green: 0.8, blue: 0.93) + Color(red: 0.4, green: 0.8, blue: 0.93), ] - + static let afternoon = [ Color(red: 0, green: 0.353, blue: 0.655), - Color(red: 1, green: 0.942, blue: 0.854) + Color(red: 1, green: 0.942, blue: 0.854), ] - + static let evening = [ Color(red: 0.15, green: 0.261, blue: 0.49), Color(red: 0.424, green: 0.357, blue: 0.482), - Color(red: 0.753, green: 0.424, blue: 0.518) + Color(red: 0.753, green: 0.424, blue: 0.518), ] - + static let night = [ Color(red: 0.007, green: 0.03, blue: 0.17), - Color(red: 0.234, green: 0.446, blue: 0.58) + Color(red: 0.234, green: 0.446, blue: 0.58), ] - + static var colors: [[Color]] { [night, dawn, morning, noon, afternoon, evening, night] } - + var colors: [[Color]] { let duration = effectiveSolar?.daylightDuration ?? 43200 let daylightHours = Int((duration / (60 * 60)) / 2) let amColors = [Self.night, Self.dawn, Self.morning] let pmColors = [Self.afternoon, Self.evening, Self.night] - + let noon = Array(repeating: Self.noon, count: daylightHours) - + if duration <= 0 { return [Self.night, Self.dawn, Self.evening, Self.night] } else if duration >= .twentyFourHours { return [Self.dawn, Self.morning] + noon + [Self.afternoon, Self.evening] } - + return amColors + noon + pmColors } - + var stops: [Color] { // All Date values are interpreted as absolute instants in time and should be constructed for the local solar time of the location. let sunrise: Date = effectiveSolar?.safeSunrise ?? .now.startOfDay @@ -109,41 +109,41 @@ struct SkyGradient: View, ShapeStyle { return [color0, color1] } - + var body: LinearGradient { LinearGradient(colors: stops, startPoint: .top, endPoint: .bottom) } } -fileprivate struct PreviewContainer: View { +private struct PreviewContainer: View { @Environment(\.timeZone) var timeZone @State var date = Date.now - + var solars: [NTSolar] { var result = [NTSolar?]() - + for i in stride(from: 0, to: 180, by: 15) { let location = CLLocationCoordinate2D(latitude: Double(i) - 90, longitude: 0) result.append(NTSolar(for: date, coordinate: location, timeZone: timeZone)) } - + return result.compactMap { $0 } } - + var body: some View { TimelineView(.animation) { t in VStack(spacing: 0) { ForEach(solars, id: \.coordinate.latitude) { solar in ZStack { SkyGradient(ntSolar: solar) - + HStack { Text(solar.date, style: .time) .font(.largeTitle) - + Spacer() VStack { - Text(solar.safeSunrise...solar.safeSunset) + Text(solar.safeSunrise ... solar.safeSunset) Text(solar.daylightDuration.localizedString) } } diff --git a/Solstice/Helper Views/StackedButtonStyle.swift b/Solstice/Helper Views/StackedButtonStyle.swift index b871e0d8..9bf9a1c7 100644 --- a/Solstice/Helper Views/StackedButtonStyle.swift +++ b/Solstice/Helper Views/StackedButtonStyle.swift @@ -15,8 +15,6 @@ struct StackedButtonStyle: ButtonStyle { } #Preview { - Button("Share", systemImage: "square.and.arrow.up") { - - } - .buttonStyle(StackedButtonStyle()) + Button("Share", systemImage: "square.and.arrow.up") {} + .buttonStyle(StackedButtonStyle()) } diff --git a/Solstice/Helper Views/TimeMachinePanelView.swift b/Solstice/Helper Views/TimeMachinePanelView.swift index 8556547b..a1408b29 100644 --- a/Solstice/Helper Views/TimeMachinePanelView.swift +++ b/Solstice/Helper Views/TimeMachinePanelView.swift @@ -1,12 +1,12 @@ // -// TimeMachineOverlayView.swift +// TimeMachinePanelView.swift // Solstice // // Created by Daniel Eden on 20/05/2025. // -import SwiftUI import Suite +import SwiftUI import TimeMachine struct SolsticeTimeMachineView: View { @@ -27,7 +27,7 @@ struct SolsticeTimeMachineView: View { struct TimeMachinePanelView: View { @Environment(\.colorScheme) private var colorScheme - + var body: some View { VStack { SolsticeTimeMachineView() @@ -38,9 +38,9 @@ struct TimeMachinePanelView: View { .padding() .clipShape(.panel) #if os(visionOS) - .glassBackgroundEffect(in: .rect(cornerRadius: 16, style: .continuous)) + .glassBackgroundEffect(in: .rect(cornerRadius: 16, style: .continuous)) #else - .modify { content in + .modify { content in if #available(iOS 26, macOS 26, *) { content .glassEffect(in: .panel) @@ -50,8 +50,8 @@ struct TimeMachinePanelView: View { .background(.regularMaterial.shadow(.drop(color: .black.opacity(0.2), radius: 8, y: 4)), in: .panel) } } - .scenePadding(.horizontal) - .scenePadding(.top) + .scenePadding(.horizontal) + .scenePadding(.top) #endif #if os(macOS) .scenePadding(.bottom) @@ -59,16 +59,15 @@ struct TimeMachinePanelView: View { } } -fileprivate enum TimeMachineDraggableBarAlignment: Int { +private enum TimeMachineDraggableBarAlignment: Int { case leading, trailing } struct TimeMachineDraggableOverlayView: View { @Environment(\.colorScheme) private var colorScheme - - @AppStorage("timeMachineDraggableBarAlignment") fileprivate - var alignment: TimeMachineDraggableBarAlignment = .trailing - + + @AppStorage("timeMachineDraggableBarAlignment") fileprivate var alignment: TimeMachineDraggableBarAlignment = .trailing + private var resolvedAlignment: Alignment { switch alignment { case .leading: @@ -77,20 +76,22 @@ struct TimeMachineDraggableOverlayView: View { return .trailing } } - + @State private var measureScreenWidthTaskID = UUID() @State private var targetAlignment: TimeMachineDraggableBarAlignment = .trailing @State private var offset: CGSize = .zero @State private var screenSize: CGSize = .zero - - var screenWidth: Double { screenSize.width } - + + var screenWidth: Double { + screenSize.width + } + private var barWidth: Double = 393 - + private var threshold: Double { (screenWidth / 2) - (barWidth / 2) } - + var body: some View { ZStack(alignment: resolvedAlignment) { TimeMachinePanelView() @@ -98,7 +99,7 @@ struct TimeMachineDraggableOverlayView: View { .gesture(DragGesture() .onChanged { value in offset = value.translation - + switch alignment { case .leading: if offset.width >= threshold { @@ -120,19 +121,16 @@ struct TimeMachineDraggableOverlayView: View { if value.predictedEndTranslation.width >= threshold { targetAlignment = .trailing } - break case .trailing: if value.predictedEndTranslation.width * -1.0 >= threshold { targetAlignment = .leading } - break } - + offset = .zero - + alignment = targetAlignment - } - ) + }) .frame(maxWidth: barWidth) } .frame(maxWidth: .infinity, alignment: resolvedAlignment) @@ -151,36 +149,36 @@ struct TimeMachineDraggableOverlayView: View { } .animation(.snappy, value: offset) .animation(.smooth, value: alignment) -#if os(iOS) - .onRotate { _ in - measureScreenWidthTaskID = UUID() - } -#endif - .task { - targetAlignment = alignment - } + #if os(iOS) + .onRotate { _ in + measureScreenWidthTaskID = UUID() + } + #endif + .task { + targetAlignment = alignment + } } } -fileprivate struct CornerAnchorView: View { +private struct CornerAnchorView: View { var corner: TimeMachineDraggableBarAlignment var isActive = false - + var size: Double { #if os(macOS) - isActive ? 32 : 24 + isActive ? 32 : 24 #else - isActive ? 72 : 64 + isActive ? 72 : 64 #endif } - + var body: some View { Image(corner == .trailing ? .curveBottomTrailing : .curveBottomLeading) .resizable() .frame(width: size, height: size) .foregroundStyle(isActive ? AnyShapeStyle(.primary) : AnyShapeStyle(.tertiary)) - #if os(macOS) + #if os(macOS) .padding(12) - #endif + #endif } } diff --git a/Solstice/Helper Views/TimeTravelCompactView.swift b/Solstice/Helper Views/TimeTravelCompactView.swift index 670ed333..81abbdcd 100644 --- a/Solstice/Helper Views/TimeTravelCompactView.swift +++ b/Solstice/Helper Views/TimeTravelCompactView.swift @@ -5,9 +5,9 @@ // Created by Dan Eden on 23/09/2025. // +import Suite import SwiftUI import TimeMachine -import Suite extension Shape where Self == RoundedRectangle { static var panel: Self { @@ -19,7 +19,7 @@ struct TimeTravelViewOverlaySizePreferenceKey: PreferenceKey { static func reduce(value: inout Double, nextValue: () -> Double) { value = nextValue() } - + static var defaultValue = Double(0) } @@ -28,17 +28,17 @@ struct _TimeTravelCompactView: View { @AppStorage("isOffscreen") var isOffscreen = false @State private var sheetContentSize = CGSize.zero @State private var timeMachineViewSize = CGSize.zero - + var insetSize: Double { isOffscreen ? 0 : sheetContentSize.height } - + var body: some View { HStack(spacing: 16) { SolsticeTimeMachineView() .scenePadding([.leading, .vertical]) .readSize($timeMachineViewSize) - + Button { withAnimation { isOffscreen.toggle() @@ -59,19 +59,19 @@ struct _TimeTravelCompactView: View { .clipShape(.panel) .modify { #if os(visionOS) - $0 - .background(.regularMaterial, in: .panel) - .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4) - .transition(.blurReplace) - #else - if #available(iOS 26, watchOS 26, macOS 26, *) { - $0.glassEffect(.regular.interactive(), in: .panel) - } else { $0 .background(.regularMaterial, in: .panel) .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4) .transition(.blurReplace) - } + #else + if #available(iOS 26, watchOS 26, macOS 26, *) { + $0.glassEffect(.regular.interactive(), in: .panel) + } else { + $0 + .background(.regularMaterial, in: .panel) + .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4) + .transition(.blurReplace) + } #endif } .offset(x: isOffscreen ? -sheetContentSize.width + 44 : 0) @@ -86,10 +86,10 @@ struct _TimeTravelCompactView: View { struct TimeTravelCompactView: View { @Environment(\.timeMachine) private var timeMachine @State private var isExpanded = false - + var body: some View { @Bindable var timeMachine = timeMachine - + BackportGlassEffectContainer { VStack { HStack { @@ -100,19 +100,19 @@ struct TimeTravelCompactView: View { .padding(.horizontal, 4) .modify { #if os(visionOS) - $0.background(.regularMaterial, in: .capsule) - #else - if #available(iOS 26, macOS 26, watchOS 26, visionOS 26, *) { - $0.glassEffect() - } else { $0.background(.regularMaterial, in: .capsule) - } + #else + if #available(iOS 26, macOS 26, watchOS 26, visionOS 26, *) { + $0.glassEffect() + } else { + $0.background(.regularMaterial, in: .capsule) + } #endif } .font(.subheadline) - + Spacer() - + Button("Reset", systemImage: "arrow.counterclockwise") { withAnimation { timeMachine.reset() @@ -125,7 +125,7 @@ struct TimeTravelCompactView: View { } else { Spacer() } - + Button { withAnimation { isExpanded.toggle() } } label: { @@ -142,20 +142,20 @@ struct TimeTravelCompactView: View { } .controlSize(.small) .fontWeight(.medium) - + if isExpanded { HStack { Button("One week earlier", systemImage: "backward") { withAnimation { timeMachine.offset -= 7 } } .buttonStyle(CompactViewButtonStyle()) - + DatePicker(selection: $timeMachine.date.animation(), displayedComponents: .date) { Text("Time travel to date") } .labelsHidden() .frame(maxWidth: .infinity) - + Button("One week later", systemImage: "forward") { withAnimation { timeMachine.offset += 7 } } @@ -166,13 +166,13 @@ struct TimeTravelCompactView: View { .transition(.blurReplace.combined(with: .opacity)) .modify { #if os(visionOS) - $0.background(.regularMaterial, in: .capsule) - #else - if #available(iOS 26, macOS 26, watchOS 26, visionOS 26, *) { - $0.glassEffect() - } else { $0.background(.regularMaterial, in: .capsule) - } + #else + if #available(iOS 26, macOS 26, watchOS 26, visionOS 26, *) { + $0.glassEffect() + } else { + $0.background(.regularMaterial, in: .capsule) + } #endif } } @@ -191,11 +191,11 @@ struct TimeTravelCompactView: View { struct TitleToggledLabelStyle: LabelStyle { var titleVisible: Bool - + func makeBody(configuration: Configuration) -> some View { HStack { configuration.icon - + if titleVisible { configuration.title .transition(.blurReplace) @@ -218,33 +218,31 @@ struct CompactViewButtonStyle: ButtonStyle { struct BackportGlassEffectContainer: View { @ViewBuilder var content: Content - + var body: some View { #if os(visionOS) - content + content #else - if #available(iOS 26, macOS 26, watchOS 26, *) { - GlassEffectContainer { + if #available(iOS 26, macOS 26, watchOS 26, *) { + GlassEffectContainer { + content + } + } else { content } - } else { - content - } #endif } } #Preview { NavigationStack { - List { - - } - .navigationTitle("Test") - .backportSafeAreaBar { - TimeTravelCompactView() - .scenePadding(.horizontal) - .frame(maxWidth: .infinity, alignment: .trailing) - } + List {} + .navigationTitle("Test") + .backportSafeAreaBar { + TimeTravelCompactView() + .scenePadding(.horizontal) + .frame(maxWidth: .infinity, alignment: .trailing) + } } .withTimeMachine(.solsticeTimeMachine) } diff --git a/Solstice/Helper Views/VariableBlurView.swift b/Solstice/Helper Views/VariableBlurView.swift index 0c495544..dfb9d059 100644 --- a/Solstice/Helper Views/VariableBlurView.swift +++ b/Solstice/Helper Views/VariableBlurView.swift @@ -7,110 +7,104 @@ import SwiftUI #if os(iOS) -import UIKit -import CoreImage.CIFilterBuiltins -import QuartzCore - -public enum VariableBlurDirection { - case blurredTopClearBottom - case blurredBottomClearTop -} - - -public struct VariableBlurView: UIViewRepresentable { - - public var maxBlurRadius: CGFloat = 20 - - public var direction: VariableBlurDirection = .blurredTopClearBottom - - /// By default, variable blur starts from 0 blur radius and linearly increases to `maxBlurRadius`. Setting `startOffset` to a small negative coefficient (e.g. -0.1) will start blur from larger radius value which might look better in some cases. - public var startOffset: CGFloat = 0 - - public func makeUIView(context: Context) -> VariableBlurUIView { - VariableBlurUIView(maxBlurRadius: maxBlurRadius, direction: direction, startOffset: startOffset) + import CoreImage.CIFilterBuiltins + import QuartzCore + import UIKit + + public enum VariableBlurDirection { + case blurredTopClearBottom + case blurredBottomClearTop } - - public func updateUIView(_ uiView: VariableBlurUIView, context: Context) { + + public struct VariableBlurView: UIViewRepresentable { + public var maxBlurRadius: CGFloat = 20 + + public var direction: VariableBlurDirection = .blurredTopClearBottom + + /// By default, variable blur starts from 0 blur radius and linearly increases to `maxBlurRadius`. Setting `startOffset` to a small negative coefficient (e.g. -0.1) will start blur from larger radius value which might look better in some cases. + public var startOffset: CGFloat = 0 + + public func makeUIView(context _: Context) -> VariableBlurUIView { + VariableBlurUIView(maxBlurRadius: maxBlurRadius, direction: direction, startOffset: startOffset) + } + + public func updateUIView(_: VariableBlurUIView, context _: Context) {} } -} - - -/// credit https://github.com/jtrivedi/VariableBlurView -open class VariableBlurUIView: UIVisualEffectView { - - public init(maxBlurRadius: CGFloat = 20, direction: VariableBlurDirection = .blurredTopClearBottom, startOffset: CGFloat = 0) { - super.init(effect: UIBlurEffect(style: .regular)) - - // `CAFilter` is a private QuartzCore class that dynamically create using Objective-C runtime. - guard let CAFilter = NSClassFromString("CAFilter")! as? NSObject.Type else { - print("[VariableBlur] Error: Can't find CAFilter class") - return + + /// credit https://github.com/jtrivedi/VariableBlurView + open class VariableBlurUIView: UIVisualEffectView { + public init(maxBlurRadius: CGFloat = 20, direction: VariableBlurDirection = .blurredTopClearBottom, startOffset: CGFloat = 0) { + super.init(effect: UIBlurEffect(style: .regular)) + + // `CAFilter` is a private QuartzCore class that dynamically create using Objective-C runtime. + guard let CAFilter = NSClassFromString("CAFilter")! as? NSObject.Type else { + print("[VariableBlur] Error: Can't find CAFilter class") + return + } + guard let variableBlur = CAFilter.perform(NSSelectorFromString("filterWithType:"), with: "variableBlur").takeUnretainedValue() as? NSObject else { + print("[VariableBlur] Error: CAFilter can't create filterWithType: variableBlur") + return + } + + // The blur radius at each pixel depends on the alpha value of the corresponding pixel in the gradient mask. + // An alpha of 1 results in the max blur radius, while an alpha of 0 is completely unblurred. + let gradientImage = makeGradientImage(startOffset: startOffset, direction: direction) + + variableBlur.setValue(maxBlurRadius, forKey: "inputRadius") + variableBlur.setValue(gradientImage, forKey: "inputMaskImage") + variableBlur.setValue(true, forKey: "inputNormalizeEdges") + + // We use a `UIVisualEffectView` here purely to get access to its `CABackdropLayer`, + // which is able to apply various, real-time CAFilters onto the views underneath. + let backdropLayer = subviews.first?.layer + + // Replace the standard filters (i.e. `gaussianBlur`, `colorSaturate`, etc.) with only the variableBlur. + backdropLayer?.filters = [variableBlur] + + // Get rid of the visual effect view's dimming/tint view, so we don't see a hard line. + for subview in subviews.dropFirst() { + subview.alpha = 0 + } } - guard let variableBlur = CAFilter.self.perform(NSSelectorFromString("filterWithType:"), with: "variableBlur").takeUnretainedValue() as? NSObject else { - print("[VariableBlur] Error: CAFilter can't create filterWithType: variableBlur") - return + + @available(*, unavailable) + public required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") } - - // The blur radius at each pixel depends on the alpha value of the corresponding pixel in the gradient mask. - // An alpha of 1 results in the max blur radius, while an alpha of 0 is completely unblurred. - let gradientImage = makeGradientImage(startOffset: startOffset, direction: direction) - - variableBlur.setValue(maxBlurRadius, forKey: "inputRadius") - variableBlur.setValue(gradientImage, forKey: "inputMaskImage") - variableBlur.setValue(true, forKey: "inputNormalizeEdges") - - // We use a `UIVisualEffectView` here purely to get access to its `CABackdropLayer`, - // which is able to apply various, real-time CAFilters onto the views underneath. - let backdropLayer = subviews.first?.layer - - // Replace the standard filters (i.e. `gaussianBlur`, `colorSaturate`, etc.) with only the variableBlur. - backdropLayer?.filters = [variableBlur] - - // Get rid of the visual effect view's dimming/tint view, so we don't see a hard line. - for subview in subviews.dropFirst() { - subview.alpha = 0 + + override open func didMoveToWindow() { + // fixes visible pixelization at unblurred edge (https://github.com/nikstar/VariableBlur/issues/1) + guard let window, let backdropLayer = subviews.first?.layer else { return } + backdropLayer.setValue(window.screen.scale, forKey: "scale") } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - open override func didMoveToWindow() { - // fixes visible pixelization at unblurred edge (https://github.com/nikstar/VariableBlur/issues/1) - guard let window, let backdropLayer = subviews.first?.layer else { return } - backdropLayer.setValue(window.screen.scale, forKey: "scale") - } - - open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - // `super.traitCollectionDidChange(previousTraitCollection)` crashes the app - } - - private func makeGradientImage(width: CGFloat = 100, height: CGFloat = 100, startOffset: CGFloat, direction: VariableBlurDirection) -> CGImage { // much lower resolution might be acceptable - let ciGradientFilter = CIFilter.linearGradient() - // let ciGradientFilter = CIFilter.smoothLinearGradient() - ciGradientFilter.color0 = CIColor.black - ciGradientFilter.color1 = CIColor.clear - ciGradientFilter.point0 = CGPoint(x: 0, y: height) - ciGradientFilter.point1 = CGPoint(x: 0, y: startOffset * height) // small negative value looks better with vertical lines - if case .blurredBottomClearTop = direction { - ciGradientFilter.point0.y = 0 - ciGradientFilter.point1.y = height - ciGradientFilter.point1.y + + override open func traitCollectionDidChange(_: UITraitCollection?) { + // `super.traitCollectionDidChange(previousTraitCollection)` crashes the app + } + + private func makeGradientImage(width: CGFloat = 100, height: CGFloat = 100, startOffset: CGFloat, direction: VariableBlurDirection) -> CGImage { // much lower resolution might be acceptable + let ciGradientFilter = CIFilter.linearGradient() + // let ciGradientFilter = CIFilter.smoothLinearGradient() + ciGradientFilter.color0 = CIColor.black + ciGradientFilter.color1 = CIColor.clear + ciGradientFilter.point0 = CGPoint(x: 0, y: height) + ciGradientFilter.point1 = CGPoint(x: 0, y: startOffset * height) // small negative value looks better with vertical lines + if case .blurredBottomClearTop = direction { + ciGradientFilter.point0.y = 0 + ciGradientFilter.point1.y = height - ciGradientFilter.point1.y + } + return CIContext().createCGImage(ciGradientFilter.outputImage!, from: CGRect(x: 0, y: 0, width: width, height: height))! } - return CIContext().createCGImage(ciGradientFilter.outputImage!, from: CGRect(x: 0, y: 0, width: width, height: height))! } -} #else -@available(*, deprecated, message: "") -struct VariableBlurView: View { - init(args: Any...) { - - } - - var body: some View { - EmptyView() + @available(*, deprecated, message: "") + struct VariableBlurView: View { + init(args _: Any...) {} + + var body: some View { + EmptyView() + } } -} #endif diff --git a/Solstice/Helpers/AnyLocation.swift b/Solstice/Helpers/AnyLocation.swift index d5cc28da..9653d3fe 100644 --- a/Solstice/Helpers/AnyLocation.swift +++ b/Solstice/Helpers/AnyLocation.swift @@ -5,9 +5,9 @@ // Created by Daniel Eden on 28/02/2023. // -import Foundation -import CoreLocation import CoreData +import CoreLocation +import Foundation protocol AnyLocation: Hashable { var title: String? { get } @@ -17,33 +17,36 @@ protocol AnyLocation: Hashable { var longitude: Double { get } } -protocol ObservableLocation: AnyLocation, AnyObject { } +protocol ObservableLocation: AnyLocation, AnyObject {} extension AnyLocation { var timeZone: TimeZone { guard let timeZoneIdentifier, - let timeZone = TimeZone(identifier: timeZoneIdentifier) else { + let timeZone = TimeZone(identifier: timeZoneIdentifier) + else { return .autoupdatingCurrent } - + return timeZone } - + var coordinate: CLLocationCoordinate2D { CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } } -extension SavedLocation: ObservableLocation { } +extension SavedLocation: ObservableLocation {} + +/// An immutable, in-memory location used before a search result is saved. +/// Its values are fixed at construction, so it needs no observation or actor +/// isolation and composes with the nonisolated `AnyLocation` protocol. +final class TemporaryLocation: ObservableLocation { + let title: String? + let subtitle: String? + let timeZoneIdentifier: String? + let latitude: Double + let longitude: Double -@Observable -class TemporaryLocation: ObservableLocation { - var title: String? - var subtitle: String? - var timeZoneIdentifier: String? - var latitude: Double = 0.0 - var longitude: Double = 0.0 - init(title: String?, subtitle: String?, timeZoneIdentifier: String?, latitude: Double, longitude: Double) { self.title = title self.subtitle = subtitle @@ -51,7 +54,7 @@ class TemporaryLocation: ObservableLocation { self.latitude = latitude self.longitude = longitude } - + func saveLocation(to context: NSManagedObjectContext) throws -> UUID? { let savedLocation = SavedLocation(context: context) savedLocation.title = title @@ -69,19 +72,18 @@ extension TemporaryLocation: Hashable, Equatable { static func == (lhs: TemporaryLocation, rhs: TemporaryLocation) -> Bool { lhs.hashValue == rhs.hashValue } - + func hash(into hasher: inout Hasher) { hasher.combine(latitude) hasher.combine(longitude) } } - extension TemporaryLocation { static var placeholderLondon: TemporaryLocation { return TemporaryLocation(title: "London", subtitle: "England", timeZoneIdentifier: "GMT", latitude: 51.5072, longitude: -0.1276) } - + static var placeholderGreenland: TemporaryLocation { return TemporaryLocation(title: "Greenland", subtitle: nil, timeZoneIdentifier: "WGT", latitude: 74.7277, longitude: -41.3450) } diff --git a/Solstice/Helpers/AppChangeMigrator.swift b/Solstice/Helpers/AppChangeMigrator.swift index 8f68d046..0f77d1a5 100644 --- a/Solstice/Helpers/AppChangeMigrator.swift +++ b/Solstice/Helpers/AppChangeMigrator.swift @@ -5,14 +5,14 @@ // Created by Daniel Eden on 19/06/2024. // -import SwiftUI import CoreData +import SwiftUI struct AppChangeMigrator: ViewModifier { @Environment(\.managedObjectContext) var modelContext @AppStorage(Preferences.NotificationSettings._notificationTime) var notificationTime @AppStorage(Preferences.NotificationSettings.notificationDateComponents) var notificationDateComponents - + func body(content: Content) -> some View { content .task(id: "Notification schedule strategy migration") { @@ -24,7 +24,7 @@ struct AppChangeMigrator: ViewModifier { do { let fetchRequest = SavedLocation.fetchRequest() let currentData = try modelContext.fetch(fetchRequest) - + if currentData.isEmpty { for entry in SavedLocation.defaultData { let newRecord = SavedLocation(context: modelContext) @@ -34,12 +34,13 @@ struct AppChangeMigrator: ViewModifier { newRecord.latitude = entry.latitude newRecord.longitude = entry.longitude newRecord.timeZoneIdentifier = entry.timeZoneIdentifier - + modelContext.insert(newRecord) try modelContext.save() } } else if let newYorkStateEntry = currentData.first(where: { $0.uuid?.uuidString == SavedLocation.nycUUIDString }), - let newYorkCityEntry = SavedLocation.defaultData.first(where: { $0.uuid?.uuidString == SavedLocation.nycUUIDString }){ + let newYorkCityEntry = SavedLocation.defaultData.first(where: { $0.uuid?.uuidString == SavedLocation.nycUUIDString }) + { newYorkStateEntry.title = newYorkCityEntry.title newYorkStateEntry.subtitle = newYorkCityEntry.subtitle newYorkStateEntry.latitude = newYorkCityEntry.latitude @@ -55,6 +56,6 @@ struct AppChangeMigrator: ViewModifier { extension View { func migrateAppFeatures() -> some View { - self.modifier(AppChangeMigrator()) + modifier(AppChangeMigrator()) } } diff --git a/Solstice/Helpers/CurrentLocation.swift b/Solstice/Helpers/CurrentLocation.swift index 22751854..420e6df2 100644 --- a/Solstice/Helpers/CurrentLocation.swift +++ b/Solstice/Helpers/CurrentLocation.swift @@ -11,7 +11,7 @@ import SwiftUI @Observable class CurrentLocation: NSObject, CLLocationManagerDelegate { private(set) var placemark: CLPlacemark? - + private(set) var location: CLLocation? { didSet { Task { @@ -36,21 +36,21 @@ class CurrentLocation: NSObject, CLLocationManagerDelegate { LocationAppGroupCache.write(locationData) } - + @ObservationIgnored private let locationManager = CLLocationManager() @ObservationIgnored private let geocoder = CLGeocoder() - + override init() { super.init() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyReduced } - + func requestAccess() { locationManager.requestWhenInUseAuthorization() } - + func requestLocation() { guard isAuthorized else { return } Task { @@ -61,11 +61,11 @@ class CurrentLocation: NSObject, CLLocationManagerDelegate { } } } - + func requestLocationFromLiveUpdates() async throws { let updates = CLLocationUpdate.liveUpdates() for try await update in updates { - self.location = update.location + location = update.location } } @@ -83,29 +83,30 @@ class CurrentLocation: NSObject, CLLocationManagerDelegate { } // MARK: Location update request methods and handlers + extension CurrentLocation { @MainActor func processLocation(_ location: CLLocation?) async { guard let location else { return } - + let reverseGeocoded = try? await geocoder.reverseGeocodeLocation(location) if let firstResult = reverseGeocoded?.first { placemark = firstResult } } - + var authorizationStatus: CLAuthorizationStatus { locationManager.authorizationStatus } - + var isAuthorized: Bool { switch authorizationStatus { case .authorizedAlways, .authorizedWhenInUse: return true default: return false } } - - func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - if location == nil && isAuthorized { + + func locationManagerDidChangeAuthorization(_: CLLocationManager) { + if location == nil, isAuthorized { Task { do { try await requestLocationFromLiveUpdates() @@ -119,30 +120,47 @@ extension CurrentLocation { extension CurrentLocation { // MARK: Fallback location request code + func legacyRequestLocation() { locationManager.startUpdatingLocation() } - + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } self.location = location manager.stopUpdatingLocation() } - - func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) { + + func locationManager(_: CLLocationManager, didFailWithError error: any Error) { print("Error with location manager delegate: \(error.localizedDescription)") } } extension CurrentLocation: ObservableLocation { - var title: String? { placemark?.locality } - var subtitle: String? { placemark?.country } - var timeZoneIdentifier: String? { placemark?.timeZone?.identifier } - var latitude: Double { location?.coordinate.latitude ?? 0 } - var longitude: Double { location?.coordinate.longitude ?? 0 } + var title: String? { + placemark?.locality + } + + var subtitle: String? { + placemark?.country + } + + var timeZoneIdentifier: String? { + placemark?.timeZone?.identifier + } + + var latitude: Double { + location?.coordinate.latitude ?? 0 + } + + var longitude: Double { + location?.coordinate.longitude ?? 0 + } } extension CurrentLocation: Identifiable { static let identifier = "currentLocation" - var id: String { CurrentLocation.identifier } + var id: String { + CurrentLocation.identifier + } } diff --git a/Solstice/Helpers/DeduplicateLocationRecordsModifier.swift b/Solstice/Helpers/DeduplicateLocationRecordsModifier.swift index 82e9d766..ee254131 100644 --- a/Solstice/Helpers/DeduplicateLocationRecordsModifier.swift +++ b/Solstice/Helpers/DeduplicateLocationRecordsModifier.swift @@ -5,14 +5,14 @@ // Created by Daniel Eden on 14/08/2025. // -import SwiftUI import CoreData import CoreLocation +import SwiftUI struct DeduplicateLocationRecordsModifier: ViewModifier { @Environment(\.managedObjectContext) private var context @FetchRequest(sortDescriptors: []) private var locations: FetchedResults - + func body(content: Content) -> some View { content .task(id: locations.count) { @@ -27,7 +27,7 @@ struct DeduplicateLocationRecordsModifier: ViewModifier { } } } - + // Phase 2: Deduplicate by name and proximity (within 1km) var kept: [(title: String?, coordinate: CLLocationCoordinate2D)] = [] for location in locations where !context.deletedObjects.contains(location) { @@ -43,7 +43,7 @@ struct DeduplicateLocationRecordsModifier: ViewModifier { kept.append((title, coord)) } } - + if context.hasChanges { do { try context.save() @@ -57,6 +57,6 @@ struct DeduplicateLocationRecordsModifier: ViewModifier { extension View { func deduplicateLocationRecords() -> some View { - self.modifier(DeduplicateLocationRecordsModifier()) + modifier(DeduplicateLocationRecordsModifier()) } } diff --git a/Solstice/Helpers/DeepLinkResolver.swift b/Solstice/Helpers/DeepLinkResolver.swift index 12852c8b..2b5d4418 100644 --- a/Solstice/Helpers/DeepLinkResolver.swift +++ b/Solstice/Helpers/DeepLinkResolver.swift @@ -5,29 +5,30 @@ // Created by Daniel Eden on 28/06/2024. // -import SwiftUI import CoreLocation +import SwiftUI struct DeepLinkResolver: ViewModifier { @SceneStorage("selectedLocation") var selectedLocation: String? @Environment(LocationSearchService.self) var searchService var items: [SavedLocation] = [] - + func body(content: Content) -> some View { content .onOpenURL { url in guard let host = url.host(), host == "location" else { return } - + switch url.lastPathComponent { case CurrentLocation.identifier: withAnimation { selectedLocation = CurrentLocation.identifier } case "coordinates": guard let queryItems = URLComponents(string: url.absoluteString)?.queryItems, - let lat = queryItems.first(where: { $0.name == "lat" })?.value, - let lon = queryItems.first(where: { $0.name == "lon" })?.value, - let latitude = Double(lat), - let longitude = Double(lon) else { + let lat = queryItems.first(where: { $0.name == "lat" })?.value, + let lon = queryItems.first(where: { $0.name == "lon" })?.value, + let latitude = Double(lat), + let longitude = Double(lon) + else { return } @@ -35,24 +36,25 @@ struct DeepLinkResolver: ViewModifier { let location = CLLocation(latitude: item.latitude, longitude: item.longitude) return location.distance(from: CLLocation(latitude: latitude, longitude: longitude)) <= 10000 }) - + if let closestLocation { withAnimation { selectedLocation = closestLocation.uuid?.uuidString } } else { guard let name = queryItems.first(where: { $0.name == "name" })?.value, - let subtitle = queryItems.first(where: { $0.name == "subtitle" })?.value, - let timeZoneIdentifier = queryItems.first(where: { $0.name == "timeZoneIdentifier" })?.value else { + let subtitle = queryItems.first(where: { $0.name == "subtitle" })?.value, + let timeZoneIdentifier = queryItems.first(where: { $0.name == "timeZoneIdentifier" })?.value + else { return } - + let location = TemporaryLocation(title: name, - subtitle: subtitle, - timeZoneIdentifier: timeZoneIdentifier, - latitude: latitude, - longitude: longitude) - + subtitle: subtitle, + timeZoneIdentifier: timeZoneIdentifier, + latitude: latitude, + longitude: longitude) + searchService.location = location } default: return @@ -63,6 +65,6 @@ struct DeepLinkResolver: ViewModifier { extension View { func resolveDeepLink(_ locations: [SavedLocation] = []) -> some View { - return self.modifier(DeepLinkResolver(items: locations)) + return modifier(DeepLinkResolver(items: locations)) } } diff --git a/Solstice/Helpers/DeviceRotationViewModifier.swift b/Solstice/Helpers/DeviceRotationViewModifier.swift index 6fa076d0..7c4bf67c 100644 --- a/Solstice/Helpers/DeviceRotationViewModifier.swift +++ b/Solstice/Helpers/DeviceRotationViewModifier.swift @@ -7,24 +7,24 @@ import SwiftUI #if os(iOS) -import UIKit + import UIKit -struct DeviceRotationViewModifier: ViewModifier { - let action: (UIDeviceOrientation) -> Void - - func body(content: Content) -> some View { - content - .onAppear() - .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in - action(UIDevice.current.orientation) - } + struct DeviceRotationViewModifier: ViewModifier { + let action: (UIDeviceOrientation) -> Void + + func body(content: Content) -> some View { + content + .onAppear() + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + action(UIDevice.current.orientation) + } + } } -} -// A View wrapper to make the modifier easier to use -extension View { - func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View { - self.modifier(DeviceRotationViewModifier(action: action)) + /// A View wrapper to make the modifier easier to use + extension View { + func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View { + modifier(DeviceRotationViewModifier(action: action)) + } } -} #endif diff --git a/Solstice/Helpers/Globals.swift b/Solstice/Helpers/Globals.swift index e41fc457..496e44c9 100644 --- a/Solstice/Helpers/Globals.swift +++ b/Solstice/Helpers/Globals.swift @@ -8,26 +8,27 @@ import Foundation var chartHeight: CGFloat = { -#if !os(watchOS) - 360 -#else - 200 -#endif + #if !os(watchOS) + 360 + #else + 200 + #endif }() var chartMarkSize: Double = { -#if os(watchOS) - 4 -#else - 6 -#endif + #if os(watchOS) + 4 + #else + 6 + #endif }() -let calendar = Calendar.autoupdatingCurrent +let calendar = Calendar.autoupdatingCurrent let localTimeZone = TimeZone.ReferenceType.local // MARK: - Unified Location Data + /// A unified struct for encoding/decoding location data across all targets. /// Used for App Group cache, AppIntents entity IDs, and JSON serialization. struct LocationData: Codable, AnyLocation { @@ -40,6 +41,7 @@ struct LocationData: Codable, AnyLocation { } // MARK: - Location App Group Cache + enum LocationAppGroupCache { struct CachedData { var location: LocationData @@ -88,6 +90,7 @@ enum LocationAppGroupCache { } // MARK: - App Constants + enum Constants { /// App Group identifier for sharing data between app and extensions static let appGroupIdentifier = "group.me.daneden.Solstice" diff --git a/Solstice/Helpers/LocationSearchService.swift b/Solstice/Helpers/LocationSearchService.swift index 7a78a566..ffcafbd6 100644 --- a/Solstice/Helpers/LocationSearchService.swift +++ b/Solstice/Helpers/LocationSearchService.swift @@ -5,77 +5,84 @@ // Created by Daniel Eden on 02/10/2022. // +import CoreLocation import Foundation -import SwiftUI import MapKit -import CoreLocation +import SwiftUI #if !os(watchOS) -@Observable -class LocationSearchService: NSObject, MKLocalSearchCompleterDelegate { - static let shared = LocationSearchService() + @MainActor + @Observable + class LocationSearchService: NSObject, MKLocalSearchCompleterDelegate { + static let shared = LocationSearchService() - enum LocationStatus: Equatable { - case idle, noResults, isSearching, result - case error(String) - } - - var queryFragment = "" { - didSet { - handleQueryChange() + enum LocationStatus: Equatable { + case idle, noResults, isSearching, result + case error(String) } - } - private(set) var status: LocationStatus = .idle - private(set) var searchResults: [MKLocalSearchCompletion] = [] - var location: TemporaryLocation? - @ObservationIgnored private var searchTask: Task? - @ObservationIgnored private let searchCompleter: MKLocalSearchCompleter + var queryFragment = "" { + didSet { + handleQueryChange() + } + } - override init() { - searchCompleter = MKLocalSearchCompleter() - super.init() - searchCompleter.delegate = self - searchCompleter.region = MKCoordinateRegion(.world) - searchCompleter.resultTypes = [.address, .pointOfInterest] - searchCompleter.pointOfInterestFilter = .init(including: [.airport, .nationalPark]) - } + private(set) var status: LocationStatus = .idle + private(set) var searchResults: [MKLocalSearchCompletion] = [] + var location: TemporaryLocation? - private func handleQueryChange() { - searchTask?.cancel() - status = .isSearching + @ObservationIgnored private var searchTask: Task? + @ObservationIgnored private let searchCompleter: MKLocalSearchCompleter - guard !queryFragment.isEmpty else { - status = .idle - searchResults = [] - return + override init() { + searchCompleter = MKLocalSearchCompleter() + super.init() + searchCompleter.delegate = self + searchCompleter.region = MKCoordinateRegion(.world) + searchCompleter.resultTypes = [.address, .pointOfInterest] + searchCompleter.pointOfInterestFilter = .init(including: [.airport, .nationalPark]) } - searchTask = Task { @MainActor in - try? await Task.sleep(for: .milliseconds(250)) - guard !Task.isCancelled else { return } - searchCompleter.queryFragment = queryFragment - } - } + private func handleQueryChange() { + searchTask?.cancel() + status = .isSearching - func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { - searchResults = completer.results.filter { result in - guard result.title.contains(",") || !result.subtitle.isEmpty else { return false } - guard !result.subtitle.contains("Nearby") else { return false } - return true + guard !queryFragment.isEmpty else { + status = .idle + searchResults = [] + return + } + + searchTask = Task { @MainActor in + try? await Task.sleep(for: .milliseconds(250)) + guard !Task.isCancelled else { return } + searchCompleter.queryFragment = queryFragment + } } - status = completer.results.isEmpty ? .noResults : .result - } + nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { + MainActor.assumeIsolated { + searchResults = completer.results.filter { result in + guard result.title.contains(",") || !result.subtitle.isEmpty else { return false } + guard !result.subtitle.contains("Nearby") else { return false } + return true + } - func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { - status = .error(error.localizedDescription) + status = completer.results.isEmpty ? .noResults : .result + } + } + + nonisolated func completer(_: MKLocalSearchCompleter, didFailWithError error: Error) { + MainActor.assumeIsolated { + status = .error(error.localizedDescription) + } + } } -} #else -@Observable -class LocationSearchService { - static var shared = LocationSearchService() - var location: TemporaryLocation? -} + @MainActor + @Observable + class LocationSearchService { + static var shared = LocationSearchService() + var location: TemporaryLocation? + } #endif diff --git a/Solstice/Helpers/NTSolar.swift b/Solstice/Helpers/NTSolar.swift index 6c91fff2..4bdd7dab 100644 --- a/Solstice/Helpers/NTSolar.swift +++ b/Solstice/Helpers/NTSolar.swift @@ -17,667 +17,689 @@ // Released to the public domain by Performance Champions, Inc., May 2019 // -import Foundation import CoreLocation +import Foundation /// Struct to calculate sunrise, sunset, twilights, and solar noon. /// /// C code originally from: http://stjarnhimlen.se/comp/sunriset.c struct NTSolar { + // MARK: - Public Swift Interface + + let date: Date + let coordinate: CLLocationCoordinate2D + let timeZone: TimeZone + let calendar: Calendar + + let sunrise: Date? + let sunset: Date? + let civilSunrise: Date? + let civilSunset: Date? + let nauticalSunrise: Date? + let nauticalSunset: Date? + let astronomicalSunrise: Date? + let astronomicalSunset: Date? + let solarNoon: Date? + + /// Initialize solar calculations for a given date, coordinate, and time zone. + /// + /// - Parameters: + /// - date: The date for the calculation. The local calendar day in `timeZone` is used. + /// - coordinate: The latitude and longitude for the calculation. + /// - timeZone: The time zone for the resulting date and times. + /// - calendar: The calendar to use for date calculations. + init?(for date: Date, + coordinate: CLLocationCoordinate2D, + timeZone: TimeZone = .autoupdatingCurrent, + calendar: Calendar = Calendar(identifier: .gregorian)) + { + var calendar = calendar + calendar.timeZone = timeZone + var components = calendar.dateComponents([.day, .year, .month], from: date) + components.calendar = calendar + + guard let year = components.year, + let month = components.month, + let day = components.day + else { + return nil + } + + self.date = date + self.coordinate = coordinate + self.timeZone = timeZone + self.calendar = calendar + + let sunriseSet = NTSolar.sun_rise_set(year: year, + month: month, + day: day, + lon: coordinate.longitude, + lat: coordinate.latitude) + + let civil = NTSolar.civil_twilight(year: year, + month: month, + day: day, + lon: coordinate.longitude, + lat: coordinate.latitude) + + let nautical = NTSolar.nautical_twilight(year: year, + month: month, + day: day, + lon: coordinate.longitude, + lat: coordinate.latitude) + + let astronomical = NTSolar.astronomical_twilight(year: year, + month: month, + day: day, + lon: coordinate.longitude, + lat: coordinate.latitude) + + sunrise = NTSolar.dateFromUTCHours(sunriseSet.trise, + year: year, + month: month, + day: day, + calendar: calendar, + shouldReturn: sunriseSet.code == .RiseAndSet) + + sunset = NTSolar.dateFromUTCHours(sunriseSet.tset, + year: year, + month: month, + day: day, + calendar: calendar, + shouldReturn: sunriseSet.code == .RiseAndSet) + + civilSunrise = NTSolar.dateFromUTCHours(civil.trise, + year: year, + month: month, + day: day, + calendar: calendar, + shouldReturn: civil.code == .RiseAndSet) + + civilSunset = NTSolar.dateFromUTCHours(civil.tset, + year: year, + month: month, + day: day, + calendar: calendar, + shouldReturn: civil.code == .RiseAndSet) + + nauticalSunrise = NTSolar.dateFromUTCHours(nautical.trise, + year: year, + month: month, + day: day, + calendar: calendar, + shouldReturn: nautical.code == .RiseAndSet) + + nauticalSunset = NTSolar.dateFromUTCHours(nautical.tset, + year: year, + month: month, + day: day, + calendar: calendar, + shouldReturn: nautical.code == .RiseAndSet) + + astronomicalSunrise = NTSolar.dateFromUTCHours(astronomical.trise, + year: year, + month: month, + day: day, + calendar: calendar, + shouldReturn: astronomical.code == .RiseAndSet) + + astronomicalSunset = NTSolar.dateFromUTCHours(astronomical.tset, + year: year, + month: month, + day: day, + calendar: calendar, + shouldReturn: astronomical.code == .RiseAndSet) + + let solarNoonUTC = (sunriseSet.trise + sunriseSet.tset) / 2 + solarNoon = NTSolar.dateFromUTCHours(solarNoonUTC, + year: year, + month: month, + day: day, + calendar: calendar, + shouldReturn: true) + } + + /// Calculate the sun rise and set times for a given date/time zone. + /// + /// - Parameters: + /// - forDate: The date for the calculation. You should ensure that the date, which is stored as UTC, is the date + /// really wanted in the given time zone. It will be converted to the given time zone before being used. + /// - atLocation: The latitude and longitude for the calculation. + /// - inTimeZone: The time zone for the resulting date and times. + /// - Returns: If the sun both rises and sets on the day requested and at the + /// latitude requested then return sun rise and set times rounded down to the minute, nil otherwise. + static func sunRiseAndSet(forDate: Date, + atLocation: CLLocationCoordinate2D, + inTimeZone: TimeZone) -> (sunrise: Date, sunset: Date)? + { + guard let solar = NTSolar(for: forDate, coordinate: atLocation, timeZone: inTimeZone) else { + return nil + } + + guard let sunrise = solar.sunrise, + let sunset = solar.sunset + else { + return nil + } + + return (sunrise, sunset) + } + + // MARK: - Date Helpers + + private static func dateFromUTCHours(_ utcHours: Double, + year: Int, + month: Int, + day: Int, + calendar: Calendar, + shouldReturn: Bool) -> Date? + { + guard shouldReturn else { return nil } + + var utcHours = utcHours + var dayOffset = 0 + while utcHours < 0 { + utcHours += 24.0 + dayOffset -= 1 + } + while utcHours >= 24.0 { + utcHours -= 24.0 + dayOffset += 1 + } + + var hour = Int(utcHours) + var minute = Int(((utcHours - Double(hour)) * 60.0).rounded()) + + if minute == 60 { + minute = 0 + hour += 1 + if hour == 24 { + hour = 0 + dayOffset += 1 + } + } + + var utcCalendar = calendar + utcCalendar.timeZone = TimeZone(secondsFromGMT: 0) ?? .gmt + + var baseComponents = DateComponents() + baseComponents.calendar = utcCalendar + baseComponents.year = year + baseComponents.month = month + baseComponents.day = day + baseComponents.hour = 0 + baseComponents.minute = 0 + baseComponents.second = 0 + + guard let baseDate = utcCalendar.date(from: baseComponents) else { + return nil + } + + guard let adjustedDay = utcCalendar.date(byAdding: .day, value: dayOffset, to: baseDate) else { + return nil + } + + var timeComponents = DateComponents() + timeComponents.hour = hour + timeComponents.minute = minute + timeComponents.second = 0 + + guard let finalDate = utcCalendar.date(byAdding: timeComponents, to: adjustedDay) else { + return nil + } + + return finalDate + } + + // MARK: - SUNRISET.C + + // C code originally from: http://stjarnhimlen.se/comp/sunriset.c + // + // The conversion process removed pointers in favor of return tuples, converted macros to function calls, + // added return code enum, and converted + // comments to work with Xcode. Of course the C code had to be converted to Swift, but that was minimal. + // + // As much as possible the original code was left intact in order to not introduce bugs. + // In other words, some code was left unfashionable by today's standards. + + enum ReturnCode: Int { + /// Sun is below the specified "horizon" 24 hours + /// "Day" length = 0 hours, trise and tset are + /// both set to the time when the sun is at south. + case SunAlwaysBelow = -1 + + /// Sun rises/sets this day, times stored at rise and set. + case RiseAndSet = 0 + + /// Sun above the specified "horizon" 24 hours. + /// trise set to time when the sun is at south, + /// minus 12 hours while tset is set to the south + /// time plus 12 hours. "Day" length = 24 hours + case SunAlwaysAbove = 1 + } + + /* +++Date last modified: 05-Jul-1997 */ + /* Updated comments, 05-Aug-2013 */ + /* + SUNRISET.C - computes Sun rise/set times, start/end of twilight, and + the length of the day at any date and latitude + + Written as DAYLEN.C, 1989-08-16 + + Modified to SUNRISET.C, 1992-12-01 + + (c) Paul Schlyter, 1989, 1992 + + Released to the public domain by Paul Schlyter, December 1992 + */ + + /// A macro to compute the number of days elapsed since 2000 Jan 0.0 + /// (which is equal to 1999 Dec 31, 0h UT) + private static func days_since_2000_Jan_0(y: Int, m: Int, d: Int) -> Int { + return 367 * y - ((7 * (y + ((m + 9) / 12))) / 4) + ((275 * m) / 9) + d - 730_530 + } + + /* Some conversion factors between radians and degrees */ + private static let PI = 3.1415926535897932384 + private static let RADEG = (180.0 / PI) + private static let DEGRAD = (PI / 180.0) + + /** The trigonometric functions in degrees */ + private static func sind(x: Double) -> Double { + return sin(x * DEGRAD) + } + + private static func cosd(x: Double) -> Double { + return cos(x * DEGRAD) + } + + private static func tand(x: Double) -> Double { + return tan(x * DEGRAD) + } + + private static func atand(x: Double) -> Double { + return RADEG * atan(x) + } + + private static func asind(x: Double) -> Double { + return RADEG * asin(x) + } + + private static func acosd(x: Double) -> Double { + return RADEG * acos(x) + } + + private static func atan2d(y: Double, x: Double) -> Double { + return RADEG * atan2(y, x) + } + + /* Following are some macros around the "workhorse" function __daylen__ */ + /* They mainly fill in the desired values for the reference altitude */ + /* below the horizon, and also selects whether this altitude should */ + /* refer to the Sun's center or its upper limb. */ + + /** This macro computes the length of the day, from sunrise to sunset. */ + /** Sunrise/set is considered to occur when the Sun's upper limb is */ + /** 35 arc minutes below the horizon (this accounts for the refraction */ + /** of the Earth's atmosphere). */ + static func day_length(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> Double { + return daylen(year: year, month: month, day: day, lon: lon, lat: lat, altit: -35.0 / 60.0, upper_limb: 1) + } + + /** This macro computes the length of the day, including civil twilight. */ + /** Civil twilight starts/ends when the Sun's center is 6 degrees below */ + /** the horizon. */ + static func day_civil_twilight_length(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> Double { + return daylen(year: year, month: month, day: day, lon: lon, lat: lat, altit: -6.0, upper_limb: 0) + } + + /** This macro computes the length of the day, incl. nautical twilight. */ + /** Nautical twilight starts/ends when the Sun's center is 12 degrees */ + /** below the horizon. */ + static func day_nautical_twilight_length(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> Double { + return daylen(year: year, month: month, day: day, lon: lon, lat: lat, altit: -12.0, upper_limb: 0) + } + + /** This macro computes the length of the day, incl. astronomical twilight. */ + /** Astronomical twilight starts/ends when the Sun's center is 18 degrees */ + /** below the horizon. */ + static func day_astronomical_twilight_length(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> Double { + return daylen(year: year, month: month, day: day, lon: lon, lat: lat, altit: -18.0, upper_limb: 0) + } + + /** This macro computes times for sunrise/sunset. */ + /** Sunrise/set is considered to occur when the Sun's upper limb is */ + /** 35 arc minutes below the horizon (this accounts for the refraction */ + /** of the Earth's atmosphere). */ + static func sun_rise_set(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> (trise: Double, tset: Double, code: ReturnCode) { + let (start, end, code) = sunriset(year: year, month: month, day: day, lon: lon, lat: lat, altit: -35.0 / 60.0, upper_limb: 1) + return (start, end, code) + } + + /** This macro computes the start and end times of civil twilight. */ + /** Civil twilight starts/ends when the Sun's center is 6 degrees below */ + /** the horizon. */ + static func civil_twilight(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> (trise: Double, tset: Double, code: ReturnCode) { + let (start, end, code) = sunriset(year: year, month: month, day: day, lon: lon, lat: lat, altit: -6.0, upper_limb: 0) + return (start, end, code) + } + + /** This macro computes the start and end times of nautical twilight. */ + /** Nautical twilight starts/ends when the Sun's center is 12 degrees */ + /** below the horizon. */ + static func nautical_twilight(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> (trise: Double, tset: Double, code: ReturnCode) { + let (start, end, code) = sunriset(year: year, month: month, day: day, lon: lon, lat: lat, altit: -12.0, upper_limb: 0) + return (start, end, code) + } + + /** This macro computes the start and end times of astronomical twilight. */ + /** Astronomical twilight starts/ends when the Sun's center is 18 degrees */ + /** below the horizon. */ + static func astronomical_twilight(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> (trise: Double, tset: Double, code: ReturnCode) { + let (start, end, code) = sunriset(year: year, month: month, day: day, lon: lon, lat: lat, altit: -18.0, upper_limb: 0) + return (start, end, code) + } - // MARK: - Public Swift Interface - - let date: Date - let coordinate: CLLocationCoordinate2D - let timeZone: TimeZone - let calendar: Calendar - - let sunrise: Date? - let sunset: Date? - let civilSunrise: Date? - let civilSunset: Date? - let nauticalSunrise: Date? - let nauticalSunset: Date? - let astronomicalSunrise: Date? - let astronomicalSunset: Date? - let solarNoon: Date? - - /// Initialize solar calculations for a given date, coordinate, and time zone. - /// - /// - Parameters: - /// - date: The date for the calculation. The local calendar day in `timeZone` is used. - /// - coordinate: The latitude and longitude for the calculation. - /// - timeZone: The time zone for the resulting date and times. - /// - calendar: The calendar to use for date calculations. - init?(for date: Date, - coordinate: CLLocationCoordinate2D, - timeZone: TimeZone = .autoupdatingCurrent, - calendar: Calendar = Calendar(identifier: .gregorian)) { - var calendar = calendar - calendar.timeZone = timeZone - var components = calendar.dateComponents([.day, .year, .month], from: date) - components.calendar = calendar - - guard let year = components.year, - let month = components.month, - let day = components.day else { - return nil - } - - self.date = date - self.coordinate = coordinate - self.timeZone = timeZone - self.calendar = calendar - - let sunriseSet = NTSolar.sun_rise_set(year: year, - month: month, - day: day, - lon: coordinate.longitude, - lat: coordinate.latitude) - - let civil = NTSolar.civil_twilight(year: year, - month: month, - day: day, - lon: coordinate.longitude, - lat: coordinate.latitude) - - let nautical = NTSolar.nautical_twilight(year: year, - month: month, - day: day, - lon: coordinate.longitude, - lat: coordinate.latitude) - - let astronomical = NTSolar.astronomical_twilight(year: year, - month: month, - day: day, - lon: coordinate.longitude, - lat: coordinate.latitude) - - self.sunrise = NTSolar.dateFromUTCHours(sunriseSet.trise, - year: year, - month: month, - day: day, - calendar: calendar, - shouldReturn: sunriseSet.code == .RiseAndSet) - - self.sunset = NTSolar.dateFromUTCHours(sunriseSet.tset, - year: year, - month: month, - day: day, - calendar: calendar, - shouldReturn: sunriseSet.code == .RiseAndSet) - - self.civilSunrise = NTSolar.dateFromUTCHours(civil.trise, - year: year, - month: month, - day: day, - calendar: calendar, - shouldReturn: civil.code == .RiseAndSet) - - self.civilSunset = NTSolar.dateFromUTCHours(civil.tset, - year: year, - month: month, - day: day, - calendar: calendar, - shouldReturn: civil.code == .RiseAndSet) - - self.nauticalSunrise = NTSolar.dateFromUTCHours(nautical.trise, - year: year, - month: month, - day: day, - calendar: calendar, - shouldReturn: nautical.code == .RiseAndSet) - - self.nauticalSunset = NTSolar.dateFromUTCHours(nautical.tset, - year: year, - month: month, - day: day, - calendar: calendar, - shouldReturn: nautical.code == .RiseAndSet) - - self.astronomicalSunrise = NTSolar.dateFromUTCHours(astronomical.trise, - year: year, - month: month, - day: day, - calendar: calendar, - shouldReturn: astronomical.code == .RiseAndSet) - - self.astronomicalSunset = NTSolar.dateFromUTCHours(astronomical.tset, - year: year, - month: month, - day: day, - calendar: calendar, - shouldReturn: astronomical.code == .RiseAndSet) - - let solarNoonUTC = (sunriseSet.trise + sunriseSet.tset) / 2 - self.solarNoon = NTSolar.dateFromUTCHours(solarNoonUTC, - year: year, - month: month, - day: day, - calendar: calendar, - shouldReturn: true) - } - - /// Calculate the sun rise and set times for a given date/time zone. - /// - /// - Parameters: - /// - forDate: The date for the calculation. You should ensure that the date, which is stored as UTC, is the date - /// really wanted in the given time zone. It will be converted to the given time zone before being used. - /// - atLocation: The latitude and longitude for the calculation. - /// - inTimeZone: The time zone for the resulting date and times. - /// - Returns: If the sun both rises and sets on the day requested and at the - /// latitude requested then return sun rise and set times rounded down to the minute, nil otherwise. - static func sunRiseAndSet(forDate: Date, - atLocation: CLLocationCoordinate2D, - inTimeZone: TimeZone) -> (sunrise: Date, sunset: Date)? { - guard let solar = NTSolar(for: forDate, coordinate: atLocation, timeZone: inTimeZone) else { - return nil - } - - guard let sunrise = solar.sunrise, - let sunset = solar.sunset else { - return nil - } - - return (sunrise, sunset) - } - - // MARK: - Date Helpers - - private static func dateFromUTCHours(_ utcHours: Double, - year: Int, - month: Int, - day: Int, - calendar: Calendar, - shouldReturn: Bool) -> Date? { - guard shouldReturn else { return nil } - - var utcHours = utcHours - var dayOffset = 0 - while utcHours < 0 { - utcHours += 24.0 - dayOffset -= 1 - } - while utcHours >= 24.0 { - utcHours -= 24.0 - dayOffset += 1 - } - - var hour = Int(utcHours) - var minute = Int(((utcHours - Double(hour)) * 60.0).rounded()) - - if minute == 60 { - minute = 0 - hour += 1 - if hour == 24 { - hour = 0 - dayOffset += 1 - } - } - - var utcCalendar = calendar - utcCalendar.timeZone = TimeZone(secondsFromGMT: 0) ?? .gmt - - var baseComponents = DateComponents() - baseComponents.calendar = utcCalendar - baseComponents.year = year - baseComponents.month = month - baseComponents.day = day - baseComponents.hour = 0 - baseComponents.minute = 0 - baseComponents.second = 0 - - guard let baseDate = utcCalendar.date(from: baseComponents) else { - return nil - } - - guard let adjustedDay = utcCalendar.date(byAdding: .day, value: dayOffset, to: baseDate) else { - return nil - } - - var timeComponents = DateComponents() - timeComponents.hour = hour - timeComponents.minute = minute - timeComponents.second = 0 - - guard let finalDate = utcCalendar.date(byAdding: timeComponents, to: adjustedDay) else { - return nil - } - - return finalDate - } - - // MARK: - SUNRISET.C - - // C code originally from: http://stjarnhimlen.se/comp/sunriset.c - // - // The conversion process removed pointers in favor of return tuples, converted macros to function calls, - // added return code enum, and converted - // comments to work with Xcode. Of course the C code had to be converted to Swift, but that was minimal. - // - // As much as possible the original code was left intact in order to not introduce bugs. - // In other words, some code was left unfashionable by today's standards. - - enum ReturnCode: Int { - /// Sun is below the specified "horizon" 24 hours - /// "Day" length = 0 hours, trise and tset are - /// both set to the time when the sun is at south. - case SunAlwaysBelow = -1 - - /// Sun rises/sets this day, times stored at rise and set. - case RiseAndSet = 0 - - /// Sun above the specified "horizon" 24 hours. - /// trise set to time when the sun is at south, - /// minus 12 hours while tset is set to the south - /// time plus 12 hours. "Day" length = 24 hours - case SunAlwaysAbove = 1 - } - - /* +++Date last modified: 05-Jul-1997 */ - /* Updated comments, 05-Aug-2013 */ - /* - SUNRISET.C - computes Sun rise/set times, start/end of twilight, and - the length of the day at any date and latitude - - Written as DAYLEN.C, 1989-08-16 - - Modified to SUNRISET.C, 1992-12-01 - - (c) Paul Schlyter, 1989, 1992 - - Released to the public domain by Paul Schlyter, December 1992 - */ - - /// A macro to compute the number of days elapsed since 2000 Jan 0.0 - /// (which is equal to 1999 Dec 31, 0h UT) - private static func days_since_2000_Jan_0(y: Int, m: Int, d: Int) -> Int { - return (367*(y)-((7*((y)+(((m)+9)/12)))/4)+((275*(m))/9)+(d)-730530) - } - - /* Some conversion factors between radians and degrees */ - private static let PI = 3.1415926535897932384 - private static let RADEG = ( 180.0 / PI ) - private static let DEGRAD = ( PI / 180.0 ) - - /* The trigonometric functions in degrees */ - private static func sind(x: Double) -> Double { return sin((x)*DEGRAD) } - private static func cosd(x: Double) -> Double { return cos((x)*DEGRAD) } - private static func tand(x: Double) -> Double { return tan((x)*DEGRAD) } - private static func atand(x: Double) -> Double { return (RADEG*atan(x)) } - private static func asind(x: Double) -> Double { return (RADEG*asin(x)) } - private static func acosd(x: Double) -> Double { return (RADEG*acos(x)) } - private static func atan2d(y: Double, x: Double) -> Double { return (RADEG*atan2(y, x)) } - - /* Following are some macros around the "workhorse" function __daylen__ */ - /* They mainly fill in the desired values for the reference altitude */ - /* below the horizon, and also selects whether this altitude should */ - /* refer to the Sun's center or its upper limb. */ - - - /** This macro computes the length of the day, from sunrise to sunset. */ - /** Sunrise/set is considered to occur when the Sun's upper limb is */ - /** 35 arc minutes below the horizon (this accounts for the refraction */ - /** of the Earth's atmosphere). */ - static func day_length(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> Double { - return daylen(year: year, month: month, day: day, lon: lon, lat: lat, altit: -35.0/60.0, upper_limb: 1) - } - - /** This macro computes the length of the day, including civil twilight. */ - /** Civil twilight starts/ends when the Sun's center is 6 degrees below */ - /** the horizon. */ - static func day_civil_twilight_length(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> Double { - return daylen(year: year, month: month, day: day, lon: lon, lat: lat, altit: -6.0, upper_limb: 0) - } - - /** This macro computes the length of the day, incl. nautical twilight. */ - /** Nautical twilight starts/ends when the Sun's center is 12 degrees */ - /** below the horizon. */ - static func day_nautical_twilight_length(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> Double { - return daylen(year: year, month: month, day: day, lon: lon, lat: lat, altit: -12.0, upper_limb: 0) - } - - /** This macro computes the length of the day, incl. astronomical twilight. */ - /** Astronomical twilight starts/ends when the Sun's center is 18 degrees */ - /** below the horizon. */ - static func day_astronomical_twilight_length(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> Double { - return daylen(year: year, month: month, day: day, lon: lon, lat: lat, altit: -18.0, upper_limb: 0) - } - - /** This macro computes times for sunrise/sunset. */ - /** Sunrise/set is considered to occur when the Sun's upper limb is */ - /** 35 arc minutes below the horizon (this accounts for the refraction */ - /** of the Earth's atmosphere). */ - static func sun_rise_set(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> (trise: Double, tset: Double, code: ReturnCode) { - let (start, end, code) = sunriset(year: year, month: month, day: day, lon: lon, lat: lat, altit: -35.0/60.0, upper_limb: 1) - return (start, end, code) - } - - /** This macro computes the start and end times of civil twilight. */ - /** Civil twilight starts/ends when the Sun's center is 6 degrees below */ - /** the horizon. */ - static func civil_twilight(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> (trise: Double, tset: Double, code: ReturnCode) { - let (start, end, code) = sunriset(year: year, month: month, day: day, lon: lon, lat: lat, altit: -6.0, upper_limb: 0) - return (start, end, code) - } - - /** This macro computes the start and end times of nautical twilight. */ - /** Nautical twilight starts/ends when the Sun's center is 12 degrees */ - /** below the horizon. */ - static func nautical_twilight(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> (trise: Double, tset: Double, code: ReturnCode) { - let (start, end, code) = sunriset(year: year, month: month, day: day, lon: lon, lat: lat, altit: -12.0, upper_limb: 0) - return (start, end, code) - } - - /** This macro computes the start and end times of astronomical twilight. */ - /** Astronomical twilight starts/ends when the Sun's center is 18 degrees */ - /** below the horizon. */ - static func astronomical_twilight(year: Int, month: Int, day: Int, lon: Double, lat: Double) -> (trise: Double, tset: Double, code: ReturnCode) { - let (start, end, code) = sunriset(year: year, month: month, day: day, lon: lon, lat: lat, altit: -18.0, upper_limb: 0) - return (start, end, code) - } - - /// The "workhorse" function for sun rise/set times - /// - /// - Parameters: - /// - year: calendar date, 1801-2099 only. - /// - month: calendar date, 1801-2099 only. - /// - day: calendar date, 1801-2099 only. - /// - lon: Eastern longitude positive, Western longitude negative. The longitude value IS critical in this function! - /// - lat: Northern latitude positive, Southern latitude negative - /// - altit: the altitude which the Sun should cross. Set to -35/60 degrees for rise/set, -6 degrees - /// for civil, -12 degrees for nautical and -18 degrees for astronomical twilight. - /// - upper_limb: non-zero -> upper limb, zero -> center - /// Set to non-zero (e.g. 1) when computing rise/set - /// times, and to zero when computing start/end of twilight. - /// - Returns: rise, set, code. - /// - /// Both times in hours UT are relative to the specified altitude, - /// and thus this function can be used to compute - /// various twilight times, as well as rise/set times. - /// - /// Code 0 = sun rises/sets this day, times stored at rise and set. - /// - /// Code +1 = sun above the specified "horizon" 24 hours. - /// *trise set to time when the sun is at south, - /// minus 12 hours while *tset is set to the south - /// time plus 12 hours. "Day" length = 24 hours - /// - /// Code -1 = sun is below the specified "horizon" 24 hours - /// "Day" length = 0 hours, *trise and *tset are - /// both set to the time when the sun is at south. - private static func sunriset(year: Int, - month: Int, - day: Int, - lon: Double, - lat: Double, - altit: Double, - upper_limb: Int) -> (trise: Double, tset: Double, code: ReturnCode) { - - var altit = altit - var d: Double /* Days since 2000 Jan 0.0 (negative before) */ - var sr: Double /* Solar distance, astronomical units */ - var sRA: Double /* Sun's Right Ascension */ - var sdec: Double /* Sun's declination */ - var sradius: Double /* Sun's apparent radius */ - var t: Double /* Diurnal arc */ - var tsouth: Double /* Time when Sun is at south */ - var sidtime: Double /* Local sidereal time */ - - var rc: ReturnCode = ReturnCode.RiseAndSet /* Return cde from function - usually 0 */ - - /* Compute d of 12h local mean solar time */ - d = Double(days_since_2000_Jan_0(y: year, m: month, d: day)) + 0.5 - lon/360.0 - - /* Compute the local sidereal time of this moment */ - sidtime = revolution(x: GMST0(d: d) + 180.0 + lon) - - /* Compute Sun's RA, Decl and distance at this moment */ - (sRA, sdec, sr) = sun_RA_dec(d: d) - - /* Compute time when Sun is at south - in hours UT */ - tsouth = 12.0 - rev180(x: sidtime - sRA)/15.0 - - /* Compute the Sun's apparent radius in degrees */ - sradius = 0.2666 / sr - - /* Do correction to upper limb, if necessary */ - if upper_limb != 0 { - altit -= sradius - } - - /* Compute the diurnal arc that the Sun traverses to reach */ - /* the specified altitude altit: */ - do { - let cost: Double = (sind(x: altit) - sind(x: lat) * sind(x: sdec)) / (cosd(x: lat) * cosd(x: sdec)) - if cost >= 1.0 { - rc = ReturnCode.SunAlwaysBelow - t = 0.0 /* Sun always below altit */ - } else if cost <= -1.0 { - rc = ReturnCode.SunAlwaysAbove - t = 12.0 /* Sun always above altit */ - } else { - t = acosd(x: cost)/15.0 /* The diurnal arc, hours */ - } - } - - /* Store rise and set times - in hours UT */ - let trise = tsouth - t - let tset = tsouth + t - - return (trise, tset, rc) - } /* __sunriset__ */ - - /// The "workhorse" function - /// - /// - Parameters: - /// - year: year,month,date = calendar date, 1801-2099 only. - /// - month: year,month,date = calendar date, 1801-2099 only. - /// - day: year,month,date = calendar date, 1801-2099 only. - /// - lon: Eastern longitude positive, Western longitude negative - /// - lat: Northern latitude positive, Southern latitude negative - /// - altit: altit = the altitude which the Sun should cross - /// Set to -35/60 degrees for rise/set, -6 degrees - /// for civil, -12 degrees for nautical and -18 - /// degrees for astronomical twilight. - /// - upper_limb: upper_limb: non-zero -> upper limb, zero -> center - /// Set to non-zero (e.g. 1) when computing day length - /// and to zero when computing day+twilight length. - /// - Returns: Day number - private static func daylen(year: Int, month: Int, day: Int, lon: Double, lat: Double, - altit: Double, upper_limb: Int) -> Double { - /**********************************************************************/ - /* Note: year,month,date = calendar date, 1801-2099 only. */ - /* Eastern longitude positive, Western longitude negative */ - /* Northern latitude positive, Southern latitude negative */ - /* The longitude value is not critical. Set it to the correct */ - /* longitude if you're picky, otherwise set to to, say, 0.0 */ - /* The latitude however IS critical - be sure to get it correct */ - /* altit = the altitude which the Sun should cross */ - /* Set to -35/60 degrees for rise/set, -6 degrees */ - /* for civil, -12 degrees for nautical and -18 */ - /* degrees for astronomical twilight. */ - /* upper_limb: non-zero -> upper limb, zero -> center */ - /* Set to non-zero (e.g. 1) when computing day length */ - /* and to zero when computing day+twilight length. */ - /**********************************************************************/ - - var altit = altit - var d: Double /* Days since 2000 Jan 0.0 (negative before) */ - var obl_ecl: Double /* Obliquity (inclination) of Earth's axis */ - var sr: Double /* Solar distance, astronomical units */ - var slon: Double /* True solar longitude */ - var sin_sdecl: Double /* Sine of Sun's declination */ - var cos_sdecl: Double /* Cosine of Sun's declination */ - var sradius: Double /* Sun's apparent radius */ - var t: Double /* Diurnal arc */ - - /* Compute d of 12h local mean solar time */ - d = Double(days_since_2000_Jan_0(y: year, m: month, d: day)) + 0.5 - lon/360.0 - - /* Compute obliquity of ecliptic (inclination of Earth's axis) */ - obl_ecl = 23.4393 - 3.563E-7 * d - - /* Compute Sun's ecliptic longitude and distance */ - (slon, sr) = sunpos(d: d) - - /* Compute sine and cosine of Sun's declination */ - sin_sdecl = sind(x: obl_ecl) * sind(x: slon) - cos_sdecl = sqrt(1.0 - sin_sdecl * sin_sdecl) - - /* Compute the Sun's apparent radius, degrees */ - sradius = 0.2666 / sr - - /* Do correction to upper limb, if necessary */ - if upper_limb != 0 { - altit -= sradius - } - - /* Compute the diurnal arc that the Sun traverses to reach */ - /* the specified altitude altit: */ - do { - let cost: Double = (sind(x: altit) - sind(x: lat) * sin_sdecl) / (cosd(x: lat) * cos_sdecl) - if cost >= 1.0 { - t = 0.0 /* Sun always below altit */ - } else if cost <= -1.0 { - t = 24.0 /* Sun always above altit */ - } else { - t = (2.0/15.0) * acosd(x: cost) /* The diurnal arc, hours */ - } - } - - return t - } /* __daylen__ */ - - - /// This function computes the Sun's position at any instant. - private static func sunpos(d: Double) -> (lon: Double, r: Double) { - /******************************************************/ - /* Computes the Sun's ecliptic longitude and distance */ - /* at an instant given in d, number of days since */ - /* 2000 Jan 0.0. The Sun's ecliptic latitude is not */ - /* computed, since it's always very near 0. */ - /******************************************************/ - - var M: Double /* Mean anomaly of the Sun */ - var w: Double /* Mean longitude of perihelion */ - /* Note: Sun's mean longitude = M + w */ - var e: Double /* Eccentricity of Earth's orbit */ - var E: Double /* Eccentric anomaly */ - var x: Double - var y: Double /* x, y coordinates in orbit */ - var v: Double /* True anomaly */ - - /* Compute mean elements */ - M = revolution(x: 356.0470 + 0.9856002585 * d) - w = 282.9404 + 4.70935E-5 * d - e = 0.016709 - 1.151E-9 * d - - /* Compute true longitude and radius vector */ - E = M + e * RADEG * sind(x: M) * (1.0 + e * cosd(x: M)) - x = cosd(x: E) - e - y = sqrt(1.0 - e*e) * sind(x: E) - let r = sqrt(x*x + y*y) /* Solar distance */ - v = atan2d(y: y, x: x) /* True anomaly */ - var lon = v + w /* True solar longitude */ - if lon >= 360.0 { - lon -= 360.0 /* Make it 0..360 degrees */ - } - return (lon, r) - } - - private static func sun_RA_dec(d: Double) -> (RA: Double, dec: Double, r: Double) { - /******************************************************/ - /* Computes the Sun's equatorial coordinates RA, Decl */ - /* and also its distance, at an instant given in d, */ - /* the number of days since 2000 Jan 0.0. */ - /******************************************************/ - - var obl_ecl: Double - var x: Double - var y: Double - var z: Double - - /* Compute Sun's ecliptical coordinates */ - let (lon, r) = sunpos(d: d) - - /* Compute ecliptic rectangular coordinates (z=0) */ - x = r * cosd(x: lon) - y = r * sind(x: lon) - - /* Compute obliquity of ecliptic (inclination of Earth's axis) */ - obl_ecl = 23.4393 - 3.563E-7 * d - - /* Convert to equatorial rectangular coordinates - x is unchanged */ - z = y * sind(x: obl_ecl) - y = y * cosd(x: obl_ecl) - - /* Convert to spherical coordinates */ - let RA = atan2d(y: y, x: x) - let dec = atan2d(y: z, x: sqrt(x*x + y*y)) - return (RA, dec, r) - } /* sun_RA_dec */ - - private static let INV360: Double = (1.0 / 360.0) - - /*******************************************************************/ - /** This function reduces any angle to within the first revolution */ - /** by subtracting or adding even multiples of 360.0 until the */ - /** result is >= 0.0 and < 360.0 */ - /*******************************************************************/ - private static func revolution(x: Double) -> Double { - /*****************************************/ - /* Reduce angle to within 0..360 degrees */ - /*****************************************/ - - return (x - 360.0 * floor(x * INV360)) - } /* revolution */ - - private static func rev180(x: Double) -> Double { - /*********************************************/ - /* Reduce angle to within +180..+180 degrees */ - /*********************************************/ - - return (x - 360.0 * floor(x * INV360 + 0.5)) - } /* revolution */ - - /********************************************************************/ - /** This function computes GMST0, the Greenwich Mean Sidereal Time */ - /** at 0h UT (i.e. the sidereal time at the Greenwhich meridian at */ - /** 0h UT). GMST is then the sidereal time at Greenwich at any */ - /** time of the day. I've generalized GMST0 as well, and define it */ - /** as: GMST0 = GMST - UT -- this allows GMST0 to be computed at */ - /** other times than 0h UT as well. While this sounds somewhat */ - /** contradictory, it is very practical: instead of computing */ - /** GMST like: */ - /** */ - /** GMST = (GMST0) + UT * (366.2422/365.2422) */ - /** */ - /** where (GMST0) is the GMST last time UT was 0 hours, one simply */ - /** computes: */ - /** */ - /** GMST = GMST0 + UT */ - /** */ - /** where GMST0 is the GMST "at 0h UT" but at the current moment! */ - /** Defined in this way, GMST0 will increase with about 4 min a */ - /** day. It also happens that GMST0 (in degrees, 1 hr = 15 degr) */ - /** is equal to the Sun's mean longitude plus/minus 180 degrees! */ - /** (if we neglect aberration, which amounts to 20 seconds of arc */ - /** or 1.33 seconds of time) */ - /** */ - /********************************************************************/ - private static func GMST0(d: Double) -> Double { - var sidtim0: Double - /* Sidtime at 0h UT = L (Sun's mean longitude) + 180.0 degr */ - /* L = M + w, as defined in sunpos(). Since I'm too lazy to */ - /* add these numbers, I'll let the C compiler do it for me. */ - /* Any decent C compiler will add the constants at compile */ - /* time, imposing no runtime or code overhead. */ - sidtim0 = revolution(x: (180.0 + 356.0470 + 282.9404) + - (0.9856002585 + 4.70935E-5) * d) - return sidtim0 - } /* GMST0 */ + /// The "workhorse" function for sun rise/set times + /// + /// - Parameters: + /// - year: calendar date, 1801-2099 only. + /// - month: calendar date, 1801-2099 only. + /// - day: calendar date, 1801-2099 only. + /// - lon: Eastern longitude positive, Western longitude negative. The longitude value IS critical in this function! + /// - lat: Northern latitude positive, Southern latitude negative + /// - altit: the altitude which the Sun should cross. Set to -35/60 degrees for rise/set, -6 degrees + /// for civil, -12 degrees for nautical and -18 degrees for astronomical twilight. + /// - upper_limb: non-zero -> upper limb, zero -> center + /// Set to non-zero (e.g. 1) when computing rise/set + /// times, and to zero when computing start/end of twilight. + /// - Returns: rise, set, code. + /// + /// Both times in hours UT are relative to the specified altitude, + /// and thus this function can be used to compute + /// various twilight times, as well as rise/set times. + /// + /// Code 0 = sun rises/sets this day, times stored at rise and set. + /// + /// Code +1 = sun above the specified "horizon" 24 hours. + /// *trise set to time when the sun is at south, + /// minus 12 hours while *tset is set to the south + /// time plus 12 hours. "Day" length = 24 hours + /// + /// Code -1 = sun is below the specified "horizon" 24 hours + /// "Day" length = 0 hours, *trise and *tset are + /// both set to the time when the sun is at south. + private static func sunriset(year: Int, + month: Int, + day: Int, + lon: Double, + lat: Double, + altit: Double, + upper_limb: Int) -> (trise: Double, tset: Double, code: ReturnCode) + { + var altit = altit + var d: Double /* Days since 2000 Jan 0.0 (negative before) */ + var sr: Double /* Solar distance, astronomical units */ + var sRA: Double /* Sun's Right Ascension */ + var sdec: Double /* Sun's declination */ + var sradius: Double /* Sun's apparent radius */ + var t: Double /* Diurnal arc */ + var tsouth: Double /* Time when Sun is at south */ + var sidtime: Double /* Local sidereal time */ + + var rc = ReturnCode.RiseAndSet /* Return cde from function - usually 0 */ + + /* Compute d of 12h local mean solar time */ + d = Double(days_since_2000_Jan_0(y: year, m: month, d: day)) + 0.5 - lon / 360.0 + + /* Compute the local sidereal time of this moment */ + sidtime = revolution(x: GMST0(d: d) + 180.0 + lon) + + /* Compute Sun's RA, Decl and distance at this moment */ + (sRA, sdec, sr) = sun_RA_dec(d: d) + + /* Compute time when Sun is at south - in hours UT */ + tsouth = 12.0 - rev180(x: sidtime - sRA) / 15.0 + + /* Compute the Sun's apparent radius in degrees */ + sradius = 0.2666 / sr + + /* Do correction to upper limb, if necessary */ + if upper_limb != 0 { + altit -= sradius + } + + /* Compute the diurnal arc that the Sun traverses to reach */ + /* the specified altitude altit: */ + do { + let cost: Double = (sind(x: altit) - sind(x: lat) * sind(x: sdec)) / (cosd(x: lat) * cosd(x: sdec)) + if cost >= 1.0 { + rc = ReturnCode.SunAlwaysBelow + t = 0.0 /* Sun always below altit */ + } else if cost <= -1.0 { + rc = ReturnCode.SunAlwaysAbove + t = 12.0 /* Sun always above altit */ + } else { + t = acosd(x: cost) / 15.0 /* The diurnal arc, hours */ + } + } + + /* Store rise and set times - in hours UT */ + let trise = tsouth - t + let tset = tsouth + t + + return (trise, tset, rc) + } /* __sunriset__ */ + + /// The "workhorse" function + /// + /// - Parameters: + /// - year: year,month,date = calendar date, 1801-2099 only. + /// - month: year,month,date = calendar date, 1801-2099 only. + /// - day: year,month,date = calendar date, 1801-2099 only. + /// - lon: Eastern longitude positive, Western longitude negative + /// - lat: Northern latitude positive, Southern latitude negative + /// - altit: altit = the altitude which the Sun should cross + /// Set to -35/60 degrees for rise/set, -6 degrees + /// for civil, -12 degrees for nautical and -18 + /// degrees for astronomical twilight. + /// - upper_limb: upper_limb: non-zero -> upper limb, zero -> center + /// Set to non-zero (e.g. 1) when computing day length + /// and to zero when computing day+twilight length. + /// - Returns: Day number + private static func daylen(year: Int, month: Int, day: Int, lon: Double, lat: Double, + altit: Double, upper_limb: Int) -> Double + { + /**********************************************************************/ + /* Note: year,month,date = calendar date, 1801-2099 only. */ + /* Eastern longitude positive, Western longitude negative */ + /* Northern latitude positive, Southern latitude negative */ + /* The longitude value is not critical. Set it to the correct */ + /* longitude if you're picky, otherwise set to to, say, 0.0 */ + /* The latitude however IS critical - be sure to get it correct */ + /* altit = the altitude which the Sun should cross */ + /* Set to -35/60 degrees for rise/set, -6 degrees */ + /* for civil, -12 degrees for nautical and -18 */ + /* degrees for astronomical twilight. */ + /* upper_limb: non-zero -> upper limb, zero -> center */ + /* Set to non-zero (e.g. 1) when computing day length */ + /* and to zero when computing day+twilight length. */ + /**********************************************************************/ + + var altit = altit + var d: Double /* Days since 2000 Jan 0.0 (negative before) */ + var obl_ecl: Double /* Obliquity (inclination) of Earth's axis */ + var sr: Double /* Solar distance, astronomical units */ + var slon: Double /* True solar longitude */ + var sin_sdecl: Double /* Sine of Sun's declination */ + var cos_sdecl: Double /* Cosine of Sun's declination */ + var sradius: Double /* Sun's apparent radius */ + var t: Double /* Diurnal arc */ + + /* Compute d of 12h local mean solar time */ + d = Double(days_since_2000_Jan_0(y: year, m: month, d: day)) + 0.5 - lon / 360.0 + + /* Compute obliquity of ecliptic (inclination of Earth's axis) */ + obl_ecl = 23.4393 - 3.563e-7 * d + + /* Compute Sun's ecliptic longitude and distance */ + (slon, sr) = sunpos(d: d) + + /* Compute sine and cosine of Sun's declination */ + sin_sdecl = sind(x: obl_ecl) * sind(x: slon) + cos_sdecl = sqrt(1.0 - sin_sdecl * sin_sdecl) + + /* Compute the Sun's apparent radius, degrees */ + sradius = 0.2666 / sr + + /* Do correction to upper limb, if necessary */ + if upper_limb != 0 { + altit -= sradius + } + + /* Compute the diurnal arc that the Sun traverses to reach */ + /* the specified altitude altit: */ + do { + let cost: Double = (sind(x: altit) - sind(x: lat) * sin_sdecl) / (cosd(x: lat) * cos_sdecl) + if cost >= 1.0 { + t = 0.0 /* Sun always below altit */ + } else if cost <= -1.0 { + t = 24.0 /* Sun always above altit */ + } else { + t = (2.0 / 15.0) * acosd(x: cost) /* The diurnal arc, hours */ + } + } + + return t + } /* __daylen__ */ + + /// This function computes the Sun's position at any instant. + private static func sunpos(d: Double) -> (lon: Double, r: Double) { + /******************************************************/ + /* Computes the Sun's ecliptic longitude and distance */ + /* at an instant given in d, number of days since */ + /* 2000 Jan 0.0. The Sun's ecliptic latitude is not */ + /* computed, since it's always very near 0. */ + /******************************************************/ + + var M: Double /* Mean anomaly of the Sun */ + var w: Double /* Mean longitude of perihelion */ + /* Note: Sun's mean longitude = M + w */ + var e: Double /* Eccentricity of Earth's orbit */ + var E: Double /* Eccentric anomaly */ + var x: Double + var y: Double /* x, y coordinates in orbit */ + var v: Double /* True anomaly */ + + /* Compute mean elements */ + M = revolution(x: 356.0470 + 0.9856002585 * d) + w = 282.9404 + 4.70935e-5 * d + e = 0.016709 - 1.151e-9 * d + + /* Compute true longitude and radius vector */ + E = M + e * RADEG * sind(x: M) * (1.0 + e * cosd(x: M)) + x = cosd(x: E) - e + y = sqrt(1.0 - e * e) * sind(x: E) + let r = sqrt(x * x + y * y) /* Solar distance */ + v = atan2d(y: y, x: x) /* True anomaly */ + var lon = v + w /* True solar longitude */ + if lon >= 360.0 { + lon -= 360.0 /* Make it 0..360 degrees */ + } + return (lon, r) + } + private static func sun_RA_dec(d: Double) -> (RA: Double, dec: Double, r: Double) { + /******************************************************/ + /* Computes the Sun's equatorial coordinates RA, Decl */ + /* and also its distance, at an instant given in d, */ + /* the number of days since 2000 Jan 0.0. */ + /******************************************************/ + + var obl_ecl: Double + var x: Double + var y: Double + var z: Double + + /* Compute Sun's ecliptical coordinates */ + let (lon, r) = sunpos(d: d) + + /* Compute ecliptic rectangular coordinates (z=0) */ + x = r * cosd(x: lon) + y = r * sind(x: lon) + + /* Compute obliquity of ecliptic (inclination of Earth's axis) */ + obl_ecl = 23.4393 - 3.563e-7 * d + + /* Convert to equatorial rectangular coordinates - x is unchanged */ + z = y * sind(x: obl_ecl) + y = y * cosd(x: obl_ecl) + + /* Convert to spherical coordinates */ + let RA = atan2d(y: y, x: x) + let dec = atan2d(y: z, x: sqrt(x * x + y * y)) + return (RA, dec, r) + } /* sun_RA_dec */ + + private static let INV360: Double = (1.0 / 360.0) + + /*******************************************************************/ + /** This function reduces any angle to within the first revolution */ + /** by subtracting or adding even multiples of 360.0 until the */ + /** result is >= 0.0 and < 360.0 */ + /*******************************************************************/ + private static func revolution(x: Double) -> Double { + /*****************************************/ + /* Reduce angle to within 0..360 degrees */ + /*****************************************/ + + return x - 360.0 * floor(x * INV360) + } /* revolution */ + + private static func rev180(x: Double) -> Double { + /*********************************************/ + /* Reduce angle to within +180..+180 degrees */ + /*********************************************/ + + return x - 360.0 * floor(x * INV360 + 0.5) + } /* revolution */ + + /********************************************************************/ + /** This function computes GMST0, the Greenwich Mean Sidereal Time */ + /** at 0h UT (i.e. the sidereal time at the Greenwhich meridian at */ + /** 0h UT). GMST is then the sidereal time at Greenwich at any */ + /** time of the day. I've generalized GMST0 as well, and define it */ + /** as: GMST0 = GMST - UT -- this allows GMST0 to be computed at */ + /** other times than 0h UT as well. While this sounds somewhat */ + /** contradictory, it is very practical: instead of computing */ + /** GMST like: */ + /** */ + /** GMST = (GMST0) + UT * (366.2422/365.2422) */ + /** */ + /** where (GMST0) is the GMST last time UT was 0 hours, one simply */ + /** computes: */ + /** */ + /** GMST = GMST0 + UT */ + /** */ + /** where GMST0 is the GMST "at 0h UT" but at the current moment! */ + /** Defined in this way, GMST0 will increase with about 4 min a */ + /** day. It also happens that GMST0 (in degrees, 1 hr = 15 degr) */ + /** is equal to the Sun's mean longitude plus/minus 180 degrees! */ + /** (if we neglect aberration, which amounts to 20 seconds of arc */ + /** or 1.33 seconds of time) */ + /** */ + /********************************************************************/ + private static func GMST0(d: Double) -> Double { + var sidtim0: Double + /* Sidtime at 0h UT = L (Sun's mean longitude) + 180.0 degr */ + /* L = M + w, as defined in sunpos(). Since I'm too lazy to */ + /* add these numbers, I'll let the C compiler do it for me. */ + /* Any decent C compiler will add the constants at compile */ + /* time, imposing no runtime or code overhead. */ + sidtim0 = revolution(x: (180.0 + 356.0470 + 282.9404) + + (0.9856002585 + 4.70935e-5) * d) + return sidtim0 + } /* GMST0 */ } extension NTSolar { @@ -692,7 +714,7 @@ extension NTSolar { utcCal.timeZone = TimeZone(secondsFromGMT: 0)! let comps = utcCal.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) guard let year = comps.year, let month = comps.month, let day = comps.day, - let hour = comps.hour, let minute = comps.minute, let second = comps.second + let hour = comps.hour, let minute = comps.minute, let second = comps.second else { return 0 } // UT as a decimal hour diff --git a/Solstice/Helpers/NotificationManager.swift b/Solstice/Helpers/NotificationManager.swift index 0a427cfb..2b6da146 100644 --- a/Solstice/Helpers/NotificationManager.swift +++ b/Solstice/Helpers/NotificationManager.swift @@ -5,13 +5,13 @@ // Created by Daniel Eden on 23/02/2023. // -import Foundation -import UserNotifications +import CoreData import CoreLocation +import Foundation import SwiftUI -import CoreData +import UserNotifications #if os(iOS) && !WIDGET_EXTENSION -import BackgroundTasks + import BackgroundTasks #endif class NotificationManager: NSObject, UNUserNotificationCenterDelegate { @@ -60,7 +60,8 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { if let latitude = objects?.first?.latitude, let longitude = objects?.first?.longitude, - let objectTimeZone = objects?.first?.timeZone { + let objectTimeZone = objects?.first?.timeZone + { location = CLLocation(latitude: latitude, longitude: longitude) timeZone = objectTimeZone } @@ -82,7 +83,7 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { return } - for i in 0...63 { + for i in 0 ... 63 { let date = calendar.date(byAdding: .day, value: i, to: Date()) ?? .now guard let solar = NTSolar(for: date, coordinate: location.coordinate, timeZone: timeZone) else { @@ -113,40 +114,40 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { } #if os(iOS) && !WIDGET_EXTENSION - scheduleBackgroundTask() + scheduleBackgroundTask() #endif } // MARK: Background Task Management #if os(iOS) && !WIDGET_EXTENSION - /// Schedules the next background app refresh task - /// This should be called after scheduling notifications to ensure periodic refresh - static func scheduleBackgroundTask() { - let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier) - - // Schedule to run after at least 1 day to catch location changes sooner - request.earliestBeginDate = Date(timeIntervalSinceNow: 24 * 60 * 60) - - do { - try BGTaskScheduler.shared.submit(request) - print("Background task scheduled successfully for notification refresh") - } catch let error as BGTaskScheduler.Error { - switch error.code { - case .unavailable: - // This is expected in the simulator or when the app is in the foreground - print("Background task scheduling unavailable (expected in simulator)") - case .tooManyPendingTaskRequests: - print("Too many pending background task requests") - case .notPermitted: - print("Background task not permitted - check Info.plist configuration") - default: + /// Schedules the next background app refresh task + /// This should be called after scheduling notifications to ensure periodic refresh + static func scheduleBackgroundTask() { + let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier) + + // Schedule to run after at least 1 day to catch location changes sooner + request.earliestBeginDate = Date(timeIntervalSinceNow: 24 * 60 * 60) + + do { + try BGTaskScheduler.shared.submit(request) + print("Background task scheduled successfully for notification refresh") + } catch let error as BGTaskScheduler.Error { + switch error.code { + case .unavailable: + // This is expected in the simulator or when the app is in the foreground + print("Background task scheduling unavailable (expected in simulator)") + case .tooManyPendingTaskRequests: + print("Too many pending background task requests") + case .notPermitted: + print("Background task not permitted - check Info.plist configuration") + default: + print("Failed to schedule background task: \(error.localizedDescription)") + } + } catch { print("Failed to schedule background task: \(error.localizedDescription)") } - } catch { - print("Failed to schedule background task: \(error.localizedDescription)") } - } #endif static func getNextNotificationDate(after date: Date, with solar: NTSolar? = nil) -> Date { @@ -233,7 +234,7 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { (SolsticeCalculator.juneSolstice(year: year), NSLocalizedString("June Solstice Today", comment: "Notification title for June solstice")), (SolsticeCalculator.decemberSolstice(year: year), - NSLocalizedString("December Solstice Today", comment: "Notification title for December solstice")) + NSLocalizedString("December Solstice Today", comment: "Notification title for December solstice")), ] return solarEvents.first { calendar.isDate(date, inSameDayAs: $0.date) }?.title @@ -244,11 +245,11 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate { let hour = calendar.component(.hour, from: date) switch hour { - case 0..<3, 18...23: + case 0 ..< 3, 18 ... 23: return NSLocalizedString("Good Evening", comment: "Notification title for evening notification") - case 3..<12: + case 3 ..< 12: return NSLocalizedString("Good Morning", comment: "Notification title for morning notification") - case 12..<18: + case 12 ..< 18: return NSLocalizedString("Good Afternoon", comment: "Notification title for afternoon notification") default: return NSLocalizedString("Today's Daylight", comment: "Notification title fallback") diff --git a/Solstice/Helpers/SolsticeCalculator.swift b/Solstice/Helpers/SolsticeCalculator.swift index 930c3f88..29898528 100644 --- a/Solstice/Helpers/SolsticeCalculator.swift +++ b/Solstice/Helpers/SolsticeCalculator.swift @@ -7,12 +7,12 @@ // Source: https://github.com/Fabiz/MeeusJs/blob/d38beae42d70f11115cca63ed2ef20ef0a92b024/lib/Astro.Solistice.js import Foundation -struct SolsticeCalculator { - static private let terms = [ +enum SolsticeCalculator { + private static let terms = [ [485, 324.96, 1934.136], [203, 337.23, 32964.467], [199, 342.08, 20.186], - [182, 27.85, 445267.112], + [182, 27.85, 445_267.112], [156, 73.14, 45036.886], [136, 171.52, 22518.443], [77, 222.54, 65928.934], @@ -21,7 +21,7 @@ struct SolsticeCalculator { [58, 119.81, 33718.147], [52, 297.17, 150.678], [50, 21.02, 2281.226], - + [45, 247.54, 29929.562], [44, 325.15, 31555.956], [29, 60.93, 4443.417], @@ -33,90 +33,90 @@ struct SolsticeCalculator { [12, 287.11, 31931.756], [12, 320.81, 34777.259], [9, 227.73, 1222.114], - [8, 15.45, 16859.074] + [8, 15.45, 16859.074], ] - - static private let mc0 = [1721139.29189, 365242.13740, 0.06134, 0.00111, -0.00071], - jc0 = [1721233.25401, 365241.72562, -0.05232, 0.00907, 0.00025], - sc0 = [1721325.70455, 365242.49558, -0.11677, -0.00297, 0.00074], - dc0 = [1721414.39987, 365242.88257, -0.00769, -0.00933, -0.00006], - - mc2 = [2451623.80984, 365242.37404, 0.05169, -0.00411, -0.00057], - jc2 = [2451716.56767, 365241.62603, 0.00325, 0.00888, -0.00030], - sc2 = [2451810.21715, 365242.01767, -0.11575, 0.00337, 0.00078], - dc2 = [2451900.05952, 365242.74049, -0.06223, -0.00823, 0.00032] - - static private let J2000 = 2451545.0 - static private let julianCentury = 36525.0 - + + private static let mc0 = [1_721_139.29189, 365_242.13740, 0.06134, 0.00111, -0.00071], + jc0 = [1_721_233.25401, 365_241.72562, -0.05232, 0.00907, 0.00025], + sc0 = [1_721_325.70455, 365_242.49558, -0.11677, -0.00297, 0.00074], + dc0 = [1_721_414.39987, 365_242.88257, -0.00769, -0.00933, -0.00006], + + mc2 = [2_451_623.80984, 365_242.37404, 0.05169, -0.00411, -0.00057], + jc2 = [2_451_716.56767, 365_241.62603, 0.00325, 0.00888, -0.00030], + sc2 = [2_451_810.21715, 365_242.01767, -0.11575, 0.00337, 0.00078], + dc2 = [2_451_900.05952, 365_242.74049, -0.06223, -0.00823, 0.00032] + + private static let J2000 = 2_451_545.0 + private static let julianCentury = 36525.0 + static func marchEquinox(year: Int) -> Date { if year < 1000 { return dateFromJd(jd: calculate(year, c: mc0)) } - - return dateFromJd(jd: calculate(year-2000, c: mc2)) + + return dateFromJd(jd: calculate(year - 2000, c: mc2)) } - + static func septemberEquinox(year: Int) -> Date { if year < 1000 { return dateFromJd(jd: calculate(year, c: sc0)) } - - return dateFromJd(jd: calculate(year-2000, c: sc2)) + + return dateFromJd(jd: calculate(year - 2000, c: sc2)) } - + static func juneSolstice(year: Int) -> Date { if year < 1000 { return dateFromJd(jd: calculate(year, c: jc0)) } - - return dateFromJd(jd: calculate(year-2000, c: jc2)) + + return dateFromJd(jd: calculate(year - 2000, c: jc2)) } - + static func decemberSolstice(year: Int) -> Date { if year < 1000 { return dateFromJd(jd: calculate(year, c: dc0)) } - - return dateFromJd(jd: calculate(year-2000, c: dc2)) + + return dateFromJd(jd: calculate(year - 2000, c: dc2)) } - + private static func calculate(_ year: Int, c: [Double]) -> Double { - let J0 = horner(x: Double(year)*0.001, c: c) ?? 0 - + let J0 = horner(x: Double(year) * 0.001, c: c) ?? 0 + let T = (J0 - J2000) / julianCentury // calc J2000 century - - let W = 35999.373 * .pi/180 * T - 2.47 * .pi/180 - let deltatheta = 1 + 0.0334*cos(W) + 0.0007*cos(2*W) + + let W = 35999.373 * .pi / 180 * T - 2.47 * .pi / 180 + let deltatheta = 1 + 0.0334 * cos(W) + 0.0007 * cos(2 * W) var S: Double = 0 - + var i = terms.count - 1 while i >= 0 { let t = terms[i] - S += t[0] * cos((t[1]+t[2]*T) * .pi/180) + S += t[0] * cos((t[1] + t[2] * T) * .pi / 180) i -= 1 } - return J0 + 0.00001*S/deltatheta + return J0 + 0.00001 * S / deltatheta } - + private static func horner(x: Double, c: [Double]) -> Double? { var i = c.count - 1 - + guard i >= 1 else { return nil } - + var y = c[i] - while (i > 0) { + while i > 0 { i -= 1 - y = y*x + c[i] + y = y * x + c[i] } return y } - - private static func dateFromJd(jd : Double) -> Date { - let JD_JAN_1_1970_0000GMT = 2440587.5 - return Date(timeIntervalSince1970: (jd - JD_JAN_1_1970_0000GMT) * 86400) + + private static func dateFromJd(jd: Double) -> Date { + let JD_JAN_1_1970_0000GMT = 2_440_587.5 + return Date(timeIntervalSince1970: (jd - JD_JAN_1_1970_0000GMT) * 86400) } } @@ -126,34 +126,34 @@ extension Date { let decemberSolstice = SolsticeCalculator.decemberSolstice(year: year) let decemberSolsticeLastYear = SolsticeCalculator.decemberSolstice(year: year - 1) let decemberSolsticeNextYear = SolsticeCalculator.decemberSolstice(year: year + 1) - + let juneSolstice = SolsticeCalculator.juneSolstice(year: year) let juneSolsticeLastYear = SolsticeCalculator.juneSolstice(year: year - 1) let juneSolsticeNextYear = SolsticeCalculator.juneSolstice(year: year + 1) - + return [decemberSolstice, decemberSolsticeNextYear, decemberSolsticeLastYear, juneSolstice, juneSolsticeNextYear, juneSolsticeLastYear].sorted() } - + var recentEquinoxes: [Date] { let year = calendar.component(.year, from: self) let marchEquinox = SolsticeCalculator.marchEquinox(year: year) let marchEquinoxNextYear = SolsticeCalculator.marchEquinox(year: year + 1) - + let septemberEquinox = SolsticeCalculator.septemberEquinox(year: year) let septemberEquinoxNextYear = SolsticeCalculator.septemberEquinox(year: year + 1) - + return [marchEquinox, marchEquinoxNextYear, septemberEquinox, septemberEquinoxNextYear].sorted() } - + var previousSolstice: Date { let index = recentSolstices.firstIndex(where: { $0 > self }) ?? 1 return recentSolstices[index - 1] } - + var nextSolstice: Date { recentSolstices.first(where: { $0 > self }) ?? .now } - + var nextEquinox: Date { recentEquinoxes.first(where: { $0 > self }) ?? .now } diff --git a/Solstice/Helpers/StringBuilder.swift b/Solstice/Helpers/StringBuilder.swift index 5e9593fc..ada98dd7 100644 --- a/Solstice/Helpers/StringBuilder.swift +++ b/Solstice/Helpers/StringBuilder.swift @@ -12,20 +12,20 @@ struct StringBuilder { static func buildBlock(_ parts: String...) -> String { parts.joined(separator: " ") } - + static func buildEither(first component: String) -> String { return component } - + static func buildEither(second component: String) -> String { return component } - + static func buildArray(_ components: [String]) -> String { components.joined(separator: "\n") } - + static func buildOptional(_ component: String?) -> String { return component ?? "" - } + } } diff --git a/Solstice/List View/GraphicalLocationListRow.swift b/Solstice/List View/GraphicalLocationListRow.swift index 10d5c066..1c6da725 100644 --- a/Solstice/List View/GraphicalLocationListRow.swift +++ b/Solstice/List View/GraphicalLocationListRow.swift @@ -11,11 +11,11 @@ import TimeMachine struct GraphicalLocationListRow: View { @Environment(\.timeMachine) private var timeMachine var location: Location - + var solar: NTSolar? { NTSolar(for: timeMachine.date, coordinate: location.coordinate, timeZone: location.timeZone) } - + var body: some View { LocationListRow(location: location, headingFontWeight: .semibold) .foregroundStyle(.white) diff --git a/Solstice/List View/LocationListRow.swift b/Solstice/List View/LocationListRow.swift index 2cd87463..b2a6adf5 100644 --- a/Solstice/List View/LocationListRow.swift +++ b/Solstice/List View/LocationListRow.swift @@ -5,36 +5,36 @@ // Created by Daniel Eden on 12/02/2023. // -import SwiftUI import Suite +import SwiftUI import TimeMachine struct LocationListRow: View { @Environment(\.timeMachine) private var timeMachine: TimeMachine var location: Location - + @FocusState private var focused: Bool - + @State private var showRemainingDaylight = false - + var headingFontWeight: Font.Weight = .medium - + private var solar: NTSolar? { NTSolar(for: timeMachine.date, coordinate: location.coordinate, timeZone: location.timeZone) } - + private var isCurrentLocation: Bool { location is CurrentLocation } - + private var subtitle: String? { - if location is CurrentLocation && location.title == nil { + if location is CurrentLocation, location.title == nil { return "—" } else { return location.subtitle } } - + @ViewBuilder var trailingContent: some View { if let solar { @@ -43,12 +43,12 @@ struct LocationListRow: View { #if os(iOS) .font(.headline.weight(headingFontWeight)) #endif - + let sunrise = solar.safeSunrise let sunset = solar.safeSunset - + if sunrise < sunset { - Text(sunrise...sunset) + Text(sunrise ... sunset) .foregroundStyle(.secondary) } } @@ -56,59 +56,58 @@ struct LocationListRow: View { .environment(\.timeZone, location.timeZone) } } - + var body: some View { #if os(watchOS) - VStack(alignment: .leading) { - HStack { - locationTitleLabel - - if !(location is CurrentLocation) { - Spacer() - Text(timeMachine.date, style: .time) - .environment(\.timeZone, location.timeZone) - .foregroundStyle(.secondary) - .contentTransition(.numericText()) + VStack(alignment: .leading) { + HStack { + locationTitleLabel + + if !(location is CurrentLocation) { + Spacer() + Text(timeMachine.date, style: .time) + .environment(\.timeZone, location.timeZone) + .foregroundStyle(.secondary) + .contentTransition(.numericText()) + } } - } - - if let solar { - Group { - Text(Duration.seconds(solar.daylightDuration).formatted(.units(allowed: [.hours, .minutes]))) - .font(.headline) - .contentTransition(.numericText()) - Text(solar.safeSunrise...solar.safeSunset) - .font(.footnote) - .foregroundStyle(.secondary) - .contentTransition(.numericText()) + + if let solar { + Group { + Text(Duration.seconds(solar.daylightDuration).formatted(.units(allowed: [.hours, .minutes]))) + .font(.headline) + .contentTransition(.numericText()) + Text(solar.safeSunrise ... solar.safeSunset) + .font(.footnote) + .foregroundStyle(.secondary) + .contentTransition(.numericText()) + } } } - } - .animation(.default, value: location.title) - .animation(.default, value: location.subtitle) - .animation(.default, value: timeMachine.date) - .shadow(radius: 4, x: 0, y: 2) - .environment(\.timeZone, location.timeZone) + .animation(.default, value: location.title) + .animation(.default, value: location.subtitle) + .animation(.default, value: timeMachine.date) + .shadow(radius: 4, x: 0, y: 2) + .environment(\.timeZone, location.timeZone) #else - HStack { - VStack(alignment: .leading, spacing: 2) { - Group { - locationTitleLabel - locationSubtitleLabel + HStack { + VStack(alignment: .leading, spacing: 2) { + Group { + locationTitleLabel + locationSubtitleLabel + } + .lineLimit(2) } - .lineLimit(2) + .animation(.default, value: location.title) + .animation(.default, value: location.subtitle) + + Spacer() + + trailingContent } - .animation(.default, value: location.title) - .animation(.default, value: location.subtitle) - - Spacer() - - trailingContent - } #endif } - - @ViewBuilder + var locationTitleLabel: some View { HStack(spacing: 4) { if location is CurrentLocation { @@ -117,18 +116,19 @@ struct LocationListRow: View { .foregroundStyle(.secondary) .symbolVariant(.fill) } - - Text(location.title ?? "Current location") + + Text(location.title ?? String(localized: "Current Location")) .contentTransition(.numericText()) .lineLimit(2) } .font(.headline.weight(headingFontWeight)) } - + @ViewBuilder var locationSubtitleLabel: some View { if let subtitle, - !subtitle.isEmpty { + !subtitle.isEmpty + { Text(subtitle) .foregroundStyle(.secondary) .transition(.blurReplace) diff --git a/Solstice/List View/SidebarListView.swift b/Solstice/List View/SidebarListView.swift index 4b477683..2dd46b91 100644 --- a/Solstice/List View/SidebarListView.swift +++ b/Solstice/List View/SidebarListView.swift @@ -5,6 +5,7 @@ // Created by Daniel Eden on 19/03/2023. // +import CoreData import SwiftUI import TimeMachine @@ -13,42 +14,68 @@ struct SidebarListView: View { @Environment(CurrentLocation.self) var currentLocation @Environment(\.timeMachine) var timeMachine: TimeMachine @Environment(LocationSearchService.self) var locationSearchService - + @Environment(\.managedObjectContext) private var viewContext - + @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \SavedLocation.title, ascending: true)], - animation: .default) + animation: .default + ) private var items: FetchedResults - + @SceneStorage("selectedLocation") private var selectedLocation: String? - + @AppStorage(Preferences.listViewSortOrder) private var itemSortOrder @AppStorage(Preferences.listViewSortDimension) private var itemSortDimension @AppStorage(Preferences.listViewShowComplication) private var showComplication - + + @State private var sortedItems: [SavedLocation] = [] + let namespace: Namespace.ID - + var body: some View { listContent .animation(.default, value: currentLocation.isAuthorized) .animation(.default, value: items.count) .overlay { emptyStateOverlay } - #if os(iOS) + #if os(iOS) .listRowSpacing(appearance == .graphical ? 8 : 0) - #endif + #endif .navigationTitle("Locations") .navigationSplitViewColumnWidth(ideal: 300) - #if os(macOS) - .searchable(text: Bindable(locationSearchService).queryFragment, - placement: .automatic, - prompt: "Search locations") - #else + #if os(macOS) .searchable(text: Bindable(locationSearchService).queryFragment, - placement: .navigationBarDrawer, - prompt: "Search locations") - #endif + placement: .automatic, + prompt: "Search locations") + #else + .searchable(text: Bindable(locationSearchService).queryFragment, + placement: .navigationBarDrawer, + prompt: "Search locations") + #endif .searchSuggestions { searchSuggestionsList } + .onChange(of: sortSignature, initial: true) { + recomputeSortedItems() + } + } + + /// The daylight-duration sort constructs an `NTSolar` per comparison, so + /// re-running it on every body evaluation is expensive. Recompute only when + /// an input that actually affects the order changes; `timeMachine.date` only + /// matters for the daylight-duration dimension. + private var sortSignature: SortSignature { + SortSignature( + ids: items.map(\.objectID), + dimension: itemSortDimension, + order: itemSortOrder, + date: itemSortDimension == .daylightDuration ? timeMachine.date : nil + ) + } + + private struct SortSignature: Equatable { + let ids: [NSManagedObjectID] + let dimension: Preferences.SortingFunction + let order: SortOrder + let date: Date? } private var listContent: some View { @@ -71,12 +98,12 @@ struct SidebarListView: View { } #if os(iOS) - private var currentLocationDragItem: NSItemProvider { - let userActivity = NSUserActivity(activityType: DetailView.userActivity) - userActivity.title = "See daylight for current location" - userActivity.targetContentIdentifier = currentLocation.id - return NSItemProvider(object: userActivity) - } + private var currentLocationDragItem: NSItemProvider { + let userActivity = NSUserActivity(activityType: DetailView.userActivity) + userActivity.title = "See daylight for current location" + userActivity.targetContentIdentifier = currentLocation.id + return NSItemProvider(object: userActivity) + } #endif private var savedLocationsList: some View { @@ -95,10 +122,10 @@ struct SidebarListView: View { } preview: { savedLocationPreview(for: item) } - #if os(iOS) + #if os(iOS) .onDrag { savedLocationDragItem(for: item) } .matchedTransitionSource(id: tag, in: namespace) - #endif + #endif .tag(tag) } } @@ -123,13 +150,13 @@ struct SidebarListView: View { } #if os(iOS) - private func savedLocationDragItem(for item: SavedLocation) -> NSItemProvider { - let userActivity = NSUserActivity(activityType: DetailView.userActivity) - let title: String = item.title ?? "location" - userActivity.title = "See daylight for \(title)" - userActivity.targetContentIdentifier = item.uuid?.uuidString - return NSItemProvider(object: userActivity) - } + private func savedLocationDragItem(for item: SavedLocation) -> NSItemProvider { + let userActivity = NSUserActivity(activityType: DetailView.userActivity) + let title: String = item.title ?? "location" + userActivity.title = "See daylight for \(title)" + userActivity.targetContentIdentifier = item.uuid?.uuidString + return NSItemProvider(object: userActivity) + } #endif @ViewBuilder @@ -155,8 +182,8 @@ struct SidebarListView: View { } extension SidebarListView { - private var sortedItems: [SavedLocation] { - items.sorted { lhs, rhs in + private func recomputeSortedItems() { + sortedItems = items.sorted { lhs, rhs in switch itemSortDimension { case .timezone: switch itemSortOrder { @@ -167,10 +194,11 @@ extension SidebarListView { } case .daylightDuration: guard let lhsSolar = NTSolar(for: timeMachine.date, coordinate: lhs.coordinate, timeZone: lhs.timeZone), - let rhsSolar = NTSolar(for: timeMachine.date, coordinate: rhs.coordinate, timeZone: rhs.timeZone) else { + let rhsSolar = NTSolar(for: timeMachine.date, coordinate: rhs.coordinate, timeZone: rhs.timeZone) + else { return true } - + switch itemSortOrder { case .forward: return lhsSolar.daylightDuration < rhsSolar.daylightDuration @@ -180,11 +208,11 @@ extension SidebarListView { } } } - + private func deleteItems(offsets: IndexSet) { withAnimation { offsets.map { sortedItems[$0] }.forEach(viewContext.delete) - + do { try viewContext.save() } catch { @@ -192,11 +220,11 @@ extension SidebarListView { } } } - + private func deleteItem(_ item: SavedLocation) { withAnimation { viewContext.delete(item) - + do { try viewContext.save() } catch { @@ -207,28 +235,28 @@ extension SidebarListView { } struct SidebarListView_Previews: PreviewProvider { - @Namespace static private var namespace + @Namespace private static var namespace static var previews: some View { NavigationStack { SidebarListView(namespace: namespace) } - .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - .withTimeMachine(.solsticeTimeMachine) - .environment(CurrentLocation()) - .environment(LocationSearchService()) + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + .withTimeMachine(.solsticeTimeMachine) + .environment(CurrentLocation()) + .environment(LocationSearchService()) } } -fileprivate extension SidebarListView { +private extension SidebarListView { struct ListRow: View { @AppStorage(Preferences.listViewAppearance) private var appearance var location: Location - + var body: some View { switch appearance { #if os(iOS) - case .graphical: - GraphicalLocationListRow(location: location) + case .graphical: + GraphicalLocationListRow(location: location) #endif default: LocationListRow(location: location) diff --git a/Solstice/LocationSearchResultRow.swift b/Solstice/LocationSearchResultRow.swift index eb1d92fc..d49cad64 100644 --- a/Solstice/LocationSearchResultRow.swift +++ b/Solstice/LocationSearchResultRow.swift @@ -1,31 +1,31 @@ // -// AddLocationView.swift +// LocationSearchResultRow.swift // Solstice // // Created by Daniel Eden on 02/10/2022. // -import SwiftUI import MapKit +import SwiftUI struct LocationSearchResultRow: View { @Environment(\.managedObjectContext) private var viewContext @Environment(\.dismissSearch) private var dismiss - + @SceneStorage("selectedLocation") private var selectedLocation: String? var searchService: LocationSearchService - - var items: Array = [] - + + var items: [SavedLocation] = [] + @State private var isAddingItem = false var result: MKLocalSearchCompletion - + var body: some View { Button { Task { guard let location = try? await getLocation(from: result) else { return } - + if let location = location as? TemporaryLocation { searchService.location = location } else if let locationId = (location as? SavedLocation)?.uuid { @@ -42,9 +42,9 @@ struct LocationSearchResultRow: View { .font(.footnote) } } - + Spacer() - + if isAddingItem { ProgressView() .controlSize(.small) @@ -54,32 +54,32 @@ struct LocationSearchResultRow: View { .contentShape(Rectangle()) .foregroundStyle(.primary) } - + func getLocation(from completion: MKLocalSearchCompletion) async throws -> (any ObservableLocation)? { isAddingItem = true let searchRequest = MKLocalSearch.Request(completion: completion) let searchResult = try await MKLocalSearch(request: searchRequest).start() if let item = searchResult.mapItems.first { let coords = item.placemark.coordinate - + guard let location = item.placemark.location else { return nil } - + if let savedLocation = items.first(where: { savedLocation in - // Avoid duplicate items by filtering locations less than 5km from the specified location - CLLocation( - latitude: savedLocation.coordinate.latitude, - longitude: savedLocation.coordinate.longitude - ).distance(from: location) < 5000 - }) { + // Avoid duplicate items by filtering locations less than 5km from the specified location + CLLocation( + latitude: savedLocation.coordinate.latitude, + longitude: savedLocation.coordinate.longitude + ).distance(from: location) < 5000 + }) { return savedLocation } let reverseGeocoding = try await CLGeocoder().reverseGeocodeLocation(location) searchService.queryFragment = "" isAddingItem = false - + return TemporaryLocation( title: completion.title, subtitle: completion.subtitle.isEmpty ? item.placemark.country : completion.subtitle, diff --git a/Solstice/Settings/AboutSolsticeView.swift b/Solstice/Settings/AboutSolsticeView.swift index 3b6a7319..3cc3248b 100644 --- a/Solstice/Settings/AboutSolsticeView.swift +++ b/Solstice/Settings/AboutSolsticeView.swift @@ -9,7 +9,7 @@ import SwiftUI struct AboutSolsticeView: View { @Environment(\.colorScheme) private var colorScheme - + var body: some View { VStack(alignment: .leading, spacing: 8) { HStack(alignment: .firstTextBaseline) { @@ -19,11 +19,12 @@ struct AboutSolsticeView: View { Text("Solstice") } .fontWeight(.medium) - + Spacer() - + if let buildVersionNumber = Bundle.main.buildVersionNumber, - let appVersionNumber = Bundle.main.releaseVersionNumber { + let appVersionNumber = Bundle.main.releaseVersionNumber + { Text("v\(appVersionNumber), #\(buildVersionNumber)") .font(.caption) .foregroundStyle(.secondary) @@ -31,13 +32,13 @@ struct AboutSolsticeView: View { } } .font(.title2) - + Group { Text("Solstice is an app made by [Dan Eden](https://daneden.me), a designer and developer based in Manchester, England.") Text("Dan is prone to Seasonal Affective Disorder (SAD), a mood disorder often associated with the reduction in total daily sunlight hours during the darker months of the year. Solstice was created to help everyone look forward to brighter days.") - + Divider() - + NavigationLink(destination: FullStoryView()) { Text("Read more") .fontWeight(.medium) @@ -48,19 +49,18 @@ struct AboutSolsticeView: View { .font(.subheadline) .foregroundStyle(.secondary) } -#if os(iOS) + #if os(iOS) .blendMode(colorScheme == .light ? .plusDarker : .plusLighter) .listRowBackground(Color.clear.background(Color.accentColor.quinary)) -#endif + #endif } } -fileprivate struct FullStoryView: View { +private struct FullStoryView: View { var aboutString: String { if let filepath = Bundle.main.path(forResource: "README", ofType: "md") { do { - let contents = try String(contentsOfFile: filepath, encoding: .utf8) - return contents + return try String(contentsOfFile: filepath, encoding: .utf8) } catch { print("Markdown file README.md could not be parsed") return "App info" @@ -70,13 +70,13 @@ fileprivate struct FullStoryView: View { return "App info" } } - + var markdownLines: [AttributedString] { aboutString.split(whereSeparator: \.isNewline).suffix(from: 1).map { line in (try? AttributedString(markdown: String(line))) ?? AttributedString() } } - + var body: some View { Form { Section { diff --git a/Solstice/Settings/AppearanceSettingsView.swift b/Solstice/Settings/AppearanceSettingsView.swift index 873d7caa..4e954a8a 100644 --- a/Solstice/Settings/AppearanceSettingsView.swift +++ b/Solstice/Settings/AppearanceSettingsView.swift @@ -12,18 +12,18 @@ struct AppearanceSettingsView: View { @AppStorage(Preferences.detailViewChartAppearance) private var chartAppearance @AppStorage(Preferences.listViewAppearance) private var listAppearance @AppStorage(Preferences.chartType) private var chartType - - var body: some View { - Form { - timeTravelSection - #if os(iOS) + + var body: some View { + Form { + timeTravelSection + #if os(iOS) listAppearanceSection - #endif - chartAppearanceSection - } - .navigationTitle("Appearance") - .formStyle(.grouped) - } + #endif + chartAppearanceSection + } + .navigationTitle("Appearance") + .formStyle(.grouped) + } private var timeTravelSection: some View { Section("Time Travel") { @@ -62,18 +62,18 @@ struct AppearanceSettingsView: View { } #if os(iOS) - private var listAppearanceSection: some View { - Section("List appearance") { - Picker(selection: $listAppearance.animation()) { - ForEach(DaylightChart.Appearance.allCases, id: \.self) { appearance in - Text(appearance.description) + private var listAppearanceSection: some View { + Section("List appearance") { + Picker(selection: $listAppearance.animation()) { + ForEach(DaylightChart.Appearance.allCases, id: \.self) { appearance in + Text(appearance.description) + } + } label: { + Text("List row style") } - } label: { - Text("List row style") + .pickerStyle(.segmented) } - .pickerStyle(.segmented) } - } #endif private var chartAppearanceSection: some View { @@ -94,7 +94,7 @@ struct AppearanceSettingsView: View { } private func chartTypeButton(for type: ChartType) -> some View { - let isActive: Bool = self.chartType == type + let isActive: Bool = chartType == type return Button { self.chartType = type } label: { @@ -130,5 +130,5 @@ struct AppearanceSettingsView: View { } #Preview { - AppearanceSettingsView() + AppearanceSettingsView() } diff --git a/Solstice/Settings/NotificationSettings.swift b/Solstice/Settings/NotificationSettings.swift index f6fdefce..8075143c 100644 --- a/Solstice/Settings/NotificationSettings.swift +++ b/Solstice/Settings/NotificationSettings.swift @@ -5,35 +5,36 @@ // Created by Daniel Eden on 24/02/2023. // -import SwiftUI import CoreLocation +import SwiftUI -fileprivate typealias NotificationFragment = (label: String, value: Binding) +private typealias NotificationFragment = (label: String, value: Binding) struct NotificationSettings: View { @AppStorage(Preferences.notificationsEnabled) var notificationsEnabled - + @AppStorage(Preferences.customNotificationLocationUUID) var customNotificationLocationUUID - + @AppStorage(Preferences.NotificationSettings.scheduleType) var scheduleType @AppStorage(Preferences.NotificationSettings.relativeOffset) var notificationOffset @AppStorage(Preferences.NotificationSettings.notificationDateComponents) var notificationDateComponents - + // Notification fragment settings @AppStorage(Preferences.notificationsIncludeSunTimes) var notifsIncludeSunTimes @AppStorage(Preferences.notificationsIncludeDaylightDuration) var notifsIncludeDaylightDuration @AppStorage(Preferences.notificationsIncludeSolsticeCountdown) var notifsIncludeSolsticeCountdown @AppStorage(Preferences.notificationsIncludeDaylightChange) var notifsIncludeDaylightChange - + @AppStorage(Preferences.sadPreference) var sadPreference - + @Environment(CurrentLocation.self) var currentLocation - + @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \SavedLocation.title, ascending: true)], - animation: .default) + animation: .default + ) private var items: FetchedResults - + fileprivate var notificationFragments: [NotificationFragment] { [ (label: "Sunrise/sunset times", value: $notifsIncludeSunTimes), @@ -42,7 +43,7 @@ struct NotificationSettings: View { (label: "Time until next solstice", value: $notifsIncludeSolsticeCountdown), ] } - + private var notificationTime: Binding { Binding { let hour = notificationDateComponents.hour ?? 0 @@ -52,7 +53,7 @@ struct NotificationSettings: View { notificationDateComponents = Calendar.autoupdatingCurrent.dateComponents([.hour, .minute, .timeZone], from: $0) } } - + var body: some View { Form { enableSection @@ -94,7 +95,7 @@ struct NotificationSettings: View { @ViewBuilder private var locationPickerContent: some View { if currentLocation.isAuthorized { - Text("Current location") + Text("Current Location") .tag(String?.none) } else if customNotificationLocationUUID == nil { Text("Select location") @@ -196,9 +197,9 @@ struct NotificationPreview: View { @AppStorage(Preferences.notificationsIncludeDaylightDuration) private var notifsIncludeDaylightDuration @AppStorage(Preferences.notificationsIncludeSolsticeCountdown) private var notifsIncludeSolsticeCountdown @AppStorage(Preferences.notificationsIncludeDaylightChange) private var notifsIncludeDaylightChange - + @State var content: NotificationManager.NotificationContent? - + var body: some View { VStack { if let content { @@ -207,7 +208,7 @@ struct NotificationPreview: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 20, height: 20) - + VStack(alignment: .leading) { Text(content.title).font(.footnote.bold()) Text(content.body).font(.footnote.leading(.tight)) @@ -215,7 +216,7 @@ struct NotificationPreview: View { .lineLimit(4) } .contentTransition(.numericText()) - + Spacer(minLength: 0) } .padding(.vertical, 8) @@ -241,7 +242,6 @@ struct NotificationPreview: View { } } - struct NotificationSettings_Previews: PreviewProvider { static var previews: some View { NotificationSettings() diff --git a/Solstice/Settings/SettingsView.swift b/Solstice/Settings/SettingsView.swift index 1d6bee87..540b1904 100644 --- a/Solstice/Settings/SettingsView.swift +++ b/Solstice/Settings/SettingsView.swift @@ -7,34 +7,34 @@ import SwiftUI #if os(macOS) -import AppKit + import AppKit #endif struct SettingsView: View { @Environment(\.dismiss) private var dismiss @Environment(\.openURL) private var openURL @Environment(CurrentLocation.self) private var currentLocation - + @AppStorage(Preferences.timeTravelAppearance) private var timeTravelAppearance @AppStorage(Preferences.chartType) private var chartType - - var body: some View { - NavigationStack { - Form { + + var body: some View { + NavigationStack { + Form { + Section { + AboutSolsticeView() + } + + if !currentLocation.isAuthorized { Section { - AboutSolsticeView() - } - - if !currentLocation.isAuthorized { - Section { - Button("Enable location services", systemImage: "location") { - #if os(macOS) + Button("Enable location services", systemImage: "location") { + #if os(macOS) // On macOS, open System Settings directly since requestWhenInUseAuthorization // doesn't reliably trigger the authorization prompt if let url = URL(string: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_LocationServices") { NSWorkspace.shared.open(url) } - #else + #else switch currentLocation.authorizationStatus { case .notDetermined: currentLocation.requestAccess() @@ -44,32 +44,32 @@ struct SettingsView: View { } default: return } - #endif - } - } header: { - Text("Location") - } footer: { - Text("Enable location services to see the daylight duration in your current location") + #endif } + } header: { + Text("Location") + } footer: { + Text("Enable location services to see the daylight duration in your current location") } - - Section { - NavigationLink { - AppearanceSettingsView() - } label: { - Label("Appearance", systemImage: "paintpalette") - } - - NavigationLink { - NotificationSettings() - } label: { - Label("Notifications", systemImage: "bell.badge") - } + } + + Section { + NavigationLink { + AppearanceSettingsView() + } label: { + Label("Appearance", systemImage: "paintpalette") + } + + NavigationLink { + NotificationSettings() + } label: { + Label("Notifications", systemImage: "bell.badge") } - - SupporterSettings() - - #if os(iOS) + } + + SupporterSettings() + + #if os(iOS) Section { NavigationLink { EquinoxAndSolsticeInfoSheet() @@ -77,27 +77,27 @@ struct SettingsView: View { Label("About solstices and equinoxes", systemImage: "info.circle") } } - #endif - } - #if os(visionOS) - .navigationTitle("Settings") #endif - .formStyle(.grouped) - } - #if !os(macOS) - .toolbar { - Button { - dismiss() - } label: { - Text("Close") - } } + #if os(visionOS) + .navigationTitle("Settings") #endif - } + .formStyle(.grouped) + } + #if !os(macOS) + .toolbar { + Button { + dismiss() + } label: { + Text("Close") + } + } + #endif + } } struct SettingsView_Previews: PreviewProvider { - static var previews: some View { - SettingsView() - } + static var previews: some View { + SettingsView() + } } diff --git a/Solstice/Settings/SupporterSettings.swift b/Solstice/Settings/SupporterSettings.swift index 6020ee88..19cb7901 100644 --- a/Solstice/Settings/SupporterSettings.swift +++ b/Solstice/Settings/SupporterSettings.swift @@ -5,25 +5,26 @@ // Created by Daniel Eden on 24/02/2023. // -import SwiftUI import StoreKit +import SwiftUI import YapKit -fileprivate extension FeedbackConfig { +private extension FeedbackConfig { static let solstice = FeedbackConfig(apiKey: Secrets.yapKitAPIKey) } // MARK: In-App Purchase Product IDs + let iapProductIDs = Set(Constants.IAPProducts.all) struct SupporterSettings: View { private let appStoreReviewURL: URL? = URL(string: "https://apps.apple.com/app/id1547580907?action=write-review") - - @State var products: [Product] = [] - @State var latestTransaction: StoreKit.Transaction? - @State var purchaseInProgress = false - @State var feedbackFormPresented = false - + + @State private var products: [Product] = [] + @State private var latestTransaction: StoreKit.Transaction? + @State private var purchaseInProgress = false + @State private var feedbackFormPresented = false + var body: some View { if let appStoreReviewURL { Link(destination: appStoreReviewURL) { @@ -33,27 +34,27 @@ struct SupporterSettings: View { await fetchProducts() } } - + Button("Submit feedback", systemImage: "exclamationmark.bubble") { feedbackFormPresented = true } .sheet(isPresented: $feedbackFormPresented) { FeedbackView(config: .solstice) } - + if !products.isEmpty { Section(header: Label("Leave a tip", systemImage: "heart")) { if latestTransaction != nil { Text("**Thank you so much for your support.** Feel free to leave another tip in the future if you’re feeling generous.") .padding(.vertical, 4) } - - ForEach(products.sorted { $0.price > $1.price }, id: \.id) { product in + + ForEach(products, id: \.id) { product in HStack { Text(product.displayName) - + Spacer() - + Button { Task { self.latestTransaction = try await purchaseProduct(product) @@ -63,7 +64,7 @@ struct SupporterSettings: View { } .buttonStyle(.bordered) #if os(iOS) - .buttonBorderShape(.capsule) + .buttonBorderShape(.capsule) #endif } } @@ -72,10 +73,11 @@ struct SupporterSettings: View { .disabled(purchaseInProgress) } } - + func fetchProducts() async { do { let products = try await Product.products(for: iapProductIDs) + .sorted { $0.price > $1.price } withAnimation { self.products = products } @@ -83,30 +85,30 @@ struct SupporterSettings: View { print("Unable to fetch products") } } - + func purchaseProduct(_ product: Product) async throws -> StoreKit.Transaction { purchaseInProgress = true - + #if os(visionOS) - guard let scene = UIApplication.shared.connectedScenes.first else { - throw PurchaseError.failed - } - let result = try await product.purchase(confirmIn: scene) + guard let scene = UIApplication.shared.connectedScenes.first else { + throw PurchaseError.failed + } + let result = try await product.purchase(confirmIn: scene) #else - - let result = try await product.purchase() + + let result = try await product.purchase() #endif - + purchaseInProgress = false - + switch result { case .pending: throw PurchaseError.pending - case .success(let verification): + case let .success(verification): switch verification { - case .verified(let transaction): + case let .verified(transaction): await transaction.finish() - + return transaction case .unverified: throw PurchaseError.failed @@ -125,7 +127,7 @@ enum PurchaseError: Error { } struct SupporterSettings_Previews: PreviewProvider { - static var previews: some View { - SupporterSettings() - } + static var previews: some View { + SupporterSettings() + } } diff --git a/Solstice/SolsticeApp.swift b/Solstice/SolsticeApp.swift index ce164876..b413316f 100644 --- a/Solstice/SolsticeApp.swift +++ b/Solstice/SolsticeApp.swift @@ -5,10 +5,10 @@ // Created by Daniel Eden on 29/09/2022. // -import SwiftUI import StoreKit -import TipKit +import SwiftUI import TimeMachine +import TipKit @main struct SolsticeApp: App { @@ -25,7 +25,7 @@ struct SolsticeApp: App { .task { for await result in Transaction.updates { switch result { - case .verified(let transaction): + case let .verified(transaction): print("Transaction verified in listener") await transaction.finish() case .unverified: @@ -49,34 +49,34 @@ struct SolsticeApp: App { #if os(visionOS) .defaultSize(width: 900, height: 720) #endif - + #if os(visionOS) - WindowGroup(id: "settings") { - SettingsView() - .environment(currentLocation) - } - .defaultSize(width: 600, height: 600) - - WindowGroup(Text("About solstices and equinoxes")) { _ in - EquinoxAndSolsticeInfoWindow() - } defaultValue: { - AnnualSolarEvent.juneSolstice - } - .defaultSize(width: 900, height: 612) + WindowGroup(id: "settings") { + SettingsView() + .environment(currentLocation) + } + .defaultSize(width: 600, height: 600) + + WindowGroup(Text("About solstices and equinoxes")) { _ in + EquinoxAndSolsticeInfoWindow() + } defaultValue: { + AnnualSolarEvent.juneSolstice + } + .defaultSize(width: 900, height: 612) #endif - + #if os(macOS) - Settings { - SettingsView() - .frame(maxWidth: 500) - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environment(currentLocation) - } - - Window("About solstices and equinoxes", id: "about-equinox-and-solstice") { - EquinoxAndSolsticeInfoSheet() - } - .defaultSize(width: 400, height: 650) + Settings { + SettingsView() + .frame(maxWidth: 500) + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environment(currentLocation) + } + + Window("About solstices and equinoxes", id: "about-equinox-and-solstice") { + EquinoxAndSolsticeInfoSheet() + } + .defaultSize(width: 400, height: 650) #endif } } diff --git a/SolsticeTests/SolsticeCalculatorTests.swift b/SolsticeTests/SolsticeCalculatorTests.swift index 17570f51..0320a43f 100644 --- a/SolsticeTests/SolsticeCalculatorTests.swift +++ b/SolsticeTests/SolsticeCalculatorTests.swift @@ -5,18 +5,19 @@ // Created with Swift Testing framework // -import Testing import Foundation @testable import Solstice +import Testing struct SolsticeCalculatorTests { // MARK: - Known astronomical dates for validation + // Reference values from the US Naval Observatory / timeanddate.com @Test("June solstice 2024 falls on June 20") - func juneSolstice2024() { + func juneSolstice2024() throws { let solstice = SolsticeCalculator.juneSolstice(year: 2024) - let components = Calendar.current.dateComponents(in: TimeZone(identifier: "UTC")!, from: solstice) + let components = try Calendar.current.dateComponents(in: #require(TimeZone(identifier: "UTC")), from: solstice) #expect(components.month == 6) #expect(components.day == 20) @@ -24,9 +25,9 @@ struct SolsticeCalculatorTests { } @Test("December solstice 2024 falls on December 21") - func decemberSolstice2024() { + func decemberSolstice2024() throws { let solstice = SolsticeCalculator.decemberSolstice(year: 2024) - let components = Calendar.current.dateComponents(in: TimeZone(identifier: "UTC")!, from: solstice) + let components = try Calendar.current.dateComponents(in: #require(TimeZone(identifier: "UTC")), from: solstice) #expect(components.month == 12) #expect(components.day == 21) @@ -34,9 +35,9 @@ struct SolsticeCalculatorTests { } @Test("March equinox 2024 falls on March 20") - func marchEquinox2024() { + func marchEquinox2024() throws { let equinox = SolsticeCalculator.marchEquinox(year: 2024) - let components = Calendar.current.dateComponents(in: TimeZone(identifier: "UTC")!, from: equinox) + let components = try Calendar.current.dateComponents(in: #require(TimeZone(identifier: "UTC")), from: equinox) #expect(components.month == 3) #expect(components.day == 20) @@ -44,9 +45,9 @@ struct SolsticeCalculatorTests { } @Test("September equinox 2024 falls on September 22") - func septemberEquinox2024() { + func septemberEquinox2024() throws { let equinox = SolsticeCalculator.septemberEquinox(year: 2024) - let components = Calendar.current.dateComponents(in: TimeZone(identifier: "UTC")!, from: equinox) + let components = try Calendar.current.dateComponents(in: #require(TimeZone(identifier: "UTC")), from: equinox) #expect(components.month == 9) #expect(components.day == 22) @@ -56,32 +57,32 @@ struct SolsticeCalculatorTests { // MARK: - Cross-year consistency @Test("Solstices and equinoxes are calculated for multiple years", arguments: [2020, 2021, 2022, 2023, 2024, 2025, 2026]) - func eventsReturnValidDates(year: Int) { + func eventsReturnValidDates(year: Int) throws { let juneSolstice = SolsticeCalculator.juneSolstice(year: year) let decemberSolstice = SolsticeCalculator.decemberSolstice(year: year) let marchEquinox = SolsticeCalculator.marchEquinox(year: year) let septemberEquinox = SolsticeCalculator.septemberEquinox(year: year) - let juneSolsticeComponents = Calendar.current.dateComponents(in: TimeZone(identifier: "UTC")!, from: juneSolstice) - let decemberSolsticeComponents = Calendar.current.dateComponents(in: TimeZone(identifier: "UTC")!, from: decemberSolstice) - let marchEquinoxComponents = Calendar.current.dateComponents(in: TimeZone(identifier: "UTC")!, from: marchEquinox) - let septemberEquinoxComponents = Calendar.current.dateComponents(in: TimeZone(identifier: "UTC")!, from: septemberEquinox) + let juneSolsticeComponents = try Calendar.current.dateComponents(in: #require(TimeZone(identifier: "UTC")), from: juneSolstice) + let decemberSolsticeComponents = try Calendar.current.dateComponents(in: #require(TimeZone(identifier: "UTC")), from: decemberSolstice) + let marchEquinoxComponents = try Calendar.current.dateComponents(in: #require(TimeZone(identifier: "UTC")), from: marchEquinox) + let septemberEquinoxComponents = try Calendar.current.dateComponents(in: #require(TimeZone(identifier: "UTC")), from: septemberEquinox) // June solstice should be in June (day 20 or 21) #expect(juneSolsticeComponents.month == 6) - #expect((20...21).contains(juneSolsticeComponents.day!)) + #expect(try (20 ... 21).contains(#require(juneSolsticeComponents.day))) // December solstice should be in December (day 21 or 22) #expect(decemberSolsticeComponents.month == 12) - #expect((21...22).contains(decemberSolsticeComponents.day!)) + #expect(try (21 ... 22).contains(#require(decemberSolsticeComponents.day))) // March equinox should be in March (day 19-21) #expect(marchEquinoxComponents.month == 3) - #expect((19...21).contains(marchEquinoxComponents.day!)) + #expect(try (19 ... 21).contains(#require(marchEquinoxComponents.day))) // September equinox should be in September (day 22-23) #expect(septemberEquinoxComponents.month == 9) - #expect((22...23).contains(septemberEquinoxComponents.day!)) + #expect(try (22 ... 23).contains(#require(septemberEquinoxComponents.day))) } // MARK: - Ordering @@ -106,7 +107,7 @@ struct SolsticeCalculatorTests { let date = Date() let solstices = date.recentSolstices - for i in 1.. some View { CountdownWidgetView(entry: timelineEntry) .containerBackground(for: .widget) { diff --git a/Widget/Countdown Widget/CountdownWidgetView+AccessoryWidgetViews.swift b/Widget/Countdown Widget/CountdownWidgetView+AccessoryWidgetViews.swift index 090c9318..8d7543c1 100644 --- a/Widget/Countdown Widget/CountdownWidgetView+AccessoryWidgetViews.swift +++ b/Widget/Countdown Widget/CountdownWidgetView+AccessoryWidgetViews.swift @@ -6,105 +6,109 @@ // #if !os(macOS) -import SwiftUI -import WidgetKit -import Suite + import Suite + import SwiftUI + import WidgetKit -extension CountdownWidgetView { - struct AccessoryInlineView: View { - var nextEvent: NTSolar.Event - var body: some View { - HStack { - Image(systemName: nextEvent.imageName) - Text("\(nextEvent.date, style: .time), \(nextEvent.date, style: .relative)") + extension CountdownWidgetView { + struct AccessoryInlineView: View { + var nextEvent: NTSolar.Event + var body: some View { + HStack { + Image(systemName: nextEvent.imageName) + Text("\(nextEvent.date, style: .time), \(nextEvent.date, style: .relative)") + } } } - } - - struct AccessoryCircularView: View { - @Environment(\.widgetRenderingMode) var widgetRenderingMode - - var entryDate: Date - - var previousEvent: NTSolar.Event - var nextEvent: NTSolar.Event - - @ViewBuilder - var currentValueLabel: some View { - let duration = entryDate.distance(to: nextEvent.date) - if duration >= 60 * 60 { - Text(Duration.seconds(duration).formatted(.units(width: .narrow, maximumUnitCount: 1))) - } else { - Text(nextEvent.date, style: .timer) - .monospacedDigit() + + struct AccessoryCircularView: View { + @Environment(\.widgetRenderingMode) var widgetRenderingMode + + var entryDate: Date + + var previousEvent: NTSolar.Event + var nextEvent: NTSolar.Event + + @ViewBuilder + var currentValueLabel: some View { + let duration = entryDate.distance(to: nextEvent.date) + if duration >= 60 * 60 { + Text(Duration.seconds(duration).formatted(.units(width: .narrow, maximumUnitCount: 1))) + } else { + Text(nextEvent.date, style: .timer) + .monospacedDigit() + } } - } - - var body: some View { - ZStack { - ProgressView(timerInterval: previousEvent.date...nextEvent.date) { - nextEventText - } currentValueLabel: { - VStack { - Image(systemName: nextEvent.imageName) - currentValueLabel + + var body: some View { + ZStack { + ProgressView(timerInterval: previousEvent.date ... nextEvent.date) { + nextEventText + } currentValueLabel: { + VStack { + Image(systemName: nextEvent.imageName) + currentValueLabel + } + .font(.caption) } - .font(.caption) + .progressViewStyle(.circular) + .tint(.accent) + .widgetAccentable() } - .progressViewStyle(.circular) - .tint(.accent) - .widgetAccentable() + .widgetLabel { nextEventText } + } + + var nextEventText: some View { + Text("\(nextEvent.description.localizedCapitalized) in \(Text(nextEvent.date, style: .relative))") } - .widgetLabel { nextEventText } - } - - var nextEventText: some View { - Text("\(nextEvent.description.localizedCapitalized) in \(Text(nextEvent.date, style: .relative))") } - } - - struct AccessoryRectangularView: View { - var nextEvent: NTSolar.Event - - var body: some View { - HStack { - VStack(alignment: .leading) { - Text("\(Image(systemName: nextEvent.imageName)) \(nextEvent.label)") + + struct AccessoryRectangularView: View { + var nextEvent: NTSolar.Event + + var body: some View { + HStack { + VStack(alignment: .leading) { + Label { + Text(nextEvent.label) + } icon: { + Image(systemName: nextEvent.imageName) + } .font(.headline) .widgetAccentable() .imageScale(.small) .transition(.move(edge: .bottom)) - Text(nextEvent.date, style: .relative) - .contentTransition(.numericText()) - Text(nextEvent.date, style: .time) - .foregroundColor(.secondary) - .contentTransition(.numericText()) + Text(nextEvent.date, style: .relative) + .contentTransition(.numericText()) + Text(nextEvent.date, style: .time) + .foregroundStyle(.secondary) + .contentTransition(.numericText()) + } + + Spacer(minLength: 0) } - - Spacer(minLength: 0) + .animation(.default, value: nextEvent) } - .animation(.default, value: nextEvent) } - } - - struct AccessoryCornerView: View { - var previousEvent: NTSolar.Event - var nextEvent: NTSolar.Event - - var body: some View { - Image(systemName: nextEvent.imageName) - .font(.title.bold()) - .imageScale(.large) - .symbolRenderingMode(.hierarchical) - .symbolVariant(.fill) - .widgetLabel { - let time = Text(nextEvent.date, style: .time) - let countdown = Text(nextEvent.date, style: .relative) - - Text("\(time), \(countdown.textScale(.secondary))") - } - .widgetAccentable() + + struct AccessoryCornerView: View { + var previousEvent: NTSolar.Event + var nextEvent: NTSolar.Event + + var body: some View { + Image(systemName: nextEvent.imageName) + .font(.title.bold()) + .imageScale(.large) + .symbolRenderingMode(.hierarchical) + .symbolVariant(.fill) + .widgetLabel { + let time = Text(nextEvent.date, style: .time) + let countdown = Text(nextEvent.date, style: .relative) + + Text("\(time), \(countdown.textScale(.secondary))") + } + .widgetAccentable() + } } } -} #endif diff --git a/Widget/Countdown Widget/CountdownWidgetView.swift b/Widget/Countdown Widget/CountdownWidgetView.swift index 31f66317..381d0b5c 100644 --- a/Widget/Countdown Widget/CountdownWidgetView.swift +++ b/Widget/Countdown Widget/CountdownWidgetView.swift @@ -5,9 +5,9 @@ // Created by Daniel Eden on 21/02/2023. // +import Suite import SwiftUI import WidgetKit -import Suite struct CountdownWidgetView: SolsticeWidgetView { @Environment(\.widgetFamily) var family @@ -15,46 +15,47 @@ struct CountdownWidgetView: SolsticeWidgetView { @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground var entry: SolsticeWidgetTimelineEntry - + var body: some View { Group { if let location, - let nextSolarEvent, - let previousSolarEvent { + let nextSolarEvent, + let previousSolarEvent + { switch family { -#if os(watchOS) || os(iOS) - case .accessoryCircular: - AccessoryCircularView( - entryDate: entry.date, - previousEvent: previousSolarEvent, - nextEvent: nextSolarEvent - ) - case .accessoryInline: - AccessoryInlineView(nextEvent: nextSolarEvent) - case .accessoryRectangular: - AccessoryRectangularView(nextEvent: nextSolarEvent) -#if os(watchOS) - case .accessoryCorner: - AccessoryCornerView(previousEvent: previousSolarEvent, nextEvent: nextSolarEvent) -#endif // end watchOS -#endif // end !macOS + #if os(watchOS) || os(iOS) + case .accessoryCircular: + AccessoryCircularView( + entryDate: entry.date, + previousEvent: previousSolarEvent, + nextEvent: nextSolarEvent + ) + case .accessoryInline: + AccessoryInlineView(nextEvent: nextSolarEvent) + case .accessoryRectangular: + AccessoryRectangularView(nextEvent: nextSolarEvent) + #if os(watchOS) + case .accessoryCorner: + AccessoryCornerView(previousEvent: previousSolarEvent, nextEvent: nextSolarEvent) + #endif // end watchOS + #endif // end !macOS default: VStack(alignment: .leading, spacing: 4) { WidgetLocationView(location: location) - + Spacer(minLength: 0) - + HStack { nextEventText .widgetHeading() .minimumScaleFactor(0.8) .lineLimit(3) .contentTransition(.numericText()) - + Spacer() } - - Label{ + + Label { Text(nextSolarEvent.date, style: .time) } icon: { Image(systemName: nextSolarEvent.imageName) @@ -89,15 +90,15 @@ extension CountdownWidgetView { var nextSolarEvent: NTSolar.Event? { solar?.nextSolarEvent } - + var previousSolarEvent: NTSolar.Event? { solar?.previousSolarEvent } - + var timeZone: TimeZone { location?.timeZone ?? .autoupdatingCurrent } - + var nextEventText: some View { if let nextSolarEvent { return Text("\(nextSolarEvent.description.localizedCapitalized) in \(Text(nextSolarEvent.date, style: .relative))") @@ -105,7 +106,7 @@ extension CountdownWidgetView { return Text("—") } } - + var currentEventImageName: String { nextSolarEvent?.phase == .sunrise ? "moon.stars" : "sun.max" } @@ -114,13 +115,13 @@ extension CountdownWidgetView { struct CountdownWidgetPreview: PreviewProvider { static var previews: some View { #if !os(watchOS) - CountdownWidgetView(entry: SolsticeWidgetTimelineEntry(date: .now)) - .previewContext(WidgetPreviewContext(family: .systemSmall)) + CountdownWidgetView(entry: SolsticeWidgetTimelineEntry(date: .now)) + .previewContext(WidgetPreviewContext(family: .systemSmall)) #endif - + #if os(watchOS) || os(iOS) - CountdownWidgetView(entry: SolsticeWidgetTimelineEntry(date: .now)) - .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) + CountdownWidgetView(entry: SolsticeWidgetTimelineEntry(date: .now)) + .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) #endif } } diff --git a/Widget/Helpers/CompactLabelStyle.swift b/Widget/Helpers/CompactLabelStyle.swift index c187f57b..4ca970b3 100644 --- a/Widget/Helpers/CompactLabelStyle.swift +++ b/Widget/Helpers/CompactLabelStyle.swift @@ -30,5 +30,5 @@ struct CompactLabelStyle: LabelStyle { } icon: { Image(systemName: "sparkles") } - .labelStyle(CompactLabelStyle()) + .labelStyle(CompactLabelStyle()) } diff --git a/Widget/Helpers/SolsticeWidgetLocationManager.swift b/Widget/Helpers/SolsticeWidgetLocationManager.swift index 29db4f5f..d2ebf73d 100644 --- a/Widget/Helpers/SolsticeWidgetLocationManager.swift +++ b/Widget/Helpers/SolsticeWidgetLocationManager.swift @@ -4,8 +4,8 @@ // // Created by Daniel Eden on 20/01/2026. // -import Foundation import CoreLocation +import Foundation actor SolsticeWidgetLocationManager { static let shared = SolsticeWidgetLocationManager() @@ -19,12 +19,12 @@ actor SolsticeWidgetLocationManager { private var cachedLocationDataCoordinate: CLLocation? private let geocoder = CLGeocoder() private let significantDistanceChange: CLLocationDistance = 500 - + static var isAuthorized: Bool { #if os(watchOS) - [.authorizedAlways, .authorizedWhenInUse].contains(CLLocationManager().authorizationStatus) + [.authorizedAlways, .authorizedWhenInUse].contains(CLLocationManager().authorizationStatus) #else - CLLocationManager().isAuthorizedForWidgetUpdates + CLLocationManager().isAuthorizedForWidgetUpdates #endif } @@ -33,7 +33,8 @@ actor SolsticeWidgetLocationManager { // First, check the in-memory cache if let cachedLocation, let cacheTimestamp, - Date().timeIntervalSince(cacheTimestamp) < cacheValidityDuration { + Date().timeIntervalSince(cacheTimestamp) < cacheValidityDuration + { return cachedLocation } @@ -105,7 +106,8 @@ actor SolsticeWidgetLocationManager { // Reuse cached location data if location hasn't moved significantly if let cached = cachedLocationData, let cachedCoord = cachedLocationDataCoordinate, - location.distance(from: cachedCoord) < significantDistanceChange { + location.distance(from: cachedCoord) < significantDistanceChange + { return (location, cached) } diff --git a/Widget/Helpers/SolsticeWidgetTimelineProvider.swift b/Widget/Helpers/SolsticeWidgetTimelineProvider.swift index 206e82f4..a6189a96 100644 --- a/Widget/Helpers/SolsticeWidgetTimelineProvider.swift +++ b/Widget/Helpers/SolsticeWidgetTimelineProvider.swift @@ -5,11 +5,11 @@ // Created by Daniel Eden on 03/04/2023. // -import WidgetKit -import CoreLocation -import CoreData import AppIntents +import CoreData +import CoreLocation import SwiftUI +import WidgetKit struct SolsticeWidgetTimelineEntry: TimelineEntry { let date: Date @@ -36,7 +36,7 @@ struct SolsticeTimelineProvider: AppIntentTimelineProvider { let context = PersistenceController.shared.container.viewContext let request = SavedLocation.fetchRequest() request.sortDescriptors = [NSSortDescriptor(keyPath: \SavedLocation.title, ascending: true)] - + let currentLocationRecommendation = AppIntentRecommendation( intent: Intent(selectedLocation: .currentLocation), description: Text("Current Location") @@ -48,7 +48,7 @@ struct SolsticeTimelineProvider: AppIntentTimelineProvider { // Deduplicate by title to avoid duplicate descriptions in the complication picker // (can happen with iCloud sync creating duplicate saved locations) - var seenTitles: Set = ["Current Location"] + var seenTitles: Set = ["Current Location"] let savedLocationRecommendations = savedLocations.compactMap { savedLocation -> AppIntentRecommendation? in let title = savedLocation.title ?? "Location" guard seenTitles.insert(title).inserted else { return nil } @@ -66,16 +66,17 @@ struct SolsticeTimelineProvider: AppIntentTimelineProvider { private func getLocation(for placemark: CLPlacemark? = nil, isRealLocation: Bool = false) -> SolsticeWidgetLocation? { guard let placemark, - let location = placemark.location else { + let location = placemark.location + else { return isRealLocation ? nil : .defaultLocation } return SolsticeWidgetLocation(title: placemark.locality, - subtitle: placemark.country, - timeZoneIdentifier: placemark.timeZone?.identifier, - latitude: location.coordinate.latitude, - longitude: location.coordinate.longitude, - isRealLocation: isRealLocation) + subtitle: placemark.country, + timeZoneIdentifier: placemark.timeZone?.identifier, + latitude: location.coordinate.latitude, + longitude: location.coordinate.longitude, + isRealLocation: isRealLocation) } /// Creates a SolsticeWidgetLocation from a LocationAppEntity @@ -144,132 +145,136 @@ struct SolsticeTimelineProvider: AppIntentTimelineProvider { func snapshot(for configuration: Intent, in context: Context) async -> SolsticeWidgetTimelineEntry { let (widgetLocation, _, error) = await fetchWidgetLocation(for: configuration) let resolvedLocation = widgetLocation ?? (context.isPreview ? .proxiedToTimeZone : nil) - let entry = SolsticeWidgetTimelineEntry( + return SolsticeWidgetTimelineEntry( date: Date(), location: resolvedLocation, locationError: context.isPreview ? nil : error ) - - return entry } func timeline(for configuration: SolsticeConfigurationIntent, in context: Context) async -> Timeline { let (widgetLocation, _, error) = await fetchWidgetLocation(for: configuration) - + let currentDate = Date() let calendar = Calendar.current - + guard let coordinate = widgetLocation?.coordinate, - let todaySolar = NTSolar(for: currentDate, coordinate: coordinate, timeZone: widgetLocation?.timeZone ?? .autoupdatingCurrent) else { + let todaySolar = NTSolar(for: currentDate, coordinate: coordinate, timeZone: widgetLocation?.timeZone ?? .autoupdatingCurrent) + else { return Timeline( entries: [ - SolsticeWidgetTimelineEntry(date: currentDate, location: widgetLocation, locationError: error) + SolsticeWidgetTimelineEntry(date: currentDate, location: widgetLocation, locationError: error), ], policy: .after(.now.addingTimeInterval(60 * 15)) ) } - + let daysToGenerate: Int switch context.family { - #if os(watchOS) - /// watchOS widget complications are always considered "viewed" when installed on a watch face. - /// To prevent timeline generation from occurring too frequently and exhausting generation budgets, - /// we lower the number of total entries generated by only generating enough for one day. - case .accessoryCircular, .accessoryCorner, .accessoryInline, .accessoryRectangular: - daysToGenerate = 1 - #endif + #if os(watchOS) + // watchOS widget complications are always considered "viewed" when installed on a watch face. + // To prevent timeline generation from occurring too frequently and exhausting generation budgets, + // we lower the number of total entries generated by only generating enough for one day. + case .accessoryCircular, .accessoryCorner, .accessoryInline, .accessoryRectangular: + daysToGenerate = 1 + #endif default: daysToGenerate = 3 } - + var allKeyTimes: [Date] = [currentDate] var allHourlyTimes: [Date] = [] var solarByDay: [Date: NTSolar] = [:] - + let today = calendar.startOfDay(for: currentDate) - for dayOffset in 0..= currentDate } allHourlyTimes = allHourlyTimes.filter { $0 >= currentDate } - + // Filter out hourly times that are within 5 minutes of any key time let fiveMinutes: TimeInterval = 5 * 60 let filteredHourlyTimes = allHourlyTimes.filter { hour in !allKeyTimes.contains { abs($0.timeIntervalSince(hour)) <= fiveMinutes } } - - // Helper to build an entry with relevance based on that day's solar data + + /// Helper to build an entry with relevance based on that day's solar data func makeEntry(at date: Date) -> Entry { let dayStart = calendar.startOfDay(for: date) let solar = solarByDay[dayStart] ?? todaySolar - + let distanceToSunrise = abs(date.distance(to: solar.safeSunrise)) let distanceToSunset = abs(date.distance(to: solar.safeSunset)) let nearestEventDistance = min(distanceToSunset, distanceToSunrise) let relevance: TimelineEntryRelevance? = nearestEventDistance < (60 * 30) - ? .init(score: 10, duration: nearestEventDistance) - : nil - + ? .init(score: 10, duration: nearestEventDistance) + : nil + return Entry( date: date, location: widgetLocation, relevance: relevance ) } - + // Create entries for key times and hourly times, then merge var mergedEntries: [Entry] = [] - for date in allKeyTimes { mergedEntries.append(makeEntry(at: date)) } - for date in filteredHourlyTimes { mergedEntries.append(makeEntry(at: date)) } - + for date in allKeyTimes { + mergedEntries.append(makeEntry(at: date)) + } + for date in filteredHourlyTimes { + mergedEntries.append(makeEntry(at: date)) + } + // Sort and dedupe entries less than 60 seconds apart let entries = mergedEntries .sorted { $0.date < $1.date } @@ -277,15 +282,15 @@ struct SolsticeTimelineProvider: AppIntentTimelineProvider { if let last = result.last, entry.date.timeIntervalSince(last.date) < 60 { return } - + result.append(entry) } - + // Refresh after the last entry, or after 3 days if no entries return Timeline(entries: entries, policy: .atEnd) } - func placeholder(in context: Context) -> SolsticeWidgetTimelineEntry { + func placeholder(in _: Context) -> SolsticeWidgetTimelineEntry { SolsticeWidgetTimelineEntry(date: Date(), location: .defaultLocation) } } @@ -293,21 +298,21 @@ struct SolsticeTimelineProvider: AppIntentTimelineProvider { extension SolsticeWidgetTimelineEntry { static func previewTimeline() async -> [SolsticeWidgetTimelineEntry] { [ - SolsticeWidgetTimelineEntry(date: .now, location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 6), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 12), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 18), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 24), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 30), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36), location: .defaultLocation), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(1), location: nil), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(2), location: nil, locationError: .locationUpdateFailed), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(3), location: nil, locationError: .notAuthorized), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(4), location: nil, locationError: .reverseGeocodingFailed), - SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(5), location: nil, locationError: .needsReconfiguration), + SolsticeWidgetTimelineEntry(date: .now, location: .defaultLocation), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 6), location: .defaultLocation), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 12), location: .defaultLocation), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 18), location: .defaultLocation), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 24), location: .defaultLocation), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 30), location: .defaultLocation), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36), location: .defaultLocation), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(1), location: nil), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(2), location: nil, locationError: .locationUpdateFailed), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(3), location: nil, locationError: .notAuthorized), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(4), location: nil, locationError: .reverseGeocodingFailed), + SolsticeWidgetTimelineEntry(date: .now.addingTimeInterval(60 * 60 * 36).addingTimeInterval(5), location: nil, locationError: .needsReconfiguration), ] } - + static var placeholder: Self { SolsticeWidgetTimelineEntry(date: .now, location: .proxiedToTimeZone) } diff --git a/Widget/Helpers/WidgetHeadingModifier.swift b/Widget/Helpers/WidgetHeadingModifier.swift index 819a6f6d..a3d8fcae 100644 --- a/Widget/Helpers/WidgetHeadingModifier.swift +++ b/Widget/Helpers/WidgetHeadingModifier.swift @@ -11,7 +11,7 @@ import WidgetKit struct WidgetHeadingModifier: ViewModifier { @Environment(\.widgetFamily) var family @Environment(\.dynamicTypeSize) var sizeCategory - + var displaySize: Font { switch family { case .systemSmall: @@ -20,7 +20,7 @@ struct WidgetHeadingModifier: ViewModifier { return .title3 } } - + func body(content: Content) -> some View { content .font(displaySize) @@ -31,6 +31,6 @@ struct WidgetHeadingModifier: ViewModifier { extension View { func widgetHeading() -> some View { - self.modifier(WidgetHeadingModifier()) + modifier(WidgetHeadingModifier()) } } diff --git a/Widget/Helpers/WidgetLocationHeadingView.swift b/Widget/Helpers/WidgetLocationHeadingView.swift index bfd140e5..06a2bd56 100644 --- a/Widget/Helpers/WidgetLocationHeadingView.swift +++ b/Widget/Helpers/WidgetLocationHeadingView.swift @@ -10,7 +10,7 @@ import WidgetKit struct WidgetLocationView: View { var location: SolsticeWidgetLocation - + var locationName: Text { guard let title = location.title else { switch location.isRealLocation { @@ -20,7 +20,7 @@ struct WidgetLocationView: View { return Text("\(Image(.solstice)) Solstice") } } - + switch location.isRealLocation { case true: return Text("\(Text(title)) \(Image(systemName: "location"))", comment: "Widget heading for real location") @@ -28,7 +28,7 @@ struct WidgetLocationView: View { return Text("\(Image(.solstice)) \(Text(title))", comment: "Widget heading for custom location") } } - + var body: some View { locationName .font(.footnote.weight(.semibold)) @@ -45,9 +45,9 @@ struct WidgetLocationView_Previews: PreviewProvider { WidgetLocationView(location: SolsticeWidgetLocation(latitude: 0, longitude: 0)) } #if os(watchOS) - .previewContext(WidgetPreviewContext(family: .accessoryCircular)) + .previewContext(WidgetPreviewContext(family: .accessoryCircular)) #else - .previewContext(WidgetPreviewContext(family: .systemMedium)) + .previewContext(WidgetPreviewContext(family: .systemMedium)) #endif } } diff --git a/Widget/Helpers/WidgetMissingLocationView.swift b/Widget/Helpers/WidgetMissingLocationView.swift index a557d0b2..e83d10eb 100644 --- a/Widget/Helpers/WidgetMissingLocationView.swift +++ b/Widget/Helpers/WidgetMissingLocationView.swift @@ -20,6 +20,7 @@ struct WidgetMissingLocationView: View { } // MARK: - Needs Reconfiguration View + /// Shown when a widget was configured with the old intent system and lost its custom location data during migration. struct WidgetNeedsReconfigurationView: View { var body: some View { diff --git a/Widget/InfoPlist.xcstrings b/Widget/InfoPlist.xcstrings new file mode 100644 index 00000000..5d2471fa --- /dev/null +++ b/Widget/InfoPlist.xcstrings @@ -0,0 +1,258 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "CFBundleDisplayName" : { + "comment" : "Bundle display name", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ودجة" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Widget" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Widget" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Widget" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Widget" + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Widget" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ウィジェット" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Widget" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Widżet" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "小组件" + } + } + } + }, + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "امتداد الودجة" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Widget-Erweiterung" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Widget Extension" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Extensión de widget" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Extension widget" + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Estensione widget" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "ウィジェット機能拡張" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Widget-extensie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Rozszerzenie widżetu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "小组件扩展" + } + } + } + }, + "NSHumanReadableCopyright" : { + "comment" : "Copyright (human-readable)", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "" + } + } + } + }, + "NSLocationWhenInUseUsageDescription" : { + "comment" : "Privacy - Location When In Use Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "يستخدم Solstice موقعك لحساب أوقات الشروق والغروب المحلية" + } + }, + "de" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice verwendet deinen Standort, um die lokalen Zeiten für Sonnenauf- und -untergang zu berechnen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Solstice uses your location to calculate local sunrise and sunset times" + } + }, + "es" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice usa tu ubicación para calcular las horas locales de amanecer y puesta de sol" + } + }, + "fr" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice utilise votre position pour calculer les heures locales de lever et de coucher du soleil." + } + }, + "it" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice usa la tua posizione per calcolare gli orari locali di alba e tramonto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solsticeは、現地の日の出と日の入りの時刻を計算するために位置情報を使用します" + } + }, + "nl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice gebruikt je locatie om de lokale tijden van zonsopkomst en zonsondergang te berekenen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice używa Twojej lokalizacji, aby obliczyć lokalne godziny wschodu i zachodu słońca" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "machine_translated", + "value" : "Solstice 使用你的位置来计算当地的日出和日落时间" + } + } + } + } + }, + "version" : "1.3" +} \ No newline at end of file diff --git a/Widget/Overview Widget/OverviewWidget.swift b/Widget/Overview Widget/OverviewWidget.swift index b0045999..03900677 100644 --- a/Widget/Overview Widget/OverviewWidget.swift +++ b/Widget/Overview Widget/OverviewWidget.swift @@ -5,16 +5,16 @@ // Created by Daniel Eden on 03/04/2023. // -import WidgetKit import SwiftUI +import WidgetKit struct OverviewWidget: Widget { #if os(iOS) - static var supportedFamilies: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge, .accessoryInline, .accessoryRectangular, .accessoryCircular] + static var supportedFamilies: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge, .accessoryInline, .accessoryRectangular, .accessoryCircular] #elseif os(macOS) || os(visionOS) - static var supportedFamilies: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] + static var supportedFamilies: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] #elseif os(watchOS) - static var supportedFamilies: [WidgetFamily] = [.accessoryInline, .accessoryCircular, .accessoryRectangular, .accessoryCorner] + static var supportedFamilies: [WidgetFamily] = [.accessoryInline, .accessoryCircular, .accessoryRectangular, .accessoryCorner] #endif var body: some WidgetConfiguration { @@ -33,7 +33,7 @@ struct OverviewWidget: Widget { } #if os(iOS) -#Preview(as: .systemMedium, - widget: { OverviewWidget() }, - timeline: SolsticeWidgetTimelineEntry.previewTimeline) + #Preview(as: .systemMedium, + widget: { OverviewWidget() }, + timeline: SolsticeWidgetTimelineEntry.previewTimeline) #endif diff --git a/Widget/Overview Widget/OverviewWidgetView+AccessoryWidgetViews.swift b/Widget/Overview Widget/OverviewWidgetView+AccessoryWidgetViews.swift index 97153447..25191aed 100644 --- a/Widget/Overview Widget/OverviewWidgetView+AccessoryWidgetViews.swift +++ b/Widget/Overview Widget/OverviewWidgetView+AccessoryWidgetViews.swift @@ -6,72 +6,72 @@ // #if !os(macOS) -import SwiftUI -import WidgetKit -import Suite + import Suite + import SwiftUI + import WidgetKit -extension OverviewWidgetView { - struct AccessoryCircularView: View { - @Environment(\.widgetRenderingMode) var renderingMode - var solar: NTSolar - var location: SolsticeWidgetLocation - - var body: some View { - DaylightChart( - solar: solar, - timeZone: location.timeZone, - showEventTypes: false, - appearance: renderingMode == .fullColor ? .graphical : .simple, - includesSummaryTitle: false, - hideXAxis: true, - markSize: 2.5 - ) - .widgetAccentable() - .background { AccessoryWidgetBackground() } - .mask(Circle()) - .widgetLabel { - Label(solar.daylightDuration.localizedString, systemImage: "sun.max") + extension OverviewWidgetView { + struct AccessoryCircularView: View { + @Environment(\.widgetRenderingMode) var renderingMode + var solar: NTSolar + var location: SolsticeWidgetLocation + + var body: some View { + DaylightChart( + solar: solar, + timeZone: location.timeZone, + showEventTypes: false, + appearance: renderingMode == .fullColor ? .graphical : .simple, + includesSummaryTitle: false, + hideXAxis: true, + markSize: 2.5 + ) + .widgetAccentable() + .background { AccessoryWidgetBackground() } + .mask(Circle()) + .widgetLabel { + Label(solar.daylightDuration.localizedString, systemImage: "sun.max") + } } } - } - - struct AccessoryRectangularView: View { - var isAfterTodaySunset: Bool - var location: SolsticeWidgetLocation? - var relevantSolar: NTSolar? - var comparisonSolar: NTSolar? - - var body: some View { - HStack { - VStack(alignment: .leading) { - Text("\(Image(systemName: "sun.max")) Daylight \(isAfterTodaySunset ? Text("Tomorrow") : Text("Today"))") - .font(.headline) - .widgetAccentable() - .imageScale(.small) - .allowsTightening(true) - .contentTransition(.interpolate) - - if let relevantSolar { - Text(relevantSolar.daylightDuration.localizedString) - .contentTransition(.numericText()) - - Group { - if let comparisonSolar { - let difference = relevantSolar.daylightDuration - comparisonSolar.daylightDuration - Text("\(difference >= 0 ? "+" : "-")\(Duration.seconds(abs(difference)).formatted(.units(maximumUnitCount: 2)))") - } else { - Text(relevantSolar.safeSunrise...relevantSolar.safeSunset) + + struct AccessoryRectangularView: View { + var isAfterTodaySunset: Bool + var location: SolsticeWidgetLocation? + var relevantSolar: NTSolar? + var comparisonSolar: NTSolar? + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text("\(Image(systemName: "sun.max")) Daylight \(isAfterTodaySunset ? Text("Tomorrow") : Text("Today"))") + .font(.headline) + .widgetAccentable() + .imageScale(.small) + .allowsTightening(true) + .contentTransition(.interpolate) + + if let relevantSolar { + Text(relevantSolar.daylightDuration.localizedString) + .contentTransition(.numericText()) + + Group { + if let comparisonSolar { + let difference = relevantSolar.daylightDuration - comparisonSolar.daylightDuration + Text("\(difference >= 0 ? "+" : "-")\(Duration.seconds(abs(difference)).formatted(.units(maximumUnitCount: 2)))") + } else { + Text(relevantSolar.safeSunrise ... relevantSolar.safeSunset) + } } + .foregroundStyle(.secondary) + .transition(.blurReplace) } - .foregroundStyle(.secondary) - .transition(.blurReplace) } + + Spacer(minLength: 0) } - - Spacer(minLength: 0) + .minimumScaleFactor(0.9) } - .minimumScaleFactor(0.9) } } -} #endif diff --git a/Widget/Overview Widget/OverviewWidgetView.swift b/Widget/Overview Widget/OverviewWidgetView.swift index d2969534..e0821ab8 100644 --- a/Widget/Overview Widget/OverviewWidgetView.swift +++ b/Widget/Overview Widget/OverviewWidgetView.swift @@ -12,37 +12,38 @@ struct OverviewWidgetView: SolsticeWidgetView { @Environment(\.widgetRenderingMode) private var renderingMode @Environment(\.widgetFamily) private var family @Environment(\.dynamicTypeSize) private var sizeCategory - + var entry: SolsticeWidgetTimelineEntry - + var body: some View { Group { if let solar, - let location { + let location + { switch family { #if os(watchOS) || os(iOS) - case .accessoryCircular: - AccessoryCircularView(solar: solar, location: location) - case .accessoryInline: - Label(solar.daylightDuration.localizedString, systemImage: "sun.max") - case .accessoryRectangular: - AccessoryRectangularView( - isAfterTodaySunset: isAfterTodaySunset, - location: location, - relevantSolar: relevantSolar, - comparisonSolar: isAfterTodaySunset ? solar : nil - ) - #if os(watchOS) - case .accessoryCorner: - Image(systemName: "sun.max") - .font(.title.bold()) - .symbolVariant(.fill) - .imageScale(.large) - .widgetLabel { - Text(solar.daylightDuration.localizedString) - } - .widgetAccentable() - #endif // end watchOS + case .accessoryCircular: + AccessoryCircularView(solar: solar, location: location) + case .accessoryInline: + Label(solar.daylightDuration.localizedString, systemImage: "sun.max") + case .accessoryRectangular: + AccessoryRectangularView( + isAfterTodaySunset: isAfterTodaySunset, + location: location, + relevantSolar: relevantSolar, + comparisonSolar: isAfterTodaySunset ? solar : nil + ) + #if os(watchOS) + case .accessoryCorner: + Image(systemName: "sun.max") + .font(.title.bold()) + .symbolVariant(.fill) + .imageScale(.large) + .widgetLabel { + Text(solar.daylightDuration.localizedString) + } + .widgetAccentable() + #endif // end watchOS #endif // end !macOS default: RectangularView(entry: entry) @@ -69,107 +70,110 @@ extension OverviewWidgetView { struct RectangularView: SolsticeWidgetView { @Environment(\.widgetFamily) private var family @Environment(\.dynamicTypeSize) private var sizeCategory - + var entry: SolsticeWidgetTimelineEntry - + var body: some View { ZStack(alignment: .bottomLeading) { #if !os(watchOS) - if family != .systemSmall, - let solar, - let location { - GeometryReader { geom in - DaylightChart( - solar: solar, - timeZone: location.timeZone, - showEventTypes: false, - includesSummaryTitle: false, - hideXAxis: true, - markSize: family == .systemSmall ? 3 : 5 - ) - .frame(maxHeight: 250) - .mask { - if family == .systemMedium || family == .systemSmall { - RadialGradient( - colors: [.black.opacity(0.1), .black], - center: .bottomLeading, - startRadius: geom.size.height / 1.2, - endRadius: geom.size.height - ) - } else { - Color.black - } - } - } - .padding(-20) - } - - VStack(alignment: .leading, spacing: 4) { - if sizeCategory < .xLarge, - let location { - WidgetLocationView(location: location) - } - - Spacer() - - if let duration = relevantSolar?.daylightDuration.localizedString { - if sizeCategory < .xLarge { - Group { - if isAfterTodaySunset { - Text("Daylight tomorrow") + if family != .systemSmall, + let solar, + let location + { + GeometryReader { geom in + DaylightChart( + solar: solar, + timeZone: location.timeZone, + showEventTypes: false, + includesSummaryTitle: false, + hideXAxis: true, + markSize: family == .systemSmall ? 3 : 5 + ) + .frame(maxHeight: 250) + .mask { + if family == .systemMedium || family == .systemSmall { + RadialGradient( + colors: [.black.opacity(0.1), .black], + center: .bottomLeading, + startRadius: geom.size.height / 1.2, + endRadius: geom.size.height + ) } else { - Text("Daylight today") + Color.black } } - .font(.footnote) } - - Text(duration) - .lineLimit(4) - .widgetHeading() - .fixedSize(horizontal: false, vertical: true) - .contentTransition(.numericText()) + .padding(-20) } - - Group { - if let location, - let begins = relevantSolar?.safeSunrise, - let ends = relevantSolar?.safeSunset { - if let differenceString = relevantSolar?.compactDifferenceString { - Text(differenceString) - .lineLimit(4) - .font(.footnote) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - } - - if family == .systemSmall { - Text(begins...ends) - .foregroundStyle(.tertiary) - } else { - HStack { - Label { - Text(begins, style: .time) - } icon: { - Image(systemName: "sunrise.fill") + + VStack(alignment: .leading, spacing: 4) { + if sizeCategory < .xLarge, + let location + { + WidgetLocationView(location: location) + } + + Spacer() + + if let duration = relevantSolar?.daylightDuration.localizedString { + if sizeCategory < .xLarge { + Group { + if isAfterTodaySunset { + Text("Daylight tomorrow") + } else { + Text("Daylight today") } - - Spacer() - - Label { - Text(ends, style: .time) - } icon: { - Image(systemName: "sunset.fill") + } + .font(.footnote) + } + + Text(duration) + .lineLimit(4) + .widgetHeading() + .fixedSize(horizontal: false, vertical: true) + .contentTransition(.numericText()) + } + + Group { + if let location, + let begins = relevantSolar?.safeSunrise, + let ends = relevantSolar?.safeSunset + { + if let differenceString = relevantSolar?.compactDifferenceString { + Text(differenceString) + .lineLimit(4) + .font(.footnote) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + if family == .systemSmall { + Text(begins ... ends) + .foregroundStyle(.tertiary) + } else { + HStack { + Label { + Text(begins, style: .time) + } icon: { + Image(systemName: "sunrise.fill") + } + + Spacer() + + Label { + Text(ends, style: .time) + } icon: { + Image(systemName: "sunset.fill") + } } } } } + .font(.footnote.weight(.semibold)) + .contentTransition(.numericText()) } - .font(.footnote.weight(.semibold)) - .contentTransition(.numericText()) - } - .symbolRenderingMode(.hierarchical) -#endif + .symbolRenderingMode(.hierarchical) + #endif } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .environment(\.timeZone, location?.timeZone ?? .autoupdatingCurrent) @@ -178,7 +182,7 @@ extension OverviewWidgetView { } #if os(iOS) -#Preview(as: .systemMedium, - widget: { OverviewWidget() }, - timeline: SolsticeWidgetTimelineEntry.previewTimeline) + #Preview(as: .systemMedium, + widget: { OverviewWidget() }, + timeline: SolsticeWidgetTimelineEntry.previewTimeline) #endif diff --git a/Widget/Solar Chart Widget/SolarChartWidget.swift b/Widget/Solar Chart Widget/SolarChartWidget.swift index 036787fd..1bf26d63 100644 --- a/Widget/Solar Chart Widget/SolarChartWidget.swift +++ b/Widget/Solar Chart Widget/SolarChartWidget.swift @@ -5,45 +5,45 @@ // Created by Daniel Eden on 12/04/2023. // -import WidgetKit import SwiftUI +import WidgetKit #if os(watchOS) || os(iOS) -struct SolarChartWidget: Widget { - static var supportedFamilies: [WidgetFamily] = [.accessoryRectangular] + struct SolarChartWidget: Widget { + static var supportedFamilies: [WidgetFamily] = [.accessoryRectangular] - var body: some WidgetConfiguration { - AppIntentConfiguration( - kind: SolsticeWidgetKind.SolarChartWidget.rawValue, - intent: SolsticeConfigurationIntent.self, - provider: SolsticeTimelineProvider(widgetKind: .SolarChartWidget) - ) { timelineEntry in - SolarChartWidgetView(entry: timelineEntry) - .widgetURL(timelineEntry.location?.url) + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: SolsticeWidgetKind.SolarChartWidget.rawValue, + intent: SolsticeConfigurationIntent.self, + provider: SolsticeTimelineProvider(widgetKind: .SolarChartWidget) + ) { timelineEntry in + SolarChartWidgetView(entry: timelineEntry) + .widgetURL(timelineEntry.location?.url) + } + .configurationDisplayName("Solar Chart") + .description("Follow the sun's journey throughout the day") + .supportedFamilies(Self.supportedFamilies) } - .configurationDisplayName("Solar Chart") - .description("Follow the sun's journey throughout the day") - .supportedFamilies(Self.supportedFamilies) } -} #endif #if !os(watchOS) -struct SundialWidget: Widget { - static var supportedFamilies: [WidgetFamily] = [.systemLarge, .systemSmall] + struct SundialWidget: Widget { + static var supportedFamilies: [WidgetFamily] = [.systemLarge, .systemSmall] - var body: some WidgetConfiguration { - AppIntentConfiguration( - kind: SolsticeWidgetKind.SundialWidget.rawValue, - intent: SolsticeConfigurationIntent.self, - provider: SolsticeTimelineProvider(widgetKind: .SundialWidget) - ) { timelineEntry in - SundialWidgetView(entry: timelineEntry) - .widgetURL(timelineEntry.location?.url) + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: SolsticeWidgetKind.SundialWidget.rawValue, + intent: SolsticeConfigurationIntent.self, + provider: SolsticeTimelineProvider(widgetKind: .SundialWidget) + ) { timelineEntry in + SundialWidgetView(entry: timelineEntry) + .widgetURL(timelineEntry.location?.url) + } + .configurationDisplayName("Sundial") + .description("Follow the sun's journey throughout the day") + .supportedFamilies(Self.supportedFamilies) } - .configurationDisplayName("Sundial") - .description("Follow the sun's journey throughout the day") - .supportedFamilies(Self.supportedFamilies) } -} #endif diff --git a/Widget/Solar Chart Widget/SolarChartWidgetView.swift b/Widget/Solar Chart Widget/SolarChartWidgetView.swift index 414076b2..f98c3904 100644 --- a/Widget/Solar Chart Widget/SolarChartWidgetView.swift +++ b/Widget/Solar Chart Widget/SolarChartWidgetView.swift @@ -10,11 +10,12 @@ import WidgetKit struct SolarChartWidgetView: SolsticeWidgetView { var entry: SolsticeWidgetTimelineEntry - + var body: some View { Group { if let solar, - let location { + let location + { ZStack(alignment: .topLeading) { HStack { Label { @@ -63,10 +64,10 @@ struct SolarChartWidgetView: SolsticeWidgetView { } #if os(watchOS) || os(iOS) -struct SolarChartWidgetPreview: PreviewProvider { - static var previews: some View { - SolarChartWidgetView(entry: SolsticeWidgetTimelineEntry(date: .now)) - .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) + struct SolarChartWidgetPreview: PreviewProvider { + static var previews: some View { + SolarChartWidgetView(entry: SolsticeWidgetTimelineEntry(date: .now)) + .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) + } } -} #endif diff --git a/Widget/Solar Chart Widget/SundialWidgetView.swift b/Widget/Solar Chart Widget/SundialWidgetView.swift index 45021da9..1ff7274c 100644 --- a/Widget/Solar Chart Widget/SundialWidgetView.swift +++ b/Widget/Solar Chart Widget/SundialWidgetView.swift @@ -5,36 +5,35 @@ // Created by Daniel Eden on 09/10/2025. // +import Suite import SwiftUI import WidgetKit -import Suite struct SundialWidgetView: SolsticeWidgetView { @Environment(\.widgetFamily) private var widgetFamily @Environment(\.widgetContentMargins) private var widgetContentMargins - + @State private var headerSize: CGSize = .zero - + var entry: SolsticeWidgetTimelineEntry - + var isSmallWidget: Bool { if widgetFamily == .systemSmall { return true } - + return false } - - @ViewBuilder + private var header: some View { HStack { if !isSmallWidget { Label("Solstice", image: .solstice) .labelStyle(CompactLabelStyle()) - + Spacer() } - + if let title = location?.title { Label { Text(title) @@ -52,24 +51,24 @@ struct SundialWidgetView: SolsticeWidgetView { .lineLimit(1) .allowsTightening(true) } - - @ViewBuilder + private var smallWidgetFooter: some View { HStack { if let sunrise = solar?.sunrise, - let sunset = solar?.sunset { - Text(sunrise...sunset) + let sunset = solar?.sunset + { + Text(sunrise ... sunset) } - + Spacer() - + if let duration = solar?.daylightDuration { Text(Duration.seconds(duration).formatted(.units(allowed: [.hours, .minutes], width: .narrow))) } } .lineLimit(1) } - + var body: some View { Group { if let location { @@ -80,12 +79,12 @@ struct SundialWidgetView: SolsticeWidgetView { .mask(LinearGradient( stops: [ .init(color: .black, location: 0.3), - .init(color: .clear, location: 0.85) + .init(color: .clear, location: 0.85), ], startPoint: .top, endPoint: .bottom )) - + VStack(alignment: .leading, spacing: 4) { header smallWidgetFooter @@ -99,9 +98,9 @@ struct SundialWidgetView: SolsticeWidgetView { header .font(.footnote) .readSize($headerSize) - + CircularSolarChart(date: entry.date, location: location) - + Color.clear.frame(height: headerSize.height) } } @@ -123,14 +122,14 @@ struct SundialWidgetView: SolsticeWidgetView { } } -fileprivate struct AdaptiveLabelContainer: View { +private struct AdaptiveLabelContainer: View { @Environment(\.widgetRenderingMode) private var renderingMode - + var padding: CGFloat = 4 - + @ViewBuilder var content: Content - + var body: some View { content .if(renderingMode == .fullColor) { content in @@ -145,9 +144,9 @@ fileprivate struct AdaptiveLabelContainer: View { } #if !os(macOS) -#Preview(as: .systemLarge) { - SundialWidget() -} timeline: { - SolsticeWidgetTimelineEntry(date: .now, location: .proxiedToTimeZone) -} + #Preview(as: .systemLarge) { + SundialWidget() + } timeline: { + SolsticeWidgetTimelineEntry(date: .now, location: .proxiedToTimeZone) + } #endif diff --git a/Widget/Widget.swift b/Widget/Widget.swift index 18192cf7..de4c8c5c 100644 --- a/Widget/Widget.swift +++ b/Widget/Widget.swift @@ -5,8 +5,8 @@ // Created by Daniel Eden on 19/02/2023. // -import Foundation import CoreLocation +import Foundation struct SolsticeWidgetLocation: AnyLocation { var title: String? @@ -16,19 +16,19 @@ struct SolsticeWidgetLocation: AnyLocation { var longitude: Double var isRealLocation = false var locationUUID: UUID? - + static let defaultLocation = SolsticeWidgetLocation(title: "London", - subtitle: "United Kingdom", - timeZoneIdentifier: "Europe/London", - latitude: 51.5072, - longitude: -0.1276) - + subtitle: "United Kingdom", + timeZoneIdentifier: "Europe/London", + latitude: 51.5072, + longitude: -0.1276) + static let proxiedToTimeZone = SolsticeWidgetLocation(title: "Your location", - timeZoneIdentifier: TimeZone.autoupdatingCurrent.identifier, - latitude: CLLocationCoordinate2D.proxiedToTimeZone.latitude, - longitude: CLLocationCoordinate2D.proxiedToTimeZone.longitude, - isRealLocation: true) - + timeZoneIdentifier: TimeZone.autoupdatingCurrent.identifier, + latitude: CLLocationCoordinate2D.proxiedToTimeZone.latitude, + longitude: CLLocationCoordinate2D.proxiedToTimeZone.longitude, + isRealLocation: true) + var url: URL? { switch isRealLocation { case true: return URL(string: "solstice://location/currentLocation") diff --git a/Widget/WidgetBundle.swift b/Widget/WidgetBundle.swift index 11308635..9ba751f0 100644 --- a/Widget/WidgetBundle.swift +++ b/Widget/WidgetBundle.swift @@ -5,8 +5,8 @@ // Created by Daniel Eden on 19/02/2023. // -import WidgetKit import SwiftUI +import WidgetKit @main struct SolsticeWidgets: WidgetBundle { @@ -14,11 +14,11 @@ struct SolsticeWidgets: WidgetBundle { OverviewWidget() CountdownWidget() #if !os(watchOS) - SundialWidget() + SundialWidget() #endif - + #if os(watchOS) || os(iOS) - SolarChartWidget() + SolarChartWidget() #endif } } diff --git a/ci_scripts/ci_pre_xcodebuild.sh b/ci_scripts/ci_pre_xcodebuild.sh new file mode 100755 index 00000000..f7b8e29c --- /dev/null +++ b/ci_scripts/ci_pre_xcodebuild.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# ci_pre_xcodebuild.sh +# Solstice +# +# This script runs before Xcode Cloud invokes xcodebuild. +# For distribution (archive) builds it fails the build when any String +# Catalog still has untranslated strings, so we never ship missing +# localizations to App Store Connect. + +set -e + +# Only gate distribution archives. Test/build workflows are left untouched so +# in-progress translation work doesn't block PR validation. +if [ "$CI_XCODEBUILD_ACTION" = "archive" ]; then + echo "Validating localizations before archive…" + swift "$CI_PRIMARY_REPOSITORY_PATH/ci_scripts/validate-localizations.swift" + echo "Localization validation passed." +else + echo "Skipping localization validation for action: ${CI_XCODEBUILD_ACTION:-unknown}" +fi diff --git a/ci_scripts/validate-localizations.swift b/ci_scripts/validate-localizations.swift new file mode 100755 index 00000000..534e26c7 --- /dev/null +++ b/ci_scripts/validate-localizations.swift @@ -0,0 +1,257 @@ +#!/usr/bin/env swift +// +// validate-localizations.swift +// Solstice +// +// Fails if any String Catalog (.xcstrings) has untranslated strings for a +// language the project ships. Intended to gate App Store distribution +// archives from Xcode Cloud (see ci_pre_xcodebuild.sh), but runnable locally: +// +// swift ci_scripts/validate-localizations.swift +// +// Policy: +// - The set of required languages is derived from the catalogs themselves +// (the union of every non-source locale that appears in any catalog), so +// adding a language to the project automatically extends coverage checks. +// - A string fails when, for a required language, it is in the "new" state +// or has no localization entry at all. +// - By default "machine_translated" and "needs_review" count as shippable, +// because the project intentionally ships machine translations. +// - Set LOCALIZATION_STRICT=1 to require human "translated" state instead, +// which additionally fails "machine_translated" and "needs_review". +// - Keys marked "shouldTranslate": false and "stale" keys are skipped. +// + +import Foundation + +// MARK: - Configuration + +let strict = ["1", "true", "yes"].contains( + (ProcessInfo.processInfo.environment["LOCALIZATION_STRICT"] ?? "").lowercased() +) + +let acceptableStates: Set = strict + ? ["translated"] + : ["translated", "machine_translated", "needs_review"] + +/// Directories that never contain first-party, shippable catalogs. +let excludedPathFragments = [ + "/Frameworks/", "/.build/", "/DerivedData/", "/Pods/", + "/Carthage/", "/.git/", "/build/", +] + +// MARK: - Locate the repository root + +func repositoryRoot() -> URL { + let env = ProcessInfo.processInfo.environment + if let ciPath = env["CI_PRIMARY_REPOSITORY_PATH"], !ciPath.isEmpty { + return URL(fileURLWithPath: ciPath, isDirectory: true) + } + // This file lives at /ci_scripts/validate-localizations.swift. + let scriptDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent() + let candidate = scriptDir.deletingLastPathComponent() + if FileManager.default.fileExists(atPath: candidate.appendingPathComponent(".git").path) + || FileManager.default.fileExists(atPath: candidate.appendingPathComponent("ci_scripts").path) + { + return candidate + } + return URL(fileURLWithPath: FileManager.default.currentDirectoryPath, isDirectory: true) +} + +let root = repositoryRoot() + +// MARK: - Discover String Catalogs + +func discoverCatalogs(under root: URL) -> [URL] { + let fm = FileManager.default + guard let enumerator = fm.enumerator( + at: root, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) else { return [] } + + var catalogs: [URL] = [] + for case let url as URL in enumerator where url.pathExtension == "xcstrings" { + let path = url.path + if excludedPathFragments.contains(where: { path.contains($0) }) { continue } + catalogs.append(url) + } + return catalogs.sorted { $0.path < $1.path } +} + +let catalogs = discoverCatalogs(under: root) + +guard !catalogs.isEmpty else { + FileHandle.standardError.write(Data( + "error: no .xcstrings catalogs found under \(root.path)\n".utf8 + )) + exit(1) +} + +// MARK: - Model + +struct Catalog { + let url: URL + let sourceLanguage: String + let strings: [String: [String: Any]] +} + +func loadCatalog(_ url: URL) -> Catalog? { + guard let data = try? Data(contentsOf: url), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let strings = json["strings"] as? [String: [String: Any]] + else { return nil } + let source = json["sourceLanguage"] as? String ?? "en" + return Catalog(url: url, sourceLanguage: source, strings: strings) +} + +let loaded = catalogs.compactMap(loadCatalog) + +// MARK: - Required languages (union of every non-source locale seen) + +var sourceLanguages = Set() +var seenLanguages = Set() +for catalog in loaded { + sourceLanguages.insert(catalog.sourceLanguage) + for (_, entry) in catalog.strings { + if let locs = entry["localizations"] as? [String: Any] { + for lang in locs.keys { + seenLanguages.insert(lang) + } + } + } +} + +let requiredLanguages = seenLanguages.subtracting(sourceLanguages).sorted() + +guard !requiredLanguages.isEmpty else { + print("No non-source localizations found; nothing to validate.") + exit(0) +} + +// MARK: - Validation + +/// Returns the state strings for a localization entry: one for a plain +/// stringUnit, or one per plural/device case for a "variations" entry. +/// Returns nil when the entry is malformed (treated as a violation). +func states(in localization: [String: Any]) -> [String]? { + if let unit = localization["stringUnit"] as? [String: Any] { + return [unit["state"] as? String ?? "new"] + } + // App Shortcut phrases store alternative phrasings in a "stringSet". + if let set = localization["stringSet"] as? [String: Any] { + return [set["state"] as? String ?? "new"] + } + if let variations = localization["variations"] as? [String: Any] { + var result: [String] = [] + for (_, cases) in variations { + guard let cases = cases as? [String: Any] else { return nil } + for (_, caseValue) in cases { + guard let caseDict = caseValue as? [String: Any], + let unit = caseDict["stringUnit"] as? [String: Any] + else { return nil } + result.append(unit["state"] as? String ?? "new") + } + } + return result.isEmpty ? nil : result + } + return nil +} + +struct Violation { + let catalog: String + let language: String + let key: String + let reason: String +} + +var violations: [Violation] = [] + +for catalog in loaded { + let relativePath = catalog.url.path + .replacingOccurrences(of: root.path + "/", with: "") + + for (key, entry) in catalog.strings { + if entry["shouldTranslate"] as? Bool == false { continue } + if entry["extractionState"] as? String == "stale" { continue } + + let localizations = entry["localizations"] as? [String: Any] ?? [:] + + // A key whose source value is empty (e.g. NSHumanReadableCopyright, + // supplied from build settings) has nothing to translate. + let sourceValue: String + if let srcLoc = localizations[catalog.sourceLanguage] as? [String: Any], + let unit = srcLoc["stringUnit"] as? [String: Any], + let value = unit["value"] as? String + { + sourceValue = value + } else { + sourceValue = key + } + if sourceValue.isEmpty { continue } + + for language in requiredLanguages where language != catalog.sourceLanguage { + guard let localization = localizations[language] as? [String: Any] else { + violations.append(Violation( + catalog: relativePath, language: language, key: key, + reason: "no translation" + )) + continue + } + guard let stateList = states(in: localization) else { + violations.append(Violation( + catalog: relativePath, language: language, key: key, + reason: "malformed localization" + )) + continue + } + let bad = stateList.filter { !acceptableStates.contains($0) } + if !bad.isEmpty { + violations.append(Violation( + catalog: relativePath, language: language, key: key, + reason: "state '\(Set(bad).sorted().joined(separator: ", "))'" + )) + } + } + } +} + +// MARK: - Report + +let mode = strict ? "strict (human 'translated' required)" : "lenient (machine translations allowed)" +print("Localization gate: \(mode)") +print("Catalogs checked: \(loaded.count)") +print("Required languages: \(requiredLanguages.joined(separator: ", "))") + +if violations.isEmpty { + print("✓ All strings are translated for every required language.") + exit(0) +} + +/// Group counts by catalog + language, and show a capped set of examples. +var counts: [String: Int] = [:] +for v in violations { + counts["\(v.catalog) [\(v.language)]", default: 0] += 1 +} + +print("") +print("✗ Found \(violations.count) untranslated string(s):") +for key in counts.keys.sorted() { + print(" \(key): \(counts[key]!)") +} + +let exampleLimit = 20 +print("") +print("Examples (up to \(exampleLimit)):") +for v in violations.prefix(exampleLimit) { + // Emit as an Xcode-recognisable error line. + FileHandle.standardError.write(Data( + "error: [\(v.catalog)] \(v.language): \"\(v.key)\" — \(v.reason)\n".utf8 + )) +} + +if violations.count > exampleLimit { + print("… and \(violations.count - exampleLimit) more.") +} + +exit(1) diff --git a/watchOS/ContentView.swift b/watchOS/ContentView.swift index 48ce69ec..46da820a 100644 --- a/watchOS/ContentView.swift +++ b/watchOS/ContentView.swift @@ -12,22 +12,23 @@ struct ContentView: View { @Environment(\.scenePhase) var scenePhase @Environment(CurrentLocation.self) var currentLocation @Environment(\.timeMachine) var timeMachine: TimeMachine - + @SceneStorage("selectedLocation") private var selectedLocation: String? - + private let timer = Timer.publish(every: 60, on: RunLoop.main, in: .common).autoconnect() - + @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \SavedLocation.title, ascending: true)], - animation: .default) + animation: .default + ) private var items: FetchedResults - + var sortedItems: [SavedLocation] { items.sorted { lhs, rhs in - return lhs.timeZone.secondsFromGMT() < rhs.timeZone.secondsFromGMT() + lhs.timeZone.secondsFromGMT() < rhs.timeZone.secondsFromGMT() } } - + var body: some View { NavigationSplitView { List(selection: $selectedLocation) { @@ -42,7 +43,7 @@ struct ContentView: View { .multilineTextAlignment(.center) .foregroundStyle(.secondary) } - + if currentLocation.isAuthorized { LocationListRow(location: currentLocation) .tag(currentLocation.id) @@ -52,7 +53,7 @@ struct ContentView: View { .clipShape(.rect(cornerRadius: 20, style: .continuous)) ) } - + ForEach(sortedItems) { item in if let tag = item.uuid?.uuidString { LocationListRow(location: item) @@ -80,7 +81,7 @@ struct ContentView: View { } } .timeTravelToolbar() - case .some(let id): + case let .some(id): if let item = items.first(where: { $0.uuid?.uuidString == id }) { DetailView(location: item) .containerBackground(for: .navigation) { @@ -99,7 +100,7 @@ struct ContentView: View { .resolveDeepLink(sortedItems) .withTimeMachine(.solsticeTimeMachine) } - + var placeholderView: some View { Image(.solstice) .foregroundStyle(.tertiary) diff --git a/watchOS/TimeTravelToolbar.swift b/watchOS/TimeTravelToolbar.swift index 1e0adc1d..8c639370 100644 --- a/watchOS/TimeTravelToolbar.swift +++ b/watchOS/TimeTravelToolbar.swift @@ -8,9 +8,9 @@ import SwiftUI import TimeMachine -struct TimeTravelToolbar: ViewModifier{ +struct TimeTravelToolbar: ViewModifier { @Environment(\.timeMachine) var timeMachine: TimeMachine - + func body(content: Content) -> some View { content .toolbar { @@ -21,7 +21,7 @@ struct TimeTravelToolbar: ViewModifier{ timeMachine.offset -= 7 } } - + if timeMachine.isActive { Button { withAnimation { @@ -34,7 +34,7 @@ struct TimeTravelToolbar: ViewModifier{ } else { Spacer() } - + Button("Time travel 1 week later", systemImage: "forward") { withAnimation { timeMachine.offset += 7