Full Code of inderdhir/DatWeatherDoe for AI

main 983df24d0f69 cached
91 files
296.0 KB
79.5k tokens
1 requests
Download .txt
Showing preview only (325K chars total). Download the full file or copy to clipboard to get everything.
Repository: inderdhir/DatWeatherDoe
Branch: main
Commit: 983df24d0f69
Files: 91
Total size: 296.0 KB

Directory structure:
gitextract_t2ij2trf/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── lint.yml
├── .gitignore
├── .swiftformat
├── .swiftlint.yml
├── CONTRIBUTING.md
├── DatWeatherDoe/
│   ├── API/
│   │   ├── NetworkClient.swift
│   │   ├── Response/
│   │   │   ├── AirQuality.swift
│   │   │   ├── ForecastData.swift
│   │   │   ├── ForecastTemperatureData.swift
│   │   │   ├── SunriseSunsetData.swift
│   │   │   ├── TemperatureData.swift
│   │   │   ├── WeatherAPIResponse.swift
│   │   │   ├── WeatherAPIResponseParser.swift
│   │   │   └── WindData.swift
│   │   ├── WeatherData.swift
│   │   └── WeatherError.swift
│   ├── Config/
│   │   ├── APIKeyParser.swift
│   │   ├── ConfigManager.swift
│   │   └── ConfigOptions.swift
│   ├── DatWeatherDoeApp.swift
│   ├── Localization/
│   │   └── en.xcloc/
│   │       ├── Localized Contents/
│   │       │   └── en.xliff
│   │       ├── Source Contents/
│   │       │   └── DatWeatherDoe/
│   │       │       ├── Resources/
│   │       │       │   └── en.lproj/
│   │       │       │       └── InfoPlist.strings
│   │       │       └── UI/
│   │       │           └── Base.lproj/
│   │       │               └── MainMenu.xib
│   │       └── contents.json
│   ├── Reachability/
│   │   └── WeatherReachability.swift
│   ├── Resources/
│   │   ├── Assets.xcassets/
│   │   │   ├── AppIcon.appiconset/
│   │   │   │   └── Contents.json
│   │   │   └── Contents.json
│   │   ├── DatWeatherDoe.entitlements
│   │   ├── DevelopmentAssets/
│   │   │   └── TestData.swift
│   │   ├── Info.plist
│   │   └── Localization/
│   │       └── Localizable.xcstrings
│   ├── UI/
│   │   ├── Configure/
│   │   │   ├── ConfigureOptionsView.swift
│   │   │   ├── ConfigureUnitOptionsView.swift
│   │   │   ├── ConfigureValueSeparatorOptionsView.swift
│   │   │   ├── ConfigureView.swift
│   │   │   ├── ConfigureViewModel.swift
│   │   │   ├── ConfigureWeatherOptionsView.swift
│   │   │   └── Options/
│   │   │       ├── MeasurementUnit.swift
│   │   │       ├── RefreshInterval.swift
│   │   │       ├── TemperatureUnit.swift
│   │   │       ├── WeatherConditionPosition.swift
│   │   │       └── WeatherSource.swift
│   │   ├── Decorator/
│   │   │   ├── Condition/
│   │   │   │   ├── WeatherCondition.swift
│   │   │   │   ├── WeatherConditionBuilder.swift
│   │   │   │   └── WeatherConditionTextMapper.swift
│   │   │   ├── Text/
│   │   │   │   ├── HumidityTextBuilder.swift
│   │   │   │   ├── SunriseAndSunsetTextBuilder.swift
│   │   │   │   ├── Temperature/
│   │   │   │   │   ├── TemperatureForecastTextBuilder.swift
│   │   │   │   │   ├── TemperatureFormatter.swift
│   │   │   │   │   ├── TemperatureTextBuilder.swift
│   │   │   │   │   └── TemperatureWithDegreesCreator.swift
│   │   │   │   ├── UVIndexTextBuilder.swift
│   │   │   │   └── WeatherTextBuilder.swift
│   │   │   └── WeatherDataBuilder.swift
│   │   ├── Forecaster/
│   │   │   └── WeatherForecaster.swift
│   │   ├── Menu Bar/
│   │   │   ├── CustomButton.swift
│   │   │   ├── DropdownIcon.swift
│   │   │   ├── MenuOptionsView.swift
│   │   │   ├── MenuView.swift
│   │   │   ├── NonInteractiveMenuOptionView.swift
│   │   │   └── WindSpeedFormatter.swift
│   │   └── Status Bar/
│   │       └── StatusBarView.swift
│   └── ViewModel/
│       ├── Parser/
│       │   ├── CityWeatherResultParser.swift
│       │   └── ZipCodeWeatherResultParser.swift
│       ├── Repository/
│       │   ├── Coordinates/
│       │   │   ├── LocationCoordinatesWeatherRepository.swift
│       │   │   ├── LocationParser.swift
│       │   │   └── LocationValidator.swift
│       │   ├── System/
│       │   │   ├── SystemLocationFetcher.swift
│       │   │   ├── SystemLocationWeatherRepository.swift
│       │   │   └── Task+Retry.swift
│       │   ├── WeatherRepositoryFactory.swift
│       │   ├── WeatherRepositoryType.swift
│       │   ├── WeatherURLBuilder.swift
│       │   └── WeatherValidatorType.swift
│       ├── WeatherDataFormatter.swift
│       ├── WeatherViewModel.swift
│       └── WeatherViewModelType.swift
├── DatWeatherDoe.xcodeproj/
│   ├── project.pbxproj
│   ├── project.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   └── xcshareddata/
│   │       ├── IDEWorkspaceChecks.plist
│   │       └── swiftpm/
│   │           └── Package.resolved
│   └── xcshareddata/
│       └── xcschemes/
│           └── DatWeatherDoe.xcscheme
├── DatWeatherDoeTests/
│   ├── API/
│   │   └── Repository/
│   │       ├── Location/
│   │       │   └── Coordinates/
│   │       │       └── LocationValidatorTests.swift
│   │       └── WeatherURLBuilderTests.swift
│   ├── DatWeatherDoe.xctestplan
│   └── UI/
│       └── Configure/
│           └── Options/
│               ├── RefreshIntervalTests.swift
│               ├── TemperatureUnitTests.swift
│               └── WeatherSourceTests.swift
├── LICENSE
└── README.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: inderdhir


================================================
FILE: .github/workflows/lint.yml
================================================
name: SwiftLint

on:
  pull_request:
    paths:
      - '.github/workflows/swiftlint.yml'
      - '.swiftlint.yml'
      - '**/*.swift'

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: GitHub Action for SwiftLint (Different working directory)
        uses: norio-nomura/action-swiftlint@3.2.1
        env:
          WORKING_DIRECTORY: Source

================================================
FILE: .gitignore
================================================
# Xcode
.DS_Store

# Backup files
*~

# JetBrains
.idea/

## Build generated
build/
DerivedData

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata

## Other
*.xccheckout
*.moved-aside
*.xcuserstate
*.xcscmblueprint

## Obj-C/Swift specific
*.hmap
*.ipa

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
The above adds CocoaPods, 

Pods/
Podfile.lock

# Keys
DatWeatherDoe/Resources/Keys.plist

# Secrets
Config.xcconfig


================================================
FILE: .swiftformat
================================================
--disable trailingCommas

================================================
FILE: .swiftlint.yml
================================================
disabled_rules: # rule identifiers to exclude from running
  - colon
  - comma
  - control_statement
  - trailing_whitespace
  - vertical_parameter_alignment
  - opening_brace
opt_in_rules: # some rules are only opt-in
  - empty_count
  # Find all the available rules by running:
  # swiftlint rules
included: # paths to include during linting. `--path` is ignored if present.
  - DatWeatherDoe
excluded: # paths to ignore during linting. Takes precedence over `included`.
  - Pods
# configurable rules can be customized from this configuration file
# configurable rules can be customized from this configuration file
# binary rules can set their severity level
cyclomatic_complexity:
  ignores_case_statements: true
force_cast: warning # implicitly
force_try:
  severity: warning # explicitly
# rules that have both warning and error levels, can set just the warning level
# implicitly
line_length: 110
# they can set both implicitly with an array
type_body_length:
  - 300 # warning
  - 400 # error
# or they can set both explicitly
file_length:
  warning: 500
  error: 1200
# naming rules can set warnings/errors for min_length and max_length
# additionally they can set excluded names
type_name:
  min_length: 4 # only warning
  max_length: # warning and error
    warning: 40
    error: 50
  excluded: iPhone # excluded via string
identifier_name:
  min_length: # only min_length
    error: 2 # only error
  excluded: # excluded via string array
    - id
    - URL
    - GlobalAPIKey
function_body_length:
  warning: 50
  error: 100
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji)


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing

Contributions / pull requests are welcome!

Please note that the goal of this project to provide a lightweight menu bar app for weather at a glance on macOS (similar to weather app indicators on Ubuntu).

**MacOS 11 provides a weather widget that can be used in conjunction with this app so a macOS widget is NOT on the roadmap.**

================================================
FILE: DatWeatherDoe/API/NetworkClient.swift
================================================
//
//  NetworkClient.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/14/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import Foundation

protocol NetworkClientType {
    func performRequest(url: URL) async throws -> Data
}

final class NetworkClient: NetworkClientType {
    func performRequest(url: URL) async throws -> Data {
        do {
            return try await URLSession.shared.data(from: url).0
        } catch {
            throw WeatherError.networkError
        }
    }
}


================================================
FILE: DatWeatherDoe/API/Response/AirQuality.swift
================================================
//
//  AirQuality.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 7/1/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import Foundation

// US - EPA standard
enum AirQualityIndex: Int, Decodable {
    case good = 1
    case moderate = 2
    case unhealthyForSensitive = 3
    case unhealthy = 4
    case veryUnhealthy = 5
    case hazardous = 6

    var description: String {
        switch self {
        case .good:
            String(localized: "Good")
        case .moderate:
            String(localized: "Moderate")
        case .unhealthyForSensitive:
            String(localized: "Unhealthy for sensitive groups")
        case .unhealthy:
            String(localized: "Unhealthy")
        case .veryUnhealthy:
            String(localized: "Very unhealthy")
        case .hazardous:
            String(localized: "Hazardous")
        }
    }
}


================================================
FILE: DatWeatherDoe/API/Response/ForecastData.swift
================================================
//
//  ForecastData.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 6/23/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import Foundation

struct Forecast: Decodable {
    let dayDataArr: [ForecastDayData]

    private enum CodingKeys: String, CodingKey {
        case dayDataArr = "forecastday"
    }
}

struct ForecastDayData: Decodable {
    let temperatureData: ForecastTemperatureData
    let astro: SunriseSunsetData
    let hour: [HourlyUVIndex]

    private enum CodingKeys: String, CodingKey {
        case temperatureData = "day"
        case astro
        case hour
    }
}

struct HourlyUVIndex: Decodable {
    // swiftlint:disable:next identifier_name
    let uv: Double
}


================================================
FILE: DatWeatherDoe/API/Response/ForecastTemperatureData.swift
================================================
//
//  ForecastTemperatureData.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 6/23/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import Foundation

struct ForecastTemperatureData: Decodable {
    let maxTempC: Double
    let maxTempF: Double
    let minTempC: Double
    let minTempF: Double

    private enum CodingKeys: String, CodingKey {
        case maxTempC = "maxtemp_c"
        case maxTempF = "maxtemp_f"
        case minTempC = "mintemp_c"
        case minTempF = "mintemp_f"
    }
}


================================================
FILE: DatWeatherDoe/API/Response/SunriseSunsetData.swift
================================================
//
//  SunriseSunsetData.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 6/22/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import Foundation

struct SunriseSunsetData: Decodable {
    let sunrise: String
    let sunset: String
}


================================================
FILE: DatWeatherDoe/API/Response/TemperatureData.swift
================================================
//
//  TemperatureData.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 6/23/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import Foundation

struct TemperatureData: Decodable {
    let tempCelsius: Double
    let feelsLikeTempCelsius: Double
    let tempFahrenheit: Double
    let feelsLikeTempFahrenheit: Double

    private enum CodingKeys: String, CodingKey {
        case tempCelsius = "temp_c"
        case feelsLikeTempCelsius = "feelslike_c"
        case tempFahrenheit = "temp_f"
        case feelsLikeTempFahrenheit = "feelslike_f"
    }
}


================================================
FILE: DatWeatherDoe/API/Response/WeatherAPIResponse.swift
================================================
//
//  WeatherAPIResponse.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 2/3/18.
//  Copyright © 2018 Inder Dhir. All rights reserved.
//

import Foundation

struct WeatherAPIResponse: Decodable {
    let locationName: String
    let temperatureData: TemperatureData
    let isDay: Bool
    let weatherConditionCode: Int
    let humidity: Int
    let windData: WindData
    let uvIndex: Double
    let forecastDayData: ForecastDayData
    let airQualityIndex: AirQualityIndex

    private enum RootKeys: String, CodingKey {
        case location, current, forecast
    }

    private enum LocationKeys: String, CodingKey {
        case name
    }

    private enum CurrentKeys: String, CodingKey {
        case isDay = "is_day"
        case condition, humidity
        case airQuality = "air_quality"
        case uvIndex = "uv"
    }

    private enum WeatherConditionKeys: String, CodingKey {
        case code
    }

    private enum ForecastKeys: String, CodingKey {
        case forecastDay = "forecastday"
    }

    private enum ForecastDayKeys: String, CodingKey {
        case day, astro
    }

    private enum AirQualityKeys: String, CodingKey {
        case usEpaIndex = "us-epa-index"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: RootKeys.self)

        let locationContainer = try container.nestedContainer(keyedBy: LocationKeys.self, forKey: .location)
        locationName = try locationContainer.decode(String.self, forKey: .name)
        temperatureData = try container.decode(TemperatureData.self, forKey: .current)

        let currentContainer = try container.nestedContainer(keyedBy: CurrentKeys.self, forKey: .current)
        let isDayInt = try currentContainer.decode(Int.self, forKey: .isDay)
        isDay = isDayInt > 0

        let weatherConditionContainer = try currentContainer.nestedContainer(
            keyedBy: WeatherConditionKeys.self,
            forKey: .condition
        )
        weatherConditionCode = try weatherConditionContainer.decode(Int.self, forKey: .code)

        humidity = try currentContainer.decode(Int.self, forKey: .humidity)

        windData = try container.decode(WindData.self, forKey: .current)

        uvIndex = try currentContainer.decode(Double.self, forKey: .uvIndex)

        let forecast = try container.decode(Forecast.self, forKey: .forecast)
        if let dayData = forecast.dayDataArr.first {
            forecastDayData = dayData
        } else {
            throw DecodingError.dataCorruptedError(
                forKey: .forecast,
                in: container,
                debugDescription: "Missing forecast day data"
            )
        }

        let airQualityContainer =
        try currentContainer.nestedContainer(keyedBy: AirQualityKeys.self, forKey: .airQuality)
        airQualityIndex = try airQualityContainer.decode(AirQualityIndex.self, forKey: .usEpaIndex)
    }

    init(
        locationName: String,
        temperatureData: TemperatureData,
        isDay: Bool,
        weatherConditionCode: Int,
        humidity: Int,
        windData: WindData,
        uvIndex: Double,
        forecastDayData: ForecastDayData,
        airQualityIndex: AirQualityIndex
    ) {
        self.locationName = locationName
        self.temperatureData = temperatureData
        self.isDay = isDay
        self.weatherConditionCode = weatherConditionCode
        self.humidity = humidity
        self.windData = windData
        self.uvIndex = uvIndex
        self.forecastDayData = forecastDayData
        self.airQualityIndex = airQualityIndex
    }

    // hour = [0-23]
    func getHourlyUVIndex(hour: Int) -> Double {
        forecastDayData.hour[safe: hour]?.uv ?? uvIndex
    }
}

private extension Array {
    subscript(safe index: Index) -> Element? { indices ~= index ? self[index] : nil }
}


================================================
FILE: DatWeatherDoe/API/Response/WeatherAPIResponseParser.swift
================================================
//
//  WeatherAPIResponseParser.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/10/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import Foundation

protocol WeatherAPIResponseParserType {
    func parse(_ data: Data) throws -> WeatherAPIResponse
}

final class WeatherAPIResponseParser: WeatherAPIResponseParserType {
    func parse(_ data: Data) throws -> WeatherAPIResponse {
        try JSONDecoder().decode(WeatherAPIResponse.self, from: data)
    }
}


================================================
FILE: DatWeatherDoe/API/Response/WindData.swift
================================================
//
//  WindData.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 6/23/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import Foundation

struct WindData: Decodable {
    let speedMph: Double
    let degrees: Int
    let direction: String

    private enum CodingKeys: String, CodingKey {
        case speedMph = "wind_mph"
        case degrees = "wind_degree"
        case direction = "wind_dir"
    }
}


================================================
FILE: DatWeatherDoe/API/WeatherData.swift
================================================
//
//  WeatherData.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 5/22/21.
//  Copyright © 2021 Inder Dhir. All rights reserved.
//

import Foundation

struct WeatherData {
    let showWeatherIcon: Bool
    let textualRepresentation: String?
    let weatherCondition: WeatherCondition
    let response: WeatherAPIResponse
}


================================================
FILE: DatWeatherDoe/API/WeatherError.swift
================================================
//
//  WeatherError.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 5/22/21.
//  Copyright © 2021 Inder Dhir. All rights reserved.
//

import Foundation

enum WeatherError: LocalizedError {
    case unableToConstructUrl
    case locationError
    case latLongIncorrect
    case networkError

    var errorDescription: String? {
        switch self {
        case .unableToConstructUrl:
            "Unable to construct URL"
        case .locationError:
            String(localized: "❗️Location")
        case .latLongIncorrect:
            String(localized: "❗️Lat/Long")
        case .networkError:
            "🖧"
        }
    }
}


================================================
FILE: DatWeatherDoe/Config/APIKeyParser.swift
================================================
//
//  APIKeyParser.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/11/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import Foundation

final class APIKeyParser {
    func parse() -> String {
        guard let apiKey = Bundle.main.infoDictionary?["WEATHER_API_KEY"] as? String else {
            fatalError("Unable to find OPENWEATHERMAP_APP_ID in `Config.xcconfig`")
        }
        return apiKey
    }
}


================================================
FILE: DatWeatherDoe/Config/ConfigManager.swift
================================================
//
//  ConfigManager.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/29/16.
//  Copyright © 2016 Inder Dhir. All rights reserved.
//

import Foundation
import SwiftUI

protocol ConfigManagerType: AnyObject {
    var measurementUnit: String { get set }
    var weatherSource: String { get set }
    var weatherSourceText: String { get set }
    var refreshInterval: TimeInterval { get set }
    var isShowingWeatherIcon: Bool { get set }
    var isShowingHumidity: Bool { get set }
    var isShowingUVIndex: Bool { get set }
    var isRoundingOffData: Bool { get set }
    var isUnitLetterOff: Bool { get set }
    var isUnitSymbolOff: Bool { get set }
    var valueSeparator: String { get set }
    var isWeatherConditionAsTextEnabled: Bool { get set }
    var weatherConditionPosition: String { get set }

