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:
Trey t
2025-12-02 02:02:00 -06:00
parent e62e7d4371
commit c726320c1e
59 changed files with 19839 additions and 757 deletions

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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))
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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()

View 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") }
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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 = ""

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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()
}
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)
}
}