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

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