localization

Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
Matt Bruce 2026-02-09 13:22:50 -06:00
parent 3b45fe2114
commit 089f8b9f7b
46 changed files with 4825 additions and 262 deletions

3
PRD.md
View File

@ -798,6 +798,9 @@ Use **iPhone 17 Pro Max (iOS 26.2)** as the primary simulator for build and test
- **Async/Await**: Modern concurrency patterns throughout the codebase - **Async/Await**: Modern concurrency patterns throughout the codebase
- **Observation Framework**: @Observable for reactive state management - **Observation Framework**: @Observable for reactive state management
- **SwiftUI Navigation**: Latest NavigationStack and navigation APIs with iOS 26 features - **SwiftUI Navigation**: Latest NavigationStack and navigation APIs with iOS 26 features
- **Localization**: User-facing runtime/accessibility strings use modern `String(localized:)` with `Localizable.xcstrings` coverage for English (`en`), Spanish - Mexico (`es-MX`), and French - Canada (`fr-CA`)
- **Settings Localization**: Clock settings screens use explicit localization keys for all section labels, control titles/subtitles, and helper copy
- **Localization Consistency**: App localization catalog uses a key-only convention (namespaced keys) to avoid mixed source-string and keyed entries
- **Accessibility**: Full VoiceOver and Dynamic Type support with iOS 26 enhancements - **Accessibility**: Full VoiceOver and Dynamic Type support with iOS 26 enhancements
- **Adaptive Layout**: Support for all device sizes and orientations with Liquid Glass - **Adaptive Layout**: Support for all device sizes and orientations with Liquid Glass
- **Performance**: Optimized for 120Hz ProMotion displays and iOS 26 performance improvements - **Performance**: Optimized for 120Hz ProMotion displays and iOS 26 performance improvements

View File

@ -146,6 +146,9 @@ Swift access is provided via:
- Alarm list ordering now prioritizes enabled alarms and sorts by the next trigger time for faster at-a-glance relevance. - Alarm list ordering now prioritizes enabled alarms and sorts by the next trigger time for faster at-a-glance relevance.
- Repeat-day controls now follow locale weekday order and include accessibility identifiers for repeat/alert options controls. - Repeat-day controls now follow locale weekday order and include accessibility identifiers for repeat/alert options controls.
- Onboarding now explicitly highlights repeat schedules plus per-alarm vibration/flash/volume customization for better feature discovery. - Onboarding now explicitly highlights repeat schedules plus per-alarm vibration/flash/volume customization for better feature discovery.
- Non-`Text` user-facing strings were migrated to `String(localized:)` and backed by `TheNoiseClock/Localizable.xcstrings` with `en`, `es-MX`, and `fr-CA` translations.
- Clock Settings UI copy now uses explicit localization keys (no raw `Text(\"...\")` literals in settings screens).
- Localization catalog is normalized to a single pattern: namespaced key-based entries only (no source-string keys).
--- ---

View File