    func updateWeatherSource(_ source: WeatherSource, sourceText: String)
    func setConfigOptions(_ options: ConfigOptions)

    var parsedMeasurementUnit: MeasurementUnit { get }
}

final class ConfigManager: ConfigManagerType {
    @AppStorage("measurementUnit")
    public var measurementUnit = MeasurementUnit.imperial.rawValue

    @AppStorage("weatherSource")
    public var weatherSource = WeatherSource.location.rawValue

    @AppStorage("weatherSourceText")
    public var weatherSourceText = ""

    @AppStorage("refreshInterval")
    public var refreshInterval = RefreshInterval.fifteenMinutes.rawValue

    @AppStorage("isShowingWeatherIcon")
    public var isShowingWeatherIcon = true

    @AppStorage("isShowingHumidity")
    public var isShowingHumidity = false

    @AppStorage("isShowingUVIndex")
    public var isShowingUVIndex = false

    @AppStorage("isRoundingOffData")
    public var isRoundingOffData = false

    @AppStorage("isUnitLetterOff")
    public var isUnitLetterOff = false

    @AppStorage("isUnitSymbolOff")
    public var isUnitSymbolOff = false

    @AppStorage("valueSeparator")
    public var valueSeparator = "\u{007C}"

    @AppStorage("isWeatherConditionAsTextEnabled")
    public var isWeatherConditionAsTextEnabled = false

    @AppStorage("weatherConditionPosition")
    public var weatherConditionPosition = WeatherConditionPosition.beforeTemperature.rawValue

    func updateWeatherSource(_ source: WeatherSource, sourceText: String) {
        weatherSource = source.rawValue
        weatherSourceText = source == .location ? "" : sourceText
    }

    func setConfigOptions(_ options: ConfigOptions) {
        refreshInterval = options.refreshInterval.rawValue
        isShowingHumidity = options.isShowingHumidity
        isShowingUVIndex = options.isShowingUVIndex
        isRoundingOffData = options.isRoundingOffData
        isUnitLetterOff = options.isUnitLetterOff
        isUnitSymbolOff = options.isUnitSymbolOff
        valueSeparator = options.valueSeparator
        isWeatherConditionAsTextEnabled = options.isWeatherConditionAsTextEnabled
    }

    var parsedMeasurementUnit: MeasurementUnit {
        MeasurementUnit(rawValue: measurementUnit) ?? .imperial
    }
}


================================================
FILE: DatWeatherDoe/Config/ConfigOptions.swift
================================================
//
//  ConfigOptions.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 8/3/23.
//  Copyright © 2023 Inder Dhir. All rights reserved.
//

import Foundation

struct ConfigOptions {
    let refreshInterval: RefreshInterval
    let isShowingHumidity: Bool
    let isShowingUVIndex: Bool
    let isRoundingOffData: Bool
    let isUnitLetterOff: Bool
    let isUnitSymbolOff: Bool
    let valueSeparator: String
    let isWeatherConditionAsTextEnabled: Bool
}


================================================
FILE: DatWeatherDoe/DatWeatherDoeApp.swift
================================================
//
//  DatWeatherDoeApp.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 6/17/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import MenuBarExtraAccess
import OSLog
import SwiftUI

@main
struct DatWeatherDoeApp: App {
    @State private var configManager: ConfigManager
    @ObservedObject private var configureViewModel = ConfigureViewModel(configManager: ConfigManager())
    @ObservedObject private var viewModel: WeatherViewModel
    @State private var isMenuPresented = false
    @State private var statusItem: NSStatusItem?

    init() {
        configManager = ConfigManager()

        let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "bundleID", category: "main")
        viewModel = WeatherViewModel(
            locationFetcher: SystemLocationFetcher(logger: logger),
            weatherFactory: WeatherRepositoryFactory(
                appId: APIKeyParser().parse(),
                networkClient: NetworkClient(),
                logger: logger
            ),
            configManager: ConfigManager(),
            logger: logger
        )
        viewModel.setup(with: WeatherDataFormatter(configManager: configManager))
    }

    var body: some Scene {
        MenuBarExtra(
            content: {
                MenuView(
                    viewModel: viewModel,
                    configureViewModel: configureViewModel,
                    onSeeWeather: {
                        viewModel.seeForecastForCurrentCity()
                        closePopover()
                    },
                    onRefresh: {
                        viewModel.getUpdatedWeatherAfterRefresh()
                        closePopover()
                    },
                    onSave: {
                        closePopover()
                    }
                )
            },
            label: {
                StatusBarView(weatherResult: viewModel.weatherResult)
                    .onAppear {
                        viewModel.getUpdatedWeatherAfterRefresh()
                    }
            }
        )
        .menuBarExtraAccess(isPresented: $isMenuPresented) { statusItem in
            self.statusItem = statusItem
        }
        .onChange(of: isMenuPresented) { newValue in
            if !newValue {
                configureViewModel.saveConfig()
                viewModel.getUpdatedWeatherAfterRefresh()
            }
        }
        .windowStyle(.hiddenTitleBar)
        .menuBarExtraStyle(.window)
    }

    private func closePopover() {
        statusItem?.togglePresented()
    }
}
    


================================================
FILE: DatWeatherDoe/Localization/en.xcloc/Localized Contents/en.xliff
================================================
<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
  <file original="DatWeatherDoe/Resources/en.lproj/InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext">
    <header>
      <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="12.5" build-num="12E262"/>
    </header>
    <body>
      <trans-unit id="CFBundleName" xml:space="preserve">
        <source>DatWeatherDoe</source>
        <target>DatWeatherDoe</target>
        <note>Bundle name</note>
      </trans-unit>
      <trans-unit id="NSHumanReadableCopyright" xml:space="preserve">
        <source>Copyright © 2016 Inder Dhir. All rights reserved.</source>
        <target>Copyright © 2016 Inder Dhir. All rights reserved.</target>
        <note>Copyright (human-readable)</note>
      </trans-unit>
      <trans-unit id="NSLocationWhenInUseUsageDescription" xml:space="preserve">
        <source>DatWeatherDoe optionally uses your current location to get the weather</source>
        <target>DatWeatherDoe optionally uses your current location to get the weather</target>
        <note>Privacy - Location When In Use Usage Description</note>
      </trans-unit>
    </body>
  </file>
  <file original="DatWeatherDoe/UI/Base.lproj/MainMenu.xib" source-language="en" target-language="en" datatype="plaintext">
    <header>
      <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="12.5" build-num="12E262"/>
    </header>
    <body>
      <trans-unit id="1UK-8n-QPP.title" xml:space="preserve">
        <source>Customize Toolbar…</source>
        <target>Customize Toolbar…</target>
        <note>Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP";</note>
      </trans-unit>
      <trans-unit id="1Xt-HY-uBw.title" xml:space="preserve">
        <source>DatWeatherDoe</source>
        <target>DatWeatherDoe</target>
        <note>Class = "NSMenuItem"; title = "DatWeatherDoe"; ObjectID = "1Xt-HY-uBw";</note>
      </trans-unit>
      <trans-unit id="1b7-l0-nxx.title" xml:space="preserve">
        <source>Find</source>
        <target>Find</target>
        <note>Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx";</note>
      </trans-unit>
      <trans-unit id="1tx-W0-xDw.title" xml:space="preserve">
        <source>Lower</source>
        <target>Lower</target>
        <note>Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw";</note>
      </trans-unit>
      <trans-unit id="2h7-ER-AoG.title" xml:space="preserve">
        <source>Raise</source>
        <target>Raise</target>
        <note>Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG";</note>
      </trans-unit>
      <trans-unit id="2oI-Rn-ZJC.title" xml:space="preserve">
        <source>Transformations</source>
        <target>Transformations</target>
        <note>Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC";</note>
      </trans-unit>
      <trans-unit id="3IN-sU-3Bg.title" xml:space="preserve">
        <source>Spelling</source>
        <target>Spelling</target>
        <note>Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg";</note>
      </trans-unit>
      <trans-unit id="3Om-Ey-2VK.title" xml:space="preserve">
        <source>Use Default</source>
        <target>Use Default</target>
        <note>Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK";</note>
      </trans-unit>
      <trans-unit id="3rS-ZA-NoH.title" xml:space="preserve">
        <source>Speech</source>
        <target>Speech</target>
        <note>Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH";</note>
      </trans-unit>
      <trans-unit id="4EN-yA-p0u.title" xml:space="preserve">
        <source>Find</source>
        <target>Find</target>
        <note>Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u";</note>
      </trans-unit>
      <trans-unit id="4sb-4s-VLi.title" xml:space="preserve">
        <source>Quit DatWeatherDoe</source>
        <target>Quit DatWeatherDoe</target>
        <note>Class = "NSMenuItem"; title = "Quit DatWeatherDoe"; ObjectID = "4sb-4s-VLi";</note>
      </trans-unit>
      <trans-unit id="5QF-Oa-p0T.title" xml:space="preserve">
        <source>Edit</source>
        <target>Edit</target>
        <note>Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T";</note>
      </trans-unit>
      <trans-unit id="5Vv-lz-BsD.title" xml:space="preserve">
        <source>Copy Style</source>
        <target>Copy Style</target>
        <note>Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD";</note>
      </trans-unit>
      <trans-unit id="5kV-Vb-QxS.title" xml:space="preserve">
        <source>About DatWeatherDoe</source>
        <target>About DatWeatherDoe</target>
        <note>Class = "NSMenuItem"; title = "About DatWeatherDoe"; ObjectID = "5kV-Vb-QxS";</note>
      </trans-unit>
      <trans-unit id="6dh-zS-Vam.title" xml:space="preserve">
        <source>Redo</source>
        <target>Redo</target>
        <note>Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam";</note>
      </trans-unit>
      <trans-unit id="8mr-sm-Yjd.title" xml:space="preserve">
        <source>Writing Direction</source>
        <target>Writing Direction</target>
        <note>Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd";</note>
      </trans-unit>
      <trans-unit id="9ic-FL-obx.title" xml:space="preserve">
        <source>Substitutions</source>
        <target>Substitutions</target>
        <note>Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx";</note>
      </trans-unit>
      <trans-unit id="9yt-4B-nSM.title" xml:space="preserve">
        <source>Smart Copy/Paste</source>
        <target>Smart Copy/Paste</target>
        <note>Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM";</note>
      </trans-unit>
      <trans-unit id="46P-cB-AYj.title" xml:space="preserve">
        <source>Tighten</source>
        <target>Tighten</target>
        <note>Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj";</note>
      </trans-unit>
      <trans-unit id="78Y-hA-62v.title" xml:space="preserve">
        <source>Correct Spelling Automatically</source>
        <target>Correct Spelling Automatically</target>
        <note>Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v";</note>
      </trans-unit>
      <trans-unit id="AYu-sK-qS6.title" xml:space="preserve">
        <source>Main Menu</source>
        <target>Main Menu</target>
        <note>Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6";</note>
      </trans-unit>
      <trans-unit id="BOF-NM-1cW.title" xml:space="preserve">
        <source>Preferences…</source>
        <target>Preferences…</target>
        <note>Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW";</note>
      </trans-unit>
      <trans-unit id="BgM-ve-c93.title" xml:space="preserve">
        <source>	Left to Right</source>
        <target>	Left to Right</target>
        <note>Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93";</note>
      </trans-unit>
      <trans-unit id="Bw7-FT-i3A.title" xml:space="preserve">
        <source>Save As…</source>
        <target>Save As…</target>
        <note>Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A";</note>
      </trans-unit>
      <trans-unit id="DVo-aG-piG.title" xml:space="preserve">
        <source>Close</source>
        <target>Close</target>
        <note>Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG";</note>
      </trans-unit>
      <trans-unit id="Dv1-io-Yv7.title" xml:space="preserve">
        <source>Spelling and Grammar</source>
        <target>Spelling and Grammar</target>
        <note>Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7";</note>
      </trans-unit>
      <trans-unit id="F2S-fz-NVQ.title" xml:space="preserve">
        <source>Help</source>
        <target>Help</target>
        <note>Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ";</note>
      </trans-unit>
      <trans-unit id="FKE-Sm-Kum.title" xml:space="preserve">
        <source>DatWeatherDoe Help</source>
        <target>DatWeatherDoe Help</target>
        <note>Class = "NSMenuItem"; title = "DatWeatherDoe Help"; ObjectID = "FKE-Sm-Kum";</note>
      </trans-unit>
      <trans-unit id="Fal-I4-PZk.title" xml:space="preserve">
        <source>Text</source>
        <target>Text</target>
        <note>Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk";</note>
      </trans-unit>
      <trans-unit id="FeM-D8-WVr.title" xml:space="preserve">
        <source>Substitutions</source>
        <target>Substitutions</target>
        <note>Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr";</note>
      </trans-unit>
      <trans-unit id="GB9-OM-e27.title" xml:space="preserve">
        <source>Bold</source>
        <target>Bold</target>
        <note>Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27";</note>
      </trans-unit>
      <trans-unit id="GEO-Iw-cKr.title" xml:space="preserve">
        <source>Format</source>
        <target>Format</target>
        <note>Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr";</note>
      </trans-unit>
      <trans-unit id="GUa-eO-cwY.title" xml:space="preserve">
        <source>Use Default</source>
        <target>Use Default</target>
        <note>Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY";</note>
      </trans-unit>
      <trans-unit id="Gi5-1S-RQB.title" xml:space="preserve">
        <source>Font</source>
        <target>Font</target>
        <note>Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB";</note>
      </trans-unit>
      <trans-unit id="H1b-Si-o9J.title" xml:space="preserve">
        <source>Writing Direction</source>
        <target>Writing Direction</target>
        <note>Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J";</note>
      </trans-unit>
      <trans-unit id="H8h-7b-M4v.title" xml:space="preserve">
        <source>View</source>
        <target>View</target>
        <note>Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v";</note>
      </trans-unit>
      <trans-unit id="HFQ-gK-NFA.title" xml:space="preserve">
        <source>Text Replacement</source>
        <target>Text Replacement</target>
        <note>Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA";</note>
      </trans-unit>
      <trans-unit id="HFo-cy-zxI.title" xml:space="preserve">
        <source>Show Spelling and Grammar</source>
        <target>Show Spelling and Grammar</target>
        <note>Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI";</note>
      </trans-unit>
      <trans-unit id="HyV-fh-RgO.title" xml:space="preserve">
        <source>View</source>
        <target>View</target>
        <note>Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO";</note>
      </trans-unit>
      <trans-unit id="I0S-gh-46l.title" xml:space="preserve">
        <source>Subscript</source>
        <target>Subscript</target>
        <note>Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l";</note>
      </trans-unit>
      <trans-unit id="IAo-SY-fd9.title" xml:space="preserve">
        <source>Open…</source>
        <target>Open…</target>
        <note>Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9";</note>
      </trans-unit>
      <trans-unit id="J5U-5w-g23.title" xml:space="preserve">
        <source>Justify</source>
        <target>Justify</target>
        <note>Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23";</note>
      </trans-unit>
      <trans-unit id="J7y-lM-qPV.title" xml:space="preserve">
        <source>Use None</source>
        <target>Use None</target>
        <note>Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV";</note>
      </trans-unit>
      <trans-unit id="KaW-ft-85H.title" xml:space="preserve">
        <source>Revert to Saved</source>
        <target>Revert to Saved</target>
        <note>Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H";</note>
      </trans-unit>
      <trans-unit id="Kd2-mp-pUS.title" xml:space="preserve">
        <source>Show All</source>
        <target>Show All</target>
        <note>Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS";</note>
      </trans-unit>
      <trans-unit id="LE2-aR-0XJ.title" xml:space="preserve">
        <source>Bring All to Front</source>
        <target>Bring All to Front</target>
        <note>Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ";</note>
      </trans-unit>
      <trans-unit id="LVM-kO-fVI.title" xml:space="preserve">
        <source>Paste Ruler</source>
        <target>Paste Ruler</target>
        <note>Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI";</note>
      </trans-unit>
      <trans-unit id="Lbh-J2-qVU.title" xml:space="preserve">
        <source>	Left to Right</source>
        <target>	Left to Right</target>
        <note>Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU";</note>
      </trans-unit>
      <trans-unit id="MkV-Pr-PK5.title" xml:space="preserve">
        <source>Copy Ruler</source>
        <target>Copy Ruler</target>
        <note>Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5";</note>
      </trans-unit>
      <trans-unit id="NMo-om-nkz.title" xml:space="preserve">
        <source>Services</source>
        <target>Services</target>
        <note>Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz";</note>
      </trans-unit>
      <trans-unit id="Nop-cj-93Q.title" xml:space="preserve">
        <source>	Default</source>
        <target>	Default</target>
        <note>Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q";</note>
      </trans-unit>
      <trans-unit id="OY7-WF-poV.title" xml:space="preserve">
        <source>Minimize</source>
        <target>Minimize</target>
        <note>Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV";</note>
      </trans-unit>
      <trans-unit id="OaQ-X3-Vso.title" xml:space="preserve">
        <source>Baseline</source>
        <target>Baseline</target>
        <note>Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso";</note>
      </trans-unit>
      <trans-unit id="Olw-nP-bQN.title" xml:space="preserve">
        <source>Hide DatWeatherDoe</source>
        <target>Hide DatWeatherDoe</target>
        <note>Class = "NSMenuItem"; title = "Hide DatWeatherDoe"; ObjectID = "Olw-nP-bQN";</note>
      </trans-unit>
      <trans-unit id="OwM-mh-QMV.title" xml:space="preserve">
        <source>Find Previous</source>
        <target>Find Previous</target>
        <note>Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV";</note>
      </trans-unit>
      <trans-unit id="Oyz-dy-DGm.title" xml:space="preserve">
        <source>Stop Speaking</source>
        <target>Stop Speaking</target>
        <note>Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm";</note>
      </trans-unit>
      <trans-unit id="Ptp-SP-VEL.title" xml:space="preserve">
        <source>Bigger</source>
        <target>Bigger</target>
        <note>Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL";</note>
      </trans-unit>
      <trans-unit id="Q5e-8K-NDq.title" xml:space="preserve">
        <source>Show Fonts</source>
        <target>Show Fonts</target>
        <note>Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq";</note>
      </trans-unit>
      <trans-unit id="QvC-M9-y7g.title" xml:space="preserve">
        <source>DatWeatherDoe</source>
        <target>DatWeatherDoe</target>
        <note>Class = "NSWindow"; title = "DatWeatherDoe"; ObjectID = "QvC-M9-y7g";</note>
      </trans-unit>
      <trans-unit id="R4o-n2-Eq4.title" xml:space="preserve">
        <source>Zoom</source>
        <target>Zoom</target>
        <note>Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4";</note>
      </trans-unit>
      <trans-unit id="RB4-Sm-HuC.title" xml:space="preserve">
        <source>	Right to Left</source>
        <target>	Right to Left</target>
        <note>Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC";</note>
      </trans-unit>
      <trans-unit id="Rqc-34-cIF.title" xml:space="preserve">
        <source>Superscript</source>
        <target>Superscript</target>
        <note>Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF";</note>
      </trans-unit>
      <trans-unit id="Ruw-6m-B2m.title" xml:space="preserve">
        <source>Select All</source>
        <target>Select All</target>
        <note>Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m";</note>
      </trans-unit>
      <trans-unit id="S0p-oC-mLd.title" xml:space="preserve">
        <source>Jump to Selection</source>
        <target>Jump to Selection</target>
        <note>Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd";</note>
      </trans-unit>
      <trans-unit id="Td7-aD-5lo.title" xml:space="preserve">
        <source>Window</source>
        <target>Window</target>
        <note>Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo";</note>
      </trans-unit>
      <trans-unit id="UEZ-Bs-lqG.title" xml:space="preserve">
        <source>Capitalize</source>
        <target>Capitalize</target>
        <note>Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG";</note>
      </trans-unit>
      <trans-unit id="VIY-Ag-zcb.title" xml:space="preserve">
        <source>Center</source>
        <target>Center</target>
        <note>Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb";</note>
      </trans-unit>
      <trans-unit id="Vdr-fp-XzO.title" xml:space="preserve">
        <source>Hide Others</source>
        <target>Hide Others</target>
        <note>Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO";</note>
      </trans-unit>
      <trans-unit id="Vjx-xi-njq.title" xml:space="preserve">
        <source>Italic</source>
        <target>Italic</target>
        <note>Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq";</note>
      </trans-unit>
      <trans-unit id="W48-6f-4Dl.title" xml:space="preserve">
        <source>Edit</source>
        <target>Edit</target>
        <note>Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl";</note>
      </trans-unit>
      <trans-unit id="WRG-CD-K1S.title" xml:space="preserve">
        <source>Underline</source>
        <target>Underline</target>
        <note>Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S";</note>
      </trans-unit>
      <trans-unit id="Was-JA-tGl.title" xml:space="preserve">
        <source>New</source>
        <target>New</target>
        <note>Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl";</note>
      </trans-unit>
      <trans-unit id="WeT-3V-zwk.title" xml:space="preserve">
        <source>Paste and Match Style</source>
        <target>Paste and Match Style</target>
        <note>Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk";</note>
      </trans-unit>
      <trans-unit id="Xz5-n4-O0W.title" xml:space="preserve">
        <source>Find…</source>
        <target>Find…</target>
        <note>Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W";</note>
      </trans-unit>
      <trans-unit id="YEy-JH-Tfz.title" xml:space="preserve">
        <source>Find and Replace…</source>
        <target>Find and Replace…</target>
        <note>Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz";</note>
      </trans-unit>
      <trans-unit id="YGs-j5-SAR.title" xml:space="preserve">
        <source>	Default</source>
        <target>	Default</target>
        <note>Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR";</note>
      </trans-unit>
      <trans-unit id="Ynk-f8-cLZ.title" xml:space="preserve">
        <source>Start Speaking</source>
        <target>Start Speaking</target>
        <note>Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ";</note>
      </trans-unit>
      <trans-unit id="ZM1-6Q-yy1.title" xml:space="preserve">
        <source>Align Left</source>
        <target>Align Left</target>
        <note>Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1";</note>
      </trans-unit>
      <trans-unit id="ZvO-Gk-QUH.title" xml:space="preserve">
        <source>Paragraph</source>
        <target>Paragraph</target>
        <note>Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH";</note>
      </trans-unit>
      <trans-unit id="aTl-1u-JFS.title" xml:space="preserve">
        <source>Print…</source>
        <target>Print…</target>
        <note>Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS";</note>
      </trans-unit>
      <trans-unit id="aUF-d1-5bR.title" xml:space="preserve">
        <source>Window</source>
        <target>Window</target>
        <note>Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR";</note>
      </trans-unit>
      <trans-unit id="aXa-aM-Jaq.title" xml:space="preserve">
        <source>Font</source>
        <target>Font</target>
        <note>Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq";</note>
      </trans-unit>
      <trans-unit id="agt-UL-0e3.title" xml:space="preserve">
        <source>Use Default</source>
        <target>Use Default</target>
        <note>Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3";</note>
      </trans-unit>
      <trans-unit id="bgn-CT-cEk.title" xml:space="preserve">
        <source>Show Colors</source>
        <target>Show Colors</target>
        <note>Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk";</note>
      </trans-unit>
      <trans-unit id="bib-Uj-vzu.title" xml:space="preserve">
        <source>File</source>
        <target>File</target>
        <note>Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu";</note>
      </trans-unit>
      <trans-unit id="buJ-ug-pKt.title" xml:space="preserve">
        <source>Use Selection for Find</source>
        <target>Use Selection for Find</target>
        <note>Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt";</note>
      </trans-unit>
      <trans-unit id="c8a-y6-VQd.title" xml:space="preserve">
        <source>Transformations</source>
        <target>Transformations</target>
        <note>Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd";</note>
      </trans-unit>
      <trans-unit id="cDB-IK-hbR.title" xml:space="preserve">
        <source>Use None</source>
        <target>Use None</target>
        <note>Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR";</note>
      </trans-unit>
      <trans-unit id="cqv-fj-IhA.title" xml:space="preserve">
        <source>Selection</source>
        <target>Selection</target>
        <note>Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA";</note>
      </trans-unit>
      <trans-unit id="cwL-P1-jid.title" xml:space="preserve">
        <source>Smart Links</source>
        <target>Smart Links</target>
        <note>Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid";</note>
      </trans-unit>
      <trans-unit id="d9M-CD-aMd.title" xml:space="preserve">
        <source>Make Lower Case</source>
        <target>Make Lower Case</target>
        <note>Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd";</note>
      </trans-unit>
      <trans-unit id="d9c-me-L2H.title" xml:space="preserve">
        <source>Text</source>
        <target>Text</target>
        <note>Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H";</note>
      </trans-unit>
      <trans-unit id="dMs-cI-mzQ.title" xml:space="preserve">
        <source>File</source>
        <target>File</target>
        <note>Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ";</note>
      </trans-unit>
      <trans-unit id="dRJ-4n-Yzg.title" xml:space="preserve">
        <source>Undo</source>
        <target>Undo</target>
        <note>Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg";</note>
      </trans-unit>
      <trans-unit id="gVA-U4-sdL.title" xml:space="preserve">
        <source>Paste</source>
        <target>Paste</target>
        <note>Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL";</note>
      </trans-unit>
      <trans-unit id="hQb-2v-fYv.title" xml:space="preserve">
        <source>Smart Quotes</source>
        <target>Smart Quotes</target>
        <note>Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv";</note>
      </trans-unit>
      <trans-unit id="hz2-CU-CR7.title" xml:space="preserve">
        <source>Check Document Now</source>
        <target>Check Document Now</target>
        <note>Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7";</note>
      </trans-unit>
      <trans-unit id="hz9-B4-Xy5.title" xml:space="preserve">
        <source>Services</source>
        <target>Services</target>
        <note>Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5";</note>
      </trans-unit>
      <trans-unit id="i1d-Er-qST.title" xml:space="preserve">
        <source>Smaller</source>
        <target>Smaller</target>
        <note>Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST";</note>
      </trans-unit>
      <trans-unit id="ijk-EB-dga.title" xml:space="preserve">
        <source>Baseline</source>
        <target>Baseline</target>
        <note>Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga";</note>
      </trans-unit>
      <trans-unit id="jBQ-r6-VK2.title" xml:space="preserve">
        <source>Kern</source>
        <target>Kern</target>
        <note>Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2";</note>
      </trans-unit>
      <trans-unit id="jFq-tB-4Kx.title" xml:space="preserve">
        <source>	Right to Left</source>
        <target>	Right to Left</target>
        <note>Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx";</note>
      </trans-unit>
      <trans-unit id="jxT-CU-nIS.title" xml:space="preserve">
        <source>Format</source>
        <target>Format</target>
        <note>Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS";</note>
      </trans-unit>
      <trans-unit id="mK6-2p-4JG.title" xml:space="preserve">
        <source>Check Grammar With Spelling</source>
        <target>Check Grammar With Spelling</target>
        <note>Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG";</note>
      </trans-unit>
      <trans-unit id="o6e-r0-MWq.title" xml:space="preserve">
        <source>Ligatures</source>
        <target>Ligatures</target>
        <note>Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq";</note>
      </trans-unit>
      <trans-unit id="oas-Oc-fiZ.title" xml:space="preserve">
        <source>Open Recent</source>
        <target>Open Recent</target>
        <note>Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ";</note>
      </trans-unit>
      <trans-unit id="ogc-rX-tC1.title" xml:space="preserve">
        <source>Loosen</source>
        <target>Loosen</target>
        <note>Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1";</note>
      </trans-unit>
      <trans-unit id="pa3-QI-u2k.title" xml:space="preserve">
        <source>Delete</source>
        <target>Delete</target>
        <note>Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k";</note>
      </trans-unit>
      <trans-unit id="pxx-59-PXV.title" xml:space="preserve">
        <source>Save…</source>
        <target>Save…</target>
        <note>Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV";</note>
      </trans-unit>
      <trans-unit id="q09-fT-Sye.title" xml:space="preserve">
        <source>Find Next</source>
        <target>Find Next</target>
        <note>Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye";</note>
      </trans-unit>
      <trans-unit id="qIS-W8-SiK.title" xml:space="preserve">
        <source>Page Setup…</source>
        <target>Page Setup…</target>
        <note>Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK";</note>
      </trans-unit>
      <trans-unit id="rbD-Rh-wIN.title" xml:space="preserve">
        <source>Check Spelling While Typing</source>
        <target>Check Spelling While Typing</target>
        <note>Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN";</note>
      </trans-unit>
      <trans-unit id="rgM-f4-ycn.title" xml:space="preserve">
        <source>Smart Dashes</source>
        <target>Smart Dashes</target>
        <note>Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn";</note>
      </trans-unit>
      <trans-unit id="snW-S8-Cw5.title" xml:space="preserve">
        <source>Show Toolbar</source>
        <target>Show Toolbar</target>
        <note>Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5";</note>
      </trans-unit>
      <trans-unit id="tRr-pd-1PS.title" xml:space="preserve">
        <source>Data Detectors</source>
        <target>Data Detectors</target>
        <note>Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS";</note>
      </trans-unit>
      <trans-unit id="tXI-mr-wws.title" xml:space="preserve">
        <source>Open Recent</source>
        <target>Open Recent</target>
        <note>Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws";</note>
      </trans-unit>
      <trans-unit id="tlD-Oa-oAM.title" xml:space="preserve">
        <source>Kern</source>
        <target>Kern</target>
        <note>Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM";</note>
      </trans-unit>
      <trans-unit id="uQy-DD-JDr.title" xml:space="preserve">
        <source>DatWeatherDoe</source>
        <target>DatWeatherDoe</target>
        <note>Class = "NSMenu"; title = "DatWeatherDoe"; ObjectID = "uQy-DD-JDr";</note>
      </trans-unit>
      <trans-unit id="uRl-iY-unG.title" xml:space="preserve">
        <source>Cut</source>
        <target>Cut</target>
        <note>Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG";</note>
      </trans-unit>
      <trans-unit id="vKC-jM-MkH.title" xml:space="preserve">
        <source>Paste Style</source>
        <target>Paste Style</target>
        <note>Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH";</note>
      </trans-unit>
      <trans-unit id="vLm-3I-IUL.title" xml:space="preserve">
        <source>Show Ruler</source>
        <target>Show Ruler</target>
        <note>Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL";</note>
      </trans-unit>
      <trans-unit id="vNY-rz-j42.title" xml:space="preserve">
        <source>Clear Menu</source>
        <target>Clear Menu</target>
        <note>Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42";</note>
      </trans-unit>
      <trans-unit id="vmV-6d-7jI.title" xml:space="preserve">
        <source>Make Upper Case</source>
        <target>Make Upper Case</target>
        <note>Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI";</note>
      </trans-unit>
      <trans-unit id="w0m-vy-SC9.title" xml:space="preserve">
        <source>Ligatures</source>
        <target>Ligatures</target>
        <note>Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9";</note>
      </trans-unit>
      <trans-unit id="wb2-vD-lq4.title" xml:space="preserve">
        <source>Align Right</source>
        <target>Align Right</target>
        <note>Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4";</note>
      </trans-unit>
      <trans-unit id="wpr-3q-Mcd.title" xml:space="preserve">
        <source>Help</source>
        <target>Help</target>
        <note>Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd";</note>
      </trans-unit>
      <trans-unit id="x3v-GG-iWU.title" xml:space="preserve">
        <source>Copy</source>
        <target>Copy</target>
        <note>Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU";</note>
      </trans-unit>
      <trans-unit id="xQD-1f-W4t.title" xml:space="preserve">
        <source>Use All</source>
        <target>Use All</target>
        <note>Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t";</note>
      </trans-unit>
      <trans-unit id="xrE-MZ-jX0.title" xml:space="preserve">
        <source>Speech</source>
        <target>Speech</target>
        <note>Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0";</note>
      </trans-unit>
      <trans-unit id="z6F-FW-3nz.title" xml:space="preserve">
        <source>Show Substitutions</source>
        <target>Show Substitutions</target>
        <note>Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz";</note>
      </trans-unit>
    </body>
  </file>
