i18n: complete app-wide localization (10 languages) + audit tooling
Android UI Tests / ui-tests (push) Has been cancelled

Localize all user-facing strings across iOS (SwiftUI), shared Kotlin, and
Android Compose into en/es/fr/de/pt/it/ja/ko/nl/zh:
- iOS String Catalogs: main + widget Localizable.xcstrings, InfoPlist.xcstrings
  (permissions), plural variations, ~200 new keys translated
- Shared Kotlin ClientStrings table + Android composeResources/values-* (884 keys
  ×10), routed Api/ViewModel/util error & UI strings through localization
- Backend-localized lookups/suggestions consumed via display names
- Widget extension catalog; theme names, home-profile fallbacks, validation,
  network errors, accessibility labels all localized

Add re-runnable verification gates:
- scripts/i18n_audit.py  — enumerate every literal, partition to GAP=0
- scripts/i18n_coverage.py — all 10 locales translated, format-specifier parity

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-06-04 20:52:28 -05:00
parent 6058013951
commit db65db6232
211 changed files with 81756 additions and 22467 deletions
@@ -10,8 +10,13 @@ import kotlinx.serialization.Serializable
@Serializable
data class ResidenceType(
val id: Int,
val name: String
)
val name: String,
@SerialName("display_name") val displayNameLocalized: String = ""
) {
/** Localized label for the current locale; falls back to [name]. */
val displayName: String
get() = displayNameLocalized.ifBlank { name }
}
/**
* Task frequency lookup - matching Go API TaskFrequencyResponse
@@ -20,12 +25,13 @@ data class ResidenceType(
data class TaskFrequency(
val id: Int,
val name: String,
@SerialName("display_name") val displayNameLocalized: String = "",
val days: Int? = null,
@SerialName("display_order") val displayOrder: Int = 0
) {
// Helper for display
/** Localized label for the current locale; falls back to [name]. */
val displayName: String
get() = name
get() = displayNameLocalized.ifBlank { name }
}
/**
@@ -35,13 +41,14 @@ data class TaskFrequency(
data class TaskPriority(
val id: Int,
val name: String,
@SerialName("display_name") val displayNameLocalized: String = "",
val level: Int = 0,
val color: String = "",
@SerialName("display_order") val displayOrder: Int = 0
) {
// Helper for display
/** Localized label for the current locale; falls back to [name]. */
val displayName: String
get() = name
get() = displayNameLocalized.ifBlank { name }
}
/**
@@ -51,11 +58,16 @@ data class TaskPriority(
data class TaskCategory(
val id: Int,
val name: String,
@SerialName("display_name") val displayNameLocalized: String = "",
val description: String = "",
val icon: String = "",
val color: String = "",
@SerialName("display_order") val displayOrder: Int = 0
)
) {
/** Localized label for the current locale; falls back to [name]. */
val displayName: String
get() = displayNameLocalized.ifBlank { name }
}
/**
* Contractor specialty lookup
@@ -64,9 +76,25 @@ data class TaskCategory(
data class ContractorSpecialty(
val id: Int,
val name: String,
@SerialName("display_name") val displayNameLocalized: String = "",
val description: String? = null,
val icon: String? = null,
@SerialName("display_order") val displayOrder: Int = 0
) {
/** Localized label for the current locale; falls back to [name]. */
val displayName: String
get() = displayNameLocalized.ifBlank { name }
}
/**
* A selectable home-profile field option (heating type, roof type, etc.),
* served localized by the backend. [value] is the stable code stored on the
* residence; [displayName] is the localized label.
*/
@Serializable
data class HomeProfileOption(
val value: String,
@SerialName("display_name") val displayName: String
)
/**
@@ -103,7 +131,10 @@ data class SeededDataResponse(
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>,
@SerialName("task_templates") val taskTemplates: TaskTemplatesGroupedResponse
@SerialName("task_templates") val taskTemplates: TaskTemplatesGroupedResponse,
@SerialName("home_profile_options") val homeProfileOptions: Map<String, List<HomeProfileOption>> = emptyMap(),
@SerialName("document_types") val documentTypes: List<HomeProfileOption> = emptyList(),
@SerialName("document_categories") val documentCategories: List<HomeProfileOption> = emptyList()
)
// Legacy wrapper responses for backward compatibility