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
@@ -248,6 +248,18 @@ object DataManager : IDataManager {
private val _contractorSpecialtiesMap = MutableStateFlow<Map<Int, ContractorSpecialty>>(emptyMap())
val contractorSpecialtiesMap: StateFlow<Map<Int, ContractorSpecialty>> = _contractorSpecialtiesMap.asStateFlow()
// Home-profile field options (heating_type -> [options], etc.), served
// localized by the backend. Replaces the previously hardcoded client lists.
private val _homeProfileOptions = MutableStateFlow<Map<String, List<HomeProfileOption>>>(emptyMap())
val homeProfileOptions: StateFlow<Map<String, List<HomeProfileOption>>> = _homeProfileOptions.asStateFlow()
// Document type / category options ({value, display_name}), served localized.
private val _documentTypes = MutableStateFlow<List<HomeProfileOption>>(emptyList())
val documentTypes: StateFlow<List<HomeProfileOption>> = _documentTypes.asStateFlow()
private val _documentCategories = MutableStateFlow<List<HomeProfileOption>>(emptyList())
val documentCategories: StateFlow<List<HomeProfileOption>> = _documentCategories.asStateFlow()
// ==================== STATE METADATA ====================
private val _isInitialized = MutableStateFlow(false)
@@ -824,6 +836,15 @@ object DataManager : IDataManager {
setTaskCategories(seededData.taskCategories)
setContractorSpecialties(seededData.contractorSpecialties)
setTaskTemplatesGrouped(seededData.taskTemplates)
if (seededData.homeProfileOptions.isNotEmpty()) {
_homeProfileOptions.value = seededData.homeProfileOptions
}
if (seededData.documentTypes.isNotEmpty()) {
_documentTypes.value = seededData.documentTypes
}
if (seededData.documentCategories.isNotEmpty()) {
_documentCategories.value = seededData.documentCategories
}
setSeededDataETag(etag)
_lookupsInitialized.value = true
// Persist lookups to disk for faster startup
@@ -890,6 +911,9 @@ object DataManager : IDataManager {
_taskCategoriesMap.value = emptyMap()
_contractorSpecialties.value = emptyList()
_contractorSpecialtiesMap.value = emptyMap()
_homeProfileOptions.value = emptyMap()
_documentTypes.value = emptyList()
_documentCategories.value = emptyList()
_taskTemplates.value = emptyList()
_taskTemplatesGrouped.value = null
_lookupsInitialized.value = false