</xliff>


================================================
FILE: DatWeatherDoe/Localization/en.xcloc/Source Contents/DatWeatherDoe/Resources/en.lproj/InfoPlist.strings
================================================
/* Bundle name */
"CFBundleName" = "DatWeatherDoe";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2016 Inder Dhir. All rights reserved.";
/* Privacy - Location When In Use Usage Description */
"NSLocationWhenInUseUsageDescription" = "DatWeatherDoe optionally uses your current location to get the weather";


================================================
FILE: DatWeatherDoe/Localization/en.xcloc/Source Contents/DatWeatherDoe/UI/Base.lproj/MainMenu.xib
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10116" systemVersion="15E65" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
    <dependencies>
        <deployment identifier="macosx"/>
        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10116"/>
    </dependencies>
    <objects>
        <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
            <connections>
                <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
            </connections>
        </customObject>
        <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
        <customObject id="-3" userLabel="Application" customClass="NSObject"/>
        <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="DatWeatherDoe" customModuleProvider="target">
            <connections>
                <outlet property="window" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
            </connections>
        </customObject>
        <customObject id="YLy-65-1bz" customClass="NSFontManager"/>
        <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
            <items>
                <menuItem title="DatWeatherDoe" id="1Xt-HY-uBw">
                    <modifierMask key="keyEquivalentModifierMask"/>
                    <menu key="submenu" title="DatWeatherDoe" systemMenu="apple" id="uQy-DD-JDr">
                        <items>
                            <menuItem title="About DatWeatherDoe" id="5kV-Vb-QxS">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <connections>
                                    <action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
                                </connections>
                            </menuItem>
                            <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
                            <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
                            <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
                            <menuItem title="Services" id="NMo-om-nkz">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
                            </menuItem>
                            <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
                            <menuItem title="Hide DatWeatherDoe" keyEquivalent="h" id="Olw-nP-bQN">
                                <connections>
                                    <action selector="hide:" target="-1" id="PnN-Uc-m68"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
                                <connections>
                                    <action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Show All" id="Kd2-mp-pUS">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <connections>
                                    <action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
                                </connections>
                            </menuItem>
                            <menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
                            <menuItem title="Quit DatWeatherDoe" keyEquivalent="q" id="4sb-4s-VLi">
                                <connections>
                                    <action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
                                </connections>
                            </menuItem>
                        </items>
                    </menu>
                </menuItem>
                <menuItem title="File" id="dMs-cI-mzQ">
                    <modifierMask key="keyEquivalentModifierMask"/>
                    <menu key="submenu" title="File" id="bib-Uj-vzu">
                        <items>
                            <menuItem title="New" keyEquivalent="n" id="Was-JA-tGl">
                                <connections>
                                    <action selector="newDocument:" target="-1" id="4Si-XN-c54"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9">
                                <connections>
                                    <action selector="openDocument:" target="-1" id="bVn-NM-KNZ"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Open Recent" id="tXI-mr-wws">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ">
                                    <items>
                                        <menuItem title="Clear Menu" id="vNY-rz-j42">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="clearRecentDocuments:" target="-1" id="Daa-9d-B3U"/>
                                            </connections>
                                        </menuItem>
                                    </items>
                                </menu>
                            </menuItem>
                            <menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
                            <menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
                                <connections>
                                    <action selector="performClose:" target="-1" id="HmO-Ls-i7Q"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV">
                                <connections>
                                    <action selector="saveDocument:" target="-1" id="teZ-XB-qJY"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A">
                                <connections>
                                    <action selector="saveDocumentAs:" target="-1" id="mDf-zr-I0C"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Revert to Saved" id="KaW-ft-85H">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <connections>
                                    <action selector="revertDocumentToSaved:" target="-1" id="iJ3-Pv-kwq"/>
                                </connections>
                            </menuItem>
                            <menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
                            <menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK">
                                <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
                                <connections>
                                    <action selector="runPageLayout:" target="-1" id="Din-rz-gC5"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS">
                                <connections>
                                    <action selector="print:" target="-1" id="qaZ-4w-aoO"/>
                                </connections>
                            </menuItem>
                        </items>
                    </menu>
                </menuItem>
                <menuItem title="Edit" id="5QF-Oa-p0T">
                    <modifierMask key="keyEquivalentModifierMask"/>
                    <menu key="submenu" title="Edit" id="W48-6f-4Dl">
                        <items>
                            <menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
                                <connections>
                                    <action selector="undo:" target="-1" id="M6e-cu-g7V"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
                                <connections>
                                    <action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
                                </connections>
                            </menuItem>
                            <menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
                            <menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
                                <connections>
                                    <action selector="cut:" target="-1" id="YJe-68-I9s"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
                                <connections>
                                    <action selector="copy:" target="-1" id="G1f-GL-Joy"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
                                <connections>
                                    <action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
                                <connections>
                                    <action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Delete" id="pa3-QI-u2k">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <connections>
                                    <action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
                                <connections>
                                    <action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
                                </connections>
                            </menuItem>
                            <menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
                            <menuItem title="Find" id="4EN-yA-p0u">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <menu key="submenu" title="Find" id="1b7-l0-nxx">
                                    <items>
                                        <menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
                                            <connections>
                                                <action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
                                            <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
                                            <connections>
                                                <action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
                                            <connections>
                                                <action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
                                            <connections>
                                                <action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
                                            <connections>
                                                <action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
                                            <connections>
                                                <action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
                                            </connections>
                                        </menuItem>
                                    </items>
                                </menu>
                            </menuItem>
                            <menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
                                    <items>
                                        <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
                                            <connections>
                                                <action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
                                            <connections>
                                                <action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
                                        <menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
                                            </connections>
                                        </menuItem>
                                    </items>
                                </menu>
                            </menuItem>
                            <menuItem title="Substitutions" id="9ic-FL-obx">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
                                    <items>
                                        <menuItem title="Show Substitutions" id="z6F-FW-3nz">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
                                        <menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Smart Quotes" id="hQb-2v-fYv">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Smart Dashes" id="rgM-f4-ycn">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Smart Links" id="cwL-P1-jid">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Data Detectors" id="tRr-pd-1PS">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Text Replacement" id="HFQ-gK-NFA">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
                                            </connections>
                                        </menuItem>
                                    </items>
                                </menu>
                            </menuItem>
                            <menuItem title="Transformations" id="2oI-Rn-ZJC">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <menu key="submenu" title="Transformations" id="c8a-y6-VQd">
                                    <items>
                                        <menuItem title="Make Upper Case" id="vmV-6d-7jI">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Make Lower Case" id="d9M-CD-aMd">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Capitalize" id="UEZ-Bs-lqG">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
                                            </connections>
                                        </menuItem>
                                    </items>
                                </menu>
                            </menuItem>
                            <menuItem title="Speech" id="xrE-MZ-jX0">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <menu key="submenu" title="Speech" id="3rS-ZA-NoH">
                                    <items>
                                        <menuItem title="Start Speaking" id="Ynk-f8-cLZ">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Stop Speaking" id="Oyz-dy-DGm">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
                                            </connections>
                                        </menuItem>
                                    </items>
                                </menu>
                            </menuItem>
                        </items>
                    </menu>
                </menuItem>
                <menuItem title="Format" id="jxT-CU-nIS">
                    <modifierMask key="keyEquivalentModifierMask"/>
                    <menu key="submenu" title="Format" id="GEO-Iw-cKr">
                        <items>
                            <menuItem title="Font" id="Gi5-1S-RQB">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <menu key="submenu" title="Font" systemMenu="font" id="aXa-aM-Jaq">
                                    <items>
                                        <menuItem title="Show Fonts" keyEquivalent="t" id="Q5e-8K-NDq">
                                            <connections>
                                                <action selector="orderFrontFontPanel:" target="YLy-65-1bz" id="WHr-nq-2xA"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Bold" tag="2" keyEquivalent="b" id="GB9-OM-e27">
                                            <connections>
                                                <action selector="addFontTrait:" target="YLy-65-1bz" id="hqk-hr-sYV"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Italic" tag="1" keyEquivalent="i" id="Vjx-xi-njq">
                                            <connections>
                                                <action selector="addFontTrait:" target="YLy-65-1bz" id="IHV-OB-c03"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Underline" keyEquivalent="u" id="WRG-CD-K1S">
                                            <connections>
                                                <action selector="underline:" target="-1" id="FYS-2b-JAY"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem isSeparatorItem="YES" id="5gT-KC-WSO"/>
                                        <menuItem title="Bigger" tag="3" keyEquivalent="+" id="Ptp-SP-VEL">
                                            <connections>
                                                <action selector="modifyFont:" target="YLy-65-1bz" id="Uc7-di-UnL"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Smaller" tag="4" keyEquivalent="-" id="i1d-Er-qST">
                                            <connections>
                                                <action selector="modifyFont:" target="YLy-65-1bz" id="HcX-Lf-eNd"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem isSeparatorItem="YES" id="kx3-Dk-x3B"/>
                                        <menuItem title="Kern" id="jBQ-r6-VK2">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <menu key="submenu" title="Kern" id="tlD-Oa-oAM">
                                                <items>
                                                    <menuItem title="Use Default" id="GUa-eO-cwY">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="useStandardKerning:" target="-1" id="6dk-9l-Ckg"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem title="Use None" id="cDB-IK-hbR">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="turnOffKerning:" target="-1" id="U8a-gz-Maa"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem title="Tighten" id="46P-cB-AYj">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="tightenKerning:" target="-1" id="hr7-Nz-8ro"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem title="Loosen" id="ogc-rX-tC1">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="loosenKerning:" target="-1" id="8i4-f9-FKE"/>
                                                        </connections>
                                                    </menuItem>
                                                </items>
                                            </menu>
                                        </menuItem>
                                        <menuItem title="Ligatures" id="o6e-r0-MWq">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <menu key="submenu" title="Ligatures" id="w0m-vy-SC9">
                                                <items>
                                                    <menuItem title="Use Default" id="agt-UL-0e3">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="useStandardLigatures:" target="-1" id="7uR-wd-Dx6"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem title="Use None" id="J7y-lM-qPV">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="turnOffLigatures:" target="-1" id="iX2-gA-Ilz"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem title="Use All" id="xQD-1f-W4t">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="useAllLigatures:" target="-1" id="KcB-kA-TuK"/>
                                                        </connections>
                                                    </menuItem>
                                                </items>
                                            </menu>
                                        </menuItem>
                                        <menuItem title="Baseline" id="OaQ-X3-Vso">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <menu key="submenu" title="Baseline" id="ijk-EB-dga">
                                                <items>
                                                    <menuItem title="Use Default" id="3Om-Ey-2VK">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="unscript:" target="-1" id="0vZ-95-Ywn"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem title="Superscript" id="Rqc-34-cIF">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="superscript:" target="-1" id="3qV-fo-wpU"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem title="Subscript" id="I0S-gh-46l">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="subscript:" target="-1" id="Q6W-4W-IGz"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem title="Raise" id="2h7-ER-AoG">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="raiseBaseline:" target="-1" id="4sk-31-7Q9"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem title="Lower" id="1tx-W0-xDw">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="lowerBaseline:" target="-1" id="OF1-bc-KW4"/>
                                                        </connections>
                                                    </menuItem>
                                                </items>
                                            </menu>
                                        </menuItem>
                                        <menuItem isSeparatorItem="YES" id="Ndw-q3-faq"/>
                                        <menuItem title="Show Colors" keyEquivalent="C" id="bgn-CT-cEk">
                                            <connections>
                                                <action selector="orderFrontColorPanel:" target="-1" id="mSX-Xz-DV3"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem isSeparatorItem="YES" id="iMs-zA-UFJ"/>
                                        <menuItem title="Copy Style" keyEquivalent="c" id="5Vv-lz-BsD">
                                            <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
                                            <connections>
                                                <action selector="copyFont:" target="-1" id="GJO-xA-L4q"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Paste Style" keyEquivalent="v" id="vKC-jM-MkH">
                                            <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
                                            <connections>
                                                <action selector="pasteFont:" target="-1" id="JfD-CL-leO"/>
                                            </connections>
                                        </menuItem>
                                    </items>
                                </menu>
                            </menuItem>
                            <menuItem title="Text" id="Fal-I4-PZk">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <menu key="submenu" title="Text" id="d9c-me-L2H">
                                    <items>
                                        <menuItem title="Align Left" keyEquivalent="{" id="ZM1-6Q-yy1">
                                            <connections>
                                                <action selector="alignLeft:" target="-1" id="zUv-R1-uAa"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Center" keyEquivalent="|" id="VIY-Ag-zcb">
                                            <connections>
                                                <action selector="alignCenter:" target="-1" id="spX-mk-kcS"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Justify" id="J5U-5w-g23">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="alignJustified:" target="-1" id="ljL-7U-jND"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Align Right" keyEquivalent="}" id="wb2-vD-lq4">
                                            <connections>
                                                <action selector="alignRight:" target="-1" id="r48-bG-YeY"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem isSeparatorItem="YES" id="4s2-GY-VfK"/>
                                        <menuItem title="Writing Direction" id="H1b-Si-o9J">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <menu key="submenu" title="Writing Direction" id="8mr-sm-Yjd">
                                                <items>
                                                    <menuItem title="Paragraph" enabled="NO" id="ZvO-Gk-QUH">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                    </menuItem>
                                                    <menuItem id="YGs-j5-SAR">
                                                        <string key="title">	Default</string>
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="makeBaseWritingDirectionNatural:" target="-1" id="qtV-5e-UBP"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem id="Lbh-J2-qVU">
                                                        <string key="title">	Left to Right</string>
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="makeBaseWritingDirectionLeftToRight:" target="-1" id="S0X-9S-QSf"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem id="jFq-tB-4Kx">
                                                        <string key="title">	Right to Left</string>
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="makeBaseWritingDirectionRightToLeft:" target="-1" id="5fk-qB-AqJ"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem isSeparatorItem="YES" id="swp-gr-a21"/>
                                                    <menuItem title="Selection" enabled="NO" id="cqv-fj-IhA">
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                    </menuItem>
                                                    <menuItem id="Nop-cj-93Q">
                                                        <string key="title">	Default</string>
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="makeTextWritingDirectionNatural:" target="-1" id="lPI-Se-ZHp"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem id="BgM-ve-c93">
                                                        <string key="title">	Left to Right</string>
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="makeTextWritingDirectionLeftToRight:" target="-1" id="caW-Bv-w94"/>
                                                        </connections>
                                                    </menuItem>
                                                    <menuItem id="RB4-Sm-HuC">
                                                        <string key="title">	Right to Left</string>
                                                        <modifierMask key="keyEquivalentModifierMask"/>
                                                        <connections>
                                                            <action selector="makeTextWritingDirectionRightToLeft:" target="-1" id="EXD-6r-ZUu"/>
                                                        </connections>
                                                    </menuItem>
                                                </items>
                                            </menu>
                                        </menuItem>
                                        <menuItem isSeparatorItem="YES" id="fKy-g9-1gm"/>
                                        <menuItem title="Show Ruler" id="vLm-3I-IUL">
                                            <modifierMask key="keyEquivalentModifierMask"/>
                                            <connections>
                                                <action selector="toggleRuler:" target="-1" id="FOx-HJ-KwY"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Copy Ruler" keyEquivalent="c" id="MkV-Pr-PK5">
                                            <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
                                            <connections>
                                                <action selector="copyRuler:" target="-1" id="71i-fW-3W2"/>
                                            </connections>
                                        </menuItem>
                                        <menuItem title="Paste Ruler" keyEquivalent="v" id="LVM-kO-fVI">
                                            <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
                                            <connections>
                                                <action selector="pasteRuler:" target="-1" id="cSh-wd-qM2"/>
                                            </connections>
                                        </menuItem>
                                    </items>
                                </menu>
                            </menuItem>
                        </items>
                    </menu>
                </menuItem>
                <menuItem title="View" id="H8h-7b-M4v">
                    <modifierMask key="keyEquivalentModifierMask"/>
                    <menu key="submenu" title="View" id="HyV-fh-RgO">
                        <items>
                            <menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5">
                                <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
                                <connections>
                                    <action selector="toggleToolbarShown:" target="-1" id="BXY-wc-z0C"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Customize Toolbar…" id="1UK-8n-QPP">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <connections>
                                    <action selector="runToolbarCustomizationPalette:" target="-1" id="pQI-g3-MTW"/>
                                </connections>
                            </menuItem>
                        </items>
                    </menu>
                </menuItem>
                <menuItem title="Window" id="aUF-d1-5bR">
                    <modifierMask key="keyEquivalentModifierMask"/>
                    <menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
                        <items>
                            <menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
                                <connections>
                                    <action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
                                </connections>
                            </menuItem>
                            <menuItem title="Zoom" id="R4o-n2-Eq4">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <connections>
                                    <action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
                                </connections>
                            </menuItem>
                            <menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
                            <menuItem title="Bring All to Front" id="LE2-aR-0XJ">
                                <modifierMask key="keyEquivalentModifierMask"/>
                                <connections>
                                    <action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
                                </connections>
                            </menuItem>
                        </items>
                    </menu>
                </menuItem>
                <menuItem title="Help" id="wpr-3q-Mcd">
                    <modifierMask key="keyEquivalentModifierMask"/>
                    <menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
                        <items>
                            <menuItem title="DatWeatherDoe Help" keyEquivalent="?" id="FKE-Sm-Kum">
                                <connections>
                                    <action selector="showHelp:" target="-1" id="y7X-2Q-9no"/>
                                </connections>
                            </menuItem>
                        </items>
                    </menu>
                </menuItem>
            </items>
        </menu>
        <window title="DatWeatherDoe" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g">
            <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
            <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
            <rect key="contentRect" x="335" y="390" width="480" height="360"/>
            <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
            <view key="contentView" id="EiT-Mj-1SZ">
                <rect key="frame" x="0.0" y="0.0" width="480" height="360"/>
                <autoresizingMask key="autoresizingMask"/>
            </view>
        </window>
    </objects>
