英文:
What happened to the `Localizable.strings` files on devices in recent versions of iOS?
问题
在之前的iOS版本中,可以通过使用Bundle类来访问设备上系统资源的本地化。
例如,可以使用以下代码将Done
翻译为德语:
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)")
它会打印出如下所示的结果:
🌑 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
请注意,这种技术在模拟器上仍然有效(例如运行iOS 16.4的iPhone 14 Pro),但在实际设备上不起作用。
当在运行iOS 16.5.1的iPhone 11上运行相同的代码时,我得到以下输出:
🌑 file:///System/Library/PrivateFrameworks/UIKitCore.framework/de.lproj/
🚖 UITableViewLocalizedSectionIndex.plist
Done in German: _fallback_
我们可以看到翻译失败,因为Localizable.strings
和Localizable.stringsdict
文件已经消失了。
在最近的iOS版本中,这些文件发生了什么变化?我们还能以某种方式访问它们吗?
英文:
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:
<!-- language: swift -->
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:
<!-- language: lang-text -->
📁 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:
<!-- language: lang-text -->
📁 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?
答案1
得分: 2
简单的答案是:“它们被移动了”。
现在,UIKit在技术上是多个看起来像单个框架的框架。你可以自己验证一下。
首先,我们需要找到iOS模拟器运行时的“根”,我们可以使用find
命令来做到这一点:
% cd /Applications/Xcode14_3_1.app
% find . -name 'RuntimeRoot'
./Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot
这是模拟器中iOS运行时的等效目录。你会看到熟悉的System
、Library
、Developer
、Applications
等文件夹。
在System/Library/Frameworks
目录下,你会看到公共框架的列表。在UIKit.framework
目录下,你会看到几乎完全为空的UIKit内容。那它们在哪里呢?
如果你用Hex Fiend等工具打开UIKit
二进制文件,你可以查看文件的Mach-O配置结构:
看到有几个LC_REEXPORT_DYLIB
命令吗?这是dyld使用的命令,用来“假装”所有来自指定框架(在截图中是UIKitCore.framework)的符号都应该被视为来自这个框架。
换句话说,这些符号都存在于私有的UIKitCore.framework
中,但你的应用程序认为它们来自UIKit.framework
。
因此,当你向运行时请求包含UINavigationController
类的Bundle
时,它会报告它来自UIKit.framework
,而不是实际的UIKitCore.framework
。
由于我们知道系统的运行时根目录在哪里,我们可以手动检查该框架中有什么:
你瞧……本地化文件就在那里!
(插入关于依赖私有内容不是一个好主意且可能不稳定的标准警告,正如你已经发现的那样)
简而言之:本地化文件位于私有的UIKitCore.framework
中。
英文:
The simple answer is: "they moved".
These days, UIKit is technically multiple frameworks that look like a single framework. You can check this out yourself.
First, we need to find the "root" of the iOS simulator runtime, which we can do using the find
command:
% cd /Applications/Xcode14_3_1.app
% find . -name 'RuntimeRoot'
./Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot
This is the iOS runtime's equivalent of /
for the simulator. You'll see the familiar-looking System
, Library
, Developer
, Applications
, etc folders.
Inside System/Library/Frameworks
, you'll see the list of public frameworks. Inside UIKit.framework
, you'll see the almost-entirely-empty UIKit contents. So where are they?
If you open the UIKit
binary in a tool like Hex Fiend, you can view the structure of the file's Mach-O configuration:
See how there are a few LC_REEXPORT_DYLIB
commands? That's a command that dyld uses to "pretend" that all the symbols coming from the specified framework (UIKitCore.framework, in the screenshot) should be treated as if they're coming from this framework.
In other words, the symbols all live in the private UIKitCore.framework
, but your app thinks they're coming from UIKit.framework
.
Because of this, when you ask the runtime for the Bundle
that contains the UINavigationController
class, it will report that it's coming from UIKit.framework
, and not the actual UIKitCore.framework
.
Since we know where the runtime root of the system is, we can manually check to see what's in that framework:
And what do you know… there are the localization files!
(insert standard caveat here about how relying on private stuff is not a good idea and can be fragile, as you've discovered)
TL;DR: The localization files are in the private UIKitCore.framework
.
答案2
得分: 0
亚历山德罗·科鲁奇(Alexandre Colucci)在Mastodon上告诉我有一个新的Localizable.loctable文件,其中包含所有本地化字符串。
有了这个信息,我就能够在Bundle
类上编写一个扩展方法,以便从任何bundle中访问本地化字符串。
有两种实现方式可以获取所有本地化字符串的字典。一种是不使用私有API,而是使用未记录的.loctable
文件格式,另一种是使用私有的localizedStringsForTable:localization:
方法。
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, !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
}
}
使用方法非常简单,在所有iOS版本的模拟器和设备上都可以正常工作。
let done = Bundle(for: UIApplication.self).localizedString(forKey: "Done", localization: "de")
print("Done in German: \(done)")
这将按预期输出Done in German: Fertig
。
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论