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

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