</document>


================================================
FILE: DatWeatherDoe/Localization/en.xcloc/contents.json
================================================
{
  "developmentRegion" : "en",
  "project" : "DatWeatherDoe.xcodeproj",
  "targetLocale" : "en",
  "toolInfo" : {
    "toolBuildNumber" : "12E262",
    "toolID" : "com.apple.dt.xcode",
    "toolName" : "Xcode",
    "toolVersion" : "12.5"
  },
  "version" : "1.0"
}

================================================
FILE: DatWeatherDoe/Reachability/WeatherReachability.swift
================================================
//
//  WeatherReachability.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/11/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import OSLog
import Reachability

final class NetworkReachability {
    private let logger: Logger
    private var reachability: Reachability?
    private var retryWhenReachable = false

    init(
        logger: Logger,
        onBecomingReachable: @escaping () -> Void
    ) {
        self.logger = logger

        setup(callback: onBecomingReachable)
    }

    private func setup(callback: @escaping () -> Void) {
        do {
            reachability = try Reachability()
            try reachability?.startNotifier()
            updateReachabilityWhenReachable(callback: callback)
            updateReachabilityWhenUnreachable()
        } catch {
            logger.error("Reachability error!")
        }
    }

    private func updateReachabilityWhenReachable(callback: @escaping () -> Void) {
        reachability?.whenReachable = { [weak self] _ in
            self?.logger.debug("Reachability status: Reachable")

            if self?.retryWhenReachable == true {
                self?.retryWhenReachable = false
                callback()
            }
        }
    }

    private func updateReachabilityWhenUnreachable() {
        reachability?.whenUnreachable = { [weak self] _ in
            self?.logger.debug("Reachability status: Unreachable")

            self?.retryWhenReachable = true
        }
    }
}


