i18n: complete app-wide localization (10 languages) + audit tooling
Android UI Tests / ui-tests (push) Has been cancelled
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:
@@ -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
Reference in New Issue
Block a user