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
+17 -11
View File
@@ -59,17 +59,21 @@ private func formatWidgetDate(_ dateString: String) -> String {
let dueDay = calendar.startOfDay(for: parsedDate)
if calendar.isDateInToday(parsedDate) {
return "Today"
return String(localized: "Today")
}
let components = calendar.dateComponents([.day], from: today, to: dueDay)
let days = components.day ?? 0
if days > 0 {
return days == 1 ? "in 1 day" : "in \(days) days"
return days == 1
? String(localized: "in 1 day")
: String(format: String(localized: "in %lld days"), days)
} else {
let overdueDays = abs(days)
return overdueDays == 1 ? "1 day ago" : "\(overdueDays) days ago"
return overdueDays == 1
? String(localized: "1 day ago")
: String(format: String(localized: "%lld days ago"), overdueDays)
}
}
@@ -380,7 +384,9 @@ struct FreeWidgetView: View {
)
}
Text(entry.taskCount == 1 ? "task waiting" : "tasks waiting")
Text(entry.taskCount == 1
? String(localized: "task waiting")
: String(localized: "tasks waiting"))
.font(.system(size: 14, weight: .semibold, design: .rounded))
.foregroundStyle(Color.appTextSecondary)
.multilineTextAlignment(.center)
@@ -423,7 +429,7 @@ struct SmallWidgetView: View {
)
if entry.taskCount > 0 {
Text(entry.taskCount == 1 ? "task" : "tasks")
Text(entry.taskCount == 1 ? String(localized: "task") : String(localized: "tasks"))
.font(.system(size: 11, weight: .semibold, design: .rounded))
.foregroundStyle(Color.appTextSecondary)
}
@@ -556,7 +562,7 @@ struct MediumWidgetView: View {
)
}
Text(entry.taskCount == 1 ? "task" : "tasks")
Text(entry.taskCount == 1 ? String(localized: "task") : String(localized: "tasks"))
.font(.system(size: 12, weight: .semibold, design: .rounded))
.foregroundStyle(Color.appTextSecondary)
@@ -748,7 +754,7 @@ struct LargeWidgetView: View {
}
if entry.upcomingTasks.count > maxTasksToShow {
Text("+ \(entry.upcomingTasks.count - maxTasksToShow) more")
Text(String(format: String(localized: "+ %lld more"), entry.upcomingTasks.count - maxTasksToShow))
.font(.system(size: 10, weight: .semibold, design: .rounded))
.foregroundStyle(Color.appTextSecondary)
.frame(maxWidth: .infinity, alignment: .center)
@@ -776,21 +782,21 @@ struct OrganicStatsView: View {
// Overdue
OrganicStatPillWidget(
value: entry.overdueCount,
label: "Overdue",
label: String(localized: "Overdue"),
color: entry.overdueCount > 0 ? Color.appError : Color.appTextSecondary
)
// Next 7 Days
OrganicStatPillWidget(
value: entry.dueNext7DaysCount,
label: "7 Days",
label: String(localized: "7 Days"),
color: Color.appAccent
)
// Next 30 Days
OrganicStatPillWidget(
value: entry.dueNext30DaysCount,
label: "30 Days",
label: String(localized: "30 Days"),
color: Color.appPrimary
)
}
@@ -816,7 +822,7 @@ struct OrganicStatPillWidget: View {
)
)
Text(label)
Text(LocalizedStringKey(label))
.font(.system(size: 9, weight: .semibold, design: .rounded))
.foregroundStyle(Color.appTextSecondary)
.lineLimit(1)
File diff suppressed because it is too large Load Diff