================================================
FILE: DatWeatherDoe/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "16.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "16x16"
    },
    {
      "filename" : "32-1.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "16x16"
    },
    {
      "filename" : "32.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "32x32"
    },
    {
      "filename" : "64.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "32x32"
    },
    {
      "filename" : "128.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "128x128"
    },
    {
      "filename" : "256-1.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "128x128"
    },
    {
      "filename" : "256.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "256x256"
    },
    {
      "filename" : "512-1.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "256x256"
    },
    {
      "filename" : "512.png",
      "idiom" : "mac",
      "scale" : "1x",
      "size" : "512x512"
    },
    {
      "filename" : "1024.png",
      "idiom" : "mac",
      "scale" : "2x",
      "size" : "512x512"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: DatWeatherDoe/Resources/Assets.xcassets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: DatWeatherDoe/Resources/DatWeatherDoe.entitlements
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>


================================================
FILE: DatWeatherDoe/Resources/DevelopmentAssets/TestData.swift
================================================
//
//  TestData.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 6/25/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import Foundation

let uvIndicesFor24Hours: [HourlyUVIndex] = {
    var arr: [HourlyUVIndex] = []
    // swiftlint:disable:next identifier_name
    for i in 1 ... 24 {
        arr.append(HourlyUVIndex(uv: 0))
    }
    return arr
}()

let response = WeatherAPIResponse(
    locationName: "New York City",
    temperatureData: .init(
        tempCelsius: 31.1,
        feelsLikeTempCelsius: 29.1,
        tempFahrenheit: 88.0,
        feelsLikeTempFahrenheit: 84.4
    ),
    isDay: true,
    weatherConditionCode: 1000,
    humidity: 45,
    windData: .init(speedMph: 12.3, degrees: 305, direction: "NW"),
    uvIndex: 7.0,
    forecastDayData: .init(
        temperatureData: .init(
            maxTempC: 32.8, maxTempF: 91.0, minTempC: 20.6, minTempF: 69.2
        ),
        astro: .init(sunrise: "05:26 AM", sunset: "08:31 PM"),
        hour: uvIndicesFor24Hours
    ),
    airQualityIndex: .good
)


================================================
FILE: DatWeatherDoe/Resources/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>WEATHER_API_KEY</key>
	<string>${WEATHER_API_KEY}</string>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIconFile</key>
	<string></string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>$(PRODUCT_NAME)</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>$(MARKETING_VERSION)</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleVersion</key>
	<string>$(CURRENT_PROJECT_VERSION)</string>
	<key>LSApplicationCategoryType</key>
	<string>public.app-category.weather</string>
	<key>LSMinimumSystemVersion</key>
	<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
	<key>LSUIElement</key>
	<true/>
	<key>NSHumanReadableCopyright</key>
	<string>Copyright © 2016 Inder Dhir. All rights reserved.</string>
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>DatWeatherDoe optionally uses your current location to get the weather</string>
	<key>NSMainNibFile</key>
	<string>MainMenu</string>
	<key>NSPrincipalClass</key>
	<string>NSApplication</string>
</dict>
</plist>


================================================
FILE: DatWeatherDoe/Resources/Localization/Localizable.xcstrings
================================================
{
  "sourceLanguage" : "en",
  "strings" : {
    "" : {

    },
    "[latitude],[longitude]" : {
      "comment" : "Placeholder hint for entering Lat/Long",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "[Breite],[Länge]"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "[latitude],[longitude]"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "[latitudine],[longitudine]"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "[緯度],[経度]"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "[纬度]、[经度]"
          }
        }
      }
    },
    "❗️City" : {
      "comment" : "City error when fetching weather",
      "extractionState" : "manual",
      "localizations" : {
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "❗️City"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "❗️Città"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "市のエラー"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "城市"
          }
        }
      }
    },
    "❗️Lat/Long" : {
      "comment" : "Lat/Long error when fetching weather",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Fehler mit Breite/Länge"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Erreur de Lat/Lon"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "❗️ Lat/Long"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "緯度/経度のエラー"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "经纬度"
          }
        }
      }
    },
    "❗️Location" : {
      "comment" : "Location error when fetching weather",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Positionsfehler"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Erreur d’emplacement"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "❗️Posizione"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "所在地のエラー"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "位置"
          }
        }
      }
    },
    "5 min" : {
      "comment" : "5 min refresh interval",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "5 Min"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "5 min"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "5 min"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "5分"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "5分钟"
          }
        }
      }
    },
    "15 min" : {
      "comment" : "15 min refresh interval",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "15 Min"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "15 min"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "15 min"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "15分"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "15分钟"
          }
        }
      }
    },
    "30 min" : {
      "comment" : "30 min refresh interval",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "30 Min"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "30 min"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "30 min"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "30分"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "30分钟"
          }
        }
      }
    },
    "60 min" : {
      "comment" : "60 min refresh interval",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "60 Min"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "60 min"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "60 min"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "60分"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "60分钟"
          }
        }
      }
    },
    "After Temperature" : {
      "comment" : "Weather condition after temperature",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Nach Temperatur"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Après température"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Dopo la temperatura"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "温度後"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "在温度后"
          }
        }
      }
    },
    "All" : {
      "comment" : "Show all temperature units",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Alle"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Tous"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Entrambe"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "全て"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "全部"
          }
        }
      }
    },
    "AQI" : {
      "comment" : "Air Quality Index",
      "extractionState" : "manual",
      "localizations" : {
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "空気質指数"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "空气质量"
          }
        }
      },
      "shouldTranslate" : false
    },
    "Before Temperature" : {
      "comment" : "Weather condition before temperature",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Vor der Temperatur"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Avant la température"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Prima della temperatura"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "温度前"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "在温度前"
          }
        }
      }
    },
    "CFBundleName" : {
      "comment" : "Bundle name",
      "extractionState" : "manual",
      "localizations" : {
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "DatWeatherDoe"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "DatWeatherDoe"
          }
        }
      },
      "shouldTranslate" : false
    },
    "Clear" : {
      "comment" : "Clear at night weather condition",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Klar"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Temps clair"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Sereno"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "クリア"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "晴朗"
          }
        }
      }
    },
    "Cloudy" : {
      "comment" : "Cloudy weather condition",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Bedeckt"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Nuageux"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Nuvoloso"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "曇り"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "多云"
          }
        }
      }
    },
    "Configure" : {
      "comment" : "Configure app",
      "extractionState" : "manual",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Konfigurieren"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Configure"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Configurer"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Configura"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "設定する"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "配置"
          }
        }
      }
    },
    "Done" : {
      "comment" : "Finish configuring app",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Fertig"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Terminé"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Fatto"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "完了"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "完成"
          }
        }
      }
    },
    "Fog" : {
      "comment" : "Fog weather condition",
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Nebbia"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "霧"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "雾"
          }
        }
      }
    },
    "Freezing rain" : {
      "comment" : "Freezing rain weather condition",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Gefrierender Regen"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pluie verglaçante"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Nevischio"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "雨氷"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "冻雨"
          }
        }
      }
    },
    "Good" : {
      "comment" : "Air Quality Index: Good",
      "extractionState" : "manual",
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Buona"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "良好"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "优"
          }
        }
      }
    },
    "Hazardous" : {
      "comment" : "Air Quality Index: Hazardous",
      "extractionState" : "manual",
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pericolosa"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "危険"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "良"
          }
        }
      }
    },
    "Heavy rain" : {
      "comment" : "Heavy rain weather condition",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Starkregen"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Forte pluie"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pioggia abbondante"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "豪雨"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "大雨"
          }
        }
      }
    },
    "Hide unit ° symbol" : {
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Nascondi simbolo °"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "°単位記号を非表示にする"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "隐藏单位符号 °"
          }
        }
      }
    },
    "Hide unit letter" : {
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Nascondi unità C"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "単位文字を非表示にする"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "隐藏单位字母 C"
          }
        }
      }
    },
    "Imperial" : {
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Imperiale"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "帝国単位"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "英制"
          }
        }
      }
    },
    "Lat/Long" : {
      "comment" : "Weather based on Lat/Long",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Breite/Länge"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Lat/Long"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Lat/Long"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "緯度/経度"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "纬度/经度"
          }
        }
      }
    },
    "Launch at Login" : {
      "comment" : "Launch app at login",
      "extractionState" : "manual",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Beim Einloggen starten"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Launch at Login"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Lancer à la connexion"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Avvia al Login"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "ログイン時に起動"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "自动启动"
          }
        }
      }
    },
    "Light rain" : {
      "comment" : "Light rain weather condition",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Leichter Regen"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pluie légère"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Lieve pioggia"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "小雨"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "小雨"
          }
        }
      }
    },
    "Location" : {
      "comment" : "Weather based on location",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Position"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Emplacement"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Posizione corrente"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "所在地"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "定位"
          }
        }
      }
    },
    "Metric" : {
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Metrico"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "メートル法"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "公制"
          }
        }
      }
    },
    "Mist" : {
      "comment" : "Mist weather condition",
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Foschia"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "靄"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "薄雾"
          }
        }
      }
    },
    "Moderate" : {
      "comment" : "Air Quality Index: Moderate",
      "extractionState" : "manual",
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Moderata"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "中等度"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "轻度污染"
          }
        }
      }
    },
    "NSHumanReadableCopyright" : {
      "comment" : "Copyright (human-readable)",
      "extractionState" : "manual",
      "localizations" : {
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Copyright © 2016 Inder Dhir. All rights reserved."
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Copyright © 2016 Inder Dhir. Alle Rechte vorbehalten."
          }
        }
      },
      "shouldTranslate" : false
    },
    "NSLocationWhenInUseUsageDescription" : {
      "comment" : "Privacy - Location When In Use Usage Description",
      "extractionState" : "manual",
      "localizations" : {
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "DatWeatherDoeは、リクエストに応じて地理的位置を使用して天気を判断します。"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "DatWeatherDoe根据请求使用您的地理位置来确定天气."
          }
        }
      },
      "shouldTranslate" : false
    },
    "Partly cloudy" : {
      "comment" : "Partly cloudy weather condition",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Wolkig"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Partiellement couvert"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Parzialmente nuvoloso"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "晴れ時々曇り"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "局部多云"
          }
        }
      }
    },
    "Partly cloudy with rain" : {
      "comment" : "Partly cloudy with rain weather condition",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Bewölkt mit Regen"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Partiellement couvert avec pluie"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Pioggia, parzialmente nuvoloso"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "部分的に曇り雨"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "多云转阴有雨"
          }
        }
      }
    },
    "Quit" : {
      "comment" : "Quit app",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Beenden"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Quitter"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Esci"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "終了する"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "退出"
          }
        }
      }
    },
    "Refresh" : {
      "comment" : "Refresh weather",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Aktualisieren"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Rafraîchir"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Aggiorna"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "再読み込みする"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "刷新"
          }
        }
      }
    },
    "Refresh Interval" : {
      "comment" : "Weather refresh interval",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Aktualisierungsintervall"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Intervalle de rafraîchissement"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Intervallo Aggiornamento"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "再読み込み間隔"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "刷新间隔"
          }
        }
      }
    },
    "Round-off Data" : {
      "comment" : "Round-off Data (temperature)",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Daten runden"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Arrondir les données"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Arrotonda Dati"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "データを四捨五入する"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "数据四舍五入"
          }
        }
      }
    },
    "See Full Weather" : {
      "comment" : "See Full Weather",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Komplettes Wetter anzeigen"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Voir la météo complète"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Meteo Completo"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "全ての天気情報を見る"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "查看完整天气"
          }
        }
      }
    },
    "Separate values with" : {
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Separa valori con"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "〜で値を区切る"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "分隔符号"
          }
        }
      }
    },
    "Show Humidity" : {
      "comment" : "Show humidity",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Luftfeuchtigkeit anzeigen"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Afficher l’humidité"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Mostra Umidità"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "湿度を表示する"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "显示湿度"
          }
        }
      }
    },
    "Show UV Index" : {
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Mostra Indice UV"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "UVインデックスを表示する"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "紫外线指数"
          }
        }
      }
    },
    "Show Weather Icon" : {
      "comment" : "\"Show Weather Icon\"",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Weather Icon anzeigen"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Afficher l'icône météo"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Mostra Icona Meteo"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "天気アイコンを表示する"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "显示天气图标"
          }
        }
      }
    },
    "Snow" : {
      "comment" : "Snow weather condition",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Schnee"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Neige"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Neve"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "雪"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "雪"
          }
        }
      }
    },
    "Sunny" : {
      "comment" : "Sunny weather condition",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Sonnig"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Ensoleillé"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Sole"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "晴れ"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "阳光"
          }
        }
      }
    },
    "Thunderstorm" : {
      "comment" : "Thunderstorm weather condition",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Gewitter"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Orage"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Temporale"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "雷雨"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "雷雨"
          }
        }
      }
    },
    "Unhealthy" : {
      "comment" : "Air Quality Index: Unhealthy",
      "extractionState" : "manual",
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Malsana"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "不健康"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "中度污染"
          }
        }
      }
    },
    "Unhealthy for sensitive groups" : {
      "comment" : "Air Quality Index: Unhealthy for sensitive groups",
      "extractionState" : "manual",
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Malsana per soggetti sensibili"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "敏感な人々にとって不健康"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "重度污染"
          }
        }
      }
    },
    "Unit" : {
      "comment" : "Temperature unit",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Einheit"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Unité"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Sistema"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "単位"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "单位"
          }
        }
      }
    },
    "Unknown" : {
      "comment" : "Unknown location",
      "extractionState" : "manual",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Unbekannt"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Unknown"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Inconnu"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Sconosciuta"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "不明"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "未知"
          }
        }
      }
    },
    "Unknown Location" : {
      "comment" : "Unknown weather location",
      "extractionState" : "manual",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Unbekannte Position"
          }
        },
        "en" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Unknown Location"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Emplacement inconnu"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Posizione Sconosciuta"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "所在地不明"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "未知地区"
          }
        }
      }
    },
    "UV Index" : {
      "comment" : "UV Index",
      "extractionState" : "manual",
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Indice UV"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "UVインデックス"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "紫外线指数"
          }
        }
      }
    },
    "Very unhealthy" : {
      "comment" : "Air Quality Index: Very unhealthy",
      "extractionState" : "manual",
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Molto bassa"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "非常に不健康"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "严重污染"
          }
        }
      }
    },
    "Weather Condition Position" : {
      "localizations" : {
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Posizione Condizioni Meteo"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "気象条件の位置"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "天气文本位置"
          }
        }
      }
    },
    "Weather Condition Text" : {
      "comment" : "Weather Condition Text",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Aktuelles Wetter Text"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Condition météo texte"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Testo Condizioni Meteo"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "気象条件テキスト"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "显示天气文本信息"
          }
        }
      }
    },
    "Weather Source" : {
      "comment" : "Source for fetching weather",
      "localizations" : {
        "de" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Wetterquelle"
          }
        },
        "fr" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Source de la météo"
          }
        },
        "it" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "Fonte Meteo"
          }
        },
        "ja" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "気象情報源"
          }
        },
        "zh-Hans" : {
          "stringUnit" : {
            "state" : "translated",
            "value" : "获取天气方式"
          }
        }
      }
    }
  },
  "version" : "1.0"
}

================================================
FILE: DatWeatherDoe/UI/Configure/ConfigureOptionsView.swift
================================================
//
//  ConfigureOptionsView.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 8/3/23.
//  Copyright © 2023 Inder Dhir. All rights reserved.
//

import SwiftUI

struct ConfigureOptionsView: View {
    @ObservedObject var viewModel: ConfigureViewModel

    var body: some View {
        Grid(verticalSpacing: 16) {
            HStack {
                Text(LocalizedStringKey("Unit"))
                Spacer()
                Picker("", selection: $viewModel.measurementUnit) {
                    Text(LocalizedStringKey("Metric")).tag(MeasurementUnit.metric)
                    Text(LocalizedStringKey("Imperial")).tag(MeasurementUnit.imperial)
                    Text(LocalizedStringKey("All")).tag(MeasurementUnit.all)
                }
                .frame(width: 120)
            }

            ConfigureWeatherOptionsView(viewModel: viewModel)

            HStack {
                Text(LocalizedStringKey("Refresh Interval"))
                Spacer()
                Picker("", selection: $viewModel.refreshInterval) {
                    Text(LocalizedStringKey("5 min")).tag(RefreshInterval.fiveMinutes)
                    Text(LocalizedStringKey("15 min")).tag(RefreshInterval.fifteenMinutes)
                    Text(LocalizedStringKey("30 min")).tag(RefreshInterval.thirtyMinutes)
                    Text(LocalizedStringKey("60 min")).tag(RefreshInterval.sixtyMinutes)
                }
                .frame(width: 120)
            }

            HStack {
                Text(LocalizedStringKey("Show Weather Icon"))
                Spacer()
                Toggle(isOn: $viewModel.isShowingWeatherIcon) {}
            }

            HStack {
                Text(LocalizedStringKey("Show Humidity"))
                Spacer()
                Toggle(isOn: $viewModel.isShowingHumidity) {}
            }

            HStack {
                Text(LocalizedStringKey("Show UV Index"))
                Spacer()
                Toggle(isOn: $viewModel.isShowingUVIndex) {}
            }

            HStack {
                Text(LocalizedStringKey("Round-off Data"))
                Spacer()
                Toggle(isOn: $viewModel.isRoundingOffData) {}
            }

            ConfigureUnitOptionsView(viewModel: viewModel)

            ConfigureValueSeparatorOptionsView(viewModel: viewModel)

            HStack {
                Text(LocalizedStringKey("Weather Condition Text"))
                Spacer()
                Toggle(isOn: $viewModel.isWeatherConditionAsTextEnabled) {}
            }

            HStack {
                Text(LocalizedStringKey("Weather Condition Position"))
                Spacer()
                Picker("", selection: $viewModel.weatherConditionPosition) {
                    Text(LocalizedStringKey("Before Temperature"))
                        .tag(WeatherConditionPosition.beforeTemperature)
                    Text(LocalizedStringKey("After Temperature"))
                        .tag(WeatherConditionPosition.afterTemperature)
                }
                .frame(maxWidth: 120)
            }
            .disabled(!viewModel.isWeatherConditionAsTextEnabled)
        }
        .padding()
    }
}

#Preview {
    ConfigureOptionsView(
        viewModel: .init(configManager: ConfigManager())
    )
    .frame(width: 380)
}


================================================
FILE: DatWeatherDoe/UI/Configure/ConfigureUnitOptionsView.swift
================================================
//
//  ConfigureUnitOptionsView.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 8/8/23.
//  Copyright © 2023 Inder Dhir. All rights reserved.
//

import SwiftUI

struct ConfigureUnitOptionsView: View {
    @ObservedObject var viewModel: ConfigureViewModel

    var body: some View {
        Group {
            HStack {
                Text(LocalizedStringKey("Hide unit letter"))
                Spacer()
                Toggle(isOn: $viewModel.isUnitLetterOff) {}
            }

            HStack {
                Text(LocalizedStringKey("Hide unit ° symbol"))
                Spacer()
                Toggle(isOn: $viewModel.isUnitSymbolOff) {}
            }
        }
    }
}

#Preview {
    Grid {
        ConfigureUnitOptionsView(
            viewModel: .init(configManager: ConfigManager())
        )
    }
}


================================================
FILE: DatWeatherDoe/UI/Configure/ConfigureValueSeparatorOptionsView.swift
================================================
//
//  ConfigureValueSeparatorOptionsView.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 8/8/23.
//  Copyright © 2023 Inder Dhir. All rights reserved.
//

import SwiftUI

struct ConfigureValueSeparatorOptionsView: View {
    @ObservedObject var viewModel: ConfigureViewModel
    let valueSeparatorPlaceholder = "\u{007C}"

    var body: some View {
        HStack {
            Text(LocalizedStringKey("Separate values with"))
            Spacer()
            TextField(valueSeparatorPlaceholder, text: $viewModel.valueSeparator)
                .font(.body)
                .foregroundColor(.primary)
                .frame(width: 114)
        }
    }
}

#Preview {
    ConfigureValueSeparatorOptionsView(
        viewModel: .init(configManager: ConfigManager())
    )
}


================================================
FILE: DatWeatherDoe/UI/Configure/ConfigureView.swift
================================================
//
//  ConfigureView.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 2/18/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import SwiftUI

struct ConfigureView: View {
    @ObservedObject var viewModel: ConfigureViewModel
    let version: String
    let onSave: () -> Void
    let onQuit: () -> Void

    var body: some View {
        VStack {
            ConfigureOptionsView(viewModel: viewModel)

            HStack {
                Text(version)
                    .font(.footnote)
                    .fontWeight(.thin)
                    .frame(maxWidth: .infinity, alignment: .leading)

                CustomButton(
                    text: LocalizedStringKey("Done"),
                    shortcutKey: "d",
                    onClick: onSave
                )
                .frame(maxWidth: .infinity, alignment: .center)

                Text(LocalizedStringKey("Quit"))
                    .foregroundStyle(Color.red)
                    .onTapGesture(perform: onQuit)
                    .frame(maxWidth: .infinity, alignment: .trailing)
            }
            .frame(maxWidth: .infinity)
            .padding([.leading, .trailing])
        }
        .padding(.bottom)
        .frame(width: 380)
    }
}

#Preview {
    ConfigureView(
        viewModel: .init(configManager: ConfigManager()),
        version: "5.0.0",
        onSave: {},
        onQuit: {}
    )
}


================================================
FILE: DatWeatherDoe/UI/Configure/ConfigureViewModel.swift
================================================
//
//  ConfigureViewModel.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 3/20/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import Combine
import Foundation

final class ConfigureViewModel: ObservableObject {
    @Published var measurementUnit: MeasurementUnit {
        didSet { configManager.measurementUnit = measurementUnit.rawValue }
    }

    @Published var weatherSource: WeatherSource

    @Published var weatherSourceText: String

    @Published var refreshInterval: RefreshInterval {
        didSet { configManager.refreshInterval = refreshInterval.rawValue }
    }

    @Published var isShowingWeatherIcon: Bool {
        didSet { configManager.isShowingWeatherIcon = isShowingWeatherIcon }
    }

    @Published var isShowingHumidity: Bool {
        didSet { configManager.isShowingHumidity = isShowingHumidity }
    }

    @Published var isShowingUVIndex: Bool {
        didSet { configManager.isShowingUVIndex = isShowingUVIndex }
    }

    @Published var isRoundingOffData: Bool {
        didSet { configManager.isRoundingOffData = isRoundingOffData }
    }

    @Published var isUnitLetterOff: Bool {
        didSet { configManager.isUnitLetterOff = isUnitLetterOff }
    }

    @Published var isUnitSymbolOff: Bool {
        didSet { configManager.isUnitSymbolOff = isUnitSymbolOff }
    }

    @Published var valueSeparator = "|" {
        didSet { configManager.valueSeparator = valueSeparator }
    }

    @Published var isWeatherConditionAsTextEnabled: Bool {
        didSet { configManager.isWeatherConditionAsTextEnabled = isWeatherConditionAsTextEnabled }
    }

    @Published var weatherConditionPosition: WeatherConditionPosition {
        didSet { configManager.weatherConditionPosition = weatherConditionPosition.rawValue }
    }

    private let configManager: ConfigManagerType

    init(configManager: ConfigManagerType) {
        self.configManager = configManager

        measurementUnit = configManager.parsedMeasurementUnit
        
        weatherSource = WeatherSource(rawValue: configManager.weatherSource) ?? .location
        weatherSourceText = configManager.weatherSourceText

        switch configManager.refreshInterval {
        case 300: refreshInterval = .fiveMinutes
        case 900: refreshInterval = .fifteenMinutes
        case 1800: refreshInterval = .thirtyMinutes
        case 3600: refreshInterval = .sixtyMinutes
        default: refreshInterval = .fifteenMinutes
        }

        isShowingWeatherIcon = configManager.isShowingWeatherIcon
        isShowingHumidity = configManager.isShowingHumidity
        isShowingUVIndex = configManager.isShowingUVIndex
        isRoundingOffData = configManager.isRoundingOffData
        isUnitLetterOff = configManager.isUnitLetterOff
        isUnitSymbolOff = configManager.isUnitSymbolOff
        isWeatherConditionAsTextEnabled = configManager.isWeatherConditionAsTextEnabled
        weatherConditionPosition = WeatherConditionPosition(rawValue: configManager.weatherConditionPosition)
            ?? .beforeTemperature
    }

    func saveConfig() {
        configManager.updateWeatherSource(weatherSource, sourceText: weatherSourceText)
        weatherSourceText = configManager.weatherSourceText

        configManager.setConfigOptions(
            .init(
                refreshInterval: refreshInterval,
                isShowingHumidity: isShowingHumidity,
                isShowingUVIndex: isShowingUVIndex,
                isRoundingOffData: isRoundingOffData,
                isUnitLetterOff: isUnitLetterOff,
                isUnitSymbolOff: isUnitSymbolOff,
                valueSeparator: valueSeparator,
                isWeatherConditionAsTextEnabled: isWeatherConditionAsTextEnabled
            )
        )
    }
}