@ -79,25 +79,25 @@ struct ContentView: View {
private var mainTabView: some View { private var mainTabView: some View {
TabView(selection: $selectedTab) { TabView(selection: $selectedTab) {
Tab("Clock", systemImage: "clock", value: AppTab.clock) { Tab(String(localized: "tab.clock", defaultValue: "Clock"), systemImage: "clock", value: AppTab.clock) {
NavigationStack { NavigationStack {
ClockView(viewModel: clockViewModel, isOnClockTab: isOnClockTab) ClockView(viewModel: clockViewModel, isOnClockTab: isOnClockTab)
} }
} }
Tab("Alarms", systemImage: "alarm", value: AppTab.alarms) { Tab(String(localized: "tab.alarms", defaultValue: "Alarms"), systemImage: "alarm", value: AppTab.alarms) {
NavigationStack { NavigationStack {
AlarmView(viewModel: alarmViewModel) AlarmView(viewModel: alarmViewModel)
} }
} }
Tab("Noise", systemImage: "waveform", value: AppTab.noise) { Tab(String(localized: "tab.noise", defaultValue: "Noise"), systemImage: "waveform", value: AppTab.noise) {
NavigationStack { NavigationStack {
NoiseView() NoiseView()
} }
} }
Tab("Settings", systemImage: "gearshape", value: AppTab.settings) { Tab(String(localized: "tab.settings", defaultValue: "Settings"), systemImage: "gearshape", value: AppTab.settings) {
NavigationStack { NavigationStack {
ClockSettingsView( ClockSettingsView(
style: clockViewModel.style, style: clockViewModel.style,

View File

@ -17,10 +17,15 @@ import Foundation
/// Intent to stop an active alarm from the Live Activity or notification. /// Intent to stop an active alarm from the Live Activity or notification.
struct StopAlarmIntent: LiveActivityIntent { struct StopAlarmIntent: LiveActivityIntent {
static let title: LocalizedStringResource = "Stop Alarm" static let title = LocalizedStringResource("alarm_intent.stop.title", defaultValue: "Stop Alarm")
static let description = IntentDescription("Stops the currently ringing alarm") static let description = IntentDescription(
LocalizedStringResource(
"alarm_intent.stop.description",
defaultValue: "Stops the currently ringing alarm"
)
)
@Parameter(title: "Alarm ID") @Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
var alarmId: String var alarmId: String
static var supportedModes: IntentModes { .background } static var supportedModes: IntentModes { .background }
@ -48,10 +53,15 @@ struct StopAlarmIntent: LiveActivityIntent {
/// Intent to snooze an active alarm from the Live Activity or notification. /// Intent to snooze an active alarm from the Live Activity or notification.
struct SnoozeAlarmIntent: LiveActivityIntent { struct SnoozeAlarmIntent: LiveActivityIntent {
static let title: LocalizedStringResource = "Snooze Alarm" static let title = LocalizedStringResource("alarm_intent.snooze.title", defaultValue: "Snooze Alarm")
static let description = IntentDescription("Snoozes the currently ringing alarm") static let description = IntentDescription(
LocalizedStringResource(
"alarm_intent.snooze.description",
defaultValue: "Snoozes the currently ringing alarm"
)
)
@Parameter(title: "Alarm ID") @Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
var alarmId: String var alarmId: String
static var supportedModes: IntentModes { .background } static var supportedModes: IntentModes { .background }
@ -80,11 +90,16 @@ struct SnoozeAlarmIntent: LiveActivityIntent {
/// Intent to open the app when the user taps the Live Activity. /// Intent to open the app when the user taps the Live Activity.
struct OpenAlarmAppIntent: LiveActivityIntent { struct OpenAlarmAppIntent: LiveActivityIntent {
static let title: LocalizedStringResource = "Open TheNoiseClock" static let title = LocalizedStringResource("alarm_intent.open_app.title", defaultValue: "Open TheNoiseClock")
static let description = IntentDescription("Opens the app to the alarm screen") static let description = IntentDescription(
LocalizedStringResource(
"alarm_intent.open_app.description",
defaultValue: "Opens the app to the alarm screen"
)
)
static let openAppWhenRun = true static let openAppWhenRun = true
@Parameter(title: "Alarm ID") @Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
var alarmId: String var alarmId: String
init() { init() {
@ -110,9 +125,9 @@ enum AlarmIntentError: Error, LocalizedError {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .invalidAlarmID: case .invalidAlarmID:
return "Invalid alarm ID" return String(localized: "alarm_intent.error.invalid_alarm_id", defaultValue: "Invalid alarm ID")
case .alarmNotFound: case .alarmNotFound:
return "Alarm not found" return String(localized: "alarm_intent.error.alarm_not_found", defaultValue: "Alarm not found")
} }
} }
} }

View File

@ -43,8 +43,8 @@ struct Alarm: Identifiable, Codable, Equatable {
isEnabled: Bool = true, isEnabled: Bool = true,
repeatWeekdays: [Int] = [], repeatWeekdays: [Int] = [],
soundName: String = AppConstants.SystemSounds.defaultSound, soundName: String = AppConstants.SystemSounds.defaultSound,
label: String = "Alarm", label: String = String(localized: "alarm.default_label", defaultValue: "Alarm"),
notificationMessage: String = "Your alarm is ringing", notificationMessage: String = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing"),
snoozeDuration: Int = 9, snoozeDuration: Int = 9,
isVibrationEnabled: Bool = true, isVibrationEnabled: Bool = true,
isLightFlashEnabled: Bool = false, isLightFlashEnabled: Bool = false,
@ -72,8 +72,8 @@ struct Alarm: Identifiable, Codable, Equatable {
try container.decodeIfPresent([Int].self, forKey: .repeatWeekdays) ?? [] try container.decodeIfPresent([Int].self, forKey: .repeatWeekdays) ?? []
) )
self.soundName = try container.decodeIfPresent(String.self, forKey: .soundName) ?? AppConstants.SystemSounds.defaultSound self.soundName = try container.decodeIfPresent(String.self, forKey: .soundName) ?? AppConstants.SystemSounds.defaultSound
self.label = try container.decodeIfPresent(String.self, forKey: .label) ?? "Alarm" self.label = try container.decodeIfPresent(String.self, forKey: .label) ?? String(localized: "alarm.default_label", defaultValue: "Alarm")
self.notificationMessage = try container.decodeIfPresent(String.self, forKey: .notificationMessage) ?? "Your alarm is ringing" self.notificationMessage = try container.decodeIfPresent(String.self, forKey: .notificationMessage) ?? String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing")
self.snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 9 self.snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 9
self.isVibrationEnabled = try container.decodeIfPresent(Bool.self, forKey: .isVibrationEnabled) ?? true self.isVibrationEnabled = try container.decodeIfPresent(Bool.self, forKey: .isVibrationEnabled) ?? true
self.isLightFlashEnabled = try container.decodeIfPresent(Bool.self, forKey: .isLightFlashEnabled) ?? false self.isLightFlashEnabled = try container.decodeIfPresent(Bool.self, forKey: .isLightFlashEnabled) ?? false
@ -127,19 +127,21 @@ struct Alarm: Identifiable, Codable, Equatable {
static func repeatSummary(for weekdays: [Int]) -> String { static func repeatSummary(for weekdays: [Int]) -> String {
let normalized = sanitizedWeekdays(weekdays) let normalized = sanitizedWeekdays(weekdays)
guard !normalized.isEmpty else { return "Once" } guard !normalized.isEmpty else {
return String(localized: "alarm.repeat.once", defaultValue: "Once")
}
let weekdaySet = Set(normalized) let weekdaySet = Set(normalized)
if weekdaySet.count == 7 { if weekdaySet.count == 7 {
return "Every day" return String(localized: "alarm.repeat.every_day", defaultValue: "Every day")
} }
if weekdaySet == Set([2, 3, 4, 5, 6]) { if weekdaySet == Set([2, 3, 4, 5, 6]) {
return "Weekdays" return String(localized: "alarm.repeat.weekdays", defaultValue: "Weekdays")
} }
if weekdaySet == Set([1, 7]) { if weekdaySet == Set([1, 7]) {
return "Weekends" return String(localized: "alarm.repeat.weekends", defaultValue: "Weekends")
} }
let symbols = Calendar.current.shortWeekdaySymbols let symbols = Calendar.current.shortWeekdaySymbols

View File

@ -83,14 +83,14 @@ final class AlarmKitService {
// Create the stop button for the alarm // Create the stop button for the alarm
let stopButton = AlarmButton( let stopButton = AlarmButton(
text: "Stop", text: LocalizedStringResource("alarm.action.stop"),
textColor: .red, textColor: .red,
systemImageName: "stop.fill" systemImageName: "stop.fill"
) )
// Create the snooze button (secondary button with countdown behavior) // Create the snooze button (secondary button with countdown behavior)
let snoozeButton = AlarmButton( let snoozeButton = AlarmButton(
text: "Snooze", text: LocalizedStringResource("alarm.action.snooze"),
textColor: .white, textColor: .white,
systemImageName: "moon.zzz" systemImageName: "moon.zzz"
) )
@ -422,9 +422,15 @@ enum AlarmKitError: Error, LocalizedError {
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .notAuthorized: case .notAuthorized:
return "AlarmKit is not authorized. Please enable alarm permissions in Settings." return String(
localized: "alarmkit.error.not_authorized",
defaultValue: "AlarmKit is not authorized. Please enable alarm permissions in Settings."
)
case .schedulingFailed(let error): case .schedulingFailed(let error):
return "Failed to schedule alarm: \(error.localizedDescription)" return String(
localized: "alarmkit.error.scheduling_failed",
defaultValue: "Failed to schedule alarm: \(error.localizedDescription)"
)
} }
} }
} }

View File

@ -152,8 +152,8 @@ final class AlarmViewModel {
time: Date, time: Date,
repeatWeekdays: [Int] = [], repeatWeekdays: [Int] = [],
soundName: String = AppConstants.SystemSounds.defaultSound, soundName: String = AppConstants.SystemSounds.defaultSound,
label: String = "Alarm", label: String = String(localized: "alarm.default_label", defaultValue: "Alarm"),
notificationMessage: String = "Your alarm is ringing", notificationMessage: String = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing"),
snoozeDuration: Int = 9, snoozeDuration: Int = 9,
isVibrationEnabled: Bool = true, isVibrationEnabled: Bool = true,
isLightFlashEnabled: Bool = false, isLightFlashEnabled: Bool = false,
@ -219,6 +219,9 @@ final class AlarmViewModel {
} else { } else {
error.localizedDescription error.localizedDescription
} }
return "Unable to \(action) alarm. \(detail)" return String(
localized: "alarm.operation.error_message",
defaultValue: "Unable to \(action) alarm. \(detail)"
)
} }
} }

View File

@ -19,8 +19,8 @@ struct AddAlarmView: View {
@State private var newAlarmTime = Calendar.current.date(bySettingHour: 6, minute: 0, second: 0, of: Date()) ?? Date() @State private var newAlarmTime = Calendar.current.date(bySettingHour: 6, minute: 0, second: 0, of: Date()) ?? Date()
@State private var repeatWeekdays: [Int] = [] @State private var repeatWeekdays: [Int] = []
@State private var selectedSoundName = AppConstants.SystemSounds.defaultSound @State private var selectedSoundName = AppConstants.SystemSounds.defaultSound
@State private var alarmLabel = "Alarm" @State private var alarmLabel = String(localized: "alarm.default_label", defaultValue: "Alarm")
@State private var notificationMessage = "Your alarm is ringing" @State private var notificationMessage = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing")
@State private var snoozeDuration = 9 // minutes @State private var snoozeDuration = 9 // minutes
@State private var isVibrationEnabled = true @State private var isVibrationEnabled = true
@State private var isLightFlashEnabled = false @State private var isLightFlashEnabled = false
@ -44,7 +44,7 @@ struct AddAlarmView: View {
Image(systemName: "textformat") Image(systemName: "textformat")
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Label") Text(String(localized: "alarm.editor.label", defaultValue: "Label"))
Spacer() Spacer()
Text(alarmLabel) Text(alarmLabel)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@ -57,7 +57,7 @@ struct AddAlarmView: View {
Image(systemName: "message") Image(systemName: "message")
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Message") Text(String(localized: "alarm.editor.message", defaultValue: "Message"))
Spacer() Spacer()
Text(notificationMessage) Text(notificationMessage)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@ -71,7 +71,7 @@ struct AddAlarmView: View {
Image(systemName: "music.note") Image(systemName: "music.note")
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Sound") Text(String(localized: "alarm.editor.sound", defaultValue: "Sound"))
Spacer() Spacer()
Text(getSoundDisplayName(selectedSoundName)) Text(getSoundDisplayName(selectedSoundName))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@ -84,7 +84,7 @@ struct AddAlarmView: View {
Image(systemName: "repeat") Image(systemName: "repeat")
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Repeat") Text(String(localized: "alarm.editor.repeat", defaultValue: "Repeat"))
Spacer() Spacer()
Text(Alarm.repeatSummary(for: repeatWeekdays)) Text(Alarm.repeatSummary(for: repeatWeekdays))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@ -98,9 +98,14 @@ struct AddAlarmView: View {
Image(systemName: "clock.arrow.circlepath") Image(systemName: "clock.arrow.circlepath")
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Snooze") Text(String(localized: "alarm.editor.snooze", defaultValue: "Snooze"))
Spacer() Spacer()
Text("for \(snoozeDuration) min") Text(
String(
localized: "alarm.editor.snooze_for_minutes",
defaultValue: "for \(snoozeDuration) min"
)
)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
@ -113,12 +118,12 @@ struct AddAlarmView: View {
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
} }
.navigationTitle("Alarm") .navigationTitle(String(localized: "alarm.add.navigation_title", defaultValue: "Alarm"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true) .navigationBarBackButtonHidden(true)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
isPresented = false isPresented = false
} }
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
@ -126,7 +131,11 @@ struct AddAlarmView: View {
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button(isSaving ? "Saving..." : "Save") { Button(
isSaving
? String(localized: "common.saving", defaultValue: "Saving...")
: String(localized: "common.save", defaultValue: "Save")
) {
Task { Task {
await saveAlarm() await saveAlarm()
} }
@ -138,8 +147,8 @@ struct AddAlarmView: View {
} }
} }
} }
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) { .alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $isShowingSaveErrorAlert) {
Button("OK", role: .cancel) { } Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) { }
} message: { } message: {
Text(saveErrorMessage) Text(saveErrorMessage)
} }

View File

@ -71,7 +71,7 @@ struct AlarmView: View {
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
} }
} }
.navigationTitle(isPad ? "" : "Alarms") .navigationTitle(isPad ? "" : String(localized: "alarms.navigation_title", defaultValue: "Alarms"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
@ -107,8 +107,8 @@ struct AlarmView: View {
} }
.sensoryFeedback(.impact(flexibility: .soft), trigger: showAddAlarm) .sensoryFeedback(.impact(flexibility: .soft), trigger: showAddAlarm)
.accessibilityIdentifier("alarms.screen") .accessibilityIdentifier("alarms.screen")
.alert("Alarm Error", isPresented: $viewModel.isShowingErrorAlert) { .alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $viewModel.isShowingErrorAlert) {
Button("OK", role: .cancel) { Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) {
viewModel.dismissErrorAlert() viewModel.dismissErrorAlert()
} }
} message: { } message: {

View File

@ -36,7 +36,12 @@ struct AlarmRowView: View {
.font(.caption) .font(.caption)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
Text("\(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))") Text(
String(
localized: "alarm.row.sound_prefix",
defaultValue: "\(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))"
)
)
.font(.caption) .font(.caption)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
@ -46,7 +51,7 @@ struct AlarmRowView: View {
.font(.caption2) .font(.caption2)
.foregroundStyle(AppStatus.warning) .foregroundStyle(AppStatus.warning)
.symbolEffect(.pulse, options: .repeating) .symbolEffect(.pulse, options: .repeating)
Text("Foreground only for full alarm sound") Text(String(localized: "alarm.row.keep_awake_warning", defaultValue: "Foreground only for full alarm sound"))
.font(.caption2) .font(.caption2)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
} }
@ -73,14 +78,23 @@ struct AlarmRowView: View {
} }
.accessibilityElement(children: .contain) .accessibilityElement(children: .contain)
.accessibilityIdentifier("alarms.row.\(alarm.id.uuidString)") .accessibilityIdentifier("alarms.row.\(alarm.id.uuidString)")
.accessibilityLabel("\(alarm.label), \(alarm.formattedTime())") .accessibilityLabel(
.accessibilityValue(alarm.isEnabled ? "Enabled" : "Disabled") String(
localized: "alarm.row.accessibility_label",
defaultValue: "\(alarm.label), \(alarm.formattedTime())"
)
)
.accessibilityValue(
alarm.isEnabled
? String(localized: "common.enabled", defaultValue: "Enabled")
: String(localized: "common.disabled", defaultValue: "Disabled")
)
} }
.swipeActions(edge: .trailing, allowsFullSwipe: true) { .swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) { Button(role: .destructive) {
onDelete() onDelete()
} label: { } label: {
Label("Delete", systemImage: "trash") Label(String(localized: "common.delete", defaultValue: "Delete"), systemImage: "trash")
} }
} }
} }

View File

@ -15,20 +15,20 @@ struct AlertOptionsSection: View {
@Binding var volume: Float @Binding var volume: Float
var body: some View { var body: some View {
Section("Alert Options") { Section(String(localized: "alarm.alert_options.section_title", defaultValue: "Alert Options")) {
Toggle("Vibration", isOn: $isVibrationEnabled) Toggle(String(localized: "alarm.alert_options.vibration", defaultValue: "Vibration"), isOn: $isVibrationEnabled)
.tint(AppAccent.primary) .tint(AppAccent.primary)
.accessibilityIdentifier("alarms.alertOptions.vibrationToggle") .accessibilityIdentifier("alarms.alertOptions.vibrationToggle")
Toggle("Flash Screen", isOn: $isLightFlashEnabled) Toggle(String(localized: "alarm.alert_options.flash_screen", defaultValue: "Flash Screen"), isOn: $isLightFlashEnabled)
.tint(AppAccent.primary) .tint(AppAccent.primary)
.accessibilityIdentifier("alarms.alertOptions.flashToggle") .accessibilityIdentifier("alarms.alertOptions.flashToggle")
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
HStack { HStack {
Text("Volume") Text(String(localized: "alarm.alert_options.volume", defaultValue: "Volume"))
Spacer() Spacer()
Text("\(Int((volume * 100).rounded()))%") Text(Double(volume), format: .percent.precision(.fractionLength(0)))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }

View File

@ -32,11 +32,11 @@ struct EmptyAlarmsView: View {
} }
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {
Text("No Alarms Set") Text(String(localized: "alarms.empty.title", defaultValue: "No Alarms Set"))
.typography(.title2Bold) .typography(.title2Bold)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Text("Create an alarm to wake up gently on your own terms.") Text(String(localized: "alarms.empty.subtitle", defaultValue: "Create an alarm to wake up gently on your own terms."))
.typography(.body) .typography(.body)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@ -48,7 +48,7 @@ struct EmptyAlarmsView: View {
HStack { HStack {
Image(systemName: "plus.circle.fill") Image(systemName: "plus.circle.fill")
.symbolEffect(.bounce, options: .nonRepeating) .symbolEffect(.bounce, options: .nonRepeating)
Text("Add Your First Alarm") Text(String(localized: "alarms.empty.cta", defaultValue: "Add Your First Alarm"))
} }
.typography(.bodyEmphasis) .typography(.bodyEmphasis)
.foregroundStyle(.white) .foregroundStyle(.white)

View File

@ -16,12 +16,15 @@ struct LabelEditView: View {
var body: some View { var body: some View {
Form { Form {
Section { Section {
TextField("Alarm Label", text: $label) TextField(
String(localized: "alarm.label.placeholder", defaultValue: "Alarm Label"),
text: $label
)
.typography(.body) .typography(.body)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
.padding(.vertical, Design.Spacing.small) .padding(.vertical, Design.Spacing.small)
} footer: { } footer: {
Text("Enter a name for your alarm.") Text(String(localized: "alarm.label.footer", defaultValue: "Enter a name for your alarm."))
.typography(.caption) .typography(.caption)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
} }
@ -29,7 +32,7 @@ struct LabelEditView: View {
} }
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea()) .background(AppSurface.primary.ignoresSafeArea())
.navigationTitle("Label") .navigationTitle(String(localized: "alarm.label.navigation_title", defaultValue: "Label"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
} }

View File

@ -22,7 +22,7 @@ struct NotificationMessageEditView: View {
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
.padding(.vertical, Design.Spacing.xxSmall) .padding(.vertical, Design.Spacing.xxSmall)
} footer: { } footer: {
Text("This message will appear when the alarm rings.") Text(String(localized: "alarm.message.footer", defaultValue: "This message will appear when the alarm rings."))
.typography(.caption) .typography(.caption)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
} }
@ -30,7 +30,7 @@ struct NotificationMessageEditView: View {
} }
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea()) .background(AppSurface.primary.ignoresSafeArea())
.navigationTitle("Message") .navigationTitle(String(localized: "alarm.message.navigation_title", defaultValue: "Message"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
} }

View File

@ -16,14 +16,14 @@ struct RepeatSelectionView: View {
var body: some View { var body: some View {
List { List {
Section("Quick Picks") { Section(String(localized: "alarm.repeat.quick_picks_section", defaultValue: "Quick Picks")) {
quickPickRow(id: "once", title: "Once", weekdays: []) quickPickRow(id: "once", title: String(localized: "alarm.repeat.once", defaultValue: "Once"), weekdays: [])
quickPickRow(id: "everyday", title: "Every Day", weekdays: allWeekdays) quickPickRow(id: "everyday", title: String(localized: "alarm.repeat.every_day", defaultValue: "Every Day"), weekdays: allWeekdays)
quickPickRow(id: "weekdays", title: "Weekdays", weekdays: [2, 3, 4, 5, 6]) quickPickRow(id: "weekdays", title: String(localized: "alarm.repeat.weekdays", defaultValue: "Weekdays"), weekdays: [2, 3, 4, 5, 6])
quickPickRow(id: "weekends", title: "Weekends", weekdays: [1, 7]) quickPickRow(id: "weekends", title: String(localized: "alarm.repeat.weekends", defaultValue: "Weekends"), weekdays: [1, 7])
} }
Section("Repeat On") { Section(String(localized: "alarm.repeat.repeat_on_section", defaultValue: "Repeat On")) {
ForEach(orderedWeekdays, id: \.self) { weekday in ForEach(orderedWeekdays, id: \.self) { weekday in
HStack { HStack {
Text(dayName(for: weekday)) Text(dayName(for: weekday))
@ -46,7 +46,7 @@ struct RepeatSelectionView: View {
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea()) .background(AppSurface.primary.ignoresSafeArea())
.navigationTitle("Repeat") .navigationTitle(String(localized: "alarm.repeat.navigation_title", defaultValue: "Repeat"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }

View File

@ -17,10 +17,15 @@ struct SnoozeSelectionView: View {
var body: some View { var body: some View {
List { List {
Section("Snooze Duration") { Section(String(localized: "alarm.snooze.duration_section", defaultValue: "Snooze Duration")) {
ForEach(snoozeOptions, id: \.self) { duration in ForEach(snoozeOptions, id: \.self) { duration in
HStack { HStack {
Text("\(duration) minutes") Text(
String(
localized: "alarm.snooze.duration_minutes",
defaultValue: "\(duration) minutes"
)
)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
if snoozeDuration == duration { if snoozeDuration == duration {
@ -39,7 +44,7 @@ struct SnoozeSelectionView: View {
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea()) .background(AppSurface.primary.ignoresSafeArea())
.navigationTitle("Snooze") .navigationTitle(String(localized: "alarm.snooze.navigation_title", defaultValue: "Snooze"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
} }

View File

@ -23,7 +23,7 @@ struct SoundSelectionView: View {
var body: some View { var body: some View {
List { List {
Section("Alarm Sounds") { Section(String(localized: "alarm.sound.section_title", defaultValue: "Alarm Sounds")) {
ForEach(alarmSounds, id: \.id) { sound in ForEach(alarmSounds, id: \.id) { sound in
HStack { HStack {
Text(sound.name) Text(sound.name)
@ -50,7 +50,7 @@ struct SoundSelectionView: View {
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea()) .background(AppSurface.primary.ignoresSafeArea())
.navigationTitle("Sound") .navigationTitle(String(localized: "alarm.sound.navigation_title", defaultValue: "Sound"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {

View File

@ -18,7 +18,7 @@ struct TimePickerSection: View {
VStack(spacing: 0) { VStack(spacing: 0) {
DatePicker( DatePicker(
"Time", String(localized: "alarm.time_picker.label", defaultValue: "Time"),
selection: $selectedTime, selection: $selectedTime,
displayedComponents: .hourAndMinute displayedComponents: .hourAndMinute
) )

View File

@ -46,23 +46,29 @@ struct TimeUntilAlarmSection: View {
let components = calendar.dateComponents([.hour, .minute], from: now, to: nextAlarmTime) let components = calendar.dateComponents([.hour, .minute], from: now, to: nextAlarmTime)
if let hours = components.hour, let minutes = components.minute { if let hours = components.hour, let minutes = components.minute {
if hours > 0 { if hours > 0 {
return "Will turn on in \(hours)h \(minutes)m" return String(
localized: "alarm.time_until.hours_minutes",
defaultValue: "Will turn on in \(hours)h \(minutes)m"
)
} else if minutes > 0 { } else if minutes > 0 {
return "Will turn on in \(minutes)m" return String(
localized: "alarm.time_until.minutes",
defaultValue: "Will turn on in \(minutes)m"
)
} else { } else {
return "Will turn on now" return String(localized: "alarm.time_until.now", defaultValue: "Will turn on now")
} }
} }
return "Will turn on tomorrow" return String(localized: "alarm.time_until.tomorrow_fallback", defaultValue: "Will turn on tomorrow")
} }
private var dayText: String { private var dayText: String {
let calendar = Calendar.current let calendar = Calendar.current
if calendar.isDateInToday(nextAlarmTime) { if calendar.isDateInToday(nextAlarmTime) {
return "Today" return String(localized: "calendar.today", defaultValue: "Today")
} else if calendar.isDateInTomorrow(nextAlarmTime) { } else if calendar.isDateInTomorrow(nextAlarmTime) {
return "Tomorrow" return String(localized: "calendar.tomorrow", defaultValue: "Tomorrow")
} else { } else {
return nextAlarmTime.formatted(.dateTime.weekday(.wide)) return nextAlarmTime.formatted(.dateTime.weekday(.wide))
} }

View File

@ -64,7 +64,7 @@ struct EditAlarmView: View {
Image(systemName: "textformat") Image(systemName: "textformat")
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Label") Text(String(localized: "alarm.editor.label", defaultValue: "Label"))
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text(alarmLabel) Text(alarmLabel)
@ -79,7 +79,7 @@ struct EditAlarmView: View {
Image(systemName: "message") Image(systemName: "message")
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Message") Text(String(localized: "alarm.editor.message", defaultValue: "Message"))
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text(notificationMessage) Text(notificationMessage)
@ -95,7 +95,7 @@ struct EditAlarmView: View {
Image(systemName: "music.note") Image(systemName: "music.note")
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Sound") Text(String(localized: "alarm.editor.sound", defaultValue: "Sound"))
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text(getSoundDisplayName(selectedSoundName)) Text(getSoundDisplayName(selectedSoundName))
@ -110,7 +110,7 @@ struct EditAlarmView: View {
Image(systemName: "repeat") Image(systemName: "repeat")
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Repeat") Text(String(localized: "alarm.editor.repeat", defaultValue: "Repeat"))
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text(Alarm.repeatSummary(for: repeatWeekdays)) Text(Alarm.repeatSummary(for: repeatWeekdays))
@ -126,10 +126,15 @@ struct EditAlarmView: View {
Image(systemName: "clock.arrow.circlepath") Image(systemName: "clock.arrow.circlepath")
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Snooze") Text(String(localized: "alarm.editor.snooze", defaultValue: "Snooze"))
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text("for \(snoozeDuration) min") Text(
String(
localized: "alarm.editor.snooze_for_minutes",
defaultValue: "for \(snoozeDuration) min"
)
)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
} }
} }
@ -146,12 +151,12 @@ struct EditAlarmView: View {
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea()) .background(AppSurface.primary.ignoresSafeArea())
} }
.navigationTitle("Edit Alarm") .navigationTitle(String(localized: "alarm.edit.navigation_title", defaultValue: "Edit Alarm"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true) .navigationBarBackButtonHidden(true)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
dismiss() dismiss()
} }
.foregroundStyle(AppAccent.primary) .foregroundStyle(AppAccent.primary)
@ -159,7 +164,11 @@ struct EditAlarmView: View {
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button(isSaving ? "Saving..." : "Save") { Button(
isSaving
? String(localized: "common.saving", defaultValue: "Saving...")
: String(localized: "common.save", defaultValue: "Save")
) {
Task { Task {
await saveAlarm() await saveAlarm()
} }
@ -171,8 +180,8 @@ struct EditAlarmView: View {
} }
} }
} }
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) { .alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $isShowingSaveErrorAlert) {
Button("OK", role: .cancel) { } Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) { }
} message: { } message: {
Text(saveErrorMessage) Text(saveErrorMessage)
} }

View File

@ -55,23 +55,23 @@ struct ClockSettingsView: View {
#if DEBUG #if DEBUG
SettingsSectionHeader( SettingsSectionHeader(
title: "Debug", title: String(localized: "settings.debug.section_title", defaultValue: "Debug"),
systemImage: "ant.fill", systemImage: "ant.fill",
accentColor: AppStatus.error accentColor: AppStatus.error
) )
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsNavigationRow( SettingsNavigationRow(
title: "Icon Generator", title: String(localized: "settings.debug.icon_generator.title", defaultValue: "Icon Generator"),
subtitle: "Generate and save app icon", subtitle: String(localized: "settings.debug.icon_generator.subtitle", defaultValue: "Generate and save app icon"),
backgroundColor: AppSurface.primary backgroundColor: AppSurface.primary
) { ) {
IconGeneratorView(config: .noiseClock, appName: "TheNoiseClock") IconGeneratorView(config: .noiseClock, appName: "TheNoiseClock")
} }
SettingsNavigationRow( SettingsNavigationRow(
title: "Branding Preview", title: String(localized: "settings.debug.branding_preview.title", defaultValue: "Branding Preview"),
subtitle: "Preview icon and launch screen", subtitle: String(localized: "settings.debug.branding_preview.subtitle", defaultValue: "Preview icon and launch screen"),
backgroundColor: AppSurface.primary backgroundColor: AppSurface.primary
) { ) {
BrandingPreviewView( BrandingPreviewView(
@ -90,10 +90,10 @@ struct ClockSettingsView: View {
} label: { } label: {
HStack { HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Reset Onboarding") Text(String(localized: "settings.debug.reset_onboarding.title", defaultValue: "Reset Onboarding"))
.typography(.body) .typography(.body)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Text("Show onboarding screens again on next launch") Text(String(localized: "settings.debug.reset_onboarding.subtitle", defaultValue: "Show onboarding screens again on next launch"))
.typography(.caption) .typography(.caption)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
} }
@ -116,7 +116,7 @@ struct ClockSettingsView: View {
.padding(.bottom, Design.Spacing.xxxLarge) .padding(.bottom, Design.Spacing.xxxLarge)
} }
.background(AppSurface.primary) .background(AppSurface.primary)
.navigationTitle(isPad ? "" : "Settings") .navigationTitle(isPad ? "" : String(localized: "settings.navigation_title", defaultValue: "Settings"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.onAppear { .onAppear {
digitColor = Color(hex: style.digitColorHex) ?? .white digitColor = Color(hex: style.digitColorHex) ?? .white

View File

@ -180,11 +180,26 @@ struct ClockView: View {
let hasDynamicIsland = windowInsets.left > 0 || windowInsets.right > 0 let hasDynamicIsland = windowInsets.left > 0 || windowInsets.right > 0
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Screen: \(Int(size.width))×\(Int(size.height))") Text(String(localized: "clock.debug.screen", defaultValue: "Screen: \(Int(size.width))×\(Int(size.height))"))
Text("Window Insets: L:\(Int(windowInsets.left)) R:\(Int(windowInsets.right)) T:\(Int(windowInsets.top)) B:\(Int(windowInsets.bottom))") Text(
Text("Symmetric Inset: \(Int(symmetricInset))") String(
Text("Dynamic Island: \(hasDynamicIsland && isLandscape ? "Yes" : "No")") localized: "clock.debug.window_insets",
Text("Orientation: \(isLandscape ? "Landscape" : "Portrait")") defaultValue: "Window Insets: L:\(Int(windowInsets.left)) R:\(Int(windowInsets.right)) T:\(Int(windowInsets.top)) B:\(Int(windowInsets.bottom))"
)
)
Text(String(localized: "clock.debug.symmetric_inset", defaultValue: "Symmetric Inset: \(Int(symmetricInset))"))
Text(
String(
localized: "clock.debug.dynamic_island",
defaultValue: "Dynamic Island: \(hasDynamicIsland && isLandscape ? "Yes" : "No")"
)
)
Text(
String(
localized: "clock.debug.orientation",
defaultValue: "Orientation: \(isLandscape ? "Landscape" : "Portrait")"
)
)
} }
.font(.system(size: 10, weight: .bold, design: .monospaced)) .font(.system(size: 10, weight: .bold, design: .monospaced))
.foregroundStyle(.green) .foregroundStyle(.green)

View File

@ -27,7 +27,7 @@ struct BatteryOverlayView: View {
.foregroundStyle(batteryColor) .foregroundStyle(batteryColor)
.symbolEffect(.pulse, options: .repeating, isActive: isCharging) .symbolEffect(.pulse, options: .repeating, isActive: isCharging)
.contentTransition(.symbolEffect(.replace)) .contentTransition(.symbolEffect(.replace))
Text("\(batteryLevel)%") Text(Double(batteryLevel) / 100, format: .percent.precision(.fractionLength(0)))
.foregroundStyle(color) .foregroundStyle(color)
.contentTransition(.numericText()) .contentTransition(.numericText())
.animation(.snappy(duration: 0.3), value: batteryLevel) .animation(.snappy(duration: 0.3), value: batteryLevel)

View File

@ -45,7 +45,7 @@ struct ClockDisplayContainer: View {
.animation(.smooth(duration: Design.Animation.standard), value: isFullScreenMode) .animation(.smooth(duration: Design.Animation.standard), value: isFullScreenMode)
.accessibilityElement(children: .ignore) .accessibilityElement(children: .ignore)
.accessibilityIdentifier("clock.timeDisplay") .accessibilityIdentifier("clock.timeDisplay")
.accessibilityLabel("Current time") .accessibilityLabel(String(localized: "clock.accessibility.current_time", defaultValue: "Current time"))
.accessibilityValue(accessibilityTimeValue) .accessibilityValue(accessibilityTimeValue)
} }
} }

View File

@ -17,7 +17,7 @@ struct ClockToolbar: View {
var body: some View { var body: some View {
let isPad = UIDevice.current.userInterfaceIdiom == .pad let isPad = UIDevice.current.userInterfaceIdiom == .pad
EmptyView() EmptyView()
.navigationTitle(isDisplayMode || isPad ? "" : "Clock") .navigationTitle(isDisplayMode || isPad ? "" : String(localized: "clock.navigation_title", defaultValue: "Clock"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(isDisplayMode) .navigationBarBackButtonHidden(isDisplayMode)
.toolbar(isDisplayMode ? .hidden : .automatic) .toolbar(isDisplayMode ? .hidden : .automatic)

View File

@ -195,7 +195,7 @@ private struct GlowAnimationModifier: ViewModifier {
animationStyle: .spring) animationStyle: .spring)
.border(Color.black) .border(Color.black)
Text(":") Text(String(localized: "clock.time.colon", defaultValue: ":"))
.font(.system(size: sharedFontSize)) .font(.system(size: sharedFontSize))
.border(Color.black) .border(Color.black)

View File

@ -36,7 +36,11 @@ struct NoiseMiniPlayer: View {
.sensoryFeedback(.impact(flexibility: .soft), trigger: isPlaying) .sensoryFeedback(.impact(flexibility: .soft), trigger: isPlaying)
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
Text(isPlaying ? "Playing" : "Paused") Text(
isPlaying
? String(localized: "noise.mini_player.playing", defaultValue: "Playing")
: String(localized: "noise.mini_player.paused", defaultValue: "Paused")
)
.font(.system(size: 8, weight: .bold)) .font(.system(size: 8, weight: .bold))
.foregroundStyle(color.opacity(0.6)) .foregroundStyle(color.opacity(0.6))
.textCase(.uppercase) .textCase(.uppercase)

View File

@ -14,7 +14,7 @@ struct AdvancedAppearanceSection: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader( SettingsSectionHeader(
title: "Advanced Appearance", title: String(localized: "settings.advanced_appearance.section_title", defaultValue: "Advanced Appearance"),
systemImage: "sparkles", systemImage: "sparkles",
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -22,14 +22,14 @@ struct AdvancedAppearanceSection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) { VStack(spacing: 0) {
SettingsNavigationRow( SettingsNavigationRow(
title: "Digit Animation", title: String(localized: "settings.advanced_appearance.digit_animation.title", defaultValue: "Digit Animation"),
subtitle: style.digitAnimationStyle.displayName, subtitle: style.digitAnimationStyle.displayName,
backgroundColor: .clear backgroundColor: .clear
) { ) {
SettingsSelectionView( SettingsSelectionView(
selection: $style.digitAnimationStyle, selection: $style.digitAnimationStyle,
options: DigitAnimationStyle.allCases, options: DigitAnimationStyle.allCases,
title: "Digit Animation", title: String(localized: "settings.advanced_appearance.digit_animation.title", defaultValue: "Digit Animation"),
toString: { $0.displayName } toString: { $0.displayName }
) )
} }
@ -40,8 +40,8 @@ struct AdvancedAppearanceSection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsToggle( SettingsToggle(
title: "Randomize Color", title: String(localized: "settings.advanced_appearance.randomize_color.title", defaultValue: "Randomize Color"),
subtitle: "Shift the color every minute", subtitle: String(localized: "settings.advanced_appearance.randomize_color.subtitle", defaultValue: "Shift the color every minute"),
isOn: $style.randomizeColor, isOn: $style.randomizeColor,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -52,8 +52,8 @@ struct AdvancedAppearanceSection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsSlider( SettingsSlider(
title: "Glow", title: String(localized: "settings.advanced_appearance.glow.title", defaultValue: "Glow"),
subtitle: "Adjust the glow intensity", subtitle: String(localized: "settings.advanced_appearance.glow.subtitle", defaultValue: "Adjust the glow intensity"),
value: $style.glowIntensity, value: $style.glowIntensity,
in: 0.0...1.0, in: 0.0...1.0,
step: 0.01, step: 0.01,
@ -67,8 +67,8 @@ struct AdvancedAppearanceSection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsSlider( SettingsSlider(
title: "Clock Opacity", title: String(localized: "settings.advanced_appearance.clock_opacity.title", defaultValue: "Clock Opacity"),
subtitle: "Set the clock transparency", subtitle: String(localized: "settings.advanced_appearance.clock_opacity.subtitle", defaultValue: "Set the clock transparency"),
value: $style.clockOpacity, value: $style.clockOpacity,
in: 0.0...1.0, in: 0.0...1.0,
step: 0.01, step: 0.01,
@ -78,7 +78,7 @@ struct AdvancedAppearanceSection: View {
} }
} }
Text("Fine-tune the visual appearance of your clock.") Text(String(localized: "settings.advanced_appearance.footer", defaultValue: "Fine-tune the visual appearance of your clock."))
.font(.caption) .font(.caption)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
} }

View File

@ -14,7 +14,7 @@ struct AdvancedDisplaySection: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader( SettingsSectionHeader(
title: "Advanced Display", title: String(localized: "settings.advanced_display.section_title", defaultValue: "Advanced Display"),
systemImage: "eye", systemImage: "eye",
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -22,8 +22,8 @@ struct AdvancedDisplaySection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) { VStack(spacing: 0) {
SettingsToggle( SettingsToggle(
title: "Keep Awake", title: String(localized: "settings.advanced_display.keep_awake.title", defaultValue: "Keep Awake"),
subtitle: "Prevent sleep in display mode", subtitle: String(localized: "settings.advanced_display.keep_awake.subtitle", defaultValue: "Prevent sleep in display mode"),
isOn: $style.keepAwake, isOn: $style.keepAwake,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -36,11 +36,11 @@ struct AdvancedDisplaySection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
HStack { HStack {
Text("Current Brightness") Text(String(localized: "settings.advanced_display.current_brightness", defaultValue: "Current Brightness"))
.font(.subheadline.weight(.medium)) .font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text("\(Int(style.effectiveBrightness * 100))%") Text(style.effectiveBrightness, format: .percent.precision(.fractionLength(0)))
.font(.subheadline) .font(.subheadline)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.contentTransition(.numericText()) .contentTransition(.numericText())
@ -52,27 +52,27 @@ struct AdvancedDisplaySection: View {
} }
} }
Text("Advanced display and system integration settings. Keep Awake helps alarms stay active while the app remains open.") Text(String(localized: "settings.advanced_display.footer", defaultValue: "Advanced display and system integration settings. Keep Awake helps alarms stay active while the app remains open."))
.font(.caption) .font(.caption)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
SettingsSectionHeader( SettingsSectionHeader(
title: "Focus Modes", title: String(localized: "settings.focus_modes.section_title", defaultValue: "Focus Modes"),
systemImage: "moon.zzz.fill", systemImage: "moon.zzz.fill",
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsToggle( SettingsToggle(
title: "Respect Focus Modes", title: String(localized: "settings.focus_modes.respect_focus.title", defaultValue: "Respect Focus Modes"),
subtitle: "Follow Do Not Disturb rules", subtitle: String(localized: "settings.focus_modes.respect_focus.subtitle", defaultValue: "Follow Do Not Disturb rules"),
isOn: $style.respectFocusModes, isOn: $style.respectFocusModes,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
.accessibilityIdentifier("settings.respectFocus.toggle") .accessibilityIdentifier("settings.respectFocus.toggle")
} }
Text("Control how the app behaves when Focus modes are active.") Text(String(localized: "settings.focus_modes.footer", defaultValue: "Control how the app behaves when Focus modes are active."))
.font(.caption) .font(.caption)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
} }

View File

@ -16,7 +16,7 @@ struct BasicAppearanceSection: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader( SettingsSectionHeader(
title: "Colors", title: String(localized: "settings.colors.section_title", defaultValue: "Colors"),
systemImage: "paintpalette.fill", systemImage: "paintpalette.fill",
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -24,16 +24,16 @@ struct BasicAppearanceSection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) { VStack(spacing: 0) {
SettingsNavigationRow( SettingsNavigationRow(
title: "Color Theme", title: String(localized: "settings.colors.theme.title", defaultValue: "Color Theme"),
subtitle: style.selectedColorTheme, subtitle: localizedThemeName(for: style.selectedColorTheme),
backgroundColor: .clear backgroundColor: .clear
) { ) {
SettingsSelectionView( SettingsSelectionView(
selection: $style.selectedColorTheme, selection: $style.selectedColorTheme,
options: ClockStyle.availableColorThemes().map { $0.0 }, options: ClockStyle.availableColorThemes().map { $0.0 },
title: "Color Theme", title: String(localized: "settings.colors.theme.title", defaultValue: "Color Theme"),
toString: { theme in toString: { theme in
ClockStyle.availableColorThemes().first(where: { $0.0 == theme })?.1 ?? theme localizedThemeName(for: theme)
} }
) )
} }
@ -51,7 +51,11 @@ struct BasicAppearanceSection: View {
.frame(height: 1) .frame(height: 1)
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
ColorPicker("Digit Color", selection: $digitColor, supportsOpacity: false) ColorPicker(
String(localized: "settings.colors.digit_color", defaultValue: "Digit Color"),
selection: $digitColor,
supportsOpacity: false
)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small) .padding(.vertical, Design.Spacing.small)
@ -61,7 +65,11 @@ struct BasicAppearanceSection: View {
.frame(height: 1) .frame(height: 1)
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
ColorPicker("Background Color", selection: $backgroundColor, supportsOpacity: true) ColorPicker(
String(localized: "settings.colors.background_color", defaultValue: "Background Color"),
selection: $backgroundColor,
supportsOpacity: true
)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small) .padding(.vertical, Design.Spacing.small)
@ -69,7 +77,7 @@ struct BasicAppearanceSection: View {
} }
} }
Text("Choose your favorite color theme or create a custom look.") Text(String(localized: "settings.colors.footer", defaultValue: "Choose your favorite color theme or create a custom look."))
.font(.caption) .font(.caption)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
} }
@ -85,33 +93,32 @@ struct BasicAppearanceSection: View {
} }
} }
/// Get the color for a theme private func localizedThemeName(for theme: String) -> String {
private func themeColor(for theme: String) -> Color {
switch theme { switch theme {
case "Custom": case "Custom":
return .gray return String(localized: "settings.colors.theme.custom", defaultValue: "Custom")
case "Night": case "Night":
return .white return String(localized: "settings.colors.theme.night", defaultValue: "Night")
case "Day": case "Day":
return .black return String(localized: "settings.colors.theme.day", defaultValue: "Day")
case "Red": case "Red":
return .red return String(localized: "settings.colors.theme.red", defaultValue: "Red")
case "Orange": case "Orange":
return .orange return String(localized: "settings.colors.theme.orange", defaultValue: "Orange")
case "Yellow": case "Yellow":
return .yellow return String(localized: "settings.colors.theme.yellow", defaultValue: "Yellow")
case "Green": case "Green":
return .green return String(localized: "settings.colors.theme.green", defaultValue: "Green")
case "Blue": case "Blue":
return .blue return String(localized: "settings.colors.theme.blue", defaultValue: "Blue")
case "Purple": case "Purple":
return .purple return String(localized: "settings.colors.theme.purple", defaultValue: "Purple")
case "Pink": case "Pink":
return .pink return String(localized: "settings.colors.theme.pink", defaultValue: "Pink")
case "White": case "White":
return .white return String(localized: "settings.colors.theme.white", defaultValue: "White")
default: default:
return .gray return theme
} }
} }
} }

View File

@ -14,7 +14,7 @@ struct BasicDisplaySection: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader( SettingsSectionHeader(
title: "Display", title: String(localized: "settings.display.section_title", defaultValue: "Display"),
systemImage: "display", systemImage: "display",
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -22,8 +22,8 @@ struct BasicDisplaySection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) { VStack(spacing: 0) {
SettingsToggle( SettingsToggle(
title: "24Hour Format", title: String(localized: "settings.display.use_24_hour.title", defaultValue: "24Hour Format"),
subtitle: "Use military time", subtitle: String(localized: "settings.display.use_24_hour.subtitle", defaultValue: "Use military time"),
isOn: $style.use24Hour, isOn: $style.use24Hour,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -34,8 +34,8 @@ struct BasicDisplaySection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsToggle( SettingsToggle(
title: "Show Seconds", title: String(localized: "settings.display.show_seconds.title", defaultValue: "Show Seconds"),
subtitle: "Display seconds in the clock", subtitle: String(localized: "settings.display.show_seconds.subtitle", defaultValue: "Display seconds in the clock"),
isOn: $style.showSeconds, isOn: $style.showSeconds,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -47,8 +47,8 @@ struct BasicDisplaySection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsToggle( SettingsToggle(
title: "Show AM/PM", title: String(localized: "settings.display.show_ampm.title", defaultValue: "Show AM/PM"),
subtitle: "Add an AM/PM indicator", subtitle: String(localized: "settings.display.show_ampm.subtitle", defaultValue: "Add an AM/PM indicator"),
isOn: $style.showAmPm, isOn: $style.showAmPm,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -60,8 +60,8 @@ struct BasicDisplaySection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsToggle( SettingsToggle(
title: "Auto Brightness", title: String(localized: "settings.display.auto_brightness.title", defaultValue: "Auto Brightness"),
subtitle: "Adapt brightness to ambient light", subtitle: String(localized: "settings.display.auto_brightness.subtitle", defaultValue: "Adapt brightness to ambient light"),
isOn: $style.autoBrightness, isOn: $style.autoBrightness,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -73,8 +73,8 @@ struct BasicDisplaySection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsToggle( SettingsToggle(
title: "Horizontal Mode", title: String(localized: "settings.display.horizontal_mode.title", defaultValue: "Horizontal Mode"),
subtitle: "Force a wide layout in portrait", subtitle: String(localized: "settings.display.horizontal_mode.subtitle", defaultValue: "Force a wide layout in portrait"),
isOn: $style.forceHorizontalMode, isOn: $style.forceHorizontalMode,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -82,7 +82,7 @@ struct BasicDisplaySection: View {
} }
} }
Text("Basic display settings for your clock.") Text(String(localized: "settings.display.footer", defaultValue: "Basic display settings for your clock."))
.font(.caption) .font(.caption)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
} }

View File

@ -31,7 +31,7 @@ struct FontSection: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader( SettingsSectionHeader(
title: "Font", title: String(localized: "settings.font.section_title", defaultValue: "Font"),
systemImage: "textformat", systemImage: "textformat",
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -40,14 +40,14 @@ struct FontSection: View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
// Font Family // Font Family
SettingsNavigationRow( SettingsNavigationRow(
title: "Family", title: String(localized: "settings.font.family.title", defaultValue: "Family"),
subtitle: style.fontFamily.rawValue, subtitle: style.fontFamily.rawValue,
backgroundColor: .clear backgroundColor: .clear
) { ) {
SettingsSelectionView( SettingsSelectionView(
selection: $style.fontFamily, selection: $style.fontFamily,
options: sortedFontFamilies, options: sortedFontFamilies,
title: "Font Family", title: String(localized: "settings.font.family.selection_title", defaultValue: "Font Family"),
toString: { $0.rawValue } toString: { $0.rawValue }
) )
} }
@ -65,18 +65,18 @@ struct FontSection: View {
Rectangle() Rectangle()
.fill(AppBorder.subtle) .fill(AppBorder.subtle)
.frame(height: 1) .frame(height: 1)
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.small)
// Font Weight // Font Weight
SettingsNavigationRow( SettingsNavigationRow(
title: "Weight", title: String(localized: "settings.font.weight.title", defaultValue: "Weight"),
subtitle: style.fontWeight.rawValue, subtitle: style.fontWeight.rawValue,
backgroundColor: .clear backgroundColor: .clear
) { ) {
SettingsSelectionView( SettingsSelectionView(
selection: $style.fontWeight, selection: $style.fontWeight,
options: availableWeights, options: availableWeights,
title: "Font Weight", title: String(localized: "settings.font.weight.selection_title", defaultValue: "Font Weight"),
toString: { $0.rawValue } toString: { $0.rawValue }
) )
} }
@ -89,14 +89,14 @@ struct FontSection: View {
// Font Design // Font Design
SettingsNavigationRow( SettingsNavigationRow(
title: "Design", title: String(localized: "settings.font.design.title", defaultValue: "Design"),
subtitle: style.fontDesign.rawValue, subtitle: style.fontDesign.rawValue,
backgroundColor: .clear backgroundColor: .clear
) { ) {
SettingsSelectionView( SettingsSelectionView(
selection: $style.fontDesign, selection: $style.fontDesign,
options: Font.Design.allCases, options: Font.Design.allCases,
title: "Font Design", title: String(localized: "settings.font.design.selection_title", defaultValue: "Font Design"),
toString: { $0.rawValue } toString: { $0.rawValue }
) )
} }
@ -108,15 +108,14 @@ struct FontSection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
HStack { HStack {
Text("Preview") Text(String(localized: "settings.font.preview.title", defaultValue: "Preview")).styled(.subheadingEmphasis)
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.secondary)
Spacer() Spacer()
Text("12:34") Text(String(localized: "settings.font.preview.sample_time", defaultValue: "12:34"))
.font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24)) .font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24))
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
} }
.padding(Design.Spacing.medium) .padding(.vertical, Design.Spacing.small)
.padding(.horizontal, Design.Spacing.small)
} }
} }
} }

View File

@ -14,7 +14,7 @@ struct NightModeSection: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader( SettingsSectionHeader(
title: "Night Mode", title: String(localized: "settings.night_mode.section_title", defaultValue: "Night Mode"),
systemImage: "moon.stars.fill", systemImage: "moon.stars.fill",
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -22,8 +22,8 @@ struct NightModeSection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) { VStack(spacing: 0) {
SettingsToggle( SettingsToggle(
title: "Enable Night Mode", title: String(localized: "settings.night_mode.enable.title", defaultValue: "Enable Night Mode"),
subtitle: "Use a red clock for low light", subtitle: String(localized: "settings.night_mode.enable.subtitle", defaultValue: "Use a red clock for low light"),
isOn: $style.nightModeEnabled, isOn: $style.nightModeEnabled,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -34,8 +34,8 @@ struct NightModeSection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsToggle( SettingsToggle(
title: "Auto Night Mode", title: String(localized: "settings.night_mode.auto.title", defaultValue: "Auto Night Mode"),
subtitle: "Trigger based on ambient light", subtitle: String(localized: "settings.night_mode.auto.subtitle", defaultValue: "Trigger based on ambient light"),
isOn: $style.autoNightMode, isOn: $style.autoNightMode,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -47,8 +47,8 @@ struct NightModeSection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsSlider( SettingsSlider(
title: "Light Threshold", title: String(localized: "settings.night_mode.light_threshold.title", defaultValue: "Light Threshold"),
subtitle: "Lower values activate sooner", subtitle: String(localized: "settings.night_mode.light_threshold.subtitle", defaultValue: "Lower values activate sooner"),
value: $style.ambientLightThreshold, value: $style.ambientLightThreshold,
in: 0.1...0.8, in: 0.1...0.8,
step: 0.01, step: 0.01,
@ -63,8 +63,8 @@ struct NightModeSection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsToggle( SettingsToggle(
title: "Scheduled Night Mode", title: String(localized: "settings.night_mode.scheduled.title", defaultValue: "Scheduled Night Mode"),
subtitle: "Enable on a daily schedule", subtitle: String(localized: "settings.night_mode.scheduled.subtitle", defaultValue: "Enable on a daily schedule"),
isOn: $style.scheduledNightMode, isOn: $style.scheduledNightMode,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -76,7 +76,7 @@ struct NightModeSection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
HStack { HStack {
Text("Start Time") Text(String(localized: "settings.night_mode.start_time", defaultValue: "Start Time"))
.font(.subheadline.weight(.medium)) .font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
@ -91,7 +91,7 @@ struct NightModeSection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
HStack { HStack {
Text("End Time") Text(String(localized: "settings.night_mode.end_time", defaultValue: "End Time"))
.font(.subheadline.weight(.medium)) .font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
@ -110,7 +110,7 @@ struct NightModeSection: View {
HStack(spacing: Design.Spacing.xSmall) { HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "moon.fill") Image(systemName: "moon.fill")
.foregroundStyle(AppStatus.error) .foregroundStyle(AppStatus.error)
Text("Night Mode Active") Text(String(localized: "settings.night_mode.active", defaultValue: "Night Mode Active"))
.font(.subheadline.weight(.medium)) .font(.subheadline.weight(.medium))
.foregroundStyle(AppStatus.error) .foregroundStyle(AppStatus.error)
Spacer() Spacer()
@ -121,7 +121,7 @@ struct NightModeSection: View {
} }
} }
Text("Night mode displays the clock in red to reduce eye strain in low light environments.") Text(String(localized: "settings.night_mode.footer", defaultValue: "Night mode displays the clock in red to reduce eye strain in low light environments."))
.font(.caption) .font(.caption)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
} }

View File

@ -16,7 +16,7 @@ struct OverlaySection: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader( SettingsSectionHeader(
title: "Overlays", title: String(localized: "settings.overlays.section_title", defaultValue: "Overlays"),
systemImage: "rectangle.on.rectangle.angled", systemImage: "rectangle.on.rectangle.angled",
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -24,8 +24,8 @@ struct OverlaySection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) { VStack(spacing: 0) {
SettingsToggle( SettingsToggle(
title: "Battery Level", title: String(localized: "settings.overlays.battery_level.title", defaultValue: "Battery Level"),
subtitle: "Show battery percentage", subtitle: String(localized: "settings.overlays.battery_level.subtitle", defaultValue: "Show battery percentage"),
isOn: $style.showBattery, isOn: $style.showBattery,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -36,8 +36,8 @@ struct OverlaySection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsToggle( SettingsToggle(
title: "Date", title: String(localized: "settings.overlays.date.title", defaultValue: "Date"),
subtitle: "Display the current date", subtitle: String(localized: "settings.overlays.date.subtitle", defaultValue: "Display the current date"),
isOn: $style.showDate, isOn: $style.showDate,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -48,8 +48,8 @@ struct OverlaySection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsToggle( SettingsToggle(
title: "Next Alarm", title: String(localized: "settings.overlays.next_alarm.title", defaultValue: "Next Alarm"),
subtitle: "Show your next scheduled alarm", subtitle: String(localized: "settings.overlays.next_alarm.subtitle", defaultValue: "Show your next scheduled alarm"),
isOn: $style.showNextAlarm, isOn: $style.showNextAlarm,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -60,8 +60,8 @@ struct OverlaySection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsToggle( SettingsToggle(
title: "Noise Controls", title: String(localized: "settings.overlays.noise_controls.title", defaultValue: "Noise Controls"),
subtitle: "Mini-player for white noise", subtitle: String(localized: "settings.overlays.noise_controls.subtitle", defaultValue: "Mini-player for white noise"),
isOn: $style.showNoiseControls, isOn: $style.showNoiseControls,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
@ -73,14 +73,14 @@ struct OverlaySection: View {
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
SettingsNavigationRow( SettingsNavigationRow(
title: "Date Format", title: String(localized: "settings.overlays.date_format.title", defaultValue: "Date Format"),
subtitle: style.dateFormat, subtitle: style.dateFormat,
backgroundColor: .clear backgroundColor: .clear
) { ) {
SettingsSelectionView( SettingsSelectionView(
selection: $style.dateFormat, selection: $style.dateFormat,
options: dateFormats.map { $0.1 }, options: dateFormats.map { $0.1 },
title: "Date Format", title: String(localized: "settings.overlays.date_format.title", defaultValue: "Date Format"),
toString: { format in toString: { format in
dateFormats.first(where: { $0.1 == format })?.0 ?? format dateFormats.first(where: { $0.1 == format })?.0 ?? format
} }

View File

@ -229,12 +229,22 @@ struct TimeDisplayView: View {
) )
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Container: \(Int(containerSize.width))×\(Int(containerSize.height))") Text(
Text("Digit Slot: \(Int(digitSlotSize.width))×\(Int(digitSlotSize.height))") String(
Text("Font Size: \(Int(fontSize))") localized: "clock.debug.container_size",
Text("Cols: \(Int(digitColumns)) Rows: \(Int(digitRows))") defaultValue: "Container: \(Int(containerSize.width))×\(Int(containerSize.height))"
Text("Portrait: \(portrait ? "true" : "false")") )
Text("Family: \(fontFamily.rawValue)") )
Text(
String(
localized: "clock.debug.digit_slot_size",
defaultValue: "Digit Slot: \(Int(digitSlotSize.width))×\(Int(digitSlotSize.height))"
)
)
Text(String(localized: "clock.debug.font_size", defaultValue: "Font Size: \(Int(fontSize))"))
Text(String(localized: "clock.debug.grid", defaultValue: "Cols: \(Int(digitColumns)) Rows: \(Int(digitRows))"))
Text(String(localized: "clock.debug.portrait", defaultValue: "Portrait: \(portrait ? "true" : "false")"))
Text(String(localized: "clock.debug.font_family", defaultValue: "Family: \(fontFamily.rawValue)"))
} }
.font(.system(size: 10, weight: .bold, design: .monospaced)) .font(.system(size: 10, weight: .bold, design: .monospaced))
.foregroundStyle(.yellow) .foregroundStyle(.yellow)

View File

@ -134,7 +134,7 @@ struct CategoryTab: View {
.styled(.subheadingEmphasis) .styled(.subheadingEmphasis)
if count > 0 { if count > 0 {
Text("\(count)") Text(count, format: .number)
.styled(.caption, emphasis: .secondary) .styled(.caption, emphasis: .secondary)
.padding(.horizontal, 6) .padding(.horizontal, 6)
.padding(.vertical, 2) .padding(.vertical, 2)

View File

@ -62,7 +62,11 @@ struct SoundControlView: View {
.font(.title2.weight(.semibold)) .font(.title2.weight(.semibold))
.foregroundStyle(.white) .foregroundStyle(.white)
Text(isPlaying ? "Stop Sound" : "Play Sound") Text(
isPlaying
? String(localized: "noise.control.stop_sound", defaultValue: "Stop Sound")
: String(localized: "noise.control.play_sound", defaultValue: "Play Sound")
)
.font(.headline.weight(.semibold)) .font(.headline.weight(.semibold))
.foregroundStyle(.white) .foregroundStyle(.white)
} }
@ -79,7 +83,11 @@ struct SoundControlView: View {
.animation(.easeInOut(duration: 0.2), value: isPlaying) .animation(.easeInOut(duration: 0.2), value: isPlaying)
.animation(.easeInOut(duration: 0.2), value: selectedSound) .animation(.easeInOut(duration: 0.2), value: selectedSound)
.accessibilityIdentifier("noise.playStopButton") .accessibilityIdentifier("noise.playStopButton")
.accessibilityLabel(isPlaying ? "Stop Sound" : "Play Sound") .accessibilityLabel(
isPlaying
? String(localized: "noise.control.stop_sound", defaultValue: "Stop Sound")
: String(localized: "noise.control.play_sound", defaultValue: "Play Sound")
)
} }
.frame(maxWidth: 400) // Reasonable max width for iPad .frame(maxWidth: 400) // Reasonable max width for iPad
.padding(Design.Spacing.medium) .padding(Design.Spacing.medium)

View File

@ -44,7 +44,10 @@ struct NoiseView: View {
Image(systemName: "magnifyingglass") Image(systemName: "magnifyingglass")
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
TextField("Search sounds", text: $searchText) TextField(
String(localized: "noise.search.placeholder", defaultValue: "Search sounds"),
text: $searchText
)
.textFieldStyle(.plain) .textFieldStyle(.plain)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
.accessibilityIdentifier("noise.searchField") .accessibilityIdentifier("noise.searchField")
@ -79,7 +82,7 @@ struct NoiseView: View {
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
} }
} }
.navigationTitle("Noise") .navigationTitle(String(localized: "noise.navigation_title", defaultValue: "Noise"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.animation(.easeInOut(duration: 0.3), value: selectedSound) .animation(.easeInOut(duration: 0.3), value: selectedSound)
.accessibilityIdentifier("noise.screen") .accessibilityIdentifier("noise.screen")
@ -125,11 +128,16 @@ struct NoiseView: View {
} }
VStack(spacing: 4) { VStack(spacing: 4) {
Text("Ready for Sleep?") Text(String(localized: "noise.empty.title", defaultValue: "Ready for Sleep?"))
.typography(.title3Bold) .typography(.title3Bold)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Text("Select a soothing sound below to begin your relaxation journey.") Text(
String(
localized: "noise.empty.subtitle_portrait",
defaultValue: "Select a soothing sound below to begin your relaxation journey."
)
)
.typography(.caption) .typography(.caption)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@ -170,7 +178,7 @@ struct NoiseView: View {
// Left side: Player controls // Left side: Player controls
VStack(alignment: .leading, spacing: Design.Spacing.medium) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
if !isPad { if !isPad {
Text("Ambient Sounds") Text(String(localized: "noise.ambient_sounds", defaultValue: "Ambient Sounds"))
.sectionTitleStyle() .sectionTitleStyle()
} }
@ -191,11 +199,11 @@ struct NoiseView: View {
} }
VStack(spacing: 4) { VStack(spacing: 4) {
Text("Ready for Sleep?") Text(String(localized: "noise.empty.title", defaultValue: "Ready for Sleep?"))
.typography(.title3Bold) .typography(.title3Bold)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Text("Select a soothing sound to begin.") Text(String(localized: "noise.empty.subtitle_landscape", defaultValue: "Select a soothing sound to begin."))
.typography(.caption) .typography(.caption)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@ -37,7 +37,11 @@ struct OnboardingBottomControls: View {
onSkip() onSkip()
} }
} label: { } label: {
Text(currentPage == 0 ? "Skip" : "Back") Text(
currentPage == 0
? String(localized: "onboarding.controls.skip", defaultValue: "Skip")
: String(localized: "onboarding.controls.back", defaultValue: "Back")
)
.typography(.bodyEmphasis) .typography(.bodyEmphasis)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -52,7 +56,11 @@ struct OnboardingBottomControls: View {
onFinish() onFinish()
} }
} label: { } label: {
Text(currentPage == totalPages - 1 ? "Get Started" : "Next") Text(
currentPage == totalPages - 1
? String(localized: "onboarding.controls.get_started", defaultValue: "Get Started")
: String(localized: "onboarding.controls.next", defaultValue: "Next")
)
.typography(.bodyEmphasis) .typography(.bodyEmphasis)
.foregroundStyle(.white) .foregroundStyle(.white)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)

View File

@ -25,22 +25,22 @@ struct OnboardingGetStartedPage: View {
.foregroundStyle(AppStatus.success) .foregroundStyle(AppStatus.success)
} }
Text("You're ready!") Text(String(localized: "onboarding.get_started.title", defaultValue: "You're ready!"))
.typography(.heroBold) .typography(.heroBold)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Text("Your alarms will work even in silent mode and Focus mode. The interface will automatically fade out to give you a clean view of the time!") Text(String(localized: "onboarding.get_started.subtitle", defaultValue: "Your alarms will work even in silent mode and Focus mode. The interface will automatically fade out to give you a clean view of the time!"))
.typography(.body) .typography(.body)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
OnboardingFeatureRow(icon: "alarm.fill", text: "Create your first alarm") OnboardingFeatureRow(icon: "alarm.fill", text: String(localized: "onboarding.get_started.feature_first_alarm", defaultValue: "Create your first alarm"))
OnboardingFeatureRow(icon: "repeat", text: "Set repeat days (weekdays/weekends)") OnboardingFeatureRow(icon: "repeat", text: String(localized: "onboarding.get_started.feature_repeat_days", defaultValue: "Set repeat days (weekdays/weekends)"))
OnboardingFeatureRow(icon: "slider.horizontal.3", text: "Customize vibration, flash, and volume") OnboardingFeatureRow(icon: "slider.horizontal.3", text: String(localized: "onboarding.get_started.feature_alert_options", defaultValue: "Customize vibration, flash, and volume"))
OnboardingFeatureRow(icon: "clock.fill", text: "Wait 5s for full screen") OnboardingFeatureRow(icon: "clock.fill", text: String(localized: "onboarding.get_started.feature_full_screen", defaultValue: "Wait 5s for full screen"))
OnboardingFeatureRow(icon: "speaker.wave.2", text: "Tap Noise to play sounds") OnboardingFeatureRow(icon: "speaker.wave.2", text: String(localized: "onboarding.get_started.feature_noise", defaultValue: "Tap Noise to play sounds"))
} }
.padding(.top, Design.Spacing.medium) .padding(.top, Design.Spacing.medium)

View File

@ -33,23 +33,23 @@ struct OnboardingPermissionsPage: View {
.symbolEffect(.pulse, options: .repeating) .symbolEffect(.pulse, options: .repeating)
} }
Text("Alarms that actually work") Text(String(localized: "onboarding.permissions.title", defaultValue: "Alarms that actually work"))
.typography(.heroBold) .typography(.heroBold)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.padding(.horizontal, Design.Spacing.large) .padding(.horizontal, Design.Spacing.large)
Text("Works in silent mode, Focus mode, and even when your phone is locked. You can then set repeat days and customize alert behavior.") Text(String(localized: "onboarding.permissions.subtitle", defaultValue: "Works in silent mode, Focus mode, and even when your phone is locked. You can then set repeat days and customize alert behavior."))
.typography(.body) .typography(.body)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
OnboardingFeatureRow(icon: "moon.zzz.fill", text: "Cuts through Do Not Disturb") OnboardingFeatureRow(icon: "moon.zzz.fill", text: String(localized: "onboarding.permissions.feature_dnd", defaultValue: "Cuts through Do Not Disturb"))
OnboardingFeatureRow(icon: "lock.iphone", text: "Shows countdown on Lock Screen") OnboardingFeatureRow(icon: "lock.iphone", text: String(localized: "onboarding.permissions.feature_lock_screen", defaultValue: "Shows countdown on Lock Screen"))
OnboardingFeatureRow(icon: "iphone.badge.play", text: "Works when app is closed") OnboardingFeatureRow(icon: "iphone.badge.play", text: String(localized: "onboarding.permissions.feature_app_closed", defaultValue: "Works when app is closed"))
} }
.padding(.top, Design.Spacing.medium) .padding(.top, Design.Spacing.medium)
@ -76,7 +76,7 @@ struct OnboardingPermissionsPage: View {
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 24)) .font(.system(size: 24))
Text("Alarms enabled!") Text(String(localized: "onboarding.permissions.enabled", defaultValue: "Alarms enabled!"))
} }
.foregroundStyle(AppStatus.success) .foregroundStyle(AppStatus.success)
.typography(.bodyEmphasis) .typography(.bodyEmphasis)
@ -89,7 +89,7 @@ struct OnboardingPermissionsPage: View {
} label: { } label: {
HStack { HStack {
Image(systemName: "alarm.fill") Image(systemName: "alarm.fill")
Text("Enable Alarms") Text(String(localized: "onboarding.permissions.enable_alarms", defaultValue: "Enable Alarms"))
} }
.typography(.bodyEmphasis) .typography(.bodyEmphasis)
.foregroundStyle(.white) .foregroundStyle(.white)
@ -106,7 +106,7 @@ struct OnboardingPermissionsPage: View {
private var keepAwakeSection: some View { private var keepAwakeSection: some View {
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {
Text("Want the clock always visible?") Text(String(localized: "onboarding.permissions.keep_awake_prompt", defaultValue: "Want the clock always visible?"))
.typography(.callout) .typography(.callout)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@ -116,7 +116,11 @@ struct OnboardingPermissionsPage: View {
} label: { } label: {
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Image(systemName: keepAwakeEnabled ? "checkmark.circle.fill" : "bolt.fill") Image(systemName: keepAwakeEnabled ? "checkmark.circle.fill" : "bolt.fill")
Text(keepAwakeEnabled ? "Keep Awake Enabled" : "Enable Keep Awake") Text(
keepAwakeEnabled
? String(localized: "onboarding.permissions.keep_awake_enabled", defaultValue: "Keep Awake Enabled")
: String(localized: "onboarding.permissions.enable_keep_awake", defaultValue: "Enable Keep Awake")
)
} }
.typography(.callout) .typography(.callout)
.foregroundStyle(keepAwakeEnabled ? AppStatus.success : AppTextColors.secondary) .foregroundStyle(keepAwakeEnabled ? AppStatus.success : AppTextColors.secondary)

View File

@ -20,26 +20,26 @@ struct OnboardingWelcomePage: View {
} }
.padding(.bottom, Design.Spacing.medium) .padding(.bottom, Design.Spacing.medium)
Text("The Noise Clock") Text(String(localized: "onboarding.welcome.title", defaultValue: "The Noise Clock"))
.typography(.heroBold) .typography(.heroBold)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
Text("Your beautiful bedside companion") Text(String(localized: "onboarding.welcome.subtitle", defaultValue: "Your beautiful bedside companion"))
.typography(.title3) .typography(.title3)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
VStack(spacing: Design.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
OnboardingFeatureRow( OnboardingFeatureRow(
icon: "moon.stars.fill", icon: "moon.stars.fill",
text: "Fall asleep to soothing sounds" text: String(localized: "onboarding.welcome.feature_sleep_sounds", defaultValue: "Fall asleep to soothing sounds")
) )
OnboardingFeatureRow( OnboardingFeatureRow(
icon: "alarm.fill", icon: "alarm.fill",
text: "Wake up your way with custom alarms" text: String(localized: "onboarding.welcome.feature_custom_alarms", defaultValue: "Wake up your way with custom alarms")
) )
OnboardingFeatureRow( OnboardingFeatureRow(
icon: "clock.fill", icon: "clock.fill",
text: "Automatic full-screen display" text: String(localized: "onboarding.welcome.feature_full_screen", defaultValue: "Automatic full-screen display")
) )
} }
.padding(.top, Design.Spacing.large) .padding(.top, Design.Spacing.large)

File diff suppressed because it is too large Load Diff

View File

@ -16,10 +16,14 @@ public enum DigitAnimationStyle: String, CaseIterable, Codable {
public var displayName: String { public var displayName: String {
switch self { switch self {
case .none: return "None" case .none:
case .spring: return "Spring" return String(localized: "digit_animation.none.display_name", defaultValue: "None")
case .bounce: return "Bounce" case .spring:
case .glitch: return "Glitch" return String(localized: "digit_animation.spring.display_name", defaultValue: "Spring")
case .bounce:
return String(localized: "digit_animation.bounce.display_name", defaultValue: "Bounce")
case .glitch:
return String(localized: "digit_animation.glitch.display_name", defaultValue: "Glitch")
} }
} }
} }

View File

@ -21,12 +21,18 @@ public enum SoundCategory: String, CaseIterable, Identifiable {
/// Display name for the category /// Display name for the category
public var displayName: String { public var displayName: String {
switch self { switch self {
case .all: return "All" case .all:
case .colored: return "Colored" return String(localized: "sound_category.all.display_name", defaultValue: "All")
case .ambient: return "Ambient" case .colored:
case .nature: return "Nature" return String(localized: "sound_category.colored.display_name", defaultValue: "Colored")
case .mechanical: return "Mechanical" case .ambient:
case .alarm: return "Alarm Sounds" return String(localized: "sound_category.ambient.display_name", defaultValue: "Ambient")
case .nature:
return String(localized: "sound_category.nature.display_name", defaultValue: "Nature")
case .mechanical:
return String(localized: "sound_category.mechanical.display_name", defaultValue: "Mechanical")
case .alarm:
return String(localized: "sound_category.alarm.display_name", defaultValue: "Alarm Sounds")
} }
} }
@ -57,12 +63,18 @@ public enum SoundCategory: String, CaseIterable, Identifiable {
/// Description of the category /// Description of the category
public var description: String { public var description: String {
switch self { switch self {
case .all: return "All available sounds" case .all:
case .colored: return "Synthetic noise signals for focus, sleep, and relaxation" return String(localized: "sound_category.all.description", defaultValue: "All available sounds")
case .ambient: return "General ambient sounds" case .colored:
case .nature: return "Natural environmental sounds" return String(localized: "sound_category.colored.description", defaultValue: "Synthetic noise signals for focus, sleep, and relaxation")
case .mechanical: return "Mechanical and electronic sounds" case .ambient:
case .alarm: return "Wake-up and notification alarm sounds" return String(localized: "sound_category.ambient.description", defaultValue: "General ambient sounds")
case .nature:
return String(localized: "sound_category.nature.description", defaultValue: "Natural environmental sounds")
case .mechanical:
return String(localized: "sound_category.mechanical.description", defaultValue: "Mechanical and electronic sounds")
case .alarm:
return String(localized: "sound_category.alarm.description", defaultValue: "Wake-up and notification alarm sounds")
} }
} }

View File

@ -20,12 +20,12 @@ struct KeepAwakePrompt: View {
.symbolEffect(.bounce, options: .nonRepeating) .symbolEffect(.bounce, options: .nonRepeating)
VStack(spacing: Design.Spacing.small) { VStack(spacing: Design.Spacing.small) {
Text("Keep Awake for Alarms") Text(String(localized: "keep_awake.prompt.title", defaultValue: "Keep Awake for Alarms"))
.typography(.title2Bold) .typography(.title2Bold)
.foregroundStyle(AppTextColors.primary) .foregroundStyle(AppTextColors.primary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
Text("Enable Keep Awake so your alarm can play loudly and show the full screen while TheNoiseClock stays open.") Text(String(localized: "keep_awake.prompt.subtitle", defaultValue: "Enable Keep Awake so your alarm can play loudly and show the full screen while TheNoiseClock stays open."))
.typography(.body) .typography(.body)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@ -34,7 +34,7 @@ struct KeepAwakePrompt: View {
VStack(spacing: Design.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
Button(action: onEnable) { Button(action: onEnable) {
Text("Enable Keep Awake") Text(String(localized: "keep_awake.prompt.enable", defaultValue: "Enable Keep Awake"))
.font(Typography.headingEmphasis.font) .font(Typography.headingEmphasis.font)
.foregroundStyle(.white) .foregroundStyle(.white)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -44,7 +44,7 @@ struct KeepAwakePrompt: View {
} }
Button(action: onDismiss) { Button(action: onDismiss) {
Text("Not Now") Text(String(localized: "common.not_now", defaultValue: "Not Now"))
.font(Typography.bodyEmphasis.font) .font(Typography.bodyEmphasis.font)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)