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

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