================================================
FILE: DatWeatherDoe/UI/Configure/ConfigureWeatherOptionsView.swift
================================================
//
//  ConfigureWeatherOptionsView.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 8/8/23.
//  Copyright © 2023 Inder Dhir. All rights reserved.
//

import SwiftUI

struct ConfigureWeatherOptionsView: View {
    @ObservedObject var viewModel: ConfigureViewModel

    var body: some View {
        Group {
            HStack {
                Text(LocalizedStringKey("Weather Source"))
                Spacer()
                Picker("", selection: $viewModel.weatherSource) {
                    Text(LocalizedStringKey("Location")).tag(WeatherSource.location)
                    Text(LocalizedStringKey("Lat/Long")).tag(WeatherSource.latLong)
                }
                .frame(width: 120)
            }

            HStack {
                Text(viewModel.weatherSource.textHint)
                    .font(.caption)
                    .foregroundColor(.secondary)
                Spacer()
                TextField(viewModel.weatherSource.placeholder, text: $viewModel.weatherSourceText)
                    .font(.caption)
                    .foregroundColor(.secondary)
                    .disabled(viewModel.weatherSource == .location)
                    .frame(width: 114)
            }
        }
    }
}

#Preview {
    Grid {
        ConfigureWeatherOptionsView(
            viewModel: .init(configManager: ConfigManager())
        )
    }
}


================================================
FILE: DatWeatherDoe/UI/Configure/Options/MeasurementUnit.swift
================================================
//
//  MeasurementUnit.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 2/7/23.
//  Copyright © 2023 Inder Dhir. All rights reserved.
//

import Foundation

enum MeasurementUnit: String, CaseIterable, Identifiable {
    case metric, imperial, all

    var id: Self { self }

    var temperatureUnit: TemperatureUnit {
        switch self {
        case .metric:
            .celsius
        case .imperial:
            .fahrenheit
        case .all:
            .all
        }
    }
}


================================================
FILE: DatWeatherDoe/UI/Configure/Options/RefreshInterval.swift
================================================
//
//  RefreshInterval.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 5/23/21.
//  Copyright © 2021 Inder Dhir. All rights reserved.
//

import Foundation

enum RefreshInterval: TimeInterval, CaseIterable, Identifiable {
    case fiveMinutes = 300
    case fifteenMinutes = 900
    case thirtyMinutes = 1800
    case sixtyMinutes = 3600

    var id: Self { self }

    var title: String {
        switch self {
        case .fiveMinutes:
            String(localized: "5 min")
        case .fifteenMinutes:
            String(localized: "15 min")
        case .thirtyMinutes:
            String(localized: "30 min")
        case .sixtyMinutes:
            String(localized: "60 min")
        }
    }
}


================================================
FILE: DatWeatherDoe/UI/Configure/Options/TemperatureUnit.swift
================================================
//
//  TemperatureUnit.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 8/8/23.
//  Copyright © 2023 Inder Dhir. All rights reserved.
//

import Foundation

enum TemperatureUnit: String, CaseIterable, Identifiable {
    case fahrenheit, celsius, all

    var id: Self { self }

    var unitString: String {
        switch self {
        case .fahrenheit:
            "F"
        case .celsius:
            "C"
        case .all:
            "All"
        }
    }

    var degreesString: String {
        "\u{00B0}\(unitString)"
    }
}


================================================
FILE: DatWeatherDoe/UI/Configure/Options/WeatherConditionPosition.swift
================================================
//
//  WeatherConditionPosition.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 3/27/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import Foundation

enum WeatherConditionPosition: String, Identifiable {
    case beforeTemperature, afterTemperature

    var id: Self { self }

    var title: String {
        switch self {
        case .beforeTemperature:
            String(localized: "Before Temperature")
        case .afterTemperature:
            String(localized: "After Temperature")
        }
    }
}


================================================
FILE: DatWeatherDoe/UI/Configure/Options/WeatherSource.swift
================================================
//
//  WeatherSource.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 5/23/21.
//  Copyright © 2021 Inder Dhir. All rights reserved.
//

import Foundation

enum WeatherSource: String, CaseIterable {
    case location, latLong

    var title: String {
        switch self {
        case .location:
            String(localized: "Location")
        case .latLong:
            String(localized: "Lat/Long")
        }
    }

    var placeholder: String {
        switch self {
        case .location:
            ""
        case .latLong:
            "42,42"
        }
    }

