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

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