In previous iOS versions, it was possible to access the localization of system resources on device by using the Bundle class.
For example, translating Done
into German was possible using the following code:
let bundle = Bundle(url: Bundle(for: UINavigationController.self).url(forResource: "de", withExtension: "lproj")!)!
print("📁 \(bundle.bundleURL)")
for file in try! FileManager.default.contentsOfDirectory(at: bundle.bundleURL, includingPropertiesForKeys: []) {
print(" 📄 \(file.lastPathComponent)")
}
let done = bundle.localizedString(forKey: "Done", value: "_fallback_", table: "Localizable")
print("Done in German: \(done)")
It was printing the following, just like expected:
📁 file:///Applications/Xcode-14.3.1.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/de.lproj/
📄 Localizable.strings
📄 Localizable.stringsdict
📄 UITableViewLocalizedSectionIndex.plist
Done in German: Fertig
Note that this technique is still working on the simulators (for example iPhone 14 Pro running iOS 16.4) but is not working on actual devices.
When running this same code on an iPhone 11 running iOS 16.5.1 I get the following output:
📁 file:///System/Library/PrivateFrameworks/UIKitCore.framework/de.lproj/
📄 UITableViewLocalizedSectionIndex.plist
Done in German: _fallback_
We can see that the translation fails because the Localizable.strings
and Localizable.stringsdict
have disappeared.
What happened to those files in recent iOS releases? Can we still access them somehow?
Alexandre Colucci informed me on Mastodon about the existence of a new Localizable.loctable file where all the localizations can be found.
With this knowledge I was able to write an extension method on the Bundle
class to access localized strings from any bundle.
There are two implementations to get the dictionary of all localized strings. One that does not use a private API but uses the undocumented .loctable
file format and one that uses the private localizedStringsForTable:localization:
method.
import Foundation
extension Bundle {
public func localizedString(forKey key: String, localization: String, value: String? = nil, table tableName: String? = nil) -> String {
if let localizedStrings = localizedStrings(forTable: tableName, localization: localization), let localizedString = localizedStrings[key] as? String {
return localizedString
} else if let url = url(forResource: localization, withExtension: "lproj"), let localizationBundle = Bundle(url: url) {
return localizationBundle.localizedString(forKey: key, value: value, table: tableName)
}
if let value, !value.isEmpty {
return value
}
return key
}
private func localizedStrings(forTable tableName: String?, localization: String) -> [String: Any]? {
#if DISABLE_PRIVATE_API
// This is not technically using a private API but it is using an undocument file and format
let loctableURL = url(forResource: tableName ?? "Localizable", withExtension: "loctable")
if let loctableURL, let data = try? Data(contentsOf: loctableURL) {
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: [String: Any]]
if let localizedStrings = plist?[localization] as? [String: Any] {
return localizedStrings
}
}
#else
// This is using a private API but one which is designed for this purpose
let localizedStringsForTable = Selector(("localizedStringsForTable:localization:"))
if responds(to: localizedStringsForTable), let localizedStrings = perform(localizedStringsForTable, with: tableName, with: localization)?.takeUnretainedValue() as? [String: Any] {
return localizedStrings
}
#endif
return nil
}
}
Usage is straightforward and works on both simulators and devices for all versions of iOS.
let done = Bundle(for: UIApplication.self).localizedString(forKey: "Done", localization: "de")
print("Done in German: \(done)")
This prints Done in German: Fertig
as expected.