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
- **Observation Framework**: @Observable for reactive state management
- **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
- **Adaptive Layout**: Support for all device sizes and orientations with Liquid Glass
- **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.
- 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.
- 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 {
TabView(selection: $selectedTab) {
Tab("Clock", systemImage: "clock", value: AppTab.clock) {
Tab(String(localized: "tab.clock", defaultValue: "Clock"), systemImage: "clock", value: AppTab.clock) {
NavigationStack {
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 {
AlarmView(viewModel: alarmViewModel)
}
}
Tab("Noise", systemImage: "waveform", value: AppTab.noise) {
Tab(String(localized: "tab.noise", defaultValue: "Noise"), systemImage: "waveform", value: AppTab.noise) {
NavigationStack {
NoiseView()
}
}
Tab("Settings", systemImage: "gearshape", value: AppTab.settings) {
Tab(String(localized: "tab.settings", defaultValue: "Settings"), systemImage: "gearshape", value: AppTab.settings) {
NavigationStack {
ClockSettingsView(
style: clockViewModel.style,

View File

@ -17,10 +17,15 @@ import Foundation
/// Intent to stop an active alarm from the Live Activity or notification.
struct StopAlarmIntent: LiveActivityIntent {
static let title: LocalizedStringResource = "Stop Alarm"
static let description = IntentDescription("Stops the currently ringing alarm")
static let title = LocalizedStringResource("alarm_intent.stop.title", defaultValue: "Stop 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
static var supportedModes: IntentModes { .background }
@ -48,10 +53,15 @@ struct StopAlarmIntent: LiveActivityIntent {
/// Intent to snooze an active alarm from the Live Activity or notification.
struct SnoozeAlarmIntent: LiveActivityIntent {
static let title: LocalizedStringResource = "Snooze Alarm"
static let description = IntentDescription("Snoozes the currently ringing alarm")
static let title = LocalizedStringResource("alarm_intent.snooze.title", defaultValue: "Snooze 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
static var supportedModes: IntentModes { .background }
@ -80,11 +90,16 @@ struct SnoozeAlarmIntent: LiveActivityIntent {
/// Intent to open the app when the user taps the Live Activity.
struct OpenAlarmAppIntent: LiveActivityIntent {
static let title: LocalizedStringResource = "Open TheNoiseClock"
static let description = IntentDescription("Opens the app to the alarm screen")
static let title = LocalizedStringResource("alarm_intent.open_app.title", defaultValue: "Open TheNoiseClock")
static let description = IntentDescription(
LocalizedStringResource(
"alarm_intent.open_app.description",
defaultValue: "Opens the app to the alarm screen"
)
)
static let openAppWhenRun = true
@Parameter(title: "Alarm ID")
@Parameter(title: LocalizedStringResource("alarm_intent.parameter.alarm_id", defaultValue: "Alarm ID"))
var alarmId: String
init() {
@ -110,9 +125,9 @@ enum AlarmIntentError: Error, LocalizedError {
var errorDescription: String? {
switch self {
case .invalidAlarmID:
return "Invalid alarm ID"
return String(localized: "alarm_intent.error.invalid_alarm_id", defaultValue: "Invalid alarm ID")
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,
repeatWeekdays: [Int] = [],
soundName: String = AppConstants.SystemSounds.defaultSound,
label: String = "Alarm",
notificationMessage: String = "Your alarm is ringing",
label: String = String(localized: "alarm.default_label", defaultValue: "Alarm"),
notificationMessage: String = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing"),
snoozeDuration: Int = 9,
isVibrationEnabled: Bool = true,
isLightFlashEnabled: Bool = false,
@ -72,8 +72,8 @@ struct Alarm: Identifiable, Codable, Equatable {
try container.decodeIfPresent([Int].self, forKey: .repeatWeekdays) ?? []
)
self.soundName = try container.decodeIfPresent(String.self, forKey: .soundName) ?? AppConstants.SystemSounds.defaultSound
self.label = try container.decodeIfPresent(String.self, forKey: .label) ?? "Alarm"
self.notificationMessage = try container.decodeIfPresent(String.self, forKey: .notificationMessage) ?? "Your alarm is ringing"
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) ?? String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing")
self.snoozeDuration = try container.decodeIfPresent(Int.self, forKey: .snoozeDuration) ?? 9
self.isVibrationEnabled = try container.decodeIfPresent(Bool.self, forKey: .isVibrationEnabled) ?? true
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 {
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)
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]) {
return "Weekdays"
return String(localized: "alarm.repeat.weekdays", defaultValue: "Weekdays")
}
if weekdaySet == Set([1, 7]) {
return "Weekends"
return String(localized: "alarm.repeat.weekends", defaultValue: "Weekends")
}
let symbols = Calendar.current.shortWeekdaySymbols

View File

@ -83,14 +83,14 @@ final class AlarmKitService {
// Create the stop button for the alarm
let stopButton = AlarmButton(
text: "Stop",
text: LocalizedStringResource("alarm.action.stop"),
textColor: .red,
systemImageName: "stop.fill"
)
// Create the snooze button (secondary button with countdown behavior)
let snoozeButton = AlarmButton(
text: "Snooze",
text: LocalizedStringResource("alarm.action.snooze"),
textColor: .white,
systemImageName: "moon.zzz"
)
@ -422,9 +422,15 @@ enum AlarmKitError: Error, LocalizedError {
var errorDescription: String? {
switch self {
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):
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,
repeatWeekdays: [Int] = [],
soundName: String = AppConstants.SystemSounds.defaultSound,
label: String = "Alarm",
notificationMessage: String = "Your alarm is ringing",
label: String = String(localized: "alarm.default_label", defaultValue: "Alarm"),
notificationMessage: String = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing"),
snoozeDuration: Int = 9,
isVibrationEnabled: Bool = true,
isLightFlashEnabled: Bool = false,
@ -219,6 +219,9 @@ final class AlarmViewModel {
} else {
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 repeatWeekdays: [Int] = []
@State private var selectedSoundName = AppConstants.SystemSounds.defaultSound
@State private var alarmLabel = "Alarm"
@State private var notificationMessage = "Your alarm is ringing"
@State private var alarmLabel = String(localized: "alarm.default_label", defaultValue: "Alarm")
@State private var notificationMessage = String(localized: "alarm.default_notification_message", defaultValue: "Your alarm is ringing")
@State private var snoozeDuration = 9 // minutes
@State private var isVibrationEnabled = true
@State private var isLightFlashEnabled = false
@ -44,7 +44,7 @@ struct AddAlarmView: View {
Image(systemName: "textformat")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text("Label")
Text(String(localized: "alarm.editor.label", defaultValue: "Label"))
Spacer()
Text(alarmLabel)
.foregroundStyle(.secondary)
@ -57,7 +57,7 @@ struct AddAlarmView: View {
Image(systemName: "message")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text("Message")
Text(String(localized: "alarm.editor.message", defaultValue: "Message"))
Spacer()
Text(notificationMessage)
.foregroundStyle(.secondary)
@ -71,7 +71,7 @@ struct AddAlarmView: View {
Image(systemName: "music.note")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text("Sound")
Text(String(localized: "alarm.editor.sound", defaultValue: "Sound"))
Spacer()
Text(getSoundDisplayName(selectedSoundName))
.foregroundStyle(.secondary)
@ -84,7 +84,7 @@ struct AddAlarmView: View {
Image(systemName: "repeat")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text("Repeat")
Text(String(localized: "alarm.editor.repeat", defaultValue: "Repeat"))
Spacer()
Text(Alarm.repeatSummary(for: repeatWeekdays))
.foregroundStyle(.secondary)
@ -98,9 +98,14 @@ struct AddAlarmView: View {
Image(systemName: "clock.arrow.circlepath")
.foregroundStyle(AppAccent.primary)
.frame(width: 24)
Text("Snooze")
Text(String(localized: "alarm.editor.snooze", defaultValue: "Snooze"))
Spacer()
Text("for \(snoozeDuration) min")
Text(
String(
localized: "alarm.editor.snooze_for_minutes",
defaultValue: "for \(snoozeDuration) min"
)
)
.foregroundStyle(.secondary)
}
}
@ -113,12 +118,12 @@ struct AddAlarmView: View {
}
.listStyle(.insetGrouped)
}
.navigationTitle("Alarm")
.navigationTitle(String(localized: "alarm.add.navigation_title", defaultValue: "Alarm"))
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
Button(String(localized: "common.cancel", defaultValue: "Cancel")) {
isPresented = false
}
.foregroundStyle(AppAccent.primary)
@ -126,7 +131,11 @@ struct AddAlarmView: View {
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(isSaving ? "Saving..." : "Save") {
Button(
isSaving
? String(localized: "common.saving", defaultValue: "Saving...")
: String(localized: "common.save", defaultValue: "Save")
) {
Task {
await saveAlarm()
}
@ -138,8 +147,8 @@ struct AddAlarmView: View {
}
}
}
.alert("Alarm Error", isPresented: $isShowingSaveErrorAlert) {
Button("OK", role: .cancel) { }
.alert(String(localized: "alarm.error.title", defaultValue: "Alarm Error"), isPresented: $isShowingSaveErrorAlert) {
Button(String(localized: "common.ok", defaultValue: "OK"), role: .cancel) { }
} message: {
Text(saveErrorMessage)
}

View File

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

View File

@ -36,7 +36,12 @@ struct AlarmRowView: View {
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
Text("\(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))")
Text(
String(
localized: "alarm.row.sound_prefix",
defaultValue: "\(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))"
)
)
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
@ -46,7 +51,7 @@ struct AlarmRowView: View {
.font(.caption2)
.foregroundStyle(AppStatus.warning)
.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)
.foregroundStyle(AppTextColors.tertiary)
}
@ -73,14 +78,23 @@ struct AlarmRowView: View {
}
.accessibilityElement(children: .contain)
.accessibilityIdentifier("alarms.row.\(alarm.id.uuidString)")
.accessibilityLabel("\(alarm.label), \(alarm.formattedTime())")
.accessibilityValue(alarm.isEnabled ? "Enabled" : "Disabled")
.accessibilityLabel(
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) {
Button(role: .destructive) {
onDelete()
} 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
var body: some View {
Section("Alert Options") {
Toggle("Vibration", isOn: $isVibrationEnabled)
Section(String(localized: "alarm.alert_options.section_title", defaultValue: "Alert Options")) {
Toggle(String(localized: "alarm.alert_options.vibration", defaultValue: "Vibration"), isOn: $isVibrationEnabled)
.tint(AppAccent.primary)
.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)
.accessibilityIdentifier("alarms.alertOptions.flashToggle")
VStack(alignment: .leading, spacing: Design.Spacing.small) {
HStack {
Text("Volume")
Text(String(localized: "alarm.alert_options.volume", defaultValue: "Volume"))
Spacer()
Text("\(Int((volume * 100).rounded()))%")
Text(Double(volume), format: .percent.precision(.fractionLength(0)))
.foregroundStyle(.secondary)
}

View File

@ -32,11 +32,11 @@ struct EmptyAlarmsView: View {
}
VStack(spacing: Design.Spacing.small) {
Text("No Alarms Set")
Text(String(localized: "alarms.empty.title", defaultValue: "No Alarms Set"))
.typography(.title2Bold)
.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)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
@ -48,7 +48,7 @@ struct EmptyAlarmsView: View {
HStack {
Image(systemName: "plus.circle.fill")
.symbolEffect(.bounce, options: .nonRepeating)
Text("Add Your First Alarm")
Text(String(localized: "alarms.empty.cta", defaultValue: "Add Your First Alarm"))
}
.typography(.bodyEmphasis)
.foregroundStyle(.white)

View File

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

View File

@ -22,7 +22,7 @@ struct NotificationMessageEditView: View {
.foregroundStyle(AppTextColors.primary)
.padding(.vertical, Design.Spacing.xxSmall)
} 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)
.foregroundStyle(AppTextColors.secondary)
}
@ -30,7 +30,7 @@ struct NotificationMessageEditView: View {
}
.scrollContentBackground(.hidden)
.background(AppSurface.primary.ignoresSafeArea())
.navigationTitle("Message")
.navigationTitle(String(localized: "alarm.message.navigation_title", defaultValue: "Message"))
.navigationBarTitleDisplayMode(.inline)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -180,11 +180,26 @@ struct ClockView: View {
let hasDynamicIsland = windowInsets.left > 0 || windowInsets.right > 0
VStack(alignment: .leading, spacing: 2) {
Text("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("Symmetric Inset: \(Int(symmetricInset))")
Text("Dynamic Island: \(hasDynamicIsland && isLandscape ? "Yes" : "No")")
Text("Orientation: \(isLandscape ? "Landscape" : "Portrait")")
Text(String(localized: "clock.debug.screen", defaultValue: "Screen: \(Int(size.width))×\(Int(size.height))"))
Text(
String(
localized: "clock.debug.window_insets",
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))
.foregroundStyle(.green)

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,11 @@ struct NoiseMiniPlayer: View {
.sensoryFeedback(.impact(flexibility: .soft), trigger: isPlaying)
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))
.foregroundStyle(color.opacity(0.6))
.textCase(.uppercase)

View File

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

View File

@ -14,7 +14,7 @@ struct AdvancedDisplaySection: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Advanced Display",
title: String(localized: "settings.advanced_display.section_title", defaultValue: "Advanced Display"),
systemImage: "eye",
accentColor: AppAccent.primary
)
@ -22,8 +22,8 @@ struct AdvancedDisplaySection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) {
SettingsToggle(
title: "Keep Awake",
subtitle: "Prevent sleep in display mode",
title: String(localized: "settings.advanced_display.keep_awake.title", defaultValue: "Keep Awake"),
subtitle: String(localized: "settings.advanced_display.keep_awake.subtitle", defaultValue: "Prevent sleep in display mode"),
isOn: $style.keepAwake,
accentColor: AppAccent.primary
)
@ -36,11 +36,11 @@ struct AdvancedDisplaySection: View {
.padding(.horizontal, Design.Spacing.medium)
HStack {
Text("Current Brightness")
Text(String(localized: "settings.advanced_display.current_brightness", defaultValue: "Current Brightness"))
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Spacer()
Text("\(Int(style.effectiveBrightness * 100))%")
Text(style.effectiveBrightness, format: .percent.precision(.fractionLength(0)))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
.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)
.foregroundStyle(AppTextColors.tertiary)
SettingsSectionHeader(
title: "Focus Modes",
title: String(localized: "settings.focus_modes.section_title", defaultValue: "Focus Modes"),
systemImage: "moon.zzz.fill",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsToggle(
title: "Respect Focus Modes",
subtitle: "Follow Do Not Disturb rules",
title: String(localized: "settings.focus_modes.respect_focus.title", defaultValue: "Respect Focus Modes"),
subtitle: String(localized: "settings.focus_modes.respect_focus.subtitle", defaultValue: "Follow Do Not Disturb rules"),
isOn: $style.respectFocusModes,
accentColor: AppAccent.primary
)
.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)
.foregroundStyle(AppTextColors.tertiary)
}

View File

@ -16,7 +16,7 @@ struct BasicAppearanceSection: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Colors",
title: String(localized: "settings.colors.section_title", defaultValue: "Colors"),
systemImage: "paintpalette.fill",
accentColor: AppAccent.primary
)
@ -24,16 +24,16 @@ struct BasicAppearanceSection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) {
SettingsNavigationRow(
title: "Color Theme",
subtitle: style.selectedColorTheme,
title: String(localized: "settings.colors.theme.title", defaultValue: "Color Theme"),
subtitle: localizedThemeName(for: style.selectedColorTheme),
backgroundColor: .clear
) {
SettingsSelectionView(
selection: $style.selectedColorTheme,
options: ClockStyle.availableColorThemes().map { $0.0 },
title: "Color Theme",
title: String(localized: "settings.colors.theme.title", defaultValue: "Color Theme"),
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)
.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)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
@ -61,7 +65,11 @@ struct BasicAppearanceSection: View {
.frame(height: 1)
.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)
.padding(.horizontal, Design.Spacing.medium)
.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)
.foregroundStyle(AppTextColors.tertiary)
}
@ -85,33 +93,32 @@ struct BasicAppearanceSection: View {
}
}
/// Get the color for a theme
private func themeColor(for theme: String) -> Color {
private func localizedThemeName(for theme: String) -> String {
switch theme {
case "Custom":
return .gray
return String(localized: "settings.colors.theme.custom", defaultValue: "Custom")
case "Night":
return .white
return String(localized: "settings.colors.theme.night", defaultValue: "Night")
case "Day":
return .black
return String(localized: "settings.colors.theme.day", defaultValue: "Day")
case "Red":
return .red
return String(localized: "settings.colors.theme.red", defaultValue: "Red")
case "Orange":
return .orange
return String(localized: "settings.colors.theme.orange", defaultValue: "Orange")
case "Yellow":
return .yellow
return String(localized: "settings.colors.theme.yellow", defaultValue: "Yellow")
case "Green":
return .green
return String(localized: "settings.colors.theme.green", defaultValue: "Green")
case "Blue":
return .blue
return String(localized: "settings.colors.theme.blue", defaultValue: "Blue")
case "Purple":
return .purple
return String(localized: "settings.colors.theme.purple", defaultValue: "Purple")
case "Pink":
return .pink
return String(localized: "settings.colors.theme.pink", defaultValue: "Pink")
case "White":
return .white
return String(localized: "settings.colors.theme.white", defaultValue: "White")
default:
return .gray
return theme
}
}
}

View File

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

View File

@ -31,7 +31,7 @@ struct FontSection: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Font",
title: String(localized: "settings.font.section_title", defaultValue: "Font"),
systemImage: "textformat",
accentColor: AppAccent.primary
)
@ -40,14 +40,14 @@ struct FontSection: View {
VStack(alignment: .leading, spacing: 0) {
// Font Family
SettingsNavigationRow(
title: "Family",
title: String(localized: "settings.font.family.title", defaultValue: "Family"),
subtitle: style.fontFamily.rawValue,
backgroundColor: .clear
) {
SettingsSelectionView(
selection: $style.fontFamily,
options: sortedFontFamilies,
title: "Font Family",
title: String(localized: "settings.font.family.selection_title", defaultValue: "Font Family"),
toString: { $0.rawValue }
)
}
@ -65,18 +65,18 @@ struct FontSection: View {
Rectangle()
.fill(AppBorder.subtle)
.frame(height: 1)
.padding(.horizontal, Design.Spacing.medium)
.padding(.horizontal, Design.Spacing.small)
// Font Weight
SettingsNavigationRow(
title: "Weight",
title: String(localized: "settings.font.weight.title", defaultValue: "Weight"),
subtitle: style.fontWeight.rawValue,
backgroundColor: .clear
) {
SettingsSelectionView(
selection: $style.fontWeight,
options: availableWeights,
title: "Font Weight",
title: String(localized: "settings.font.weight.selection_title", defaultValue: "Font Weight"),
toString: { $0.rawValue }
)
}
@ -89,14 +89,14 @@ struct FontSection: View {
// Font Design
SettingsNavigationRow(
title: "Design",
title: String(localized: "settings.font.design.title", defaultValue: "Design"),
subtitle: style.fontDesign.rawValue,
backgroundColor: .clear
) {
SettingsSelectionView(
selection: $style.fontDesign,
options: Font.Design.allCases,
title: "Font Design",
title: String(localized: "settings.font.design.selection_title", defaultValue: "Font Design"),
toString: { $0.rawValue }
)
}
@ -108,15 +108,14 @@ struct FontSection: View {
.padding(.horizontal, Design.Spacing.medium)
HStack {
Text("Preview")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.secondary)
Text(String(localized: "settings.font.preview.title", defaultValue: "Preview")).styled(.subheadingEmphasis)
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))
.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 {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Night Mode",
title: String(localized: "settings.night_mode.section_title", defaultValue: "Night Mode"),
systemImage: "moon.stars.fill",
accentColor: AppAccent.primary
)
@ -22,8 +22,8 @@ struct NightModeSection: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(spacing: 0) {
SettingsToggle(
title: "Enable Night Mode",
subtitle: "Use a red clock for low light",
title: String(localized: "settings.night_mode.enable.title", defaultValue: "Enable Night Mode"),
subtitle: String(localized: "settings.night_mode.enable.subtitle", defaultValue: "Use a red clock for low light"),
isOn: $style.nightModeEnabled,
accentColor: AppAccent.primary
)
@ -34,8 +34,8 @@ struct NightModeSection: View {
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
title: "Auto Night Mode",
subtitle: "Trigger based on ambient light",
title: String(localized: "settings.night_mode.auto.title", defaultValue: "Auto Night Mode"),
subtitle: String(localized: "settings.night_mode.auto.subtitle", defaultValue: "Trigger based on ambient light"),
isOn: $style.autoNightMode,
accentColor: AppAccent.primary
)
@ -47,8 +47,8 @@ struct NightModeSection: View {
.padding(.horizontal, Design.Spacing.medium)
SettingsSlider(
title: "Light Threshold",
subtitle: "Lower values activate sooner",
title: String(localized: "settings.night_mode.light_threshold.title", defaultValue: "Light Threshold"),
subtitle: String(localized: "settings.night_mode.light_threshold.subtitle", defaultValue: "Lower values activate sooner"),
value: $style.ambientLightThreshold,
in: 0.1...0.8,
step: 0.01,
@ -63,8 +63,8 @@ struct NightModeSection: View {
.padding(.horizontal, Design.Spacing.medium)
SettingsToggle(
title: "Scheduled Night Mode",
subtitle: "Enable on a daily schedule",
title: String(localized: "settings.night_mode.scheduled.title", defaultValue: "Scheduled Night Mode"),
subtitle: String(localized: "settings.night_mode.scheduled.subtitle", defaultValue: "Enable on a daily schedule"),
isOn: $style.scheduledNightMode,
accentColor: AppAccent.primary
)
@ -76,7 +76,7 @@ struct NightModeSection: View {
.padding(.horizontal, Design.Spacing.medium)
HStack {
Text("Start Time")
Text(String(localized: "settings.night_mode.start_time", defaultValue: "Start Time"))
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Spacer()
@ -91,7 +91,7 @@ struct NightModeSection: View {
.padding(.horizontal, Design.Spacing.medium)
HStack {
Text("End Time")
Text(String(localized: "settings.night_mode.end_time", defaultValue: "End Time"))
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Spacer()
@ -110,7 +110,7 @@ struct NightModeSection: View {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "moon.fill")
.foregroundStyle(AppStatus.error)
Text("Night Mode Active")
Text(String(localized: "settings.night_mode.active", defaultValue: "Night Mode Active"))
.font(.subheadline.weight(.medium))
.foregroundStyle(AppStatus.error)
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)
.foregroundStyle(AppTextColors.tertiary)
}

View File

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

View File

@ -229,12 +229,22 @@ struct TimeDisplayView: View {
)
VStack(alignment: .leading, spacing: 2) {
Text("Container: \(Int(containerSize.width))×\(Int(containerSize.height))")
Text("Digit Slot: \(Int(digitSlotSize.width))×\(Int(digitSlotSize.height))")
Text("Font Size: \(Int(fontSize))")
Text("Cols: \(Int(digitColumns)) Rows: \(Int(digitRows))")
Text("Portrait: \(portrait ? "true" : "false")")
Text("Family: \(fontFamily.rawValue)")
Text(
String(
localized: "clock.debug.container_size",
defaultValue: "Container: \(Int(containerSize.width))×\(Int(containerSize.height))"
)
)
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))
.foregroundStyle(.yellow)

View File

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

View File

@ -62,7 +62,11 @@ struct SoundControlView: View {
.font(.title2.weight(.semibold))
.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))
.foregroundStyle(.white)
}
@ -79,7 +83,11 @@ struct SoundControlView: View {
.animation(.easeInOut(duration: 0.2), value: isPlaying)
.animation(.easeInOut(duration: 0.2), value: selectedSound)
.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
.padding(Design.Spacing.medium)

View File

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

View File

@ -37,7 +37,11 @@ struct OnboardingBottomControls: View {
onSkip()
}
} 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)
.foregroundStyle(AppTextColors.secondary)
.frame(maxWidth: .infinity)
@ -52,7 +56,11 @@ struct OnboardingBottomControls: View {
onFinish()
}
} 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)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)

View File

@ -25,22 +25,22 @@ struct OnboardingGetStartedPage: View {
.foregroundStyle(AppStatus.success)
}
Text("You're ready!")
Text(String(localized: "onboarding.get_started.title", defaultValue: "You're ready!"))
.typography(.heroBold)
.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)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, Design.Spacing.xxLarge)
VStack(alignment: .leading, spacing: Design.Spacing.small) {
OnboardingFeatureRow(icon: "alarm.fill", text: "Create your first alarm")
OnboardingFeatureRow(icon: "repeat", text: "Set repeat days (weekdays/weekends)")
OnboardingFeatureRow(icon: "slider.horizontal.3", text: "Customize vibration, flash, and volume")
OnboardingFeatureRow(icon: "clock.fill", text: "Wait 5s for full screen")
OnboardingFeatureRow(icon: "speaker.wave.2", text: "Tap Noise to play sounds")
OnboardingFeatureRow(icon: "alarm.fill", text: String(localized: "onboarding.get_started.feature_first_alarm", defaultValue: "Create your first alarm"))
OnboardingFeatureRow(icon: "repeat", text: String(localized: "onboarding.get_started.feature_repeat_days", defaultValue: "Set repeat days (weekdays/weekends)"))
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: String(localized: "onboarding.get_started.feature_full_screen", defaultValue: "Wait 5s for full screen"))
OnboardingFeatureRow(icon: "speaker.wave.2", text: String(localized: "onboarding.get_started.feature_noise", defaultValue: "Tap Noise to play sounds"))
}
.padding(.top, Design.Spacing.medium)

View File

@ -33,23 +33,23 @@ struct OnboardingPermissionsPage: View {
.symbolEffect(.pulse, options: .repeating)
}
Text("Alarms that actually work")
Text(String(localized: "onboarding.permissions.title", defaultValue: "Alarms that actually work"))
.typography(.heroBold)
.foregroundStyle(AppTextColors.primary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.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)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, Design.Spacing.xxLarge)
VStack(alignment: .leading, spacing: Design.Spacing.small) {
OnboardingFeatureRow(icon: "moon.zzz.fill", text: "Cuts through Do Not Disturb")
OnboardingFeatureRow(icon: "lock.iphone", text: "Shows countdown on Lock Screen")
OnboardingFeatureRow(icon: "iphone.badge.play", text: "Works when app is closed")
OnboardingFeatureRow(icon: "moon.zzz.fill", text: String(localized: "onboarding.permissions.feature_dnd", defaultValue: "Cuts through Do Not Disturb"))
OnboardingFeatureRow(icon: "lock.iphone", text: String(localized: "onboarding.permissions.feature_lock_screen", defaultValue: "Shows countdown on Lock Screen"))
OnboardingFeatureRow(icon: "iphone.badge.play", text: String(localized: "onboarding.permissions.feature_app_closed", defaultValue: "Works when app is closed"))
}
.padding(.top, Design.Spacing.medium)
@ -76,7 +76,7 @@ struct OnboardingPermissionsPage: View {
HStack(spacing: Design.Spacing.small) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 24))
Text("Alarms enabled!")
Text(String(localized: "onboarding.permissions.enabled", defaultValue: "Alarms enabled!"))
}
.foregroundStyle(AppStatus.success)
.typography(.bodyEmphasis)
@ -89,7 +89,7 @@ struct OnboardingPermissionsPage: View {
} label: {
HStack {
Image(systemName: "alarm.fill")
Text("Enable Alarms")
Text(String(localized: "onboarding.permissions.enable_alarms", defaultValue: "Enable Alarms"))
}
.typography(.bodyEmphasis)
.foregroundStyle(.white)
@ -106,7 +106,7 @@ struct OnboardingPermissionsPage: View {
private var keepAwakeSection: some View {
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)
.foregroundStyle(AppTextColors.tertiary)
.multilineTextAlignment(.center)
@ -116,7 +116,11 @@ struct OnboardingPermissionsPage: View {
} label: {
HStack(spacing: Design.Spacing.small) {
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)
.foregroundStyle(keepAwakeEnabled ? AppStatus.success : AppTextColors.secondary)

View File

@ -20,26 +20,26 @@ struct OnboardingWelcomePage: View {
}
.padding(.bottom, Design.Spacing.medium)
Text("The Noise Clock")
Text(String(localized: "onboarding.welcome.title", defaultValue: "The Noise Clock"))
.typography(.heroBold)
.foregroundStyle(AppTextColors.primary)
Text("Your beautiful bedside companion")
Text(String(localized: "onboarding.welcome.subtitle", defaultValue: "Your beautiful bedside companion"))
.typography(.title3)
.foregroundStyle(AppTextColors.secondary)
VStack(spacing: Design.Spacing.medium) {
OnboardingFeatureRow(
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(
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(
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)

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 {
switch self {
case .none: return "None"
case .spring: return "Spring"
case .bounce: return "Bounce"
case .glitch: return "Glitch"
case .none:
return String(localized: "digit_animation.none.display_name", defaultValue: "None")
case .spring:
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
public var displayName: String {
switch self {
case .all: return "All"
case .colored: return "Colored"
case .ambient: return "Ambient"
case .nature: return "Nature"
case .mechanical: return "Mechanical"
case .alarm: return "Alarm Sounds"
case .all:
return String(localized: "sound_category.all.display_name", defaultValue: "All")
case .colored:
return String(localized: "sound_category.colored.display_name", defaultValue: "Colored")
case .ambient:
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
public var description: String {
switch self {
case .all: return "All available sounds"
case .colored: return "Synthetic noise signals for focus, sleep, and relaxation"
case .ambient: return "General ambient sounds"
case .nature: return "Natural environmental sounds"
case .mechanical: return "Mechanical and electronic sounds"
case .alarm: return "Wake-up and notification alarm sounds"
case .all:
return String(localized: "sound_category.all.description", defaultValue: "All available sounds")
case .colored:
return String(localized: "sound_category.colored.description", defaultValue: "Synthetic noise signals for focus, sleep, and relaxation")
case .ambient:
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)
VStack(spacing: Design.Spacing.small) {
Text("Keep Awake for Alarms")
Text(String(localized: "keep_awake.prompt.title", defaultValue: "Keep Awake for Alarms"))
.typography(.title2Bold)
.foregroundStyle(AppTextColors.primary)
.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)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
@ -34,7 +34,7 @@ struct KeepAwakePrompt: View {
VStack(spacing: Design.Spacing.medium) {
Button(action: onEnable) {
Text("Enable Keep Awake")
Text(String(localized: "keep_awake.prompt.enable", defaultValue: "Enable Keep Awake"))
.font(Typography.headingEmphasis.font)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
@ -44,7 +44,7 @@ struct KeepAwakePrompt: View {
}
Button(action: onDismiss) {
Text("Not Now")
Text(String(localized: "common.not_now", defaultValue: "Not Now"))
.font(Typography.bodyEmphasis.font)
.foregroundStyle(AppTextColors.secondary)
.frame(maxWidth: .infinity)