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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user