Skip to content

Commit 542a74e

Browse files
committed
chore: plural localization
1 parent 88b4cdd commit 542a74e

File tree

2 files changed

+77
-2
lines changed

2 files changed

+77
-2
lines changed

Bitkit/Utilities/LocalizeHelpers.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,76 @@ enum LocalizationHelper {
5555
return key
5656
}
5757
}
58+
59+
/// Formats a string using ICU MessageFormat with pluralization support
60+
static func formatPlural(_ pattern: String, arguments: [String: Any], locale: Locale = Locale.current) -> String {
61+
// Convert arguments dictionary to format expected by MessageFormatter
62+
var formattedArgs: [String: Any] = [:]
63+
var argumentArray: [Any] = []
64+
var argumentNames: [String] = []
65+
66+
// Extract argument names and values, maintaining order
67+
for (key, value) in arguments {
68+
formattedArgs[key] = value
69+
argumentNames.append(key)
70+
argumentArray.append(value)
71+
}
72+
73+
return formatterPlural(pattern, arguments: arguments)
74+
}
75+
76+
// TODO: implement a ICU message format library
77+
/// Fallback pluralization formatter for when ICU MessageFormat isn't available
78+
private static func formatterPlural(_ pattern: String, arguments: [String: Any]) -> String {
79+
var result = pattern
80+
81+
// Handle basic plural syntax: {count, plural, one {...} other {...}}
82+
let pluralRegex = try! NSRegularExpression(pattern: "\\{(\\w+),\\s*plural,\\s*one\\s*\\{([^}]+)\\}\\s*other\\s*\\{([^}]+)\\}\\}", options: [])
83+
84+
let matches = pluralRegex.matches(in: pattern, options: [], range: NSRange(location: 0, length: pattern.count))
85+
86+
for match in matches.reversed() { // Process in reverse to maintain string indices
87+
let fullMatchRange = match.range
88+
let countVarRange = match.range(at: 1)
89+
let oneFormRange = match.range(at: 2)
90+
let otherFormRange = match.range(at: 3)
91+
92+
let countVarName = String(pattern[Range(countVarRange, in: pattern)!])
93+
let oneForm = String(pattern[Range(oneFormRange, in: pattern)!])
94+
let otherForm = String(pattern[Range(otherFormRange, in: pattern)!])
95+
96+
if let countValue = arguments[countVarName] {
97+
let count: Int = if let intValue = countValue as? Int {
98+
intValue
99+
} else if let doubleValue = countValue as? Double {
100+
Int(doubleValue)
101+
} else if let stringValue = countValue as? String, let intValue = Int(stringValue) {
102+
intValue
103+
} else {
104+
0
105+
}
106+
107+
let selectedForm = (count == 1) ? oneForm : otherForm
108+
var processedForm = selectedForm.replacingOccurrences(of: "#", with: "\(count)")
109+
110+
// Replace other variables in the selected form
111+
for (key, value) in arguments {
112+
if key != countVarName {
113+
processedForm = processedForm.replacingOccurrences(of: "{\(key)}", with: "\(value)")
114+
}
115+
}
116+
117+
result = result.replacingCharacters(in: Range(fullMatchRange, in: result)!, with: processedForm)
118+
}
119+
}
120+
121+
// Replace any remaining simple variables
122+
for (key, value) in arguments {
123+
result = result.replacingOccurrences(of: "{\(key)}", with: "\(value)")
124+
}
125+
126+
return result
127+
}
58128
}
59129

60130
// MARK: - Public API
@@ -71,6 +141,11 @@ func t(_ key: String, comment: String = "", variables: [String: String] = [:]) -
71141
return localizedString
72142
}
73143

144+
func tPlural(_ key: String, comment: String = "", arguments: [String: Any] = [:]) -> String {
145+
let localizedString = LocalizationHelper.getString(for: key, comment: comment)
146+
return LocalizationHelper.formatPlural(localizedString, arguments: arguments)
147+
}
148+
74149
// These are for keys that are not yet translated
75150
func tTodo(_ key: String, comment: String = "", variables: [String: String] = [:]) -> String {
76151
var localizedString = key

Bitkit/Views/Wallets/Activity/ActivityExplorerView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ struct ActivityExplorerView: View {
117117
)
118118

119119
if let txDetails {
120-
CaptionText("Inputs (\(txDetails.vin.count))")
120+
CaptionText(tPlural("wallet__activity_input", arguments: ["count": txDetails.vin.count]))
121121
.textCase(.uppercase)
122122
.padding(.bottom, 8)
123123
VStack(alignment: .leading, spacing: 4) {
@@ -133,7 +133,7 @@ struct ActivityExplorerView: View {
133133
Divider()
134134
.padding(.vertical, 16)
135135

136-
CaptionText("OUTPUTS (\(txDetails.vout.count))")
136+
CaptionText(tPlural("wallet__activity_output", arguments: ["count": txDetails.vout.count]))
137137
.textCase(.uppercase)
138138
.padding(.bottom, 8)
139139
VStack(alignment: .leading, spacing: 4) {

0 commit comments

Comments
 (0)