    var textHint: String {
        switch self {
        case .location:
            ""
        case .latLong:
            String(localized: "[latitude],[longitude]")
        }
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Condition/WeatherCondition.swift
================================================
//
//  WeatherCondition.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/29/16.
//  Copyright © 2016 Inder Dhir. All rights reserved.
//

import AppKit
import Foundation

enum WeatherCondition {
    case cloudy, partlyCloudy, partlyCloudyNight
    case sunny, clearNight
    case snow
    case heavyRain, freezingRain, lightRain, partlyCloudyRain
    case thunderstorm
    case mist, fog

    static func getFallback(isDay: Bool) -> WeatherCondition {
        isDay ? .sunny : .clearNight
    }

    var symbolName: String {
        switch self {
        case .cloudy:
            "cloud"
        case .partlyCloudy:
            "cloud.sun"
        case .partlyCloudyNight:
            "cloud.moon"
        case .sunny:
            "sun.max"
        case .clearNight:
            "moon"
        case .snow:
            "cloud.snow"
        case .lightRain, .heavyRain, .freezingRain:
            "cloud.rain"
        case .partlyCloudyRain:
            "cloud.sun.rain"
        case .thunderstorm:
            "cloud.bolt.rain"
        case .mist, .fog:
            "cloud.fog"
        }
    }

    var accessibilityLabel: String {
        switch self {
        case .cloudy:
            "Cloudy"
        case .partlyCloudy:
            "Partly Cloudy"
        case .partlyCloudyNight:
            "Partly Cloudy"
        case .sunny:
            "Sunny"
        case .clearNight:
            "Clear"
        case .snow:
            "Snow"
        case .lightRain, .heavyRain, .freezingRain:
            "Rainy"
        case .partlyCloudyRain:
            "Partly cloudy with rain"
        case .thunderstorm:
            "Thunderstorm"
        case .mist, .fog:
            "Cloudy with Fog"
        }
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Condition/WeatherConditionBuilder.swift
================================================
//
//  WeatherConditionBuilder.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/11/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import Foundation

protocol WeatherConditionBuilderType {
    func build() -> WeatherCondition
}

final class WeatherConditionBuilder: WeatherConditionBuilderType {
    private let response: WeatherAPIResponse

    init(response: WeatherAPIResponse) {
        self.response = response
    }

    func build() -> WeatherCondition {
        switch response.weatherConditionCode {
        case 1006, 1009:
            .cloudy
        case 1003:
            response.isDay ? .partlyCloudy : .partlyCloudyNight
        case 1000:
            response.isDay ? .sunny : .clearNight
        case 1030:
            .mist
        case 1135, 1147:
            .fog
        case 1066,
             1114, 1117,
             1210, 1213, 1216, 1219, 1222, 1225, 1237, 1249, 1252, 1255, 1258, 1261, 1264, 1279, 1282:
            .snow
        case 1192, 1195, 1243, 1246, 1276:
            .heavyRain
        case 1069, 1072, 1168, 1171, 1198, 1201, 1204, 1207:
            .freezingRain
        case 1063, 1150, 1153, 1180, 1183, 1186, 1189, 1240:
            response.isDay ? .partlyCloudyRain : .lightRain
        case 1087, 1273:
            .thunderstorm
        default:
            WeatherCondition.getFallback(isDay: response.isDay)
        }
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Condition/WeatherConditionTextMapper.swift
================================================
//
//  WeatherConditionTextMapper.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/11/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import Foundation

protocol WeatherConditionTextMapperType {
    func map(_ condition: WeatherCondition) -> String
}

final class WeatherConditionTextMapper: WeatherConditionTextMapperType {
    func map(_ condition: WeatherCondition) -> String {
        switch condition {
        case .cloudy:
            String(localized: "Cloudy")

        case .partlyCloudy, .partlyCloudyNight:
            String(localized: "Partly cloudy")

        case .sunny:
            String(localized: "Sunny")

        case .clearNight:
            String(localized: "Clear")

        case .snow:
            String(localized: "Snow")

        case .heavyRain:
            String(localized: "Heavy rain")

        case .freezingRain:
            String(localized: "Freezing rain")

        case .lightRain:
            String(localized: "Light rain")

        case .partlyCloudyRain:
            String(localized: "Partly cloudy with rain")

        case .thunderstorm:
            String(localized: "Thunderstorm")

        case .mist:
            String(localized: "Mist")

        case .fog:
            String(localized: "Fog")
        }
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Text/HumidityTextBuilder.swift
================================================
//
//  HumidityTextBuilder.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/15/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import Foundation
import OSLog

protocol HumidityTextBuilderType {
    func build() -> String
}

final class HumidityTextBuilder: HumidityTextBuilderType {
    private let initial: String
    private let valueSeparator: String
    private let humidity: Int
    private let logger: Logger
    private let percentString = "\u{0025}"

    private let humidityFormatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .none
        formatter.maximumFractionDigits = 0
        return formatter
    }()

    init(
        initial: String,
        valueSeparator: String,
        humidity: Int,
        logger: Logger
    ) {
        self.initial = initial
        self.valueSeparator = valueSeparator
        self.humidity = humidity
        self.logger = logger
    }

    func build() -> String {
        guard let humidityString = buildHumidity() else {
            logger.error("Unable to construct humidity string")

            return initial
        }

        return "\(initial) \(valueSeparator) \(humidityString)"
    }

    private func buildHumidity() -> String? {
        guard let formattedString = buildFormattedString() else { return nil }

        return "\(formattedString)\(percentString)"
    }

    private func buildFormattedString() -> String? {
        humidityFormatter.string(from: NSNumber(value: humidity))
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Text/SunriseAndSunsetTextBuilder.swift
================================================
//
//  SunriseAndSunsetTextBuilder.swift
//  DatWeatherDoe
//
//  Created by Markus Markus on 2022-07-26.
//  Copyright © 2022 Markus Mayer.
//

import Foundation

protocol SunriseAndSunsetTextBuilderType {
    func build() -> String
}

final class SunriseAndSunsetTextBuilder: SunriseAndSunsetTextBuilderType {
    private let sunset: String
    private let sunrise: String

    private let upArrowStr = "⬆"
    private let downArrowStr = "⬇"

    init(sunset: String, sunrise: String) {
        self.sunset = sunset
        self.sunrise = sunrise
    }

    func build() -> String {
        "\(upArrowStr)\(sunrise) \(downArrowStr)\(sunset)"
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureForecastTextBuilder.swift
================================================
//
//  TemperatureForecastTextBuilder.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 6/25/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import Foundation

protocol TemperatureForecastTextBuilderType {
    func build() -> String
}

final class TemperatureForecastTextBuilder: TemperatureForecastTextBuilderType {
    private let temperatureData: TemperatureData
    private let forecastTemperatureData: ForecastTemperatureData
    private let options: TemperatureTextBuilder.Options
    private let upArrowStr = "⬆"
    private let downArrowStr = "⬇"
    private let degreeString = "\u{00B0}"

    init(
        temperatureData: TemperatureData,
        forecastTemperatureData: ForecastTemperatureData,
        options: TemperatureTextBuilder.Options
    ) {
        self.temperatureData = temperatureData
        self.forecastTemperatureData = forecastTemperatureData
        self.options = options
    }

    func build() -> String {
        if options.unit == .all {
            buildTemperatureForAllUnits()
        } else {
            buildTemperatureForUnit(options.unit)
        }
    }

    private func buildTemperatureForAllUnits() -> String {
        let feelsLikeTempFahrenheit = buildFormattedTemperature(
            temperatureData.feelsLikeTempFahrenheit, unit: .fahrenheit
        )
        let feelsLikeTempCelsius = buildFormattedTemperature(
            temperatureData.feelsLikeTempCelsius, unit: .celsius
        )
        let feelsLikeTemperatureCombined = [feelsLikeTempFahrenheit, feelsLikeTempCelsius]
            .compactMap { $0 }
            .joined(separator: " / ")

        let maxTempFahrenheit = buildFormattedTemperature(
            forecastTemperatureData.maxTempF, unit: .fahrenheit
        )
        let maxTempCelsius = buildFormattedTemperature(
            forecastTemperatureData.maxTempC, unit: .celsius
        )
        let maxTempCombined = [maxTempFahrenheit, maxTempCelsius]
            .compactMap { $0 }
            .joined(separator: " / ")
        let maxTempStr = [upArrowStr, maxTempCombined]
            .compactMap { $0 }
            .joined()

        let minTempFahrenheit = buildFormattedTemperature(
            forecastTemperatureData.minTempF, unit: .fahrenheit
        )
        let minTempCelsius = buildFormattedTemperature(
            forecastTemperatureData.minTempC, unit: .celsius
        )
        let minTempCombined = [minTempFahrenheit, minTempCelsius]
            .compactMap { $0 }
            .joined(separator: " / ")
        let minTempStr = [downArrowStr, minTempCombined]
            .compactMap { $0 }
            .joined()

        let maxAndMinTempStr = [maxTempStr, minTempStr]
            .compactMap { $0 }
            .joined(separator: " ")
        return [feelsLikeTemperatureCombined, maxAndMinTempStr]
            .compactMap { $0 }
            .joined(separator: " - ")
    }

    private func buildTemperatureForUnit(_ unit: TemperatureUnit) -> String {
        let maxTemp = unit == .fahrenheit ?
        forecastTemperatureData.maxTempF : forecastTemperatureData.maxTempC
        let formattedMaxTemp = buildFormattedTemperature(maxTemp, unit: unit)
        let maxTempStr = [upArrowStr, formattedMaxTemp]
            .compactMap { $0 }
            .joined()

        let minTemp = unit == .fahrenheit ?
        forecastTemperatureData.minTempF : forecastTemperatureData.minTempC
        let formatedMinTemp = buildFormattedTemperature(minTemp, unit: unit)
        let minTempStr = [downArrowStr, formatedMinTemp]
            .compactMap { $0 }
            .joined()

        let maxAndMinTempStr = [maxTempStr, minTempStr]
            .compactMap { $0 }
            .joined(separator: " ")

        let feelsLikeTemp = unit == .fahrenheit ?
            temperatureData.feelsLikeTempFahrenheit :
            temperatureData.feelsLikeTempCelsius
        let formattedFeelsLikeTemp = buildFormattedTemperature(feelsLikeTemp, unit: unit)
        return [formattedFeelsLikeTemp, maxAndMinTempStr]
            .compactMap { $0 }
            .joined(separator: " - ")
    }

    private func buildFormattedTemperature(
        _ temperatureForUnit: Double,
        unit: TemperatureUnit
    ) -> String? {
        guard let temperatureString = TemperatureFormatter()
            .getFormattedTemperatureString(temperatureForUnit, isRoundingOff: options.isRoundingOff)
        else {
            return nil
        }

        return combineTemperatureWithUnitDegrees(
            temperature: temperatureString,
            unit: unit.unitString,
            isUnitLetterOff: options.isUnitLetterOff,
            isUnitSymbolOff: options.isUnitSymbolOff
        )
    }

    private func combineTemperatureWithUnitDegrees(
        temperature: String,
        unit: String,
        isUnitLetterOff: Bool,
        isUnitSymbolOff: Bool
    ) -> String {
        let unitLetter = isUnitLetterOff ? "" : unit
        let unitSymbol = isUnitSymbolOff ? "" : degreeString
        return [temperature, unitLetter].joined(separator: unitSymbol)
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureFormatter.swift
================================================
//
//  TemperatureFormatter.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/12/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import Foundation

protocol TemperatureFormatterType {
    func getFormattedTemperatureString(
        _ temperature: Double,
        isRoundingOff: Bool
    ) -> String?
}

final class TemperatureFormatter: TemperatureFormatterType {
    private let formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        formatter.maximumFractionDigits = 1
        formatter.roundingMode = .halfUp
        return formatter
    }()

    func getFormattedTemperatureString(
        _ temperature: Double,
        isRoundingOff: Bool
    ) -> String? {
        setupTemperatureRounding(isRoundingOff: isRoundingOff)
        return formatTemperatureString(temperature)
    }

    private func setupTemperatureRounding(isRoundingOff: Bool) {
        formatter.maximumFractionDigits = isRoundingOff ? 0 : 1
    }

    private func formatTemperatureString(_ temperature: Double) -> String? {
        let formattedTemperature = formatter.string(from: NSNumber(value: temperature))
        return fixRoundingIssues(formattedTemperature)
    }

    private func fixRoundingIssues(_ temperature: String?) -> String? {
        if temperature == "-0" {
            return "0"
        }
        return temperature
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureTextBuilder.swift
================================================
//
//  TemperatureTextBuilder.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/15/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

protocol TemperatureTextBuilderType {
    func build() -> String?
}

final class TemperatureTextBuilder: TemperatureTextBuilderType {
    struct Options {
        let unit: TemperatureUnit
        let isRoundingOff: Bool
        let isUnitLetterOff: Bool
        let isUnitSymbolOff: Bool
    }

    private let response: WeatherAPIResponse
    private let options: Options
    private let temperatureCreator: TemperatureWithDegreesCreatorType
    private let degreeString = "\u{00B0}"

    init(
        response: WeatherAPIResponse,
        options: Options,
        temperatureCreator: TemperatureWithDegreesCreatorType
    ) {
        self.response = response
        self.options = options
        self.temperatureCreator = temperatureCreator
    }

    func build() -> String? {
        if options.unit == .all {
            buildTemperatureTextForAllUnits()
        } else {
            buildTemperatureText(for: options.unit)
        }
    }

    private func buildTemperatureTextForAllUnits() -> String? {
        let temperatureWithDegrees = temperatureCreator.getTemperatureWithDegrees(
            temperatureInMultipleUnits:
            .init(
                fahrenheit: response.temperatureData.tempFahrenheit,
                celsius: response.temperatureData.tempCelsius
            ),
            isRoundingOff: options.isRoundingOff,
            isUnitLetterOff: options.isUnitLetterOff,
            isUnitSymbolOff: options.isUnitSymbolOff
        )
        return temperatureWithDegrees
    }

    private func buildTemperatureText(for unit: TemperatureUnit) -> String? {
        let temperatureForUnit = unit == .fahrenheit ?
            response.temperatureData.tempFahrenheit :
            response.temperatureData.tempCelsius
        let temperatureWithDegrees = temperatureCreator.getTemperatureWithDegrees(
            temperatureForUnit,
            unit: unit,
            isRoundingOff: options.isRoundingOff,
            isUnitLetterOff: options.isUnitLetterOff,
            isUnitSymbolOff: options.isUnitSymbolOff
        )
        return temperatureWithDegrees
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureWithDegreesCreator.swift
================================================
//
//  TemperatureWithDegreesCreator.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 8/9/23.
//  Copyright © 2023 Inder Dhir. All rights reserved.
//

import Foundation

struct TemperatureInMultipleUnits {
    let fahrenheit: Double
    let celsius: Double
}

protocol TemperatureWithDegreesCreatorType {
    var degreeString: String { get }

    func getTemperatureWithDegrees(
        temperatureInMultipleUnits: TemperatureInMultipleUnits,
        isRoundingOff: Bool,
        isUnitLetterOff: Bool,
        isUnitSymbolOff: Bool
    ) -> String?

    func getTemperatureWithDegrees(
        _ temperature: Double,
        unit: TemperatureUnit,
        isRoundingOff: Bool,
        isUnitLetterOff: Bool,
        isUnitSymbolOff: Bool
    ) -> String?
}

final class TemperatureWithDegreesCreator: TemperatureWithDegreesCreatorType {
    let degreeString = "\u{00B0}"

    func getTemperatureWithDegrees(
        temperatureInMultipleUnits: TemperatureInMultipleUnits,
        isRoundingOff: Bool,
        isUnitLetterOff: Bool,
        isUnitSymbolOff: Bool
    ) -> String? {
        guard let fahrenheitString = TemperatureFormatter().getFormattedTemperatureString(
            temperatureInMultipleUnits.fahrenheit,
            isRoundingOff: isRoundingOff
        ) else {
            return nil
        }
        guard let celsiusString = TemperatureFormatter().getFormattedTemperatureString(
            temperatureInMultipleUnits.celsius,
            isRoundingOff: isRoundingOff
        ) else {
            return nil
        }

        let formattedFahrenheit = combineTemperatureWithUnitDegrees(
            temperature: fahrenheitString,
            unit: .fahrenheit,
            isUnitLetterOff: isUnitLetterOff,
            isUnitSymbolOff: isUnitSymbolOff
        )
        let formattedCelsius = combineTemperatureWithUnitDegrees(
            temperature: celsiusString,
            unit: .celsius,
            isUnitLetterOff: isUnitLetterOff,
            isUnitSymbolOff: isUnitSymbolOff
        )

        return [formattedFahrenheit, formattedCelsius]
            .joined(separator: " / ")
    }

    func getTemperatureWithDegrees(
        _ temperature: Double,
        unit: TemperatureUnit,
        isRoundingOff: Bool,
        isUnitLetterOff: Bool,
        isUnitSymbolOff: Bool
    ) -> String? {
        guard let temperatureString = TemperatureFormatter().getFormattedTemperatureString(
            temperature,
            isRoundingOff: isRoundingOff
        ) else {
            return nil
        }

        return combineTemperatureWithUnitDegrees(
            temperature: temperatureString,
            unit: unit,
            isUnitLetterOff: isUnitLetterOff,
            isUnitSymbolOff: isUnitSymbolOff
        )
    }

    private func combineTemperatureWithUnitDegrees(
        temperature: String,
        unit: TemperatureUnit,
        isUnitLetterOff: Bool,
        isUnitSymbolOff: Bool
    ) -> String {
        let unitLetter = isUnitLetterOff ? "" : unit.unitString
        let unitSymbol = isUnitSymbolOff ? "" : degreeString
        return [temperature, unitLetter].joined(separator: unitSymbol)
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Text/UVIndexTextBuilder.swift
================================================
//
//  UVIndexTextBuilder.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 10/18/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import Foundation

final class UVIndexTextBuilder {
    private let initial: String
    private let separator: String

    init(initial: String, separator: String) {
        self.initial = initial
        self.separator = separator
    }

    func build(from response: WeatherAPIResponse) -> String {
        "\(initial) \(separator) \(constructUVString(from: response))"
    }

    func constructUVString(from response: WeatherAPIResponse) -> String {
        let currentHour = Calendar.current.component(.hour, from: Date())
        let currentUVIndex = response.getHourlyUVIndex(hour: currentHour)
        return "UV Index: \(currentUVIndex)"
    }
}


================================================
FILE: DatWeatherDoe/UI/Decorator/Text/WeatherTextBuilder.swift
================================================
//
//  WeatherTextBuilder.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/15/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import OSLog

protocol WeatherTextBuilderType {
    func build() -> String
}

final class WeatherTextBuilder: WeatherTextBuilderType {
    struct Options {
        let isWeatherConditionAsTextEnabled: Bool
        let conditionPosition: WeatherConditionPosition
        let valueSeparator: String
        let temperatureOptions: TemperatureTextBuilder.Options
        let isShowingHumidity: Bool
        let isShowingUVIndex: Bool
    }

    private let response: WeatherAPIResponse
    private let options: Options
    private let logger: Logger

    init(
        response: WeatherAPIResponse,
        options: Options,
        logger: Logger
    ) {
        self.response = response
        self.options = options
        self.logger = logger
    }

    func build() -> String {
        let finalString = appendTemperatureAsText() |>
            appendUVIndex |>
            appendHumidityText |>
            buildWeatherConditionAsText
        return finalString
    }

    private func appendTemperatureAsText() -> String {
        TemperatureTextBuilder(
            response: response,
            options: options.temperatureOptions,
            temperatureCreator: TemperatureWithDegreesCreator()
        ).build() ?? ""
    }

    private func appendUVIndex(initial: String) -> String {
        guard options.isShowingUVIndex else { return initial }

        return UVIndexTextBuilder(
            initial: initial,
            separator: options.valueSeparator
        ).build(from: response)
    }

    private func appendHumidityText(initial: String) -> String {
        guard options.isShowingHumidity else { return initial }

        return HumidityTextBuilder(
            initial: initial,
            valueSeparator: options.valueSeparator,
            humidity: response.humidity,
            logger: logger
        ).build()
    }

    private func buildWeatherConditionAsText(initial: String) -> String {
        guard options.isWeatherConditionAsTextEnabled else { return initial }

        let weatherCondition = WeatherConditionBuilder(response: response).build()
        let weatherConditionText = WeatherConditionTextMapper().map(weatherCondition)

        let combinedString = options.conditionPosition == .beforeTemperature ?
            [weatherConditionText, initial] :
            [initial, weatherConditionText.lowercased()]

        return combinedString
            .compactMap { $0 }
            .joined(separator: ", ")
    }
}

precedencegroup ForwardPipe {
    associativity: left
}

infix operator |>: ForwardPipe

private func |> <T, U>(value: T, function: (T) -> U) -> U {
    function(value)
}


================================================
FILE: DatWeatherDoe/UI/Decorator/WeatherDataBuilder.swift
================================================
//
//  WeatherDataBuilder.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 5/22/21.
//  Copyright © 2021 Inder Dhir. All rights reserved.
//

import Foundation
import OSLog

protocol WeatherDataBuilderType: AnyObject {
    func build() -> WeatherData
}

final class WeatherDataBuilder: WeatherDataBuilderType {
    struct Options {
        let unit: MeasurementUnit
        let showWeatherIcon: Bool
        let textOptions: WeatherTextBuilder.Options
    }

    private let response: WeatherAPIResponse
    private let options: WeatherDataBuilder.Options
    private let logger: Logger

    init(
        response: WeatherAPIResponse,
        options: WeatherDataBuilder.Options,
        logger: Logger
    ) {
        self.response = response
        self.options = options
        self.logger = logger
    }

    func build() -> WeatherData {
        .init(
            showWeatherIcon: options.showWeatherIcon,
            textualRepresentation: buildTextualRepresentation(),
            weatherCondition: buildWeatherCondition(),
            response: response
        )
    }

    private func buildTextualRepresentation() -> String {
        WeatherTextBuilder(
            response: response,
            options: options.textOptions,
            logger: logger
        ).build()
    }

    private func buildWeatherCondition() -> WeatherCondition {
        WeatherConditionBuilder(response: response).build()
    }
}


================================================
FILE: DatWeatherDoe/UI/Forecaster/WeatherForecaster.swift
================================================
//
//  WeatherForecaster.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 1/11/22.
//  Copyright © 2022 Inder Dhir. All rights reserved.
//

import Cocoa
import Foundation

protocol WeatherForecasterType {
    func seeForecastForCity()
}

final class WeatherForecaster: WeatherForecasterType {
    private let fullWeatherUrl = URL(string: "https://www.weatherapi.com/weather/")!

    func seeForecastForCity() {
        NSWorkspace.shared.open(fullWeatherUrl)
    }
}


================================================
FILE: DatWeatherDoe/UI/Menu Bar/CustomButton.swift
================================================
//
//  CustomButton.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 6/21/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import SwiftUI

struct CustomButton: View {
    let text: LocalizedStringKey
    let textColor: Color
    let shortcutKey: KeyEquivalent
    let onClick: () -> Void

    init(
        text: LocalizedStringKey,
        textColor: Color = Color.primary,
        shortcutKey: KeyEquivalent,
        onClick: @escaping () -> Void
    ) {
        self.text = text
        self.textColor = textColor
        self.shortcutKey = shortcutKey
        self.onClick = onClick
    }

    var body: some View {
        Button(action: onClick) {
            Text(text)
                .foregroundStyle(textColor)
                .frame(width: 110, height: 22)
        }.keyboardShortcut(KeyboardShortcut(shortcutKey))
    }
}

#Preview {
    CustomButton(text: "See Full Weather", shortcutKey: "f") {}
}


================================================
FILE: DatWeatherDoe/UI/Menu Bar/DropdownIcon.swift
================================================
//
//  DropdownIcon.swift
//  DatWeatherDoe
//
//  Created by Markus Mayer on 2022-11-21.
//  Copyright © 2022 Markus Mayer.
//

// Icons used by the dropdown menu
enum DropdownIcon {
    case location
    case thermometer
    case sun
    case wind
    case uvIndexAndAirQualityText

    var symbolName: String {
        switch self {
        case .location:
            "location.north.circle"
        case .thermometer:
            "thermometer.snowflake.circle"
        case .sun:
            "sun.horizon.circle"
        case .wind:
            "wind.circle"
        case .uvIndexAndAirQualityText:
            "sun.max.circle"
        }
    }

    var accessibilityLabel: String {
        switch self {
        case .location:
            "Location"
        case .thermometer:
            "Temperature"
        case .sun:
            "Sunrise and Sunset"
        case .wind:
            "Wind data"
        case .uvIndexAndAirQualityText:
            "UV Index and Air Quality Index"
        }
    }
}


================================================
FILE: DatWeatherDoe/UI/Menu Bar/MenuOptionsView.swift
================================================
//
//  MenuOptionsView.swift
//  DatWeatherDoe
//
//  Created by Inder Dhir on 6/21/24.
//  Copyright © 2024 Inder Dhir. All rights reserved.
//

import SwiftUI

struct MenuOptionData {
    let locationText: String
    let weatherText: String
    let sunriseSunsetText: String
    let tempHumidityWindText: String
    let uvIndexAndAirQualityText: String
}

struct MenuOptionsView: View {
    let data: MenuOptionData?
    let onSeeWeather: () -> Void
    let onRefresh: () -> Void

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            VStack(alignment: .leading) {
                NonInteractiveMenuOptionView(icon: .location, text: data?.locationText)
                NonInteractive
Download .txt
gitextract_t2ij2trf/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── lint.yml
├── .gitignore
├── .swiftformat
├── .swiftlint.yml
├── CONTRIBUTING.md
├── DatWeatherDoe/
│   ├── API/
│   │   ├── NetworkClient.swift
│   │   ├── Response/
│   │   │   ├── AirQuality.swift
│   │   │   ├── ForecastData.swift
│   │   │   ├── ForecastTemperatureData.swift
│   │   │   ├── SunriseSunsetData.swift
│   │   │   ├── TemperatureData.swift
│   │   │   ├── WeatherAPIResponse.swift
│   │   │   ├── WeatherAPIResponseParser.swift
│   │   │   └── WindData.swift
│   │   ├── WeatherData.swift
│   │   └── WeatherError.swift
│   ├── Config/
│   │   ├── APIKeyParser.swift
│   │   ├── ConfigManager.swift
│   │   └── ConfigOptions.swift
│   ├── DatWeatherDoeApp.swift
│   ├── Localization/
│   │   └── en.xcloc/
│   │       ├── Localized Contents/
│   │       │   └── en.xliff
│   │       ├── Source Contents/
│   │       │   └── DatWeatherDoe/
│   │       │       ├── Resources/
│   │       │       │   └── en.lproj/
│   │       │       │       └── InfoPlist.strings
│   │       │       └── UI/
│   │       │           └── Base.lproj/
│   │       │               └── MainMenu.xib
│   │       └── contents.json
│   ├── Reachability/
│   │   └── WeatherReachability.swift
│   ├── Resources/
│   │   ├── Assets.xcassets/
│   │   │   ├── AppIcon.appiconset/
│   │   │   │   └── Contents.json
│   │   │   └── Contents.json
│   │   ├── DatWeatherDoe.entitlements
│   │   ├── DevelopmentAssets/
│   │   │   └── TestData.swift
│   │   ├── Info.plist
│   │   └── Localization/
│   │       └── Localizable.xcstrings
│   ├── UI/
│   │   ├── Configure/
│   │   │   ├── ConfigureOptionsView.swift
│   │   │   ├── ConfigureUnitOptionsView.swift
│   │   │   ├── ConfigureValueSeparatorOptionsView.swift
│   │   │   ├── ConfigureView.swift
│   │   │   ├── ConfigureViewModel.swift
│   │   │   ├── ConfigureWeatherOptionsView.swift
│   │   │   └── Options/
│   │   │       ├── MeasurementUnit.swift
│   │   │       ├── RefreshInterval.swift
│   │   │       ├── TemperatureUnit.swift
│   │   │       ├── WeatherConditionPosition.swift
│   │   │       └── WeatherSource.swift
│   │   ├── Decorator/
│   │   │   ├── Condition/
│   │   │   │   ├── WeatherCondition.swift
│   │   │   │   ├── WeatherConditionBuilder.swift
│   │   │   │   └── WeatherConditionTextMapper.swift
│   │   │   ├── Text/
│   │   │   │   ├── HumidityTextBuilder.swift
│   │   │   │   ├── SunriseAndSunsetTextBuilder.swift
│   │   │   │   ├── Temperature/
│   │   │   │   │   ├── TemperatureForecastTextBuilder.swift
│   │   │   │   │   ├── TemperatureFormatter.swift
│   │   │   │   │   ├── TemperatureTextBuilder.swift
│   │   │   │   │   └── TemperatureWithDegreesCreator.swift
│   │   │   │   ├── UVIndexTextBuilder.swift
│   │   │   │   └── WeatherTextBuilder.swift
│   │   │   └── WeatherDataBuilder.swift
│   │   ├── Forecaster/
│   │   │   └── WeatherForecaster.swift
│   │   ├── Menu Bar/
│   │   │   ├── CustomButton.swift
│   │   │   ├── DropdownIcon.swift
│   │   │   ├── MenuOptionsView.swift
│   │   │   ├── MenuView.swift
│   │   │   ├── NonInteractiveMenuOptionView.swift
│   │   │   └── WindSpeedFormatter.swift
│   │   └── Status Bar/
│   │       └── StatusBarView.swift
│   └── ViewModel/
│       ├── Parser/
│       │   ├── CityWeatherResultParser.swift
│       │   └── ZipCodeWeatherResultParser.swift
│       ├── Repository/
│       │   ├── Coordinates/
│       │   │   ├── LocationCoordinatesWeatherRepository.swift
│       │   │   ├── LocationParser.swift
│       │   │   └── LocationValidator.swift
│       │   ├── System/
│       │   │   ├── SystemLocationFetcher.swift
│       │   │   ├── SystemLocationWeatherRepository.swift
│       │   │   └── Task+Retry.swift
│       │   ├── WeatherRepositoryFactory.swift
│       │   ├── WeatherRepositoryType.swift
│       │   ├── WeatherURLBuilder.swift
│       │   └── WeatherValidatorType.swift
│       ├── WeatherDataFormatter.swift
│       ├── WeatherViewModel.swift
│       └── WeatherViewModelType.swift
├── DatWeatherDoe.xcodeproj/
│   ├── project.pbxproj
│   ├── project.xcworkspace/
│   │   ├── contents.xcworkspacedata
│   │   └── xcshareddata/
│   │       ├── IDEWorkspaceChecks.plist
│   │       └── swiftpm/
│   │           └── Package.resolved
│   └── xcshareddata/
│       └── xcschemes/
│           └── DatWeatherDoe.xcscheme
├── DatWeatherDoeTests/
│   ├── API/
│   │   └── Repository/
│   │       ├── Location/
│   │       │   └── Coordinates/
│   │       │       └── LocationValidatorTests.swift
│   │       └── WeatherURLBuilderTests.swift
│   ├── DatWeatherDoe.xctestplan
│   └── UI/
│       └── Configure/
│           └── Options/
│               ├── RefreshIntervalTests.swift
│               ├── TemperatureUnitTests.swift
│               └── WeatherSourceTests.swift
├── LICENSE
└── README.md
Condensed preview — 91 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (331K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 65,
    "preview": "# These are supported funding model platforms\n\ngithub: inderdhir\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 393,
    "preview": "name: SwiftLint\n\non:\n  pull_request:\n    paths:\n      - '.github/workflows/swiftlint.yml'\n      - '.swiftlint.yml'\n     "
  },
  {
    "path": ".gitignore",
    "chars": 756,
    "preview": "# Xcode\n.DS_Store\n\n# Backup files\n*~\n\n# JetBrains\n.idea/\n\n## Build generated\nbuild/\nDerivedData\n\n## Various settings\n*.p"
  },
  {
    "path": ".swiftformat",
    "chars": 24,
    "preview": "--disable trailingCommas"
  },
  {
    "path": ".swiftlint.yml",
    "chars": 1623,
    "preview": "disabled_rules: # rule identifiers to exclude from running\n  - colon\n  - comma\n  - control_statement\n  - trailing_whites"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 346,
    "preview": "# Contributing\n\nContributions / pull requests are welcome!\n\nPlease note that the goal of this project to provide a light"
  },
  {
    "path": "DatWeatherDoe/API/NetworkClient.swift",
    "chars": 512,
    "preview": "//\n//  NetworkClient.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/14/22.\n//  Copyright © 2022 Inder Dhir. A"
  },
  {
    "path": "DatWeatherDoe/API/Response/AirQuality.swift",
    "chars": 876,
    "preview": "//\n//  AirQuality.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 7/1/24.\n//  Copyright © 2024 Inder Dhir. All r"
  },
  {
    "path": "DatWeatherDoe/API/Response/ForecastData.swift",
    "chars": 710,
    "preview": "//\n//  ForecastData.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/23/24.\n//  Copyright © 2024 Inder Dhir. Al"
  },
  {
    "path": "DatWeatherDoe/API/Response/ForecastTemperatureData.swift",
    "chars": 519,
    "preview": "//\n//  ForecastTemperatureData.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/23/24.\n//  Copyright © 2024 Ind"
  },
  {
    "path": "DatWeatherDoe/API/Response/SunriseSunsetData.swift",
    "chars": 254,
    "preview": "//\n//  SunriseSunsetData.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/22/24.\n//  Copyright © 2024 Inder Dhi"
  },
  {
    "path": "DatWeatherDoe/API/Response/TemperatureData.swift",
    "chars": 573,
    "preview": "//\n//  TemperatureData.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/23/24.\n//  Copyright © 2024 Inder Dhir."
  },
  {
    "path": "DatWeatherDoe/API/Response/WeatherAPIResponse.swift",
    "chars": 3858,
    "preview": "//\n//  WeatherAPIResponse.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 2/3/18.\n//  Copyright © 2018 Inder Dhi"
  },
  {
    "path": "DatWeatherDoe/API/Response/WeatherAPIResponseParser.swift",
    "chars": 482,
    "preview": "//\n//  WeatherAPIResponseParser.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/10/22.\n//  Copyright © 2022 In"
  },
  {
    "path": "DatWeatherDoe/API/Response/WindData.swift",
    "chars": 425,
    "preview": "//\n//  WindData.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/23/24.\n//  Copyright © 2024 Inder Dhir. All ri"
  },
  {
    "path": "DatWeatherDoe/API/WeatherData.swift",
    "chars": 333,
    "preview": "//\n//  WeatherData.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 5/22/21.\n//  Copyright © 2021 Inder Dhir. All"
  },
  {
    "path": "DatWeatherDoe/API/WeatherError.swift",
    "chars": 643,
    "preview": "//\n//  WeatherError.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 5/22/21.\n//  Copyright © 2021 Inder Dhir. Al"
  },
  {
    "path": "DatWeatherDoe/Config/APIKeyParser.swift",
    "chars": 434,
    "preview": "//\n//  APIKeyParser.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/11/22.\n//  Copyright © 2022 Inder Dhir. Al"
  },
  {
    "path": "DatWeatherDoe/Config/ConfigManager.swift",
    "chars": 3047,
    "preview": "//\n//  ConfigManager.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/29/16.\n//  Copyright © 2016 Inder Dhir. A"
  },
  {
    "path": "DatWeatherDoe/Config/ConfigOptions.swift",
    "chars": 460,
    "preview": "//\n//  ConfigOptions.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 8/3/23.\n//  Copyright © 2023 Inder Dhir. Al"
  },
  {
    "path": "DatWeatherDoe/DatWeatherDoeApp.swift",
    "chars": 2560,
    "preview": "//\n//  DatWeatherDoeApp.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/17/24.\n//  Copyright © 2024 Inder Dhir"
  },
  {
    "path": "DatWeatherDoe/Localization/en.xcloc/Localized Contents/en.xliff",
    "chars": 33447,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" xmlns:xsi=\"http://www.w3.org"
  },
  {
    "path": "DatWeatherDoe/Localization/en.xcloc/Source Contents/DatWeatherDoe/Resources/en.lproj/InfoPlist.strings",
    "chars": 336,
    "preview": "/* Bundle name */\n\"CFBundleName\" = \"DatWeatherDoe\";\n/* Copyright (human-readable) */\n\"NSHumanReadableCopyright\" = \"Copyr"
  },
  {
    "path": "DatWeatherDoe/Localization/en.xcloc/Source Contents/DatWeatherDoe/UI/Base.lproj/MainMenu.xib",
    "chars": 49798,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3"
  },
  {
    "path": "DatWeatherDoe/Localization/en.xcloc/contents.json",
    "chars": 265,
    "preview": "{\n  \"developmentRegion\" : \"en\",\n  \"project\" : \"DatWeatherDoe.xcodeproj\",\n  \"targetLocale\" : \"en\",\n  \"toolInfo\" : {\n    \""
  },
  {
    "path": "DatWeatherDoe/Reachability/WeatherReachability.swift",
    "chars": 1475,
    "preview": "//\n//  WeatherReachability.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/11/22.\n//  Copyright © 2022 Inder D"
  },
  {
    "path": "DatWeatherDoe/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 1207,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"16.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"16x16\"\n"
  },
  {
    "path": "DatWeatherDoe/Resources/Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "DatWeatherDoe/Resources/DatWeatherDoe.entitlements",
    "chars": 181,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "DatWeatherDoe/Resources/DevelopmentAssets/TestData.swift",
    "chars": 1042,
    "preview": "//\n//  TestData.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/25/24.\n//  Copyright © 2024 Inder Dhir. All ri"
  },
  {
    "path": "DatWeatherDoe/Resources/Info.plist",
    "chars": 1442,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "DatWeatherDoe/Resources/Localization/Localizable.xcstrings",
    "chars": 41393,
    "preview": "{\n  \"sourceLanguage\" : \"en\",\n  \"strings\" : {\n    \"\" : {\n\n    },\n    \"[latitude],[longitude]\" : {\n      \"comment\" : \"Plac"
  },
  {
    "path": "DatWeatherDoe/UI/Configure/ConfigureOptionsView.swift",
    "chars": 3296,
    "preview": "//\n//  ConfigureOptionsView.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 8/3/23.\n//  Copyright © 2023 Inder D"
  },
  {
    "path": "DatWeatherDoe/UI/Configure/ConfigureUnitOptionsView.swift",
    "chars": 826,
    "preview": "//\n//  ConfigureUnitOptionsView.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 8/8/23.\n//  Copyright © 2023 Ind"
  },
  {
    "path": "DatWeatherDoe/UI/Configure/ConfigureValueSeparatorOptionsView.swift",
    "chars": 781,
    "preview": "//\n//  ConfigureValueSeparatorOptionsView.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 8/8/23.\n//  Copyright "
  },
  {
    "path": "DatWeatherDoe/UI/Configure/ConfigureView.swift",
    "chars": 1408,
    "preview": "//\n//  ConfigureView.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 2/18/22.\n//  Copyright © 2022 Inder Dhir. A"
  },
  {
    "path": "DatWeatherDoe/UI/Configure/ConfigureViewModel.swift",
    "chars": 3759,
    "preview": "//\n//  ConfigureViewModel.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 3/20/22.\n//  Copyright © 2022 Inder Dh"
  },
  {
    "path": "DatWeatherDoe/UI/Configure/ConfigureWeatherOptionsView.swift",
    "chars": 1369,
    "preview": "//\n//  ConfigureWeatherOptionsView.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 8/8/23.\n//  Copyright © 2023 "
  },
  {
    "path": "DatWeatherDoe/UI/Configure/Options/MeasurementUnit.swift",
    "chars": 492,
    "preview": "//\n//  MeasurementUnit.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 2/7/23.\n//  Copyright © 2023 Inder Dhir. "
  },
  {
    "path": "DatWeatherDoe/UI/Configure/Options/RefreshInterval.swift",
    "chars": 711,
    "preview": "//\n//  RefreshInterval.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 5/23/21.\n//  Copyright © 2021 Inder Dhir."
  },
  {
    "path": "DatWeatherDoe/UI/Configure/Options/TemperatureUnit.swift",
    "chars": 543,
    "preview": "//\n//  TemperatureUnit.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 8/8/23.\n//  Copyright © 2023 Inder Dhir. "
  },
  {
    "path": "DatWeatherDoe/UI/Configure/Options/WeatherConditionPosition.swift",
    "chars": 533,
    "preview": "//\n//  WeatherConditionPosition.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 3/27/24.\n//  Copyright © 2024 In"
  },
  {
    "path": "DatWeatherDoe/UI/Configure/Options/WeatherSource.swift",
    "chars": 764,
    "preview": "//\n//  WeatherSource.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 5/23/21.\n//  Copyright © 2021 Inder Dhir. A"
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Condition/WeatherCondition.swift",
    "chars": 1720,
    "preview": "//\n//  WeatherCondition.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/29/16.\n//  Copyright © 2016 Inder Dhir"
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Condition/WeatherConditionBuilder.swift",
    "chars": 1397,
    "preview": "//\n//  WeatherConditionBuilder.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/11/22.\n//  Copyright © 2022 Ind"
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Condition/WeatherConditionTextMapper.swift",
    "chars": 1290,
    "preview": "//\n//  WeatherConditionTextMapper.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/11/22.\n//  Copyright © 2022 "
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Text/HumidityTextBuilder.swift",
    "chars": 1534,
    "preview": "//\n//  HumidityTextBuilder.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/15/22.\n//  Copyright © 2022 Inder D"
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Text/SunriseAndSunsetTextBuilder.swift",
    "chars": 652,
    "preview": "//\n//  SunriseAndSunsetTextBuilder.swift\n//  DatWeatherDoe\n//\n//  Created by Markus Markus on 2022-07-26.\n//  Copyright "
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureForecastTextBuilder.swift",
    "chars": 5062,
    "preview": "//\n//  TemperatureForecastTextBuilder.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/25/22.\n//  Copyright © 2"
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureFormatter.swift",
    "chars": 1411,
    "preview": "//\n//  TemperatureFormatter.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/12/22.\n//  Copyright © 2022 Inder "
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureTextBuilder.swift",
    "chars": 2256,
    "preview": "//\n//  TemperatureTextBuilder.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/15/22.\n//  Copyright © 2022 Inde"
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureWithDegreesCreator.swift",
    "chars": 3168,
    "preview": "//\n//  TemperatureWithDegreesCreator.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 8/9/23.\n//  Copyright © 202"
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Text/UVIndexTextBuilder.swift",
    "chars": 804,
    "preview": "//\n//  UVIndexTextBuilder.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 10/18/24.\n//  Copyright © 2024 Inder D"
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/Text/WeatherTextBuilder.swift",
    "chars": 2785,
    "preview": "//\n//  WeatherTextBuilder.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/15/22.\n//  Copyright © 2022 Inder Dh"
  },
  {
    "path": "DatWeatherDoe/UI/Decorator/WeatherDataBuilder.swift",
    "chars": 1433,
    "preview": "//\n//  WeatherDataBuilder.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 5/22/21.\n//  Copyright © 2021 Inder Dh"
  },
  {
    "path": "DatWeatherDoe/UI/Forecaster/WeatherForecaster.swift",
    "chars": 475,
    "preview": "//\n//  WeatherForecaster.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/11/22.\n//  Copyright © 2022 Inder Dhi"
  },
  {
    "path": "DatWeatherDoe/UI/Menu Bar/CustomButton.swift",
    "chars": 932,
    "preview": "//\n//  CustomButton.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/21/24.\n//  Copyright © 2024 Inder Dhir. Al"
  },
  {
    "path": "DatWeatherDoe/UI/Menu Bar/DropdownIcon.swift",
    "chars": 1008,
    "preview": "//\n//  DropdownIcon.swift\n//  DatWeatherDoe\n//\n//  Created by Markus Mayer on 2022-11-21.\n//  Copyright © 2022 Markus Ma"
  },
  {
    "path": "DatWeatherDoe/UI/Menu Bar/MenuOptionsView.swift",
    "chars": 1769,
    "preview": "//\n//  MenuOptionsView.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/21/24.\n//  Copyright © 2024 Inder Dhir."
  },
  {
    "path": "DatWeatherDoe/UI/Menu Bar/MenuView.swift",
    "chars": 1512,
    "preview": "//\n//  MenuView.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/23/24.\n//  Copyright © 2024 Inder Dhir. All ri"
  },
  {
    "path": "DatWeatherDoe/UI/Menu Bar/NonInteractiveMenuOptionView.swift",
    "chars": 935,
    "preview": "//\n//  NonInteractiveMenuOptionView.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/21/24.\n//  Copyright © 202"
  },
  {
    "path": "DatWeatherDoe/UI/Menu Bar/WindSpeedFormatter.swift",
    "chars": 2670,
    "preview": "//\n//  WindSpeedFormatter.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 10/12/23.\n//  Copyright © 2023 Inder D"
  },
  {
    "path": "DatWeatherDoe/UI/Status Bar/StatusBarView.swift",
    "chars": 1483,
    "preview": "//\n//  StatusBarView.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/23/24.\n//  Copyright © 2024 Inder Dhir. A"
  },
  {
    "path": "DatWeatherDoe/ViewModel/Parser/CityWeatherResultParser.swift",
    "chars": 754,
    "preview": "//\n//  CityWeatherResultParser.swift\n//  DatWeatherDoe\n//\n//  Created by preckrasno on 14.02.2023.\n//  Copyright © 2023 "
  },
  {
    "path": "DatWeatherDoe/ViewModel/Parser/ZipCodeWeatherResultParser.swift",
    "chars": 744,
    "preview": "//\n//  ZipCodeWeatherResultParser.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/17/22.\n//  Copyright © 2022 "
  },
  {
    "path": "DatWeatherDoe/ViewModel/Repository/Coordinates/LocationCoordinatesWeatherRepository.swift",
    "chars": 1487,
    "preview": "//\n//  LocationCoordinatesWeatherRepository.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/14/22.\n//  Copyrig"
  },
  {
    "path": "DatWeatherDoe/ViewModel/Repository/Coordinates/LocationParser.swift",
    "chars": 1288,
    "preview": "//\n//  LocationParser.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/10/22.\n//  Copyright © 2022 Inder Dhir. "
  },
  {
    "path": "DatWeatherDoe/ViewModel/Repository/Coordinates/LocationValidator.swift",
    "chars": 519,
    "preview": "//\n//  LocationValidator.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/13/22.\n//  Copyright © 2022 Inder Dhi"
  },
  {
    "path": "DatWeatherDoe/ViewModel/Repository/System/SystemLocationFetcher.swift",
    "chars": 4419,
    "preview": "//\n//  SystemLocationFetcher.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/9/22.\n//  Copyright © 2022 Inder "
  },
  {
    "path": "DatWeatherDoe/ViewModel/Repository/System/SystemLocationWeatherRepository.swift",
    "chars": 1183,
    "preview": "//\n//  SystemLocationWeatherRepository.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/15/22.\n//  Copyright © "
  },
  {
    "path": "DatWeatherDoe/ViewModel/Repository/System/Task+Retry.swift",
    "chars": 1042,
    "preview": "//\n//  Task+Retry.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 10/13/25.\n//  Copyright © 2025 Inder Dhir. All"
  },
  {
    "path": "DatWeatherDoe/ViewModel/Repository/WeatherRepositoryFactory.swift",
    "chars": 1377,
    "preview": "//\n//  WeatherRepositoryFactory.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 8/7/23.\n//  Copyright © 2023 Ind"
  },
  {
    "path": "DatWeatherDoe/ViewModel/Repository/WeatherRepositoryType.swift",
    "chars": 269,
    "preview": "////\n////  WeatherRepositoryType.swift\n////  DatWeatherDoe\n////\n////  Created by Inder Dhir on 1/30/16.\n////  Copyright "
  },
  {
    "path": "DatWeatherDoe/ViewModel/Repository/WeatherURLBuilder.swift",
    "chars": 1423,
    "preview": "//\n//  WeatherURLBuilder.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/10/22.\n//  Copyright © 2022 Inder Dhi"
  },
  {
    "path": "DatWeatherDoe/ViewModel/Repository/WeatherValidatorType.swift",
    "chars": 223,
    "preview": "//\n//  WeatherValidatorType.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/10/22.\n//  Copyright © 2022 Inder "
  },
  {
    "path": "DatWeatherDoe/ViewModel/WeatherDataFormatter.swift",
    "chars": 2799,
    "preview": "//\n//  WeatherDataFormatter.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 6/24/24.\n//  Copyright © 2024 Inder "
  },
  {
    "path": "DatWeatherDoe/ViewModel/WeatherViewModel.swift",
    "chars": 5669,
    "preview": "//\n//  WeatherViewModel.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/9/22.\n//  Copyright © 2022 Inder Dhir."
  },
  {
    "path": "DatWeatherDoe/ViewModel/WeatherViewModelType.swift",
    "chars": 342,
    "preview": "//\n//  WeatherViewModelType.swift\n//  DatWeatherDoe\n//\n//  Created by Inder Dhir on 1/9/22.\n//  Copyright © 2022 Inder D"
  },
  {
    "path": "DatWeatherDoe.xcodeproj/project.pbxproj",
    "chars": 57979,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
  },
  {
    "path": "DatWeatherDoe.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "chars": 135,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef"
  },
  {
    "path": "DatWeatherDoe.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "DatWeatherDoe.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
    "chars": 3733,
    "preview": "{\n  \"originHash\" : \"cf72def9a17d22a974c73e8b0f0a4d80bccc1e736e0ce0e89a85e671a37bb5b6\",\n  \"pins\" : [\n    {\n      \"identit"
  },
  {
    "path": "DatWeatherDoe.xcodeproj/xcshareddata/xcschemes/DatWeatherDoe.xcscheme",
    "chars": 3061,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"2600\"\n   version = \"1.7\">\n   <BuildAction\n      "
  },
  {
    "path": "DatWeatherDoeTests/API/Repository/Location/Coordinates/LocationValidatorTests.swift",
    "chars": 592,
    "preview": "//\n//  LocationValidatorTests.swift\n//  DatWeatherDoeTests\n//\n//  Created by Inder Dhir on 1/26/22.\n//  Copyright © 2022"
  },
  {
    "path": "DatWeatherDoeTests/API/Repository/WeatherURLBuilderTests.swift",
    "chars": 796,
    "preview": "//\n//  WeatherURLBuilderTests.swift\n//  DatWeatherDoeTests\n//\n//  Created by Inder Dhir on 1/25/22.\n//  Copyright © 2022"
  },
  {
    "path": "DatWeatherDoeTests/DatWeatherDoe.xctestplan",
    "chars": 699,
    "preview": "{\n  \"configurations\" : [\n    {\n      \"id\" : \"8E1BA5A7-5E49-4CF1-8D13-2F0DA4BF0476\",\n      \"name\" : \"Main Config\",\n      "
  },
  {
    "path": "DatWeatherDoeTests/UI/Configure/Options/RefreshIntervalTests.swift",
    "chars": 852,
    "preview": "//\n//  RefreshIntervalTests.swift\n//  DatWeatherDoeTests\n//\n//  Created by Inder Dhir on 2/12/22.\n//  Copyright © 2022 I"
  },
  {
    "path": "DatWeatherDoeTests/UI/Configure/Options/TemperatureUnitTests.swift",
    "chars": 850,
    "preview": "//\n//  TemperatureUnitTests.swift\n//  DatWeatherDoeTests\n//\n//  Created by Inder Dhir on 2/12/22.\n//  Copyright © 2022 I"
  },
  {
    "path": "DatWeatherDoeTests/UI/Configure/Options/WeatherSourceTests.swift",
    "chars": 750,
    "preview": "//\n//  WeatherSourceTests.swift\n//  DatWeatherDoeTests\n//\n//  Created by Inder Dhir on 2/12/22.\n//  Copyright © 2022 Ind"
  },
  {
    "path": "LICENSE",
    "chars": 10252,
    "preview": "Apache License,\nVersion 2.0 Apache License Version 2.0,\nJanuary 2004 http://www.apache.org/licenses/\n\nTERMS AND CONDITIO"
  },
  {
    "path": "README.md",
    "chars": 1272,
    "preview": "# [<img src=\"logo.png\" width=\"20\"/>](image.png) DatWeatherDoe\n\n> **Note**\nOpenWeatherMap API 2.5 support is ending in Ju"
  }
]

About this extraction

This page contains the full source code of the inderdhir/DatWeatherDoe GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 91 files (296.0 KB), approximately 79.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!