Skip to content

Commit ffdb297

Browse files
committed
fix: improve plural localization handling in LocalLocalizationExtractor and LocalizationProvider
1 parent 9fe51a3 commit ffdb297

File tree

3 files changed

+78
-31
lines changed

3 files changed

+78
-31
lines changed

Sources/CrowdinSDK/CrowdinSDK/Localization/Extractor/LocalLocalizationExtractor.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,20 @@ final class LocalLocalizationExtractor {
113113
func localizedString(for key: String) -> String? {
114114
// Check plurals first - if a key has plurals defined, those take precedence over simple strings
115115
// This fixes the issue where keys exist in both .strings and .stringsdict files
116-
var string = self.pluralsBundle?.bundle?.swizzled_LocalizedString(forKey: key, value: nil, table: nil)
117-
// Plurals localization works as default bundle localization. In case localized string for key is missing the key string will be returned.
118-
// To prevent issues with localization where key equals value(for example for english language) we need to set nil here.
119-
if string == key {
120-
string = nil
116+
var string: String?
117+
if let bundle = self.pluralsBundle?.bundle {
118+
// Use swizzled_LocalizedString only when Bundle is swizzled (methods are exchanged),
119+
// otherwise call localizedString directly to avoid infinite recursion.
120+
if Bundle.isSwizzled {
121+
string = bundle.swizzled_LocalizedString(forKey: key, value: nil, table: nil)
122+
} else {
123+
string = bundle.localizedString(forKey: key, value: nil, table: nil)
124+
}
125+
// Plurals localization works as default bundle localization. In case localized string for key is missing the key string will be returned.
126+
// To prevent issues with localization where key equals value(for example for english language) we need to set nil here.
127+
if string == key {
128+
string = nil
129+
}
121130
}
122131

123132
// If no plural exists, fall back to simple strings

Sources/CrowdinSDK/CrowdinSDK/Localization/Provider/LocalizationProvider.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,21 @@ class LocalizationProvider: NSObject, LocalizationProviderProtocol {
172172
func localizedString(for key: String) -> String? {
173173
// Check plurals first - if a key has plurals defined, those take precedence over simple strings
174174
// This fixes the issue where keys exist in both .strings and .stringsdict files
175-
var string = self.pluralsBundle?.bundle?.swizzled_LocalizedString(forKey: key, value: nil, table: nil)
176-
// Plurals localization works as default bundle localization.
177-
// In case localized string for key is missing the key string will be returned.
178-
// To prevent issues with localization where key equals value(for example for english language) we need to set nil here.
179-
if string == key {
180-
string = nil
175+
var string: String?
176+
if let bundle = self.pluralsBundle?.bundle {
177+
// Use swizzled_LocalizedString only when Bundle is swizzled (methods are exchanged),
178+
// otherwise call localizedString directly to avoid infinite recursion.
179+
if Bundle.isSwizzled {
180+
string = bundle.swizzled_LocalizedString(forKey: key, value: nil, table: nil)
181+
} else {
182+
string = bundle.localizedString(forKey: key, value: nil, table: nil)
183+
}
184+
// Plurals localization works as default bundle localization.
185+
// In case localized string for key is missing the key string will be returned.
186+
// To prevent issues with localization where key equals value(for example for english language) we need to set nil here.
187+
if string == key {
188+
string = nil
189+
}
181190
}
182191

183192
// If no plural exists, fall back to simple strings

Tests/UnitTests/BundleStringTests.swift

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,55 @@ class BundleStringTests: XCTestCase {
2424

2525
func testPluralLocalizationWithKeyInBothFiles() {
2626
// Test for issue #347: When a key exists in both .strings and .stringsdict files,
27-
// plural forms should take precedence over the simple string
28-
29-
// The key "johns_pineapples_count" exists in both:
30-
// - Localizable.strings: "John has pineapples" (simple fallback string)
31-
// - Localizable.stringsdict: proper plural definitions (zero/one/other)
32-
33-
// Format for zero (should use plural from stringsdict, not simple string)
34-
let formatZero = NSLocalizedString("johns_pineapples_count", comment: "")
35-
let stringZero = String.localizedStringWithFormat(formatZero, 0)
36-
XCTAssert(stringZero == "John has no pineapples", "Zero plural form should work: '\(stringZero)'")
37-
38-
// Format for one (should use plural from stringsdict)
39-
let formatOne = NSLocalizedString("johns_pineapples_count", comment: "")
40-
let stringOne = String.localizedStringWithFormat(formatOne, 1)
41-
XCTAssert(stringOne == "John has 1 pineapple", "Singular plural form should work: '\(stringOne)'")
42-
43-
// Format for multiple (should use plural from stringsdict)
44-
let formatMany = NSLocalizedString("johns_pineapples_count", comment: "")
45-
let stringMany = String.localizedStringWithFormat(formatMany, 5)
46-
XCTAssert(stringMany == "John has 5 pineapples", "Plural form should work: '\(stringMany)'")
27+
// plural forms should take precedence over the simple string.
28+
// This test validates the SDK swizzling path (LocalizationProvider) where the same key
29+
// exists in both strings and plurals dictionaries.
30+
31+
let localization = "en"
32+
let key = "johns_pineapples_count"
33+
let simpleStringValue = "John has pineapples"
34+
35+
let strings: [String: String] = [key: simpleStringValue]
36+
let plurals: [AnyHashable: Any] = [
37+
key: [
38+
"NSStringLocalizedFormatKey": "%#@pineapples@",
39+
"pineapples": [
40+
"NSStringFormatSpecTypeKey": "NSStringPluralRuleType",
41+
"NSStringFormatValueTypeKey": "d",
42+
"zero": "John has no pineapples",
43+
"one": "John has %d pineapple",
44+
"other": "John has %d pineapples"
45+
]
46+
]
47+
]
48+
49+
let localStorage = LocalLocalizationStorage(localization: localization)
50+
localStorage.strings = strings
51+
localStorage.plurals = plurals
52+
53+
let remoteStorage = MockRemoteStorage()
54+
let provider = LocalizationProvider(localization: localization, localStorage: localStorage, remoteStorage: remoteStorage)
55+
56+
Bundle.swizzle()
57+
Localization.current = Localization(provider: provider)
58+
defer {
59+
Bundle.unswizzle()
60+
Localization.current = nil
61+
provider.deintegrate()
62+
}
63+
64+
// The provider should return the plural form, not the simple string
65+
let result = provider.localizedString(for: key)
66+
XCTAssertNotNil(result, "Plural key should resolve to a non-nil value")
67+
XCTAssertNotEqual(result, simpleStringValue, "Plural should take precedence over simple string")
68+
69+
// Verify through Bundle.main which exercises the full swizzled path
70+
let format = Bundle.main.localizedString(forKey: key, value: nil, table: nil)
71+
let formattedOne = String.localizedStringWithFormat(format, 1)
72+
XCTAssertEqual(formattedOne, "John has 1 pineapple", "Singular plural form should work via swizzled path: '\(formattedOne)'")
73+
74+
let formattedMany = String.localizedStringWithFormat(format, 5)
75+
XCTAssertEqual(formattedMany, "John has 5 pineapples", "Plural form should work via swizzled path: '\(formattedMany)'")
4776
}
4877
}
4978

0 commit comments

Comments
 (0)