Add comprehensive i18n localization for KMM and iOS
KMM (Android/Shared): - Add strings.xml with 200+ localized strings - Add translation files for es, fr, de, pt languages - Update all screens to use stringResource() for i18n - Add Accept-Language header to API client for all platforms iOS: - Add L10n.swift helper with type-safe string accessors - Add Localizable.xcstrings with translations for all 5 languages - Update all SwiftUI views to use L10n.* for localized strings - Localize Auth, Residence, Task, Contractor, Document, and Profile views Supported languages: English, Spanish, French, German, Portuguese 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -29,19 +29,19 @@ struct ContractorDetailView: View {
|
||||
viewModel.loadContractorDetail(id: contractorId)
|
||||
}}) {
|
||||
Label(
|
||||
contractor.isFavorite ? "Remove from Favorites" : "Add to Favorites",
|
||||
contractor.isFavorite ? L10n.Contractors.removeFromFavorites : L10n.Contractors.addToFavorites,
|
||||
systemImage: contractor.isFavorite ? "star.slash" : "star"
|
||||
)
|
||||
}
|
||||
|
||||
Button(action: { showingEditSheet = true }) {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
Label(L10n.Common.edit, systemImage: "pencil")
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button(role: .destructive, action: { showingDeleteAlert = true }) {
|
||||
Label("Delete", systemImage: "trash")
|
||||
Label(L10n.Common.delete, systemImage: "trash")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
@@ -59,13 +59,13 @@ struct ContractorDetailView: View {
|
||||
)
|
||||
.presentationDetents([.large])
|
||||
}
|
||||
.alert("Delete Contractor", isPresented: $showingDeleteAlert) {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Delete", role: .destructive) {
|
||||
.alert(L10n.Contractors.deleteConfirm, isPresented: $showingDeleteAlert) {
|
||||
Button(L10n.Common.cancel, role: .cancel) {}
|
||||
Button(L10n.Common.delete, role: .destructive) {
|
||||
deleteContractor()
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to delete this contractor? This action cannot be undone.")
|
||||
Text(L10n.Contractors.deleteMessage)
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.loadContractorDetail(id: contractorId)
|
||||
@@ -200,7 +200,7 @@ struct ContractorDetailView: View {
|
||||
}
|
||||
|
||||
if contractor.taskCount > 0 {
|
||||
Text("\(contractor.taskCount) completed tasks")
|
||||
Text(String(format: L10n.Contractors.completedTasks, contractor.taskCount))
|
||||
.font(.callout)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -232,7 +232,7 @@ struct ContractorDetailView: View {
|
||||
if let phone = phone, !phone.isEmpty {
|
||||
QuickActionButton(
|
||||
icon: "phone.fill",
|
||||
label: "Call",
|
||||
label: L10n.Contractors.callAction,
|
||||
color: Color.appPrimary
|
||||
) {
|
||||
if let url = URL(string: "tel:\(phone.replacingOccurrences(of: " ", with: ""))") {
|
||||
@@ -247,7 +247,7 @@ struct ContractorDetailView: View {
|
||||
if let email = email, !email.isEmpty {
|
||||
QuickActionButton(
|
||||
icon: "envelope.fill",
|
||||
label: "Email",
|
||||
label: L10n.Contractors.emailAction,
|
||||
color: Color.appSecondary
|
||||
) {
|
||||
if let url = URL(string: "mailto:\(email)") {
|
||||
@@ -262,7 +262,7 @@ struct ContractorDetailView: View {
|
||||
if let website = website, !website.isEmpty {
|
||||
QuickActionButton(
|
||||
icon: "safari.fill",
|
||||
label: "Website",
|
||||
label: L10n.Contractors.websiteAction,
|
||||
color: Color.appAccent
|
||||
) {
|
||||
var urlString = website
|
||||
@@ -283,7 +283,7 @@ struct ContractorDetailView: View {
|
||||
if hasAddress {
|
||||
QuickActionButton(
|
||||
icon: "map.fill",
|
||||
label: "Directions",
|
||||
label: L10n.Contractors.directionsAction,
|
||||
color: Color.appError
|
||||
) {
|
||||
let address = [
|
||||
@@ -310,7 +310,7 @@ struct ContractorDetailView: View {
|
||||
let hasWebsite = contractor.website != nil && !contractor.website!.isEmpty
|
||||
|
||||
if hasPhone || hasEmail || hasWebsite {
|
||||
DetailSection(title: "Contact Information") {
|
||||
DetailSection(title: L10n.Contractors.contactInfoSection) {
|
||||
phoneContactRow(phone: contractor.phone)
|
||||
emailContactRow(email: contractor.email)
|
||||
websiteContactRow(website: contractor.website)
|
||||
@@ -323,7 +323,7 @@ struct ContractorDetailView: View {
|
||||
if let phone = phone, !phone.isEmpty {
|
||||
ContactDetailRow(
|
||||
icon: "phone.fill",
|
||||
label: "Phone",
|
||||
label: L10n.Contractors.phoneLabel,
|
||||
value: phone,
|
||||
iconColor: Color.appPrimary
|
||||
) {
|
||||
@@ -339,7 +339,7 @@ struct ContractorDetailView: View {
|
||||
if let email = email, !email.isEmpty {
|
||||
ContactDetailRow(
|
||||
icon: "envelope.fill",
|
||||
label: "Email",
|
||||
label: L10n.Contractors.emailLabel,
|
||||
value: email,
|
||||
iconColor: Color.appSecondary
|
||||
) {
|
||||
@@ -355,7 +355,7 @@ struct ContractorDetailView: View {
|
||||
if let website = website, !website.isEmpty {
|
||||
ContactDetailRow(
|
||||
icon: "safari.fill",
|
||||
label: "Website",
|
||||
label: L10n.Contractors.websiteLabel,
|
||||
value: website,
|
||||
iconColor: Color.appAccent
|
||||
) {
|
||||
@@ -385,7 +385,7 @@ struct ContractorDetailView: View {
|
||||
].compactMap { $0 }.filter { !$0.isEmpty }
|
||||
|
||||
if !addressComponents.isEmpty {
|
||||
DetailSection(title: "Address") {
|
||||
DetailSection(title: L10n.Contractors.addressSection) {
|
||||
addressButton(contractor: contractor, addressComponents: addressComponents)
|
||||
}
|
||||
}
|
||||
@@ -414,7 +414,7 @@ struct ContractorDetailView: View {
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
||||
Text("Location")
|
||||
Text(L10n.Contractors.locationLabel)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
|
||||
@@ -439,14 +439,14 @@ struct ContractorDetailView: View {
|
||||
@ViewBuilder
|
||||
private func residenceSection(residenceId: Int32?) -> some View {
|
||||
if let residenceId = residenceId {
|
||||
DetailSection(title: "Associated Property") {
|
||||
DetailSection(title: L10n.Contractors.associatedPropertySection) {
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
Image(systemName: "house.fill")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
||||
Text("Property")
|
||||
Text(L10n.Contractors.propertyLabel)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
|
||||
@@ -473,7 +473,7 @@ struct ContractorDetailView: View {
|
||||
@ViewBuilder
|
||||
private func notesSection(notes: String?) -> some View {
|
||||
if let notes = notes, !notes.isEmpty {
|
||||
DetailSection(title: "Notes") {
|
||||
DetailSection(title: L10n.Contractors.notesSection) {
|
||||
HStack(alignment: .top, spacing: AppSpacing.sm) {
|
||||
Image(systemName: "note.text")
|
||||
.foregroundColor(Color.appAccent)
|
||||
@@ -493,12 +493,12 @@ struct ContractorDetailView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func statisticsSection(contractor: Contractor) -> some View {
|
||||
DetailSection(title: "Statistics") {
|
||||
DetailSection(title: L10n.Contractors.statisticsSection) {
|
||||
HStack(spacing: AppSpacing.lg) {
|
||||
StatCard(
|
||||
icon: "checkmark.circle.fill",
|
||||
value: "\(contractor.taskCount)",
|
||||
label: "Tasks Completed",
|
||||
label: L10n.Contractors.tasksCompletedLabel,
|
||||
color: Color.appPrimary
|
||||
)
|
||||
|
||||
@@ -506,7 +506,7 @@ struct ContractorDetailView: View {
|
||||
StatCard(
|
||||
icon: "star.fill",
|
||||
value: String(format: "%.1f", rating.doubleValue),
|
||||
label: "Average Rating",
|
||||
label: L10n.Contractors.averageRatingLabel,
|
||||
color: Color.appAccent
|
||||
)
|
||||
}
|
||||
@@ -519,7 +519,7 @@ struct ContractorDetailView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func metadataSection(contractor: Contractor) -> some View {
|
||||
DetailSection(title: "Info") {
|
||||
DetailSection(title: L10n.Contractors.infoSection) {
|
||||
VStack(spacing: 0) {
|
||||
createdByRow(createdBy: contractor.createdBy)
|
||||
memberSinceRow(createdAt: contractor.createdAt)
|
||||
@@ -536,7 +536,7 @@ struct ContractorDetailView: View {
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
||||
Text("Added By")
|
||||
Text(L10n.Contractors.addedByLabel)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
|
||||
@@ -562,7 +562,7 @@ struct ContractorDetailView: View {
|
||||
.frame(width: 20)
|
||||
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
||||
Text("Member Since")
|
||||
Text(L10n.Contractors.memberSinceLabel)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "person")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
TextField("Name", text: $name)
|
||||
TextField(L10n.Contractors.nameLabel, text: $name)
|
||||
.focused($focusedField, equals: .name)
|
||||
}
|
||||
|
||||
@@ -65,13 +65,13 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "building.2")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
TextField("Company", text: $company)
|
||||
TextField(L10n.Contractors.companyLabel, text: $company)
|
||||
.focused($focusedField, equals: .company)
|
||||
}
|
||||
} header: {
|
||||
Text("Basic Information")
|
||||
Text(L10n.Contractors.basicInfoSection)
|
||||
} footer: {
|
||||
Text("Required: Name")
|
||||
Text(L10n.Contractors.basicInfoFooter)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
@@ -84,7 +84,7 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "house")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
Text(selectedResidenceName ?? "Personal (No Residence)")
|
||||
Text(selectedResidenceName ?? L10n.Contractors.personalNoResidence)
|
||||
.foregroundColor(selectedResidenceName == nil ? Color.appTextSecondary.opacity(0.7) : Color.appTextPrimary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.down")
|
||||
@@ -93,11 +93,11 @@ struct ContractorFormSheet: View {
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Residence (Optional)")
|
||||
Text(L10n.Contractors.residenceSection)
|
||||
} footer: {
|
||||
Text(selectedResidenceId == nil
|
||||
? "Only you will see this contractor"
|
||||
: "All users of \(selectedResidenceName ?? "") will see this contractor")
|
||||
? L10n.Contractors.residenceFooterPersonal
|
||||
: String(format: L10n.Contractors.residenceFooterShared, selectedResidenceName ?? ""))
|
||||
.font(.caption)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
@@ -108,7 +108,7 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "phone.fill")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
TextField("Phone", text: $phone)
|
||||
TextField(L10n.Contractors.phoneLabel, text: $phone)
|
||||
.keyboardType(.phonePad)
|
||||
.focused($focusedField, equals: .phone)
|
||||
}
|
||||
@@ -117,7 +117,7 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "envelope.fill")
|
||||
.foregroundColor(Color.appAccent)
|
||||
.frame(width: 24)
|
||||
TextField("Email", text: $email)
|
||||
TextField(L10n.Contractors.emailLabel, text: $email)
|
||||
.keyboardType(.emailAddress)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
@@ -128,14 +128,14 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "globe")
|
||||
.foregroundColor(Color.appAccent)
|
||||
.frame(width: 24)
|
||||
TextField("Website", text: $website)
|
||||
TextField(L10n.Contractors.websiteLabel, text: $website)
|
||||
.keyboardType(.URL)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused($focusedField, equals: .website)
|
||||
}
|
||||
} header: {
|
||||
Text("Contact Information")
|
||||
Text(L10n.Contractors.contactInfoSection)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -147,7 +147,7 @@ struct ContractorFormSheet: View {
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
if selectedSpecialtyIds.isEmpty {
|
||||
Text("Select Specialties")
|
||||
Text(L10n.Contractors.selectSpecialtiesPlaceholder)
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.5))
|
||||
} else {
|
||||
let selectedNames = specialties
|
||||
@@ -164,7 +164,7 @@ struct ContractorFormSheet: View {
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Specialties")
|
||||
Text(L10n.Contractors.specialtiesSection)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -174,7 +174,7 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "location.fill")
|
||||
.foregroundColor(Color.appError)
|
||||
.frame(width: 24)
|
||||
TextField("Street Address", text: $streetAddress)
|
||||
TextField(L10n.Contractors.streetAddressLabel, text: $streetAddress)
|
||||
.focused($focusedField, equals: .streetAddress)
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "building.2.crop.circle")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
TextField("City", text: $city)
|
||||
TextField(L10n.Contractors.cityLabel, text: $city)
|
||||
.focused($focusedField, equals: .city)
|
||||
}
|
||||
|
||||
@@ -191,20 +191,20 @@ struct ContractorFormSheet: View {
|
||||
Image(systemName: "map")
|
||||
.foregroundColor(Color.appAccent)
|
||||
.frame(width: 24)
|
||||
TextField("State", text: $stateProvince)
|
||||
TextField(L10n.Contractors.stateLabel, text: $stateProvince)
|
||||
.focused($focusedField, equals: .stateProvince)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.frame(height: 24)
|
||||
|
||||
TextField("ZIP", text: $postalCode)
|
||||
TextField(L10n.Contractors.zipLabel, text: $postalCode)
|
||||
.keyboardType(.numberPad)
|
||||
.focused($focusedField, equals: .postalCode)
|
||||
.frame(maxWidth: 100)
|
||||
}
|
||||
} header: {
|
||||
Text("Address")
|
||||
Text(L10n.Contractors.addressSection)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -221,9 +221,9 @@ struct ContractorFormSheet: View {
|
||||
.focused($focusedField, equals: .notes)
|
||||
}
|
||||
} header: {
|
||||
Text("Notes")
|
||||
Text(L10n.Contractors.notesSection)
|
||||
} footer: {
|
||||
Text("Private notes about this contractor")
|
||||
Text(L10n.Contractors.notesFooter)
|
||||
.font(.caption)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
@@ -231,7 +231,7 @@ struct ContractorFormSheet: View {
|
||||
// Favorite
|
||||
Section {
|
||||
Toggle(isOn: $isFavorite) {
|
||||
Label("Mark as Favorite", systemImage: "star.fill")
|
||||
Label(L10n.Contractors.favoriteLabel, systemImage: "star.fill")
|
||||
.foregroundColor(isFavorite ? Color.appAccent : Color.appTextPrimary)
|
||||
}
|
||||
.tint(Color.appAccent)
|
||||
@@ -255,11 +255,11 @@ struct ContractorFormSheet: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle(contractor == nil ? "Add Contractor" : "Edit Contractor")
|
||||
.navigationTitle(contractor == nil ? L10n.Contractors.addTitle : L10n.Contractors.editTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
Button(L10n.Common.cancel) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@@ -269,7 +269,7 @@ struct ContractorFormSheet: View {
|
||||
if viewModel.isCreating || viewModel.isUpdating {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text(contractor == nil ? "Add" : "Save")
|
||||
Text(contractor == nil ? L10n.Contractors.addButton : L10n.Common.save)
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
@@ -305,7 +305,7 @@ struct ContractorFormSheet: View {
|
||||
showingResidencePicker = false
|
||||
}) {
|
||||
HStack {
|
||||
Text("Personal (No Residence)")
|
||||
Text(L10n.Contractors.personalNoResidence)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Spacer()
|
||||
if selectedResidenceId == nil {
|
||||
@@ -348,11 +348,11 @@ struct ContractorFormSheet: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Select Residence")
|
||||
.navigationTitle(L10n.Contractors.selectResidence)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
Button(L10n.Common.done) {
|
||||
showingResidencePicker = false
|
||||
}
|
||||
}
|
||||
@@ -390,16 +390,16 @@ struct ContractorFormSheet: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Select Specialties")
|
||||
.navigationTitle(L10n.Contractors.selectSpecialties)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Clear") {
|
||||
Button(L10n.Contractors.clearAction) {
|
||||
selectedSpecialtyIds.removeAll()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
Button(L10n.Common.done) {
|
||||
showingSpecialtyPicker = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ struct ContractorsListView: View {
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Search Bar
|
||||
SearchBar(text: $searchText, placeholder: "Search contractors...")
|
||||
SearchBar(text: $searchText, placeholder: L10n.Contractors.searchPlaceholder)
|
||||
.padding(.horizontal, AppSpacing.md)
|
||||
.padding(.top, AppSpacing.sm)
|
||||
|
||||
@@ -56,7 +56,7 @@ struct ContractorsListView: View {
|
||||
HStack(spacing: AppSpacing.xs) {
|
||||
if showFavoritesOnly {
|
||||
FilterChip(
|
||||
title: "Favorites",
|
||||
title: L10n.Contractors.favorites,
|
||||
icon: "star.fill",
|
||||
onRemove: { showFavoritesOnly = false }
|
||||
)
|
||||
@@ -106,7 +106,7 @@ struct ContractorsListView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Contractors")
|
||||
.navigationTitle(L10n.Contractors.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@@ -124,7 +124,7 @@ struct ContractorsListView: View {
|
||||
Button(action: {
|
||||
selectedSpecialty = nil
|
||||
}) {
|
||||
Label("All Specialties", systemImage: selectedSpecialty == nil ? "checkmark" : "")
|
||||
Label(L10n.Contractors.allSpecialties, systemImage: selectedSpecialty == nil ? "checkmark" : "")
|
||||
}
|
||||
|
||||
Divider()
|
||||
@@ -302,12 +302,12 @@ struct EmptyContractorsView: View {
|
||||
.font(.system(size: 64))
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.7))
|
||||
|
||||
Text(hasFilters ? "No contractors found" : "No contractors yet")
|
||||
Text(hasFilters ? L10n.Contractors.emptyFiltered : L10n.Contractors.emptyTitle)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
|
||||
if !hasFilters {
|
||||
Text("Add your first contractor to get started")
|
||||
Text(L10n.Contractors.emptyNoFilters)
|
||||
.font(.callout)
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.7))
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ struct DocumentsTabContent: View {
|
||||
if !subscriptionCache.shouldShowUpgradePrompt(currentCount: 0, limitKey: "documents") {
|
||||
EmptyStateView(
|
||||
icon: "doc",
|
||||
title: "No documents found",
|
||||
message: "Add documents related to your residence"
|
||||
title: L10n.Documents.noDocumentsFound,
|
||||
message: L10n.Documents.noDocumentsMessage
|
||||
)
|
||||
} else {
|
||||
UpgradeFeatureView(
|
||||
|
||||
@@ -30,8 +30,8 @@ struct WarrantiesTabContent: View {
|
||||
if !subscriptionCache.shouldShowUpgradePrompt(currentCount: 0, limitKey: "documents") {
|
||||
EmptyStateView(
|
||||
icon: "doc.text.viewfinder",
|
||||
title: "No warranties found",
|
||||
message: "Add warranties to track coverage periods"
|
||||
title: L10n.Documents.noWarrantiesFound,
|
||||
message: L10n.Documents.noWarrantiesMessage
|
||||
)
|
||||
} else {
|
||||
UpgradeFeatureView(
|
||||
|
||||
@@ -29,10 +29,10 @@ struct WarrantyCard: View {
|
||||
}
|
||||
|
||||
// Fallback to client-side calculation (shouldn't happen with updated backend)
|
||||
if !document.isActive { return "Inactive" }
|
||||
if daysUntilExpiration < 0 { return "Expired" }
|
||||
if daysUntilExpiration < 30 { return "Expiring soon" }
|
||||
return "Active"
|
||||
if !document.isActive { return L10n.Documents.inactive }
|
||||
if daysUntilExpiration < 0 { return L10n.Documents.expired }
|
||||
if daysUntilExpiration < 30 { return L10n.Documents.expiringSoon }
|
||||
return L10n.Documents.activeStatus
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -68,10 +68,10 @@ struct WarrantyCard: View {
|
||||
// Details
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Provider")
|
||||
Text(L10n.Documents.provider)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
Text(document.provider ?? "N/A")
|
||||
Text(document.provider ?? L10n.Documents.na)
|
||||
.font(.body)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
@@ -80,10 +80,10 @@ struct WarrantyCard: View {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text("Expires")
|
||||
Text(L10n.Documents.expires)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
Text(DateUtils.formatDateMedium(document.endDate) ?? "N/A")
|
||||
Text(DateUtils.formatDateMedium(document.endDate) ?? L10n.Documents.na)
|
||||
.font(.body)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
@@ -91,7 +91,7 @@ struct WarrantyCard: View {
|
||||
}
|
||||
|
||||
if document.isActive && daysUntilExpiration >= 0 {
|
||||
Text("\(daysUntilExpiration) days remaining")
|
||||
Text(String(format: L10n.Documents.daysRemainingCount, daysUntilExpiration))
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(statusColor)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ struct DocumentDetailView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if viewModel.documentDetailState is DocumentDetailStateLoading {
|
||||
ProgressView("Loading document...")
|
||||
ProgressView(L10n.Documents.loadingDocument)
|
||||
} else if let successState = viewModel.documentDetailState as? DocumentDetailStateSuccess {
|
||||
documentDetailContent(document: successState.document)
|
||||
} else if let errorState = viewModel.documentDetailState as? DocumentDetailStateError {
|
||||
@@ -24,7 +24,7 @@ struct DocumentDetailView: View {
|
||||
.foregroundColor(.red)
|
||||
Text(errorState.message)
|
||||
.foregroundColor(.secondary)
|
||||
Button("Retry") {
|
||||
Button(L10n.Common.retry) {
|
||||
viewModel.loadDocumentDetail(id: documentId)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
@@ -33,7 +33,7 @@ struct DocumentDetailView: View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Document Details")
|
||||
.navigationTitle(L10n.Documents.documentDetails)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.background(
|
||||
// Hidden NavigationLink for programmatic navigation to edit
|
||||
@@ -55,13 +55,13 @@ struct DocumentDetailView: View {
|
||||
Button {
|
||||
navigateToEdit = true
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
Label(L10n.Common.edit, systemImage: "pencil")
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
showDeleteAlert = true
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
Label(L10n.Common.delete, systemImage: "trash")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
@@ -72,13 +72,13 @@ struct DocumentDetailView: View {
|
||||
.onAppear {
|
||||
viewModel.loadDocumentDetail(id: documentId)
|
||||
}
|
||||
.alert("Delete Document", isPresented: $showDeleteAlert) {
|
||||
Button("Cancel", role: .cancel) { }
|
||||
Button("Delete", role: .destructive) {
|
||||
.alert(L10n.Documents.deleteDocument, isPresented: $showDeleteAlert) {
|
||||
Button(L10n.Common.cancel, role: .cancel) { }
|
||||
Button(L10n.Common.delete, role: .destructive) {
|
||||
viewModel.deleteDocument(id: documentId)
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to delete this document? This action cannot be undone.")
|
||||
Text(L10n.Documents.deleteConfirmMessage)
|
||||
}
|
||||
.onReceive(viewModel.$deleteState) { newState in
|
||||
if newState is DeleteStateSuccess {
|
||||
@@ -112,17 +112,17 @@ struct DocumentDetailView: View {
|
||||
|
||||
// Basic Information
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
sectionHeader("Basic Information")
|
||||
sectionHeader(L10n.Documents.basicInformation)
|
||||
|
||||
detailRow(label: "Title", value: document.title)
|
||||
detailRow(label: "Type", value: DocumentTypeHelper.displayName(for: document.documentType))
|
||||
detailRow(label: L10n.Documents.titleField, value: document.title)
|
||||
detailRow(label: L10n.Documents.documentType, value: DocumentTypeHelper.displayName(for: document.documentType))
|
||||
|
||||
if let category = document.category {
|
||||
detailRow(label: "Category", value: DocumentCategoryHelper.displayName(for: category))
|
||||
detailRow(label: L10n.Documents.category, value: DocumentCategoryHelper.displayName(for: category))
|
||||
}
|
||||
|
||||
if let description = document.description_, !description.isEmpty {
|
||||
detailRow(label: "Description", value: description)
|
||||
detailRow(label: L10n.Documents.description, value: description)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -135,22 +135,22 @@ struct DocumentDetailView: View {
|
||||
if document.itemName != nil || document.modelNumber != nil ||
|
||||
document.serialNumber != nil || document.provider != nil {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
sectionHeader("Item Details")
|
||||
sectionHeader(L10n.Documents.itemDetails)
|
||||
|
||||
if let itemName = document.itemName {
|
||||
detailRow(label: "Item Name", value: itemName)
|
||||
detailRow(label: L10n.Documents.itemName, value: itemName)
|
||||
}
|
||||
if let modelNumber = document.modelNumber {
|
||||
detailRow(label: "Model Number", value: modelNumber)
|
||||
detailRow(label: L10n.Documents.modelNumber, value: modelNumber)
|
||||
}
|
||||
if let serialNumber = document.serialNumber {
|
||||
detailRow(label: "Serial Number", value: serialNumber)
|
||||
detailRow(label: L10n.Documents.serialNumber, value: serialNumber)
|
||||
}
|
||||
if let provider = document.provider {
|
||||
detailRow(label: "Provider", value: provider)
|
||||
detailRow(label: L10n.Documents.provider, value: provider)
|
||||
}
|
||||
if let providerContact = document.providerContact {
|
||||
detailRow(label: "Provider Contact", value: providerContact)
|
||||
detailRow(label: L10n.Documents.providerContact, value: providerContact)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -163,16 +163,16 @@ struct DocumentDetailView: View {
|
||||
if document.claimPhone != nil || document.claimEmail != nil ||
|
||||
document.claimWebsite != nil {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
sectionHeader("Claim Information")
|
||||
sectionHeader(L10n.Documents.claimInformation)
|
||||
|
||||
if let claimPhone = document.claimPhone {
|
||||
detailRow(label: "Claim Phone", value: claimPhone)
|
||||
detailRow(label: L10n.Documents.claimPhone, value: claimPhone)
|
||||
}
|
||||
if let claimEmail = document.claimEmail {
|
||||
detailRow(label: "Claim Email", value: claimEmail)
|
||||
detailRow(label: L10n.Documents.claimEmail, value: claimEmail)
|
||||
}
|
||||
if let claimWebsite = document.claimWebsite {
|
||||
detailRow(label: "Claim Website", value: claimWebsite)
|
||||
detailRow(label: L10n.Documents.claimWebsite, value: claimWebsite)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -185,16 +185,16 @@ struct DocumentDetailView: View {
|
||||
if document.purchaseDate != nil || document.startDate != nil ||
|
||||
document.endDate != nil {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
sectionHeader("Important Dates")
|
||||
sectionHeader(L10n.Documents.importantDates)
|
||||
|
||||
if let purchaseDate = document.purchaseDate {
|
||||
detailRow(label: "Purchase Date", value: DateUtils.formatDateMedium(purchaseDate))
|
||||
detailRow(label: L10n.Documents.purchaseDate, value: DateUtils.formatDateMedium(purchaseDate))
|
||||
}
|
||||
if let startDate = document.startDate {
|
||||
detailRow(label: "Start Date", value: DateUtils.formatDateMedium(startDate))
|
||||
detailRow(label: L10n.Documents.startDate, value: DateUtils.formatDateMedium(startDate))
|
||||
}
|
||||
if let endDate = document.endDate {
|
||||
detailRow(label: "End Date", value: DateUtils.formatDateMedium(endDate))
|
||||
detailRow(label: L10n.Documents.endDate, value: DateUtils.formatDateMedium(endDate))
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -207,7 +207,7 @@ struct DocumentDetailView: View {
|
||||
// Images
|
||||
if !document.images.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
sectionHeader("Images (\(document.images.count))")
|
||||
sectionHeader("\(L10n.Documents.images) (\(document.images.count))")
|
||||
|
||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 8) {
|
||||
ForEach(Array(document.images.prefix(6).enumerated()), id: \.element.id) { index, image in
|
||||
@@ -256,16 +256,16 @@ struct DocumentDetailView: View {
|
||||
|
||||
// Associations
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
sectionHeader("Associations")
|
||||
sectionHeader(L10n.Documents.associations)
|
||||
|
||||
if let residenceAddress = document.residenceAddress {
|
||||
detailRow(label: "Residence", value: residenceAddress)
|
||||
detailRow(label: L10n.Documents.residence, value: residenceAddress)
|
||||
}
|
||||
if let contractorName = document.contractorName {
|
||||
detailRow(label: "Contractor", value: contractorName)
|
||||
detailRow(label: L10n.Documents.contractor, value: contractorName)
|
||||
}
|
||||
if let contractorPhone = document.contractorPhone {
|
||||
detailRow(label: "Contractor Phone", value: contractorPhone)
|
||||
detailRow(label: L10n.Documents.contractorPhone, value: contractorPhone)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -276,13 +276,13 @@ struct DocumentDetailView: View {
|
||||
// Additional Information
|
||||
if document.tags != nil || document.notes != nil {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
sectionHeader("Additional Information")
|
||||
sectionHeader(L10n.Documents.additionalInformation)
|
||||
|
||||
if let tags = document.tags, !tags.isEmpty {
|
||||
detailRow(label: "Tags", value: tags)
|
||||
detailRow(label: L10n.Documents.tags, value: tags)
|
||||
}
|
||||
if let notes = document.notes, !notes.isEmpty {
|
||||
detailRow(label: "Notes", value: notes)
|
||||
detailRow(label: L10n.Documents.notes, value: notes)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -294,13 +294,13 @@ struct DocumentDetailView: View {
|
||||
// File Information
|
||||
if document.fileUrl != nil {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
sectionHeader("Attached File")
|
||||
sectionHeader(L10n.Documents.attachedFile)
|
||||
|
||||
if let fileType = document.fileType {
|
||||
detailRow(label: "File Type", value: fileType)
|
||||
detailRow(label: L10n.Documents.fileType, value: fileType)
|
||||
}
|
||||
if let fileSize = document.fileSize {
|
||||
detailRow(label: "File Size", value: formatFileSize(bytes: Int(fileSize)))
|
||||
detailRow(label: L10n.Documents.fileSize, value: formatFileSize(bytes: Int(fileSize)))
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
@@ -308,7 +308,7 @@ struct DocumentDetailView: View {
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
Text("Download File")
|
||||
Text(L10n.Documents.downloadFile)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
@@ -325,16 +325,16 @@ struct DocumentDetailView: View {
|
||||
|
||||
// Metadata
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
sectionHeader("Metadata")
|
||||
sectionHeader(L10n.Documents.metadata)
|
||||
|
||||
if let uploadedBy = document.uploadedByUsername {
|
||||
detailRow(label: "Uploaded By", value: uploadedBy)
|
||||
detailRow(label: L10n.Documents.uploadedBy, value: uploadedBy)
|
||||
}
|
||||
if let createdAt = document.createdAt {
|
||||
detailRow(label: "Created", value: DateUtils.formatDateTime(createdAt))
|
||||
detailRow(label: L10n.Documents.created, value: DateUtils.formatDateTime(createdAt))
|
||||
}
|
||||
if let updatedAt = document.updatedAt {
|
||||
detailRow(label: "Updated", value: DateUtils.formatDateTime(updatedAt))
|
||||
detailRow(label: L10n.Documents.updated, value: DateUtils.formatDateTime(updatedAt))
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -355,7 +355,7 @@ struct DocumentDetailView: View {
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Status")
|
||||
Text(L10n.Documents.status)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(statusText)
|
||||
@@ -368,7 +368,7 @@ struct DocumentDetailView: View {
|
||||
|
||||
if document.isActive && daysUntilExpiration >= 0 {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text("Days Remaining")
|
||||
Text(L10n.Documents.daysRemaining)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(daysUntilExpiration)")
|
||||
@@ -421,13 +421,13 @@ struct DocumentDetailView: View {
|
||||
|
||||
private func getStatusText(isActive: Bool, daysUntilExpiration: Int32) -> String {
|
||||
if !isActive {
|
||||
return "Inactive"
|
||||
return L10n.Documents.inactive
|
||||
} else if daysUntilExpiration < 0 {
|
||||
return "Expired"
|
||||
return L10n.Documents.expired
|
||||
} else if daysUntilExpiration < 30 {
|
||||
return "Expiring Soon"
|
||||
return L10n.Documents.expiringSoon
|
||||
} else {
|
||||
return "Active"
|
||||
return L10n.Documents.activeStatus
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,47 +115,47 @@ struct DocumentFormView: View {
|
||||
private var warrantySection: some View {
|
||||
if isWarranty {
|
||||
Section {
|
||||
TextField("Item Name", text: $itemName)
|
||||
TextField(L10n.Documents.itemName, text: $itemName)
|
||||
if !itemNameError.isEmpty {
|
||||
Text(itemNameError)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
|
||||
TextField("Model Number (optional)", text: $modelNumber)
|
||||
TextField("Serial Number (optional)", text: $serialNumber)
|
||||
TextField(L10n.Documents.modelNumberOptional, text: $modelNumber)
|
||||
TextField(L10n.Documents.serialNumberOptional, text: $serialNumber)
|
||||
|
||||
TextField("Provider/Company", text: $provider)
|
||||
TextField(L10n.Documents.providerCompany, text: $provider)
|
||||
if !providerError.isEmpty {
|
||||
Text(providerError)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
|
||||
TextField("Provider Contact (optional)", text: $providerContact)
|
||||
TextField(L10n.Documents.providerContactOptional, text: $providerContact)
|
||||
} header: {
|
||||
Text("Warranty Details")
|
||||
Text(L10n.Documents.warrantyDetails)
|
||||
} footer: {
|
||||
Text("Required for warranties: Item Name and Provider")
|
||||
Text(L10n.Documents.requiredWarrantyFields)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section("Warranty Claims") {
|
||||
TextField("Claim Phone (optional)", text: $claimPhone)
|
||||
Section(L10n.Documents.warrantyClaims) {
|
||||
TextField(L10n.Documents.claimPhoneOptional, text: $claimPhone)
|
||||
.keyboardType(.phonePad)
|
||||
TextField("Claim Email (optional)", text: $claimEmail)
|
||||
TextField(L10n.Documents.claimEmailOptional, text: $claimEmail)
|
||||
.keyboardType(.emailAddress)
|
||||
TextField("Claim Website (optional)", text: $claimWebsite)
|
||||
TextField(L10n.Documents.claimWebsiteOptional, text: $claimWebsite)
|
||||
.keyboardType(.URL)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section("Warranty Dates") {
|
||||
TextField("Purchase Date (YYYY-MM-DD)", text: $purchaseDate)
|
||||
TextField("Warranty Start Date (YYYY-MM-DD)", text: $startDate)
|
||||
TextField("Warranty End Date (YYYY-MM-DD)", text: $endDate)
|
||||
Section(L10n.Documents.warrantyDates) {
|
||||
TextField(L10n.Documents.purchaseDate, text: $purchaseDate)
|
||||
TextField(L10n.Documents.startDate, text: $startDate)
|
||||
TextField(L10n.Documents.endDate, text: $endDate)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
}
|
||||
@@ -164,7 +164,7 @@ struct DocumentFormView: View {
|
||||
@ViewBuilder
|
||||
private var photosSections: some View {
|
||||
if isEditMode && !existingImages.isEmpty {
|
||||
Section("Existing Photos") {
|
||||
Section(L10n.Documents.existingPhotos) {
|
||||
ForEach(existingImages, id: \.id) { image in
|
||||
AsyncImage(url: URL(string: image.imageUrl)) { phase in
|
||||
switch phase {
|
||||
@@ -187,19 +187,19 @@ struct DocumentFormView: View {
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
}
|
||||
|
||||
Section("Photos") {
|
||||
Section(L10n.Documents.photos) {
|
||||
PhotosPicker(selection: $selectedPhotoItems, maxSelectionCount: isEditMode ? 10 : 5, matching: .images) {
|
||||
Label("Select from Library", systemImage: "photo")
|
||||
Label(L10n.Documents.selectFromLibrary, systemImage: "photo")
|
||||
}
|
||||
|
||||
Button {
|
||||
showCamera = true
|
||||
} label: {
|
||||
Label("Take Photo", systemImage: "camera")
|
||||
Label(L10n.Documents.takePhoto, systemImage: "camera")
|
||||
}
|
||||
|
||||
if !selectedImages.isEmpty {
|
||||
Text("\(selectedImages.count) photo(s) selected")
|
||||
Text(String(format: L10n.Documents.photosSelected, selectedImages.count))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
@@ -215,11 +215,11 @@ struct DocumentFormView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle(isEditMode ? (isWarranty ? "Edit Warranty" : "Edit Document") : (isWarranty ? "Add Warranty" : "Add Document"))
|
||||
.navigationTitle(isEditMode ? (isWarranty ? L10n.Documents.editWarranty : L10n.Documents.editDocument) : (isWarranty ? L10n.Documents.addWarranty : L10n.Documents.addDocument))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
Button(L10n.Common.cancel) {
|
||||
if isEditMode {
|
||||
dismiss()
|
||||
} else {
|
||||
@@ -229,7 +229,7 @@ struct DocumentFormView: View {
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(isEditMode ? "Update" : "Save") {
|
||||
Button(isEditMode ? L10n.Documents.update : L10n.Common.save) {
|
||||
submitForm()
|
||||
}
|
||||
.disabled(!canSave || isProcessing)
|
||||
@@ -264,8 +264,8 @@ struct DocumentFormView: View {
|
||||
existingImages = doc.images
|
||||
}
|
||||
}
|
||||
.alert("Error", isPresented: $showAlert) {
|
||||
Button("OK", role: .cancel) {}
|
||||
.alert(L10n.Common.error, isPresented: $showAlert) {
|
||||
Button(L10n.Common.ok, role: .cancel) {}
|
||||
} message: {
|
||||
Text(alertMessage)
|
||||
}
|
||||
@@ -280,8 +280,8 @@ struct DocumentFormView: View {
|
||||
if residenceViewModel.isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Picker("Select Property", selection: $selectedResidenceId) {
|
||||
Text("Select Property").tag(nil as Int?)
|
||||
Picker(L10n.Documents.selectProperty, selection: $selectedResidenceId) {
|
||||
Text(L10n.Documents.selectProperty).tag(nil as Int?)
|
||||
ForEach(residencesArray, id: \.id) { residence in
|
||||
Text(residence.name).tag(residence.id as Int?)
|
||||
}
|
||||
@@ -294,9 +294,9 @@ struct DocumentFormView: View {
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Property")
|
||||
Text(L10n.Documents.property)
|
||||
} footer: {
|
||||
Text("Required")
|
||||
Text(L10n.Documents.required)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
@@ -307,16 +307,16 @@ struct DocumentFormView: View {
|
||||
Section {
|
||||
if isEditMode {
|
||||
HStack {
|
||||
Text("Document Type")
|
||||
Text(L10n.Documents.documentType)
|
||||
Spacer()
|
||||
Text(DocumentTypeHelper.displayName(for: selectedDocumentType))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Text("Document type cannot be changed")
|
||||
Text(L10n.Documents.documentTypeCannotChange)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Picker("Document Type", selection: $selectedDocumentType) {
|
||||
Picker(L10n.Documents.documentType, selection: $selectedDocumentType) {
|
||||
ForEach(DocumentTypeHelper.allTypes, id: \.self) { type in
|
||||
Text(DocumentTypeHelper.displayName(for: type)).tag(type)
|
||||
}
|
||||
@@ -327,19 +327,19 @@ struct DocumentFormView: View {
|
||||
|
||||
// Basic Information
|
||||
Section {
|
||||
TextField("Title", text: $title)
|
||||
TextField(L10n.Documents.titleField, text: $title)
|
||||
if !titleError.isEmpty {
|
||||
Text(titleError)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
|
||||
TextField("Description (optional)", text: $description, axis: .vertical)
|
||||
TextField(L10n.Documents.descriptionOptional, text: $description, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
} header: {
|
||||
Text("Basic Information")
|
||||
Text(L10n.Documents.basicInformation)
|
||||
} footer: {
|
||||
Text("Required: Title")
|
||||
Text(L10n.Documents.requiredTitle)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
@@ -350,9 +350,9 @@ struct DocumentFormView: View {
|
||||
|
||||
// Category
|
||||
if isWarranty || ["inspection", "manual", "receipt"].contains(selectedDocumentType) {
|
||||
Section("Category") {
|
||||
Picker("Category (optional)", selection: $selectedCategory) {
|
||||
Text("None").tag(nil as String?)
|
||||
Section(L10n.Documents.category) {
|
||||
Picker(L10n.Documents.categoryOptional, selection: $selectedCategory) {
|
||||
Text(L10n.Documents.none).tag(nil as String?)
|
||||
ForEach(DocumentCategoryHelper.allCategories, id: \.self) { category in
|
||||
Text(DocumentCategoryHelper.displayName(for: category)).tag(category as String?)
|
||||
}
|
||||
@@ -362,10 +362,10 @@ struct DocumentFormView: View {
|
||||
}
|
||||
|
||||
// Additional Information
|
||||
Section("Additional Information") {
|
||||
TextField("Tags (optional)", text: $tags)
|
||||
Section(L10n.Documents.additionalInformation) {
|
||||
TextField(L10n.Documents.tagsOptional, text: $tags)
|
||||
.textInputAutocapitalization(.never)
|
||||
TextField("Notes (optional)", text: $notes, axis: .vertical)
|
||||
TextField(L10n.Documents.notesOptional, text: $notes, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
@@ -373,7 +373,7 @@ struct DocumentFormView: View {
|
||||
// Active Status (Edit mode only)
|
||||
if isEditMode {
|
||||
Section {
|
||||
Toggle("Active", isOn: $isActive)
|
||||
Toggle(L10n.Documents.active, isOn: $isActive)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
}
|
||||
@@ -391,22 +391,22 @@ struct DocumentFormView: View {
|
||||
residenceError = ""
|
||||
|
||||
if title.isEmpty {
|
||||
titleError = "Title is required"
|
||||
titleError = L10n.Documents.titleRequired
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if needsResidenceSelection && selectedResidenceId == nil {
|
||||
residenceError = "Property is required"
|
||||
residenceError = L10n.Documents.propertyRequired
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if isWarranty {
|
||||
if itemName.isEmpty {
|
||||
itemNameError = "Item name is required for warranties"
|
||||
itemNameError = L10n.Documents.itemNameRequired
|
||||
isValid = false
|
||||
}
|
||||
if provider.isEmpty {
|
||||
providerError = "Provider is required for warranties"
|
||||
providerError = L10n.Documents.providerRequired
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
@@ -416,7 +416,7 @@ struct DocumentFormView: View {
|
||||
|
||||
private func submitForm() {
|
||||
guard validateForm() else {
|
||||
alertMessage = "Please fill in all required fields"
|
||||
alertMessage = L10n.Documents.fillRequiredFields
|
||||
showAlert = true
|
||||
return
|
||||
}
|
||||
@@ -432,7 +432,7 @@ struct DocumentFormView: View {
|
||||
actualResidenceId = Int32(selectedId)
|
||||
} else {
|
||||
isProcessing = false
|
||||
alertMessage = "No residence selected"
|
||||
alertMessage = L10n.Documents.noResidenceSelected
|
||||
showAlert = true
|
||||
return
|
||||
}
|
||||
@@ -441,7 +441,7 @@ struct DocumentFormView: View {
|
||||
// Update document
|
||||
guard let docId = doc.id else {
|
||||
isProcessing = false
|
||||
alertMessage = "Document ID is missing"
|
||||
alertMessage = L10n.Documents.documentIdMissing
|
||||
showAlert = true
|
||||
return
|
||||
}
|
||||
@@ -474,7 +474,7 @@ struct DocumentFormView: View {
|
||||
documentViewModel.loadDocuments(residenceId: residenceId)
|
||||
dismiss()
|
||||
} else {
|
||||
alertMessage = error ?? "Failed to update document"
|
||||
alertMessage = error ?? L10n.Documents.failedToUpdate
|
||||
showAlert = true
|
||||
}
|
||||
}
|
||||
@@ -509,7 +509,7 @@ struct DocumentFormView: View {
|
||||
documentViewModel.loadDocuments(residenceId: actualResidenceId)
|
||||
isPresented = false
|
||||
} else {
|
||||
alertMessage = error ?? "Failed to create document"
|
||||
alertMessage = error ?? L10n.Documents.failedToCreate
|
||||
showAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ struct DocumentsWarrantiesView: View {
|
||||
VStack(spacing: 0) {
|
||||
// Segmented Control for Tabs
|
||||
Picker("", selection: $selectedTab) {
|
||||
Label("Warranties", systemImage: "checkmark.shield")
|
||||
Label(L10n.Documents.warranties, systemImage: "checkmark.shield")
|
||||
.tag(DocumentWarrantyTab.warranties)
|
||||
Label("Documents", systemImage: "doc.text")
|
||||
Label(L10n.Documents.documents, systemImage: "doc.text")
|
||||
.tag(DocumentWarrantyTab.documents)
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
@@ -50,7 +50,7 @@ struct DocumentsWarrantiesView: View {
|
||||
.padding(.top, AppSpacing.sm)
|
||||
|
||||
// Search Bar
|
||||
SearchBar(text: $searchText, placeholder: "Search...")
|
||||
SearchBar(text: $searchText, placeholder: L10n.Documents.searchPlaceholder)
|
||||
.padding(.horizontal, AppSpacing.md)
|
||||
.padding(.top, AppSpacing.xs)
|
||||
|
||||
@@ -60,7 +60,7 @@ struct DocumentsWarrantiesView: View {
|
||||
HStack(spacing: AppSpacing.xs) {
|
||||
if selectedTab == .warranties && showActiveOnly {
|
||||
FilterChip(
|
||||
title: "Active Only",
|
||||
title: L10n.Documents.activeOnly,
|
||||
icon: "checkmark.circle.fill",
|
||||
onRemove: { showActiveOnly = false }
|
||||
)
|
||||
@@ -99,7 +99,7 @@ struct DocumentsWarrantiesView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Documents & Warranties")
|
||||
.navigationTitle(L10n.Documents.documentsAndWarranties)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@@ -122,7 +122,7 @@ struct DocumentsWarrantiesView: View {
|
||||
selectedCategory = nil
|
||||
loadWarranties()
|
||||
}) {
|
||||
Label("All Categories", systemImage: selectedCategory == nil ? "checkmark" : "")
|
||||
Label(L10n.Documents.allCategories, systemImage: selectedCategory == nil ? "checkmark" : "")
|
||||
}
|
||||
|
||||
Divider()
|
||||
@@ -140,7 +140,7 @@ struct DocumentsWarrantiesView: View {
|
||||
selectedDocType = nil
|
||||
loadDocuments()
|
||||
}) {
|
||||
Label("All Types", systemImage: selectedDocType == nil ? "checkmark" : "")
|
||||
Label(L10n.Documents.allTypes, systemImage: selectedDocType == nil ? "checkmark" : "")
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
577
iosApp/iosApp/Helpers/L10n.swift
Normal file
577
iosApp/iosApp/Helpers/L10n.swift
Normal file
@@ -0,0 +1,577 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - L10n - Type-safe Localized Strings
|
||||
// Usage: L10n.auth.loginTitle
|
||||
|
||||
enum L10n {
|
||||
// MARK: - App
|
||||
enum App {
|
||||
static var name: String { String(localized: "app_name") }
|
||||
static var tagline: String { String(localized: "app_tagline") }
|
||||
}
|
||||
|
||||
// MARK: - Auth
|
||||
enum Auth {
|
||||
// Login
|
||||
static var loginTitle: String { String(localized: "auth_login_title") }
|
||||
static var loginSubtitle: String { String(localized: "auth_login_subtitle") }
|
||||
static var loginUsernameLabel: String { String(localized: "auth_login_username_label") }
|
||||
static var loginPasswordLabel: String { String(localized: "auth_login_password_label") }
|
||||
static var loginButton: String { String(localized: "auth_login_button") }
|
||||
static var forgotPassword: String { String(localized: "auth_forgot_password") }
|
||||
static var noAccount: String { String(localized: "auth_no_account") }
|
||||
static var loginFailed: String { String(localized: "auth_login_failed") }
|
||||
static var showPassword: String { String(localized: "auth_show_password") }
|
||||
static var hidePassword: String { String(localized: "auth_hide_password") }
|
||||
static var welcomeBack: String { String(localized: "auth_welcome_back") }
|
||||
static var signInSubtitle: String { String(localized: "auth_sign_in_subtitle") }
|
||||
static var enterEmail: String { String(localized: "auth_enter_email") }
|
||||
static var enterPassword: String { String(localized: "auth_enter_password") }
|
||||
static var signingIn: String { String(localized: "auth_signing_in") }
|
||||
static var orDivider: String { String(localized: "auth_or_divider") }
|
||||
static var signingInWithApple: String { String(localized: "auth_signing_in_with_apple") }
|
||||
static var dontHaveAccount: String { String(localized: "auth_dont_have_account") }
|
||||
static var signUp: String { String(localized: "auth_sign_up") }
|
||||
|
||||
// Register
|
||||
static var registerTitle: String { String(localized: "auth_register_title") }
|
||||
static var registerSubtitle: String { String(localized: "auth_register_subtitle") }
|
||||
static var registerFirstName: String { String(localized: "auth_register_first_name") }
|
||||
static var registerLastName: String { String(localized: "auth_register_last_name") }
|
||||
static var registerEmail: String { String(localized: "auth_register_email") }
|
||||
static var registerUsername: String { String(localized: "auth_register_username") }
|
||||
static var registerPassword: String { String(localized: "auth_register_password") }
|
||||
static var registerConfirmPassword: String { String(localized: "auth_register_confirm_password") }
|
||||
static var registerButton: String { String(localized: "auth_register_button") }
|
||||
static var haveAccount: String { String(localized: "auth_have_account") }
|
||||
static var passwordsDontMatch: String { String(localized: "auth_passwords_dont_match") }
|
||||
static var joinCasera: String { String(localized: "auth_join_casera") }
|
||||
static var startManaging: String { String(localized: "auth_start_managing") }
|
||||
static var accountInfo: String { String(localized: "auth_account_info") }
|
||||
static var security: String { String(localized: "auth_security") }
|
||||
static var passwordSuggestion: String { String(localized: "auth_password_suggestion") }
|
||||
|
||||
// Verify Email
|
||||
static var verifyTitle: String { String(localized: "auth_verify_title") }
|
||||
static var verifySubtitle: String { String(localized: "auth_verify_subtitle") }
|
||||
static var verifyCodeLabel: String { String(localized: "auth_verify_code_label") }
|
||||
static var verifyButton: String { String(localized: "auth_verify_button") }
|
||||
static var verifyResend: String { String(localized: "auth_verify_resend") }
|
||||
static var verifySuccess: String { String(localized: "auth_verify_success") }
|
||||
static var verifyYourEmail: String { String(localized: "auth_verify_your_email") }
|
||||
static var verifyMustVerify: String { String(localized: "auth_verify_must_verify") }
|
||||
static var verifyCheckInbox: String { String(localized: "auth_verify_check_inbox") }
|
||||
static var verifyCodeMustBe6: String { String(localized: "auth_verify_code_must_be_6") }
|
||||
static var verifyEmailButton: String { String(localized: "auth_verify_email_button") }
|
||||
static var verifyHelpText: String { String(localized: "auth_verify_help_text") }
|
||||
static var logout: String { String(localized: "auth_logout") }
|
||||
|
||||
// Forgot Password
|
||||
static var forgotTitle: String { String(localized: "auth_forgot_title") }
|
||||
static var forgotSubtitle: String { String(localized: "auth_forgot_subtitle") }
|
||||
static var forgotEmailLabel: String { String(localized: "auth_forgot_email_label") }
|
||||
static var forgotButton: String { String(localized: "auth_forgot_button") }
|
||||
static var forgotSuccess: String { String(localized: "auth_forgot_success") }
|
||||
|
||||
// Reset Password
|
||||
static var resetTitle: String { String(localized: "auth_reset_title") }
|
||||
static var resetSubtitle: String { String(localized: "auth_reset_subtitle") }
|
||||
static var resetCodeLabel: String { String(localized: "auth_reset_code_label") }
|
||||
static var resetNewPassword: String { String(localized: "auth_reset_new_password") }
|
||||
static var resetConfirmPassword: String { String(localized: "auth_reset_confirm_password") }
|
||||
static var resetButton: String { String(localized: "auth_reset_button") }
|
||||
static var resetSuccess: String { String(localized: "auth_reset_success") }
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
enum Properties {
|
||||
static var title: String { String(localized: "properties_title") }
|
||||
static var emptyTitle: String { String(localized: "properties_empty_title") }
|
||||
static var emptySubtitle: String { String(localized: "properties_empty_subtitle") }
|
||||
static var addButton: String { String(localized: "properties_add_button") }
|
||||
static var addTitle: String { String(localized: "properties_add_title") }
|
||||
static var editTitle: String { String(localized: "properties_edit_title") }
|
||||
static var nameLabel: String { String(localized: "properties_name_label") }
|
||||
static var addressLabel: String { String(localized: "properties_address_label") }
|
||||
static var typeLabel: String { String(localized: "properties_type_label") }
|
||||
static var notesLabel: String { String(localized: "properties_notes_label") }
|
||||
static var deleteConfirm: String { String(localized: "properties_delete_confirm") }
|
||||
static var deleted: String { String(localized: "properties_deleted") }
|
||||
}
|
||||
|
||||
// MARK: - Residences
|
||||
enum Residences {
|
||||
// List
|
||||
static var title: String { String(localized: "residences_title") }
|
||||
static var yourProperties: String { String(localized: "residences_your_properties") }
|
||||
static var property: String { String(localized: "residences_property") }
|
||||
static var properties: String { String(localized: "residences_properties") }
|
||||
|
||||
// Form
|
||||
static var addTitle: String { String(localized: "residences_add_title") }
|
||||
static var editTitle: String { String(localized: "residences_edit_title") }
|
||||
static var propertyName: String { String(localized: "residences_property_name") }
|
||||
static var propertyType: String { String(localized: "residences_property_type") }
|
||||
static var selectType: String { String(localized: "residences_select_type") }
|
||||
static var propertyDetails: String { String(localized: "residences_property_details") }
|
||||
static var requiredName: String { String(localized: "residences_required_name") }
|
||||
static var nameRequired: String { String(localized: "residences_name_required") }
|
||||
|
||||
// Address
|
||||
static var address: String { String(localized: "residences_address") }
|
||||
static var streetAddress: String { String(localized: "residences_street_address") }
|
||||
static var apartmentUnit: String { String(localized: "residences_apartment_unit") }
|
||||
static var city: String { String(localized: "residences_city") }
|
||||
static var stateProvince: String { String(localized: "residences_state_province") }
|
||||
static var postalCode: String { String(localized: "residences_postal_code") }
|
||||
static var country: String { String(localized: "residences_country") }
|
||||
|
||||
// Features
|
||||
static var propertyFeatures: String { String(localized: "residences_property_features") }
|
||||
static var bedrooms: String { String(localized: "residences_bedrooms") }
|
||||
static var bathrooms: String { String(localized: "residences_bathrooms") }
|
||||
static var squareFootage: String { String(localized: "residences_square_footage") }
|
||||
static var lotSize: String { String(localized: "residences_lot_size") }
|
||||
static var yearBuilt: String { String(localized: "residences_year_built") }
|
||||
|
||||
// Additional
|
||||
static var additionalDetails: String { String(localized: "residences_additional_details") }
|
||||
static var description: String { String(localized: "residences_description") }
|
||||
static var primaryResidence: String { String(localized: "residences_primary_residence") }
|
||||
|
||||
// Detail
|
||||
static var loadingResidence: String { String(localized: "residences_loading_residence") }
|
||||
static var generateReport: String { String(localized: "residences_generate_report") }
|
||||
static var generateReportMessage: String { String(localized: "residences_generate_report_message") }
|
||||
static var maintenanceReport: String { String(localized: "residences_maintenance_report") }
|
||||
static var contractors: String { String(localized: "residences_contractors") }
|
||||
static var noContractors: String { String(localized: "residences_no_contractors") }
|
||||
static var addContractorsPrompt: String { String(localized: "residences_add_contractors_prompt") }
|
||||
static var loadingTasks: String { String(localized: "residences_loading_tasks") }
|
||||
static var errorLoadingTasks: String { String(localized: "residences_error_loading_tasks") }
|
||||
static var generate: String { String(localized: "residences_generate") }
|
||||
|
||||
// Delete
|
||||
static var deleteTitle: String { String(localized: "residences_delete_title") }
|
||||
static var deleteConfirmMessage: String { String(localized: "residences_delete_confirm_message") }
|
||||
|
||||
// Join
|
||||
static var joinTitle: String { String(localized: "residences_join_title") }
|
||||
static var shareCode: String { String(localized: "residences_share_code") }
|
||||
static var enterShareCode: String { String(localized: "residences_enter_share_code") }
|
||||
static var shareCodeFooter: String { String(localized: "residences_share_code_footer") }
|
||||
static var joinButton: String { String(localized: "residences_join_button") }
|
||||
static var shareCodeMust6: String { String(localized: "residences_share_code_must_6") }
|
||||
|
||||
// Manage Users
|
||||
static var manageUsers: String { String(localized: "residences_manage_users") }
|
||||
static var users: String { String(localized: "residences_users") }
|
||||
}
|
||||
|
||||
// MARK: - Tasks
|
||||
enum Tasks {
|
||||
static var title: String { String(localized: "tasks_title") }
|
||||
static var emptyTitle: String { String(localized: "tasks_empty_title") }
|
||||
static var emptySubtitle: String { String(localized: "tasks_empty_subtitle") }
|
||||
static var addButton: String { String(localized: "tasks_add_button") }
|
||||
static var addTitle: String { String(localized: "tasks_add_title") }
|
||||
static var editTitle: String { String(localized: "tasks_edit_title") }
|
||||
static var titleLabel: String { String(localized: "tasks_title_label") }
|
||||
static var descriptionLabel: String { String(localized: "tasks_description_label") }
|
||||
static var deleteConfirm: String { String(localized: "tasks_delete_confirm") }
|
||||
|
||||
// Form Fields
|
||||
static var property: String { String(localized: "tasks_property") }
|
||||
static var selectProperty: String { String(localized: "tasks_select_property") }
|
||||
static var required: String { String(localized: "tasks_required") }
|
||||
static var taskDetails: String { String(localized: "tasks_task_details") }
|
||||
static var titleRequired: String { String(localized: "tasks_title_required") }
|
||||
static var descriptionOptional: String { String(localized: "tasks_description_optional") }
|
||||
static var category: String { String(localized: "tasks_category") }
|
||||
static var selectCategory: String { String(localized: "tasks_select_category") }
|
||||
static var scheduling: String { String(localized: "tasks_scheduling") }
|
||||
static var frequency: String { String(localized: "tasks_frequency") }
|
||||
static var selectFrequency: String { String(localized: "tasks_select_frequency") }
|
||||
static var customInterval: String { String(localized: "tasks_custom_interval") }
|
||||
static var dueDate: String { String(localized: "tasks_due_date") }
|
||||
static var priorityAndStatus: String { String(localized: "tasks_priority_status") }
|
||||
static var priority: String { String(localized: "tasks_priority") }
|
||||
static var selectPriority: String { String(localized: "tasks_select_priority") }
|
||||
static var status: String { String(localized: "tasks_status") }
|
||||
static var selectStatus: String { String(localized: "tasks_select_status") }
|
||||
static var bothRequired: String { String(localized: "tasks_both_required") }
|
||||
static var cost: String { String(localized: "tasks_cost") }
|
||||
static var estimatedCost: String { String(localized: "tasks_estimated_cost") }
|
||||
static var loading: String { String(localized: "tasks_loading") }
|
||||
|
||||
// All Tasks View
|
||||
static var allTasks: String { String(localized: "tasks_all_tasks") }
|
||||
static var noTasksYet: String { String(localized: "tasks_no_tasks_yet") }
|
||||
static var createFirst: String { String(localized: "tasks_create_first") }
|
||||
static var addPropertyFirst: String { String(localized: "tasks_add_property_first") }
|
||||
static var archiveTask: String { String(localized: "tasks_archive_task") }
|
||||
static var deleteTask: String { String(localized: "tasks_delete_task") }
|
||||
static var archiveConfirm: String { String(localized: "tasks_archive_confirm") }
|
||||
static var archive: String { String(localized: "tasks_archive") }
|
||||
static var noTasks: String { String(localized: "tasks_no_tasks") }
|
||||
|
||||
// Complete Task View
|
||||
static var completeTask: String { String(localized: "tasks_complete_task") }
|
||||
static var selectContractor: String { String(localized: "tasks_select_contractor") }
|
||||
static var contractorOptional: String { String(localized: "tasks_contractor_optional") }
|
||||
static var contractorHelper: String { String(localized: "tasks_contractor_helper") }
|
||||
static var completedBy: String { String(localized: "tasks_completed_by") }
|
||||
static var yourName: String { String(localized: "tasks_your_name") }
|
||||
static var actualCost: String { String(localized: "tasks_actual_cost") }
|
||||
static var optionalInfo: String { String(localized: "tasks_optional_info") }
|
||||
static var optionalDetails: String { String(localized: "tasks_optional_details") }
|
||||
static var notes: String { String(localized: "tasks_notes") }
|
||||
static var optionalNotes: String { String(localized: "tasks_optional_notes") }
|
||||
static var qualityRating: String { String(localized: "tasks_quality_rating") }
|
||||
static var rateQuality: String { String(localized: "tasks_rate_quality") }
|
||||
static var photos: String { String(localized: "tasks_photos") }
|
||||
static var addPhotos: String { String(localized: "tasks_add_photos") }
|
||||
static var takePhoto: String { String(localized: "tasks_take_photo") }
|
||||
static var library: String { String(localized: "tasks_library") }
|
||||
static var none: String { String(localized: "tasks_none") }
|
||||
static var noneManual: String { String(localized: "tasks_none_manual") }
|
||||
static var enterManually: String { String(localized: "tasks_enter_manually") }
|
||||
static var error: String { String(localized: "tasks_error") }
|
||||
|
||||
// Completion History
|
||||
static var completionHistory: String { String(localized: "tasks_completion_history") }
|
||||
static var loadingCompletions: String { String(localized: "tasks_loading_completions") }
|
||||
static var failedToLoad: String { String(localized: "tasks_failed_to_load") }
|
||||
static var noCompletionsYet: String { String(localized: "tasks_no_completions_yet") }
|
||||
static var notCompleted: String { String(localized: "tasks_not_completed") }
|
||||
static var completions: String { String(localized: "tasks_completions") }
|
||||
static var completion: String { String(localized: "tasks_completion") }
|
||||
static var completedByName: String { String(localized: "tasks_completed_by_name") }
|
||||
static var viewPhotos: String { String(localized: "tasks_view_photos") }
|
||||
|
||||
// Task Card Actions
|
||||
static var inProgress: String { String(localized: "tasks_in_progress") }
|
||||
static var complete: String { String(localized: "tasks_complete") }
|
||||
static var edit: String { String(localized: "tasks_edit") }
|
||||
static var cancel: String { String(localized: "tasks_cancel") }
|
||||
static var restore: String { String(localized: "tasks_restore") }
|
||||
static var unarchive: String { String(localized: "tasks_unarchive") }
|
||||
}
|
||||
|
||||
// MARK: - Contractors
|
||||
enum Contractors {
|
||||
static var title: String { String(localized: "contractors_title") }
|
||||
static var emptyTitle: String { String(localized: "contractors_empty_title") }
|
||||
static var emptySubtitle: String { String(localized: "contractors_empty_subtitle") }
|
||||
static var emptyNoFilters: String { String(localized: "contractors_empty_no_filters") }
|
||||
static var emptyFiltered: String { String(localized: "contractors_empty_filtered") }
|
||||
static var addButton: String { String(localized: "contractors_add_button") }
|
||||
static var addTitle: String { String(localized: "contractors_add_title") }
|
||||
static var editTitle: String { String(localized: "contractors_edit_title") }
|
||||
|
||||
// Search & Filter
|
||||
static var searchPlaceholder: String { String(localized: "contractors_search_placeholder") }
|
||||
static var favorites: String { String(localized: "contractors_favorites") }
|
||||
static var allSpecialties: String { String(localized: "contractors_all_specialties") }
|
||||
|
||||
// Form Fields
|
||||
static var nameLabel: String { String(localized: "contractors_name_label") }
|
||||
static var companyLabel: String { String(localized: "contractors_company_label") }
|
||||
static var phoneLabel: String { String(localized: "contractors_phone_label") }
|
||||
static var emailLabel: String { String(localized: "contractors_email_label") }
|
||||
static var websiteLabel: String { String(localized: "contractors_website_label") }
|
||||
static var streetAddressLabel: String { String(localized: "contractors_street_address_label") }
|
||||
static var cityLabel: String { String(localized: "contractors_city_label") }
|
||||
static var stateLabel: String { String(localized: "contractors_state_label") }
|
||||
static var zipLabel: String { String(localized: "contractors_zip_label") }
|
||||
static var notesLabel: String { String(localized: "contractors_notes_label") }
|
||||
|
||||
// Form Sections
|
||||
static var basicInfoSection: String { String(localized: "contractors_basic_info_section") }
|
||||
static var basicInfoFooter: String { String(localized: "contractors_basic_info_footer") }
|
||||
static var residenceSection: String { String(localized: "contractors_residence_section") }
|
||||
static var residenceFooterPersonal: String { String(localized: "contractors_residence_footer_personal") }
|
||||
static var residenceFooterShared: String { String(localized: "contractors_residence_footer_shared") }
|
||||
static var contactInfoSection: String { String(localized: "contractors_contact_info_section") }
|
||||
static var specialtiesSection: String { String(localized: "contractors_specialties_section") }
|
||||
static var addressSection: String { String(localized: "contractors_address_section") }
|
||||
static var notesSection: String { String(localized: "contractors_notes_section") }
|
||||
static var notesFooter: String { String(localized: "contractors_notes_footer") }
|
||||
static var favoriteLabel: String { String(localized: "contractors_favorite_label") }
|
||||
|
||||
// Detail View
|
||||
static var removeFromFavorites: String { String(localized: "contractors_remove_from_favorites") }
|
||||
static var addToFavorites: String { String(localized: "contractors_add_to_favorites") }
|
||||
static var deleteConfirm: String { String(localized: "contractors_delete_confirm") }
|
||||
static var deleteMessage: String { String(localized: "contractors_delete_message") }
|
||||
static var completedTasks: String { String(localized: "contractors_completed_tasks") }
|
||||
static var callAction: String { String(localized: "contractors_call_action") }
|
||||
static var emailAction: String { String(localized: "contractors_email_action") }
|
||||
static var websiteAction: String { String(localized: "contractors_website_action") }
|
||||
static var directionsAction: String { String(localized: "contractors_directions_action") }
|
||||
static var locationLabel: String { String(localized: "contractors_location_label") }
|
||||
static var propertyLabel: String { String(localized: "contractors_property_label") }
|
||||
static var associatedPropertySection: String { String(localized: "contractors_associated_property_section") }
|
||||
static var statisticsSection: String { String(localized: "contractors_statistics_section") }
|
||||
static var tasksCompletedLabel: String { String(localized: "contractors_tasks_completed_label") }
|
||||
static var averageRatingLabel: String { String(localized: "contractors_average_rating_label") }
|
||||
static var infoSection: String { String(localized: "contractors_info_section") }
|
||||
static var addedByLabel: String { String(localized: "contractors_added_by_label") }
|
||||
static var memberSinceLabel: String { String(localized: "contractors_member_since_label") }
|
||||
|
||||
// Picker Sheets
|
||||
static var selectResidence: String { String(localized: "contractors_select_residence") }
|
||||
static var personalNoResidence: String { String(localized: "contractors_personal_no_residence") }
|
||||
static var selectSpecialties: String { String(localized: "contractors_select_specialties") }
|
||||
static var selectSpecialtiesPlaceholder: String { String(localized: "contractors_select_specialties_placeholder") }
|
||||
static var clearAction: String { String(localized: "contractors_clear_action") }
|
||||
|
||||
// Stats
|
||||
static var tasksLabel: String { String(localized: "contractors_tasks_label") }
|
||||
}
|
||||
|
||||
// MARK: - Documents
|
||||
enum Documents {
|
||||
// Main view
|
||||
static var title: String { String(localized: "documents_title") }
|
||||
static var documentsAndWarranties: String { String(localized: "documents_and_warranties") }
|
||||
static var warranties: String { String(localized: "documents_warranties") }
|
||||
static var documents: String { String(localized: "documents_documents") }
|
||||
static var searchPlaceholder: String { String(localized: "documents_search_placeholder") }
|
||||
|
||||
// Filters
|
||||
static var activeOnly: String { String(localized: "documents_active_only") }
|
||||
static var allCategories: String { String(localized: "documents_all_categories") }
|
||||
static var allTypes: String { String(localized: "documents_all_types") }
|
||||
|
||||
// Empty states
|
||||
static var emptyTitle: String { String(localized: "documents_empty_title") }
|
||||
static var emptySubtitle: String { String(localized: "documents_empty_subtitle") }
|
||||
static var noDocumentsFound: String { String(localized: "documents_no_documents_found") }
|
||||
static var noDocumentsMessage: String { String(localized: "documents_no_documents_message") }
|
||||
static var noWarrantiesFound: String { String(localized: "documents_no_warranties_found") }
|
||||
static var noWarrantiesMessage: String { String(localized: "documents_no_warranties_message") }
|
||||
|
||||
// Actions
|
||||
static var addButton: String { String(localized: "documents_add_button") }
|
||||
|
||||
// Form titles
|
||||
static var addWarranty: String { String(localized: "documents_add_warranty") }
|
||||
static var addDocument: String { String(localized: "documents_add_document") }
|
||||
static var editWarranty: String { String(localized: "documents_edit_warranty") }
|
||||
static var editDocument: String { String(localized: "documents_edit_document") }
|
||||
|
||||
// Form sections
|
||||
static var property: String { String(localized: "documents_property") }
|
||||
static var selectProperty: String { String(localized: "documents_select_property") }
|
||||
static var documentType: String { String(localized: "documents_document_type") }
|
||||
static var documentTypeCannotChange: String { String(localized: "documents_document_type_cannot_change") }
|
||||
static var basicInformation: String { String(localized: "documents_basic_information") }
|
||||
static var warrantyDetails: String { String(localized: "documents_warranty_details") }
|
||||
static var warrantyClaims: String { String(localized: "documents_warranty_claims") }
|
||||
static var warrantyDates: String { String(localized: "documents_warranty_dates") }
|
||||
static var category: String { String(localized: "documents_category") }
|
||||
static var additionalInformation: String { String(localized: "documents_additional_information") }
|
||||
static var active: String { String(localized: "documents_active") }
|
||||
static var photos: String { String(localized: "documents_photos") }
|
||||
static var existingPhotos: String { String(localized: "documents_existing_photos") }
|
||||
|
||||
// Form fields
|
||||
static var titleField: String { String(localized: "documents_title_field") }
|
||||
static var description: String { String(localized: "documents_description") }
|
||||
static var descriptionOptional: String { String(localized: "documents_description_optional") }
|
||||
static var itemName: String { String(localized: "documents_item_name") }
|
||||
static var modelNumber: String { String(localized: "documents_model_number") }
|
||||
static var modelNumberOptional: String { String(localized: "documents_model_number_optional") }
|
||||
static var serialNumber: String { String(localized: "documents_serial_number") }
|
||||
static var serialNumberOptional: String { String(localized: "documents_serial_number_optional") }
|
||||
static var provider: String { String(localized: "documents_provider") }
|
||||
static var providerCompany: String { String(localized: "documents_provider_company") }
|
||||
static var providerContact: String { String(localized: "documents_provider_contact") }
|
||||
static var providerContactOptional: String { String(localized: "documents_provider_contact_optional") }
|
||||
static var claimPhone: String { String(localized: "documents_claim_phone") }
|
||||
static var claimPhoneOptional: String { String(localized: "documents_claim_phone_optional") }
|
||||
static var claimEmail: String { String(localized: "documents_claim_email") }
|
||||
static var claimEmailOptional: String { String(localized: "documents_claim_email_optional") }
|
||||
static var claimWebsite: String { String(localized: "documents_claim_website") }
|
||||
static var claimWebsiteOptional: String { String(localized: "documents_claim_website_optional") }
|
||||
static var purchaseDate: String { String(localized: "documents_purchase_date") }
|
||||
static var startDate: String { String(localized: "documents_start_date") }
|
||||
static var endDate: String { String(localized: "documents_end_date") }
|
||||
static var tags: String { String(localized: "documents_tags") }
|
||||
static var tagsOptional: String { String(localized: "documents_tags_optional") }
|
||||
static var notes: String { String(localized: "documents_notes") }
|
||||
static var notesOptional: String { String(localized: "documents_notes_optional") }
|
||||
static var categoryOptional: String { String(localized: "documents_category_optional") }
|
||||
|
||||
// Form footer messages
|
||||
static var required: String { String(localized: "documents_required") }
|
||||
static var requiredTitle: String { String(localized: "documents_required_title") }
|
||||
static var requiredWarrantyFields: String { String(localized: "documents_required_warranty_fields") }
|
||||
|
||||
// Photo actions
|
||||
static var selectFromLibrary: String { String(localized: "documents_select_from_library") }
|
||||
static var takePhoto: String { String(localized: "documents_take_photo") }
|
||||
static var photosSelected: String { String(localized: "documents_photos_selected") }
|
||||
|
||||
// Detail view
|
||||
static var documentDetails: String { String(localized: "documents_document_details") }
|
||||
static var loadingDocument: String { String(localized: "documents_loading_document") }
|
||||
static var status: String { String(localized: "documents_status") }
|
||||
static var daysRemaining: String { String(localized: "documents_days_remaining") }
|
||||
static var itemDetails: String { String(localized: "documents_item_details") }
|
||||
static var claimInformation: String { String(localized: "documents_claim_information") }
|
||||
static var importantDates: String { String(localized: "documents_important_dates") }
|
||||
static var images: String { String(localized: "documents_images") }
|
||||
static var associations: String { String(localized: "documents_associations") }
|
||||
static var residence: String { String(localized: "documents_residence") }
|
||||
static var contractor: String { String(localized: "documents_contractor") }
|
||||
static var contractorPhone: String { String(localized: "documents_contractor_phone") }
|
||||
static var attachedFile: String { String(localized: "documents_attached_file") }
|
||||
static var fileType: String { String(localized: "documents_file_type") }
|
||||
static var fileSize: String { String(localized: "documents_file_size") }
|
||||
static var downloadFile: String { String(localized: "documents_download_file") }
|
||||
static var metadata: String { String(localized: "documents_metadata") }
|
||||
static var uploadedBy: String { String(localized: "documents_uploaded_by") }
|
||||
static var created: String { String(localized: "documents_created") }
|
||||
static var updated: String { String(localized: "documents_updated") }
|
||||
|
||||
// Warranty statuses
|
||||
static var inactive: String { String(localized: "documents_inactive") }
|
||||
static var expired: String { String(localized: "documents_expired") }
|
||||
static var expiringSoon: String { String(localized: "documents_expiring_soon") }
|
||||
static var activeStatus: String { String(localized: "documents_active_status") }
|
||||
|
||||
// Warranty card
|
||||
static var expires: String { String(localized: "documents_expires") }
|
||||
static var daysRemainingCount: String { String(localized: "documents_days_remaining_count") }
|
||||
|
||||
// Delete
|
||||
static var deleteDocument: String { String(localized: "documents_delete_document") }
|
||||
static var deleteConfirmMessage: String { String(localized: "documents_delete_confirm_message") }
|
||||
|
||||
// Validation errors
|
||||
static var titleRequired: String { String(localized: "documents_title_required") }
|
||||
static var propertyRequired: String { String(localized: "documents_property_required") }
|
||||
static var itemNameRequired: String { String(localized: "documents_item_name_required") }
|
||||
static var providerRequired: String { String(localized: "documents_provider_required") }
|
||||
static var fillRequiredFields: String { String(localized: "documents_fill_required_fields") }
|
||||
static var noResidenceSelected: String { String(localized: "documents_no_residence_selected") }
|
||||
static var documentIdMissing: String { String(localized: "documents_document_id_missing") }
|
||||
static var failedToUpdate: String { String(localized: "documents_failed_to_update") }
|
||||
static var failedToCreate: String { String(localized: "documents_failed_to_create") }
|
||||
|
||||
// Document type names
|
||||
static var typeWarranty: String { String(localized: "documents_type_warranty") }
|
||||
static var typeManual: String { String(localized: "documents_type_manual") }
|
||||
static var typeReceipt: String { String(localized: "documents_type_receipt") }
|
||||
static var typeInspection: String { String(localized: "documents_type_inspection") }
|
||||
static var typePermit: String { String(localized: "documents_type_permit") }
|
||||
static var typeDeed: String { String(localized: "documents_type_deed") }
|
||||
static var typeInsurance: String { String(localized: "documents_type_insurance") }
|
||||
static var typeContract: String { String(localized: "documents_type_contract") }
|
||||
static var typePhoto: String { String(localized: "documents_type_photo") }
|
||||
static var typeOther: String { String(localized: "documents_type_other") }
|
||||
static var typeUnknown: String { String(localized: "documents_type_unknown") }
|
||||
|
||||
// Category names
|
||||
static var categoryAppliance: String { String(localized: "documents_category_appliance") }
|
||||
static var categoryHvac: String { String(localized: "documents_category_hvac") }
|
||||
static var categoryPlumbing: String { String(localized: "documents_category_plumbing") }
|
||||
static var categoryElectrical: String { String(localized: "documents_category_electrical") }
|
||||
static var categoryRoofing: String { String(localized: "documents_category_roofing") }
|
||||
static var categoryStructural: String { String(localized: "documents_category_structural") }
|
||||
static var categoryOther: String { String(localized: "documents_category_other") }
|
||||
static var categoryUnknown: String { String(localized: "documents_category_unknown") }
|
||||
|
||||
// Common labels
|
||||
static var none: String { String(localized: "documents_none") }
|
||||
static var na: String { String(localized: "documents_na") }
|
||||
static var update: String { String(localized: "documents_update") }
|
||||
}
|
||||
|
||||
// MARK: - Profile
|
||||
enum Profile {
|
||||
static var title: String { String(localized: "profile_title") }
|
||||
static var logout: String { String(localized: "profile_logout") }
|
||||
static var logoutConfirm: String { String(localized: "profile_logout_confirm") }
|
||||
static var loadingProfile: String { String(localized: "profile_loading_profile") }
|
||||
static var profileSettings: String { String(localized: "profile_profile_settings") }
|
||||
static var firstName: String { String(localized: "profile_first_name") }
|
||||
static var lastName: String { String(localized: "profile_last_name") }
|
||||
static var personalInformation: String { String(localized: "profile_personal_information") }
|
||||
static var email: String { String(localized: "profile_email") }
|
||||
static var contact: String { String(localized: "profile_contact") }
|
||||
static var emailRequiredUnique: String { String(localized: "profile_email_required_unique") }
|
||||
static var saveChanges: String { String(localized: "profile_save_changes") }
|
||||
static var editProfile: String { String(localized: "profile_edit_profile") }
|
||||
static var account: String { String(localized: "profile_account") }
|
||||
static var notifications: String { String(localized: "profile_notifications") }
|
||||
static var privacy: String { String(localized: "profile_privacy") }
|
||||
static var subscription: String { String(localized: "profile_subscription") }
|
||||
static var proPlan: String { String(localized: "profile_pro_plan") }
|
||||
static var freePlan: String { String(localized: "profile_free_plan") }
|
||||
static var activeUntil: String { String(localized: "profile_active_until") }
|
||||
static var limitedFeatures: String { String(localized: "profile_limited_features") }
|
||||
static var upgradeToPro: String { String(localized: "profile_upgrade_to_pro") }
|
||||
static var manageSubscription: String { String(localized: "profile_manage_subscription") }
|
||||
static var restorePurchases: String { String(localized: "profile_restore_purchases") }
|
||||
static var appearance: String { String(localized: "profile_appearance") }
|
||||
static var theme: String { String(localized: "profile_theme") }
|
||||
static var appName: String { String(localized: "profile_app_name") }
|
||||
static var version: String { String(localized: "profile_version") }
|
||||
static var purchasesRestored: String { String(localized: "profile_purchases_restored") }
|
||||
static var purchasesRestoredMessage: String { String(localized: "profile_purchases_restored_message") }
|
||||
|
||||
// Notification Preferences
|
||||
static var notificationPreferences: String { String(localized: "profile_notification_preferences") }
|
||||
static var notificationPreferencesSubtitle: String { String(localized: "profile_notification_preferences_subtitle") }
|
||||
static var taskDueSoon: String { String(localized: "profile_task_due_soon") }
|
||||
static var taskDueSoonDescription: String { String(localized: "profile_task_due_soon_description") }
|
||||
static var taskOverdue: String { String(localized: "profile_task_overdue") }
|
||||
static var taskOverdueDescription: String { String(localized: "profile_task_overdue_description") }
|
||||
static var taskCompleted: String { String(localized: "profile_task_completed") }
|
||||
static var taskCompletedDescription: String { String(localized: "profile_task_completed_description") }
|
||||
static var taskAssigned: String { String(localized: "profile_task_assigned") }
|
||||
static var taskAssignedDescription: String { String(localized: "profile_task_assigned_description") }
|
||||
static var taskNotifications: String { String(localized: "profile_task_notifications") }
|
||||
static var propertyShared: String { String(localized: "profile_property_shared") }
|
||||
static var propertySharedDescription: String { String(localized: "profile_property_shared_description") }
|
||||
static var warrantyExpiring: String { String(localized: "profile_warranty_expiring") }
|
||||
static var warrantyExpiringDescription: String { String(localized: "profile_warranty_expiring_description") }
|
||||
static var otherNotifications: String { String(localized: "profile_other_notifications") }
|
||||
}
|
||||
|
||||
// MARK: - Settings
|
||||
enum Settings {
|
||||
static var title: String { String(localized: "settings_title") }
|
||||
static var language: String { String(localized: "settings_language") }
|
||||
}
|
||||
|
||||
// MARK: - Common
|
||||
enum Common {
|
||||
static var save: String { String(localized: "common_save") }
|
||||
static var cancel: String { String(localized: "common_cancel") }
|
||||
static var delete: String { String(localized: "common_delete") }
|
||||
static var edit: String { String(localized: "common_edit") }
|
||||
static var done: String { String(localized: "common_done") }
|
||||
static var close: String { String(localized: "common_close") }
|
||||
static var back: String { String(localized: "common_back") }
|
||||
static var loading: String { String(localized: "common_loading") }
|
||||
static var error: String { String(localized: "common_error") }
|
||||
static var retry: String { String(localized: "common_retry") }
|
||||
static var success: String { String(localized: "common_success") }
|
||||
static var yes: String { String(localized: "common_yes") }
|
||||
static var no: String { String(localized: "common_no") }
|
||||
static var ok: String { String(localized: "common_ok") }
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
enum Error {
|
||||
static var generic: String { String(localized: "error_generic") }
|
||||
static var network: String { String(localized: "error_network") }
|
||||
static var unauthorized: String { String(localized: "error_unauthorized") }
|
||||
static var notFound: String { String(localized: "error_not_found") }
|
||||
static var requiredField: String { String(localized: "error_required_field") }
|
||||
}
|
||||
}
|
||||
16033
iosApp/iosApp/Localizable.xcstrings
Normal file
16033
iosApp/iosApp/Localizable.xcstrings
Normal file
File diff suppressed because it is too large
Load Diff
@@ -59,11 +59,11 @@ struct LoginView: View {
|
||||
}
|
||||
|
||||
VStack(spacing: AppSpacing.xs) {
|
||||
Text("Welcome Back")
|
||||
Text(L10n.Auth.welcomeBack)
|
||||
.font(.title2.weight(.bold))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text("Sign in to manage your properties")
|
||||
Text(L10n.Auth.signInSubtitle)
|
||||
.font(.body)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -73,7 +73,7 @@ struct LoginView: View {
|
||||
VStack(spacing: AppSpacing.lg) {
|
||||
// Username Field
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xs) {
|
||||
Text("Email or Username")
|
||||
Text(L10n.Auth.loginUsernameLabel)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
|
||||
@@ -82,7 +82,7 @@ struct LoginView: View {
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.frame(width: 20)
|
||||
|
||||
TextField("Enter your email", text: $viewModel.username)
|
||||
TextField(L10n.Auth.enterEmail, text: $viewModel.username)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.keyboardType(.emailAddress)
|
||||
@@ -110,7 +110,7 @@ struct LoginView: View {
|
||||
|
||||
// Password Field
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xs) {
|
||||
Text("Password")
|
||||
Text(L10n.Auth.loginPasswordLabel)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
|
||||
@@ -121,7 +121,7 @@ struct LoginView: View {
|
||||
|
||||
Group {
|
||||
if isPasswordVisible {
|
||||
TextField("Enter your password", text: $viewModel.password)
|
||||
TextField(L10n.Auth.enterPassword, text: $viewModel.password)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.textContentType(.password)
|
||||
@@ -132,7 +132,7 @@ struct LoginView: View {
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.passwordField)
|
||||
} else {
|
||||
SecureField("Enter your password", text: $viewModel.password)
|
||||
SecureField(L10n.Auth.enterPassword, text: $viewModel.password)
|
||||
.textContentType(.password)
|
||||
.focused($focusedField, equals: .password)
|
||||
.submitLabel(.go)
|
||||
@@ -169,7 +169,7 @@ struct LoginView: View {
|
||||
// Forgot Password
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Forgot Password?") {
|
||||
Button(L10n.Auth.forgotPassword) {
|
||||
showPasswordReset = true
|
||||
}
|
||||
.font(.subheadline.weight(.medium))
|
||||
@@ -204,7 +204,7 @@ struct LoginView: View {
|
||||
Rectangle()
|
||||
.fill(Color.appTextSecondary.opacity(0.3))
|
||||
.frame(height: 1)
|
||||
Text("or")
|
||||
Text(L10n.Auth.orDivider)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.padding(.horizontal, AppSpacing.sm)
|
||||
@@ -241,7 +241,7 @@ struct LoginView: View {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
Text("Signing in with Apple...")
|
||||
Text(L10n.Auth.signingInWithApple)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -265,11 +265,11 @@ struct LoginView: View {
|
||||
|
||||
// Sign Up Link
|
||||
HStack(spacing: AppSpacing.xs) {
|
||||
Text("Don't have an account?")
|
||||
Text(L10n.Auth.dontHaveAccount)
|
||||
.font(.body)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
|
||||
Button("Sign Up") {
|
||||
Button(L10n.Auth.signUp) {
|
||||
showingRegister = true
|
||||
}
|
||||
.font(.body)
|
||||
@@ -357,7 +357,7 @@ struct LoginView: View {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
}
|
||||
Text(viewModel.isLoading ? "Signing In..." : "Sign In")
|
||||
Text(viewModel.isLoading ? L10n.Auth.signingIn : L10n.Auth.loginButton)
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ struct NotificationPreferencesView: View {
|
||||
.font(.system(size: 60))
|
||||
.foregroundStyle(Color.appPrimary.gradient)
|
||||
|
||||
Text("Notification Preferences")
|
||||
Text(L10n.Profile.notificationPreferences)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text("Choose which notifications you'd like to receive")
|
||||
Text(L10n.Profile.notificationPreferencesSubtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -51,7 +51,7 @@ struct NotificationPreferencesView: View {
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
Button("Retry") {
|
||||
Button(L10n.Common.retry) {
|
||||
viewModel.loadPreferences()
|
||||
}
|
||||
.foregroundColor(Color.appPrimary)
|
||||
@@ -63,9 +63,9 @@ struct NotificationPreferencesView: View {
|
||||
Toggle(isOn: $viewModel.taskDueSoon) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Task Due Soon")
|
||||
Text(L10n.Profile.taskDueSoon)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text("Reminders for upcoming tasks")
|
||||
Text(L10n.Profile.taskDueSoonDescription)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -82,9 +82,9 @@ struct NotificationPreferencesView: View {
|
||||
Toggle(isOn: $viewModel.taskOverdue) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Task Overdue")
|
||||
Text(L10n.Profile.taskOverdue)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text("Alerts for overdue tasks")
|
||||
Text(L10n.Profile.taskOverdueDescription)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -101,9 +101,9 @@ struct NotificationPreferencesView: View {
|
||||
Toggle(isOn: $viewModel.taskCompleted) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Task Completed")
|
||||
Text(L10n.Profile.taskCompleted)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text("When someone completes a task")
|
||||
Text(L10n.Profile.taskCompletedDescription)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -120,9 +120,9 @@ struct NotificationPreferencesView: View {
|
||||
Toggle(isOn: $viewModel.taskAssigned) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Task Assigned")
|
||||
Text(L10n.Profile.taskAssigned)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text("When a task is assigned to you")
|
||||
Text(L10n.Profile.taskAssignedDescription)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -136,7 +136,7 @@ struct NotificationPreferencesView: View {
|
||||
viewModel.updatePreference(taskAssigned: newValue)
|
||||
}
|
||||
} header: {
|
||||
Text("Task Notifications")
|
||||
Text(L10n.Profile.taskNotifications)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -145,9 +145,9 @@ struct NotificationPreferencesView: View {
|
||||
Toggle(isOn: $viewModel.residenceShared) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Property Shared")
|
||||
Text(L10n.Profile.propertyShared)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text("When someone shares a property with you")
|
||||
Text(L10n.Profile.propertySharedDescription)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -172,9 +172,9 @@ struct NotificationPreferencesView: View {
|
||||
Toggle(isOn: $viewModel.warrantyExpiring) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Warranty Expiring")
|
||||
Text(L10n.Profile.warrantyExpiring)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text("Reminders for expiring warranties")
|
||||
Text(L10n.Profile.warrantyExpiringDescription)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -188,7 +188,7 @@ struct NotificationPreferencesView: View {
|
||||
viewModel.updatePreference(warrantyExpiring: newValue)
|
||||
}
|
||||
} header: {
|
||||
Text("Other Notifications")
|
||||
Text(L10n.Profile.otherNotifications)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
}
|
||||
@@ -196,11 +196,11 @@ struct NotificationPreferencesView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Notifications")
|
||||
.navigationTitle(L10n.Profile.notifications)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Done") {
|
||||
Button(L10n.Common.done) {
|
||||
dismiss()
|
||||
}
|
||||
.foregroundColor(Color.appPrimary)
|
||||
|
||||
@@ -35,11 +35,11 @@ struct ProfileTabView: View {
|
||||
// .listRowBackground(Color.appBackgroundSecondary)
|
||||
// }
|
||||
|
||||
Section("Account") {
|
||||
Section(L10n.Profile.account) {
|
||||
Button(action: {
|
||||
showingProfileEdit = true
|
||||
}) {
|
||||
Label("Edit Profile", systemImage: "person.crop.circle")
|
||||
Label(L10n.Profile.editProfile, systemImage: "person.crop.circle")
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ struct ProfileTabView: View {
|
||||
showingNotificationPreferences = true
|
||||
}) {
|
||||
HStack {
|
||||
Label("Notifications", systemImage: "bell")
|
||||
Label(L10n.Profile.notifications, systemImage: "bell")
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
@@ -56,31 +56,31 @@ struct ProfileTabView: View {
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(destination: Text("Privacy")) {
|
||||
Label("Privacy", systemImage: "lock.shield")
|
||||
NavigationLink(destination: Text(L10n.Profile.privacy)) {
|
||||
Label(L10n.Profile.privacy, systemImage: "lock.shield")
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
// Subscription Section - Only show if limitations are enabled on backend
|
||||
if let subscription = subscriptionCache.currentSubscription, subscription.limitationsEnabled {
|
||||
Section("Subscription") {
|
||||
Section(L10n.Profile.subscription) {
|
||||
HStack {
|
||||
Image(systemName: "crown.fill")
|
||||
.foregroundColor(subscriptionCache.currentTier == "pro" ? Color.appAccent : Color.appTextSecondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(subscriptionCache.currentTier == "pro" ? "Pro Plan" : "Free Plan")
|
||||
Text(subscriptionCache.currentTier == "pro" ? L10n.Profile.proPlan : L10n.Profile.freePlan)
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
if subscriptionCache.currentTier == "pro",
|
||||
let expiresAt = subscription.expiresAt {
|
||||
Text("Active until \(DateUtils.formatDateMedium(expiresAt))")
|
||||
Text("\(L10n.Profile.activeUntil) \(DateUtils.formatDateMedium(expiresAt))")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
} else {
|
||||
Text("Limited features")
|
||||
Text(L10n.Profile.limitedFeatures)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -90,7 +90,7 @@ struct ProfileTabView: View {
|
||||
|
||||
if subscriptionCache.currentTier != "pro" {
|
||||
Button(action: { showUpgradePrompt = true }) {
|
||||
Label("Upgrade to Pro", systemImage: "arrow.up.circle.fill")
|
||||
Label(L10n.Profile.upgradeToPro, systemImage: "arrow.up.circle.fill")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
} else {
|
||||
@@ -99,7 +99,7 @@ struct ProfileTabView: View {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Label("Manage Subscription", systemImage: "gearshape.fill")
|
||||
Label(L10n.Profile.manageSubscription, systemImage: "gearshape.fill")
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
}
|
||||
}
|
||||
@@ -110,19 +110,19 @@ struct ProfileTabView: View {
|
||||
showRestoreSuccess = true
|
||||
}
|
||||
}) {
|
||||
Label("Restore Purchases", systemImage: "arrow.clockwise")
|
||||
Label(L10n.Profile.restorePurchases, systemImage: "arrow.clockwise")
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
}
|
||||
|
||||
Section("Appearance") {
|
||||
Section(L10n.Profile.appearance) {
|
||||
Button(action: {
|
||||
showingThemeSelection = true
|
||||
}) {
|
||||
HStack {
|
||||
Label("Theme", systemImage: "paintpalette")
|
||||
Label(L10n.Profile.theme, systemImage: "paintpalette")
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Spacer()
|
||||
@@ -143,7 +143,7 @@ struct ProfileTabView: View {
|
||||
Button(action: {
|
||||
showingLogoutAlert = true
|
||||
}) {
|
||||
Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
Label(L10n.Profile.logout, systemImage: "rectangle.portrait.and.arrow.right")
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Profile.logoutButton)
|
||||
@@ -152,12 +152,12 @@ struct ProfileTabView: View {
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Casera")
|
||||
Text(L10n.Profile.appName)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text("Version 1.0.0")
|
||||
Text(L10n.Profile.version)
|
||||
.font(.caption2)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -166,7 +166,7 @@ struct ProfileTabView: View {
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Profile")
|
||||
.navigationTitle(L10n.Profile.title)
|
||||
.sheet(isPresented: $showingProfileEdit) {
|
||||
ProfileView()
|
||||
}
|
||||
@@ -176,21 +176,21 @@ struct ProfileTabView: View {
|
||||
.sheet(isPresented: $showingNotificationPreferences) {
|
||||
NotificationPreferencesView()
|
||||
}
|
||||
.alert("Log Out", isPresented: $showingLogoutAlert) {
|
||||
Button("Cancel", role: .cancel) { }
|
||||
Button("Log Out", role: .destructive) {
|
||||
.alert(L10n.Profile.logout, isPresented: $showingLogoutAlert) {
|
||||
Button(L10n.Common.cancel, role: .cancel) { }
|
||||
Button(L10n.Profile.logout, role: .destructive) {
|
||||
AuthenticationManager.shared.logout()
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to log out?")
|
||||
Text(L10n.Profile.logoutConfirm)
|
||||
}
|
||||
.sheet(isPresented: $showUpgradePrompt) {
|
||||
UpgradePromptView(triggerKey: "user_profile", isPresented: $showUpgradePrompt)
|
||||
}
|
||||
.alert("Purchases Restored", isPresented: $showRestoreSuccess) {
|
||||
Button("OK", role: .cancel) { }
|
||||
.alert(L10n.Profile.purchasesRestored, isPresented: $showRestoreSuccess) {
|
||||
Button(L10n.Common.ok, role: .cancel) { }
|
||||
} message: {
|
||||
Text("Your purchases have been restored successfully.")
|
||||
Text(L10n.Profile.purchasesRestoredMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ struct ProfileView: View {
|
||||
if viewModel.isLoadingUser {
|
||||
VStack {
|
||||
ProgressView()
|
||||
Text("Loading profile...")
|
||||
Text(L10n.Profile.loadingProfile)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.padding(.top, 8)
|
||||
@@ -27,7 +27,7 @@ struct ProfileView: View {
|
||||
.font(.system(size: 60))
|
||||
.foregroundStyle(Color.appPrimary.gradient)
|
||||
|
||||
Text("Profile Settings")
|
||||
Text(L10n.Profile.profileSettings)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
@@ -38,7 +38,7 @@ struct ProfileView: View {
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
Section {
|
||||
TextField("First Name", text: $viewModel.firstName)
|
||||
TextField(L10n.Profile.firstName, text: $viewModel.firstName)
|
||||
.textInputAutocapitalization(.words)
|
||||
.autocorrectionDisabled()
|
||||
.focused($focusedField, equals: .firstName)
|
||||
@@ -47,7 +47,7 @@ struct ProfileView: View {
|
||||
focusedField = .lastName
|
||||
}
|
||||
|
||||
TextField("Last Name", text: $viewModel.lastName)
|
||||
TextField(L10n.Profile.lastName, text: $viewModel.lastName)
|
||||
.textInputAutocapitalization(.words)
|
||||
.autocorrectionDisabled()
|
||||
.focused($focusedField, equals: .lastName)
|
||||
@@ -56,12 +56,12 @@ struct ProfileView: View {
|
||||
focusedField = .email
|
||||
}
|
||||
} header: {
|
||||
Text("Personal Information")
|
||||
Text(L10n.Profile.personalInformation)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section {
|
||||
TextField("Email", text: $viewModel.email)
|
||||
TextField(L10n.Profile.email, text: $viewModel.email)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.keyboardType(.emailAddress)
|
||||
@@ -71,9 +71,9 @@ struct ProfileView: View {
|
||||
viewModel.updateProfile()
|
||||
}
|
||||
} header: {
|
||||
Text("Contact")
|
||||
Text(L10n.Profile.contact)
|
||||
} footer: {
|
||||
Text("Email is required and must be unique")
|
||||
Text(L10n.Profile.emailRequiredUnique)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -110,7 +110,7 @@ struct ProfileView: View {
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("Save Changes")
|
||||
Text(L10n.Profile.saveChanges)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
Spacer()
|
||||
@@ -123,11 +123,11 @@ struct ProfileView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Profile")
|
||||
.navigationTitle(L10n.Profile.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
Button(L10n.Common.cancel) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@ struct ThemeSelectionView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Appearance")
|
||||
.navigationTitle(L10n.Profile.appearance)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Done") {
|
||||
Button(L10n.Common.done) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ struct RegisterView: View {
|
||||
.font(.system(size: 60))
|
||||
.foregroundStyle(Color.appPrimary.gradient)
|
||||
|
||||
Text("Join Casera")
|
||||
Text(L10n.Auth.joinCasera)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text("Start managing your properties today")
|
||||
Text(L10n.Auth.startManaging)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -34,7 +34,7 @@ struct RegisterView: View {
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
Section {
|
||||
TextField("Username", text: $viewModel.username)
|
||||
TextField(L10n.Auth.registerUsername, text: $viewModel.username)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.textContentType(.username)
|
||||
@@ -45,7 +45,7 @@ struct RegisterView: View {
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerUsernameField)
|
||||
|
||||
TextField("Email", text: $viewModel.email)
|
||||
TextField(L10n.Auth.registerEmail, text: $viewModel.email)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.keyboardType(.emailAddress)
|
||||
@@ -57,14 +57,14 @@ struct RegisterView: View {
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerEmailField)
|
||||
} header: {
|
||||
Text("Account Information")
|
||||
Text(L10n.Auth.accountInfo)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section {
|
||||
// Using .newPassword enables iOS Strong Password generation
|
||||
// iOS will automatically offer to save to iCloud Keychain after successful registration
|
||||
SecureField("Password", text: $viewModel.password)
|
||||
SecureField(L10n.Auth.registerPassword, text: $viewModel.password)
|
||||
.textContentType(.newPassword)
|
||||
.focused($focusedField, equals: .password)
|
||||
.submitLabel(.next)
|
||||
@@ -73,7 +73,7 @@ struct RegisterView: View {
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerPasswordField)
|
||||
|
||||
SecureField("Confirm Password", text: $viewModel.confirmPassword)
|
||||
SecureField(L10n.Auth.registerConfirmPassword, text: $viewModel.confirmPassword)
|
||||
.textContentType(.newPassword)
|
||||
.focused($focusedField, equals: .confirmPassword)
|
||||
.submitLabel(.go)
|
||||
@@ -82,9 +82,9 @@ struct RegisterView: View {
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerConfirmPasswordField)
|
||||
} header: {
|
||||
Text("Security")
|
||||
Text(L10n.Auth.security)
|
||||
} footer: {
|
||||
Text("Tap the password field for a strong password suggestion")
|
||||
Text(L10n.Auth.passwordSuggestion)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -108,7 +108,7 @@ struct RegisterView: View {
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("Create Account")
|
||||
Text(L10n.Auth.registerButton)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
Spacer()
|
||||
@@ -122,11 +122,11 @@ struct RegisterView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Create Account")
|
||||
.navigationTitle(L10n.Auth.registerTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
Button(L10n.Common.cancel) {
|
||||
dismiss()
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerCancelButton)
|
||||
|
||||
@@ -12,7 +12,7 @@ struct JoinResidenceView: View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section {
|
||||
TextField("Share Code", text: $shareCode)
|
||||
TextField(L10n.Residences.shareCode, text: $shareCode)
|
||||
.textInputAutocapitalization(.characters)
|
||||
.autocorrectionDisabled()
|
||||
.onChange(of: shareCode) { newValue in
|
||||
@@ -25,9 +25,9 @@ struct JoinResidenceView: View {
|
||||
}
|
||||
.disabled(viewModel.isLoading)
|
||||
} header: {
|
||||
Text("Enter Share Code")
|
||||
Text(L10n.Residences.enterShareCode)
|
||||
} footer: {
|
||||
Text("Enter the 6-character code shared with you to join a residence")
|
||||
Text(L10n.Residences.shareCodeFooter)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
@@ -48,7 +48,7 @@ struct JoinResidenceView: View {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
} else {
|
||||
Text("Join Residence")
|
||||
Text(L10n.Residences.joinButton)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
Spacer()
|
||||
@@ -61,11 +61,11 @@ struct JoinResidenceView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Join Residence")
|
||||
.navigationTitle(L10n.Residences.joinTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
Button(L10n.Common.cancel) {
|
||||
dismiss()
|
||||
}
|
||||
.disabled(viewModel.isLoading)
|
||||
@@ -76,7 +76,7 @@ struct JoinResidenceView: View {
|
||||
|
||||
private func joinResidence() {
|
||||
guard shareCode.count == 6 else {
|
||||
viewModel.errorMessage = "Share code must be 6 characters"
|
||||
viewModel.errorMessage = L10n.Residences.shareCodeMust6
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ struct ManageUsersView: View {
|
||||
|
||||
// Users list
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Users (\(users.count))")
|
||||
Text("\(L10n.Residences.users) (\(users.count))")
|
||||
.font(.headline)
|
||||
.padding(.horizontal)
|
||||
|
||||
@@ -66,11 +66,11 @@ struct ManageUsersView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Manage Users")
|
||||
.navigationTitle(L10n.Residences.manageUsers)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Close") {
|
||||
Button(L10n.Common.close) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,35 +54,35 @@ struct ResidenceDetailView: View {
|
||||
leadingToolbar
|
||||
trailingToolbar
|
||||
}
|
||||
|
||||
|
||||
// MARK: Alerts
|
||||
.alert("Generate Report", isPresented: $showReportConfirmation) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
.alert(L10n.Residences.generateReport, isPresented: $showReportConfirmation) {
|
||||
Button(L10n.Common.cancel, role: .cancel) {
|
||||
showReportConfirmation = false
|
||||
}
|
||||
Button("Generate") {
|
||||
Button(L10n.Residences.generate) {
|
||||
viewModel.generateTasksReport(residenceId: residenceId, email: "")
|
||||
showReportConfirmation = false
|
||||
}
|
||||
} message: {
|
||||
Text("This will generate a comprehensive report of your property including all tasks, documents, and contractors.")
|
||||
Text(L10n.Residences.generateReportMessage)
|
||||
}
|
||||
|
||||
.alert("Delete Residence", isPresented: $showDeleteConfirmation) {
|
||||
Button("Cancel", role: .cancel) { }
|
||||
|
||||
.alert(L10n.Residences.deleteTitle, isPresented: $showDeleteConfirmation) {
|
||||
Button(L10n.Common.cancel, role: .cancel) { }
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Alert.cancelButton)
|
||||
Button("Delete", role: .destructive) {
|
||||
Button(L10n.Common.delete, role: .destructive) {
|
||||
deleteResidence()
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Alert.deleteButton)
|
||||
} message: {
|
||||
if let residence = viewModel.selectedResidence {
|
||||
Text("Are you sure you want to delete \(residence.name)? This action cannot be undone and will delete all associated tasks, documents, and data.")
|
||||
Text("\(L10n.Residences.deleteConfirmMessage)")
|
||||
}
|
||||
}
|
||||
|
||||
.alert("Maintenance Report", isPresented: $showReportAlert) {
|
||||
Button("OK", role: .cancel) { }
|
||||
|
||||
.alert(L10n.Residences.maintenanceReport, isPresented: $showReportAlert) {
|
||||
Button(L10n.Common.ok, role: .cancel) { }
|
||||
} message: {
|
||||
Text(viewModel.reportMessage ?? "")
|
||||
}
|
||||
@@ -189,7 +189,7 @@ private extension ResidenceDetailView {
|
||||
var loadingView: some View {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
Text("Loading residence...")
|
||||
Text(L10n.Residences.loadingResidence)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -226,9 +226,9 @@ private extension ResidenceDetailView {
|
||||
reloadTasks: { loadResidenceTasks() }
|
||||
)
|
||||
} else if isLoadingTasks {
|
||||
ProgressView("Loading tasks...")
|
||||
ProgressView(L10n.Residences.loadingTasks)
|
||||
} else if let tasksError = tasksError {
|
||||
Text("Error loading tasks: \(tasksError)")
|
||||
Text("\(L10n.Residences.errorLoadingTasks): \(tasksError)")
|
||||
.foregroundColor(Color.appError)
|
||||
.padding()
|
||||
}
|
||||
@@ -242,7 +242,7 @@ private extension ResidenceDetailView {
|
||||
Image(systemName: "person.2.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(Color.appPrimary)
|
||||
Text("Contractors")
|
||||
Text(L10n.Residences.contractors)
|
||||
.font(.title2.weight(.bold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
@@ -256,7 +256,7 @@ private extension ResidenceDetailView {
|
||||
}
|
||||
.padding()
|
||||
} else if let error = contractorsError {
|
||||
Text("Error: \(error)")
|
||||
Text("\(L10n.Common.error): \(error)")
|
||||
.foregroundColor(Color.appError)
|
||||
.padding()
|
||||
} else if contractors.isEmpty {
|
||||
@@ -265,10 +265,10 @@ private extension ResidenceDetailView {
|
||||
Image(systemName: "person.crop.circle.badge.plus")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.6))
|
||||
Text("No contractors yet")
|
||||
Text(L10n.Residences.noContractors)
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text("Add contractors from the Contractors tab")
|
||||
Text(L10n.Residences.addContractorsPrompt)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -303,7 +303,7 @@ private extension ResidenceDetailView {
|
||||
var leadingToolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
if viewModel.selectedResidence != nil {
|
||||
Button("Edit") {
|
||||
Button(L10n.Common.edit) {
|
||||
showEditResidence = true
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.editButton)
|
||||
|
||||
@@ -43,7 +43,7 @@ struct ResidencesListView: View {
|
||||
})
|
||||
}
|
||||
}
|
||||
.navigationTitle("My Properties")
|
||||
.navigationTitle(L10n.Residences.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
@@ -134,10 +134,10 @@ private struct ResidencesContent: View {
|
||||
// Properties Header
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
||||
Text("Your Properties")
|
||||
Text(L10n.Residences.yourProperties)
|
||||
.font(.title3.weight(.semibold))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Text("\(residences.count) \(residences.count == 1 ? "property" : "properties")")
|
||||
Text("\(residences.count) \(residences.count == 1 ? L10n.Residences.property : L10n.Residences.properties)")
|
||||
.font(.callout)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ struct ResidenceFormView: View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section {
|
||||
TextField("Property Name", text: $name)
|
||||
TextField(L10n.Residences.propertyName, text: $name)
|
||||
.focused($focusedField, equals: .name)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.nameField)
|
||||
|
||||
@@ -58,54 +58,54 @@ struct ResidenceFormView: View {
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
|
||||
Picker("Property Type", selection: $selectedPropertyType) {
|
||||
Text("Select Type").tag(nil as ResidenceType?)
|
||||
Picker(L10n.Residences.propertyType, selection: $selectedPropertyType) {
|
||||
Text(L10n.Residences.selectType).tag(nil as ResidenceType?)
|
||||
ForEach(residenceTypes, id: \.id) { type in
|
||||
Text(type.name).tag(type as ResidenceType?)
|
||||
}
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.propertyTypePicker)
|
||||
} header: {
|
||||
Text("Property Details")
|
||||
Text(L10n.Residences.propertyDetails)
|
||||
} footer: {
|
||||
Text("Required: Name")
|
||||
Text(L10n.Residences.requiredName)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section {
|
||||
TextField("Street Address", text: $streetAddress)
|
||||
TextField(L10n.Residences.streetAddress, text: $streetAddress)
|
||||
.focused($focusedField, equals: .streetAddress)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.streetAddressField)
|
||||
|
||||
TextField("Apartment/Unit (optional)", text: $apartmentUnit)
|
||||
TextField(L10n.Residences.apartmentUnit, text: $apartmentUnit)
|
||||
.focused($focusedField, equals: .apartmentUnit)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.apartmentUnitField)
|
||||
|
||||
TextField("City", text: $city)
|
||||
TextField(L10n.Residences.city, text: $city)
|
||||
.focused($focusedField, equals: .city)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.cityField)
|
||||
|
||||
TextField("State/Province", text: $stateProvince)
|
||||
TextField(L10n.Residences.stateProvince, text: $stateProvince)
|
||||
.focused($focusedField, equals: .stateProvince)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.stateProvinceField)
|
||||
|
||||
TextField("Postal Code", text: $postalCode)
|
||||
TextField(L10n.Residences.postalCode, text: $postalCode)
|
||||
.focused($focusedField, equals: .postalCode)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.postalCodeField)
|
||||
|
||||
TextField("Country", text: $country)
|
||||
TextField(L10n.Residences.country, text: $country)
|
||||
.focused($focusedField, equals: .country)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.countryField)
|
||||
} header: {
|
||||
Text("Address")
|
||||
Text(L10n.Residences.address)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section(header: Text("Property Features")) {
|
||||
Section(header: Text(L10n.Residences.propertyFeatures)) {
|
||||
HStack {
|
||||
Text("Bedrooms")
|
||||
Text(L10n.Residences.bedrooms)
|
||||
Spacer()
|
||||
TextField("0", text: $bedrooms)
|
||||
.keyboardType(.numberPad)
|
||||
@@ -116,7 +116,7 @@ struct ResidenceFormView: View {
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Bathrooms")
|
||||
Text(L10n.Residences.bathrooms)
|
||||
Spacer()
|
||||
TextField("0.0", text: $bathrooms)
|
||||
.keyboardType(.decimalPad)
|
||||
@@ -126,29 +126,29 @@ struct ResidenceFormView: View {
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.bathroomsField)
|
||||
}
|
||||
|
||||
TextField("Square Footage", text: $squareFootage)
|
||||
TextField(L10n.Residences.squareFootage, text: $squareFootage)
|
||||
.keyboardType(.numberPad)
|
||||
.focused($focusedField, equals: .squareFootage)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.squareFootageField)
|
||||
|
||||
TextField("Lot Size (acres)", text: $lotSize)
|
||||
TextField(L10n.Residences.lotSize, text: $lotSize)
|
||||
.keyboardType(.decimalPad)
|
||||
.focused($focusedField, equals: .lotSize)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.lotSizeField)
|
||||
|
||||
TextField("Year Built", text: $yearBuilt)
|
||||
TextField(L10n.Residences.yearBuilt, text: $yearBuilt)
|
||||
.keyboardType(.numberPad)
|
||||
.focused($focusedField, equals: .yearBuilt)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.yearBuiltField)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section(header: Text("Additional Details")) {
|
||||
TextField("Description (optional)", text: $description, axis: .vertical)
|
||||
Section(header: Text(L10n.Residences.additionalDetails)) {
|
||||
TextField(L10n.Residences.description, text: $description, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.descriptionField)
|
||||
|
||||
Toggle("Primary Residence", isOn: $isPrimary)
|
||||
Toggle(L10n.Residences.primaryResidence, isOn: $isPrimary)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.isPrimaryToggle)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
@@ -165,18 +165,18 @@ struct ResidenceFormView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle(isEditMode ? "Edit Residence" : "Add Residence")
|
||||
.navigationTitle(isEditMode ? L10n.Residences.editTitle : L10n.Residences.addTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
Button(L10n.Common.cancel) {
|
||||
isPresented = false
|
||||
}
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.formCancelButton)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
Button(L10n.Common.save) {
|
||||
submitForm()
|
||||
}
|
||||
.disabled(!canSave || viewModel.isLoading)
|
||||
@@ -244,7 +244,7 @@ struct ResidenceFormView: View {
|
||||
var isValid = true
|
||||
|
||||
if name.isEmpty {
|
||||
nameError = "Name is required"
|
||||
nameError = L10n.Residences.nameRequired
|
||||
isValid = false
|
||||
} else {
|
||||
nameError = ""
|
||||
|
||||
@@ -53,7 +53,7 @@ struct DynamicTaskColumnView: View {
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(columnColor.opacity(0.3))
|
||||
|
||||
Text("No tasks")
|
||||
Text(L10n.Tasks.noTasks)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ struct TaskCard: View {
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundColor(Color.appAccent)
|
||||
}
|
||||
Text("Completions (\(task.completions.count))")
|
||||
Text("\(L10n.Tasks.completions.capitalized) (\(task.completions.count))")
|
||||
.font(.footnote.weight(.medium))
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
@@ -121,7 +121,7 @@ struct TaskCard: View {
|
||||
HStack(spacing: AppSpacing.xs) {
|
||||
Image(systemName: "play.circle.fill")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
Text("In Progress")
|
||||
Text(L10n.Tasks.inProgress)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
@@ -138,7 +138,7 @@ struct TaskCard: View {
|
||||
HStack(spacing: AppSpacing.xs) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
Text("Complete")
|
||||
Text(L10n.Tasks.complete)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
@@ -159,7 +159,7 @@ struct TaskCard: View {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "pencil")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
Text("Edit")
|
||||
Text(L10n.Tasks.edit)
|
||||
.font(.footnote.weight(.medium))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -174,7 +174,7 @@ struct TaskCard: View {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "xmark.circle")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
Text("Cancel")
|
||||
Text(L10n.Tasks.cancel)
|
||||
.font(.footnote.weight(.medium))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -188,7 +188,7 @@ struct TaskCard: View {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "arrow.uturn.backward")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
Text("Restore")
|
||||
Text(L10n.Tasks.restore)
|
||||
.font(.footnote.weight(.medium))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -206,7 +206,7 @@ struct TaskCard: View {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "tray.and.arrow.up")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
Text("Unarchive")
|
||||
Text(L10n.Tasks.unarchive)
|
||||
.font(.footnote.weight(.medium))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -222,7 +222,7 @@ struct TaskCard: View {
|
||||
HStack(spacing: AppSpacing.xxs) {
|
||||
Image(systemName: "archivebox")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
Text("Archive")
|
||||
Text(L10n.Tasks.archive)
|
||||
.font(.footnote.weight(.medium))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
@@ -57,11 +57,11 @@ struct AllTasksView: View {
|
||||
.sheet(isPresented: $showingUpgradePrompt) {
|
||||
UpgradePromptView(triggerKey: "add_11th_task", isPresented: $showingUpgradePrompt)
|
||||
}
|
||||
.alert("Archive Task", isPresented: $showArchiveConfirmation) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
.alert(L10n.Tasks.archiveTask, isPresented: $showArchiveConfirmation) {
|
||||
Button(L10n.Common.cancel, role: .cancel) {
|
||||
selectedTaskForArchive = nil
|
||||
}
|
||||
Button("Archive", role: .destructive) {
|
||||
Button(L10n.Tasks.archive, role: .destructive) {
|
||||
if let task = selectedTaskForArchive {
|
||||
taskViewModel.archiveTask(id: task.id) { _ in
|
||||
loadAllTasks()
|
||||
@@ -71,14 +71,14 @@ struct AllTasksView: View {
|
||||
}
|
||||
} message: {
|
||||
if let task = selectedTaskForArchive {
|
||||
Text("Are you sure you want to archive \"\(task.title)\"? You can unarchive it later from archived tasks.")
|
||||
Text(L10n.Tasks.archiveConfirm.replacingOccurrences(of: "this task", with: "\"\(task.title)\""))
|
||||
}
|
||||
}
|
||||
.alert("Delete Task", isPresented: $showCancelConfirmation) {
|
||||
Button("Cancel", role: .cancel) {
|
||||
.alert(L10n.Tasks.deleteTask, isPresented: $showCancelConfirmation) {
|
||||
Button(L10n.Common.cancel, role: .cancel) {
|
||||
selectedTaskForCancel = nil
|
||||
}
|
||||
Button("Archive", role: .destructive) {
|
||||
Button(L10n.Tasks.archive, role: .destructive) {
|
||||
if let task = selectedTaskForCancel {
|
||||
taskViewModel.cancelTask(id: task.id) { _ in
|
||||
loadAllTasks()
|
||||
@@ -88,7 +88,7 @@ struct AllTasksView: View {
|
||||
}
|
||||
} message: {
|
||||
if let task = selectedTaskForCancel {
|
||||
Text("Are you sure you want to archive \"\(task.title)\"? You can unarchive it later from archived tasks.")
|
||||
Text(L10n.Tasks.archiveConfirm.replacingOccurrences(of: "this task", with: "\"\(task.title)\""))
|
||||
}
|
||||
}
|
||||
.onChange(of: showAddTask) { isShowing in
|
||||
@@ -129,16 +129,16 @@ struct AllTasksView: View {
|
||||
.font(.system(size: 64))
|
||||
.foregroundStyle(Color.appPrimary.opacity(0.6))
|
||||
|
||||
Text("No tasks yet")
|
||||
Text(L10n.Tasks.noTasksYet)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text("Create your first task to get started")
|
||||
Text(L10n.Tasks.createFirst)
|
||||
.font(.body)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
|
||||
Button(action: {
|
||||
// Check if we should show upgrade prompt before adding
|
||||
if subscriptionCache.shouldShowUpgradePrompt(currentCount: totalTaskCount, limitKey: "tasks") {
|
||||
@@ -149,7 +149,7 @@ struct AllTasksView: View {
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "plus")
|
||||
Text("Add Task")
|
||||
Text(L10n.Tasks.addButton)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -160,13 +160,13 @@ struct AllTasksView: View {
|
||||
.padding(.horizontal, 48)
|
||||
.disabled(residenceViewModel.myResidences?.residences.isEmpty ?? true)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Task.addButton)
|
||||
|
||||
|
||||
if residenceViewModel.myResidences?.residences.isEmpty ?? true {
|
||||
Text("Add a property first from the Residences tab")
|
||||
Text(L10n.Tasks.addPropertyFirst)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
@@ -235,7 +235,7 @@ struct AllTasksView: View {
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("All Tasks")
|
||||
.navigationTitle(L10n.Tasks.allTasks)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
|
||||
@@ -51,7 +51,7 @@ struct CompleteTaskView: View {
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Task Details")
|
||||
Text(L10n.Tasks.taskDetails)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -61,7 +61,7 @@ struct CompleteTaskView: View {
|
||||
showContractorPicker = true
|
||||
}) {
|
||||
HStack {
|
||||
Label("Select Contractor", systemImage: "wrench.and.screwdriver")
|
||||
Label(L10n.Tasks.selectContractor, systemImage: "wrench.and.screwdriver")
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Spacer()
|
||||
@@ -77,7 +77,7 @@ struct CompleteTaskView: View {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("None")
|
||||
Text(L10n.Tasks.none)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
|
||||
@@ -87,20 +87,20 @@ struct CompleteTaskView: View {
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Contractor (Optional)")
|
||||
Text(L10n.Tasks.contractorOptional)
|
||||
} footer: {
|
||||
Text("Select a contractor if they completed this work, or leave blank for manual entry.")
|
||||
Text(L10n.Tasks.contractorHelper)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
// Completion Details Section
|
||||
Section {
|
||||
LabeledContent {
|
||||
TextField("Your name", text: $completedByName)
|
||||
TextField(L10n.Tasks.yourName, text: $completedByName)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.disabled(selectedContractor != nil)
|
||||
} label: {
|
||||
Label("Completed By", systemImage: "person")
|
||||
Label(L10n.Tasks.completedBy, systemImage: "person")
|
||||
}
|
||||
|
||||
LabeledContent {
|
||||
@@ -113,19 +113,19 @@ struct CompleteTaskView: View {
|
||||
}
|
||||
.padding(.leading, 12)
|
||||
} label: {
|
||||
Label("Actual Cost", systemImage: "dollarsign.circle")
|
||||
Label(L10n.Tasks.actualCost, systemImage: "dollarsign.circle")
|
||||
}
|
||||
} header: {
|
||||
Text("Optional Information")
|
||||
Text(L10n.Tasks.optionalInfo)
|
||||
} footer: {
|
||||
Text("Add any additional details about completing this task.")
|
||||
Text(L10n.Tasks.optionalDetails)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
// Notes Section
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label("Notes", systemImage: "note.text")
|
||||
Label(L10n.Tasks.notes, systemImage: "note.text")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@@ -134,7 +134,7 @@ struct CompleteTaskView: View {
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
} footer: {
|
||||
Text("Optional notes about the work completed.")
|
||||
Text(L10n.Tasks.optionalNotes)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -142,7 +142,7 @@ struct CompleteTaskView: View {
|
||||
Section {
|
||||
VStack(spacing: 12) {
|
||||
HStack {
|
||||
Label("Quality Rating", systemImage: "star")
|
||||
Label(L10n.Tasks.qualityRating, systemImage: "star")
|
||||
.font(.subheadline)
|
||||
|
||||
Spacer()
|
||||
@@ -168,7 +168,7 @@ struct CompleteTaskView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
} footer: {
|
||||
Text("Rate the quality of work from 1 to 5 stars.")
|
||||
Text(L10n.Tasks.rateQuality)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -179,7 +179,7 @@ struct CompleteTaskView: View {
|
||||
Button(action: {
|
||||
showCamera = true
|
||||
}) {
|
||||
Label("Take Photo", systemImage: "camera")
|
||||
Label(L10n.Tasks.takePhoto, systemImage: "camera")
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
}
|
||||
@@ -191,7 +191,7 @@ struct CompleteTaskView: View {
|
||||
matching: .images,
|
||||
photoLibrary: .shared()
|
||||
) {
|
||||
Label("Library", systemImage: "photo.on.rectangle.angled")
|
||||
Label(L10n.Tasks.library, systemImage: "photo.on.rectangle.angled")
|
||||
.frame(maxWidth: .infinity)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
}
|
||||
@@ -230,9 +230,9 @@ struct CompleteTaskView: View {
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Photos (\(selectedImages.count)/5)")
|
||||
Text("\(L10n.Tasks.photos) (\(selectedImages.count)/5)")
|
||||
} footer: {
|
||||
Text("Add up to 5 photos documenting the completed work.")
|
||||
Text(L10n.Tasks.addPhotos)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
@@ -244,7 +244,7 @@ struct CompleteTaskView: View {
|
||||
ProgressView()
|
||||
.tint(.white)
|
||||
} else {
|
||||
Label("Complete Task", systemImage: "checkmark.circle.fill")
|
||||
Label(L10n.Tasks.completeTask, systemImage: "checkmark.circle.fill")
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -258,17 +258,17 @@ struct CompleteTaskView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle("Complete Task")
|
||||
.navigationTitle(L10n.Tasks.completeTask)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
Button(L10n.Common.cancel) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Error", isPresented: $showError) {
|
||||
Button("OK", role: .cancel) {}
|
||||
.alert(L10n.Tasks.error, isPresented: $showError) {
|
||||
Button(L10n.Common.ok, role: .cancel) {}
|
||||
} message: {
|
||||
Text(errorMessage)
|
||||
}
|
||||
@@ -386,9 +386,9 @@ struct ContractorPickerView: View {
|
||||
}) {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text("None (Manual Entry)")
|
||||
Text(L10n.Tasks.noneManual)
|
||||
.foregroundStyle(.primary)
|
||||
Text("Enter name manually")
|
||||
Text(L10n.Tasks.enterManually)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -450,11 +450,11 @@ struct ContractorPickerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Select Contractor")
|
||||
.navigationTitle(L10n.Tasks.selectContractor)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
Button(L10n.Common.cancel) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ struct CompletionHistorySheet: View {
|
||||
completionsList
|
||||
}
|
||||
}
|
||||
.navigationTitle("Completion History")
|
||||
.navigationTitle(L10n.Tasks.completionHistory)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Done") {
|
||||
Button(L10n.Common.done) {
|
||||
isPresented = false
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ struct CompletionHistorySheet: View {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: Color.appPrimary))
|
||||
.scaleEffect(1.5)
|
||||
Text("Loading completions...")
|
||||
Text(L10n.Tasks.loadingCompletions)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -60,7 +60,7 @@ struct CompletionHistorySheet: View {
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(Color.appError)
|
||||
|
||||
Text("Failed to load completions")
|
||||
Text(L10n.Tasks.failedToLoad)
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
@@ -73,7 +73,7 @@ struct CompletionHistorySheet: View {
|
||||
Button(action: {
|
||||
viewModel.loadCompletions(taskId: taskId)
|
||||
}) {
|
||||
Label("Retry", systemImage: "arrow.clockwise")
|
||||
Label(L10n.Common.retry, systemImage: "arrow.clockwise")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
.padding(.top, AppSpacing.sm)
|
||||
@@ -87,11 +87,11 @@ struct CompletionHistorySheet: View {
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.5))
|
||||
|
||||
Text("No Completions Yet")
|
||||
Text(L10n.Tasks.noCompletionsYet)
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text("This task has not been completed.")
|
||||
Text(L10n.Tasks.notCompleted)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -110,7 +110,7 @@ struct CompletionHistorySheet: View {
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
Spacer()
|
||||
Text("\(viewModel.completions.count) \(viewModel.completions.count == 1 ? "completion" : "completions")")
|
||||
Text("\(viewModel.completions.count) \(viewModel.completions.count == 1 ? L10n.Tasks.completion : L10n.Tasks.completions)")
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -146,7 +146,7 @@ struct CompletionHistoryCard: View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "person.fill")
|
||||
.font(.caption2)
|
||||
Text("Completed by \(completedBy)")
|
||||
Text("\(L10n.Tasks.completedByName) \(completedBy)")
|
||||
.font(.caption)
|
||||
}
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
@@ -213,7 +213,7 @@ struct CompletionHistoryCard: View {
|
||||
// Notes
|
||||
if !completion.notes.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Notes")
|
||||
Text(L10n.Tasks.notes)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
@@ -233,7 +233,7 @@ struct CompletionHistoryCard: View {
|
||||
HStack {
|
||||
Image(systemName: "photo.on.rectangle.angled")
|
||||
.font(.subheadline)
|
||||
Text("View Photos (\(completion.images.count))")
|
||||
Text("\(L10n.Tasks.viewPhotos) (\(completion.images.count))")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
@@ -97,8 +97,8 @@ struct TaskFormView: View {
|
||||
// Residence Picker (only if needed)
|
||||
if needsResidenceSelection, let residences = residences {
|
||||
Section {
|
||||
Picker("Property", selection: $selectedResidence) {
|
||||
Text("Select Property").tag(nil as ResidenceResponse?)
|
||||
Picker(L10n.Tasks.property, selection: $selectedResidence) {
|
||||
Text(L10n.Tasks.selectProperty).tag(nil as ResidenceResponse?)
|
||||
ForEach(residences, id: \.id) { residence in
|
||||
Text(residence.name).tag(residence as ResidenceResponse?)
|
||||
}
|
||||
@@ -110,9 +110,9 @@ struct TaskFormView: View {
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
} header: {
|
||||
Text("Property")
|
||||
Text(L10n.Tasks.property)
|
||||
} footer: {
|
||||
Text("Required")
|
||||
Text(L10n.Tasks.required)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
@@ -120,7 +120,7 @@ struct TaskFormView: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField("Title", text: $title)
|
||||
TextField(L10n.Tasks.titleLabel, text: $title)
|
||||
.focused($focusedField, equals: .title)
|
||||
|
||||
if !titleError.isEmpty {
|
||||
@@ -129,83 +129,83 @@ struct TaskFormView: View {
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
|
||||
TextField("Description (optional)", text: $description, axis: .vertical)
|
||||
TextField(L10n.Tasks.descriptionOptional, text: $description, axis: .vertical)
|
||||
.lineLimit(3...6)
|
||||
.focused($focusedField, equals: .description)
|
||||
} header: {
|
||||
Text("Task Details")
|
||||
Text(L10n.Tasks.taskDetails)
|
||||
} footer: {
|
||||
Text("Required: Title")
|
||||
Text(L10n.Tasks.titleRequired)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section {
|
||||
Picker("Category", selection: $selectedCategory) {
|
||||
Text("Select Category").tag(nil as TaskCategory?)
|
||||
Picker(L10n.Tasks.category, selection: $selectedCategory) {
|
||||
Text(L10n.Tasks.selectCategory).tag(nil as TaskCategory?)
|
||||
ForEach(taskCategories, id: \.id) { category in
|
||||
Text(category.name.capitalized).tag(category as TaskCategory?)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Category")
|
||||
Text(L10n.Tasks.category)
|
||||
} footer: {
|
||||
Text("Required")
|
||||
Text(L10n.Tasks.required)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section {
|
||||
Picker("Frequency", selection: $selectedFrequency) {
|
||||
Text("Select Frequency").tag(nil as TaskFrequency?)
|
||||
Picker(L10n.Tasks.frequency, selection: $selectedFrequency) {
|
||||
Text(L10n.Tasks.selectFrequency).tag(nil as TaskFrequency?)
|
||||
ForEach(taskFrequencies, id: \.id) { frequency in
|
||||
Text(frequency.displayName).tag(frequency as TaskFrequency?)
|
||||
}
|
||||
}
|
||||
|
||||
if selectedFrequency?.name != "once" {
|
||||
TextField("Custom Interval (days, optional)", text: $intervalDays)
|
||||
TextField(L10n.Tasks.customInterval, text: $intervalDays)
|
||||
.keyboardType(.numberPad)
|
||||
.focused($focusedField, equals: .intervalDays)
|
||||
}
|
||||
|
||||
DatePicker("Due Date", selection: $dueDate, displayedComponents: .date)
|
||||
DatePicker(L10n.Tasks.dueDate, selection: $dueDate, displayedComponents: .date)
|
||||
} header: {
|
||||
Text("Scheduling")
|
||||
Text(L10n.Tasks.scheduling)
|
||||
} footer: {
|
||||
Text("Required: Frequency")
|
||||
Text(L10n.Tasks.required)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section {
|
||||
Picker("Priority", selection: $selectedPriority) {
|
||||
Text("Select Priority").tag(nil as TaskPriority?)
|
||||
Picker(L10n.Tasks.priority, selection: $selectedPriority) {
|
||||
Text(L10n.Tasks.selectPriority).tag(nil as TaskPriority?)
|
||||
ForEach(taskPriorities, id: \.id) { priority in
|
||||
Text(priority.displayName).tag(priority as TaskPriority?)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Status", selection: $selectedStatus) {
|
||||
Text("Select Status").tag(nil as TaskStatus?)
|
||||
Picker(L10n.Tasks.status, selection: $selectedStatus) {
|
||||
Text(L10n.Tasks.selectStatus).tag(nil as TaskStatus?)
|
||||
ForEach(taskStatuses, id: \.id) { status in
|
||||
Text(status.displayName).tag(status as TaskStatus?)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Priority & Status")
|
||||
Text(L10n.Tasks.priorityAndStatus)
|
||||
} footer: {
|
||||
Text("Required: Both Priority and Status")
|
||||
Text(L10n.Tasks.bothRequired)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
.listRowBackground(Color.appBackgroundSecondary)
|
||||
|
||||
Section(header: Text("Cost")) {
|
||||
TextField("Estimated Cost (optional)", text: $estimatedCost)
|
||||
Section(header: Text(L10n.Tasks.cost)) {
|
||||
TextField(L10n.Tasks.estimatedCost, text: $estimatedCost)
|
||||
.keyboardType(.decimalPad)
|
||||
.focused($focusedField, equals: .estimatedCost)
|
||||
}
|
||||
@@ -227,7 +227,7 @@ struct TaskFormView: View {
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
.scaleEffect(1.5)
|
||||
Text("Loading...")
|
||||
Text(L10n.Tasks.loading)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
@@ -237,18 +237,18 @@ struct TaskFormView: View {
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackgroundPrimary)
|
||||
.navigationTitle(isEditMode ? "Edit Task" : "Add Task")
|
||||
.navigationTitle(isEditMode ? L10n.Tasks.editTitle : L10n.Tasks.addTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
Button(L10n.Common.cancel) {
|
||||
isPresented = false
|
||||
}
|
||||
.disabled(isLoadingLookups)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
Button(L10n.Common.save) {
|
||||
submitForm()
|
||||
}
|
||||
.disabled(!canSave || viewModel.isLoading || isLoadingLookups)
|
||||
|
||||
@@ -23,12 +23,12 @@ struct VerifyEmailView: View {
|
||||
.foregroundStyle(Color.appPrimary.gradient)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text("Verify Your Email")
|
||||
Text(L10n.Auth.verifyYourEmail)
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text("You must verify your email address to continue")
|
||||
Text(L10n.Auth.verifyMustVerify)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -42,7 +42,7 @@ struct VerifyEmailView: View {
|
||||
.foregroundColor(Color.appAccent)
|
||||
.font(.title2)
|
||||
|
||||
Text("Email verification is required. Check your inbox for a 6-digit code.")
|
||||
Text(L10n.Auth.verifyCheckInbox)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
.fontWeight(.semibold)
|
||||
@@ -53,7 +53,7 @@ struct VerifyEmailView: View {
|
||||
|
||||
// Code Input
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Verification Code")
|
||||
Text(L10n.Auth.verifyCodeLabel)
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
.padding(.horizontal)
|
||||
@@ -76,7 +76,7 @@ struct VerifyEmailView: View {
|
||||
viewModel.code = newValue.filter { $0.isNumber }
|
||||
}
|
||||
|
||||
Text("Code must be 6 digits")
|
||||
Text(L10n.Auth.verifyCodeMustBe6)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.padding(.horizontal)
|
||||
@@ -98,7 +98,7 @@ struct VerifyEmailView: View {
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
} else {
|
||||
Image(systemName: "checkmark.shield.fill")
|
||||
Text("Verify Email")
|
||||
Text(L10n.Auth.verifyEmailButton)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ struct VerifyEmailView: View {
|
||||
Spacer().frame(height: 20)
|
||||
|
||||
// Help Text
|
||||
Text("Didn't receive the code? Check your spam folder or contact support.")
|
||||
Text(L10n.Auth.verifyHelpText)
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -133,7 +133,7 @@ struct VerifyEmailView: View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "rectangle.portrait.and.arrow.right")
|
||||
.font(.system(size: 16))
|
||||
Text("Logout")
|
||||
Text(L10n.Auth.logout)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user