最近的iOS版本中,设备上的`Localizable.strings`文件发生了什么变化?

huangapple go评论81阅读模式
英文:

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.stringsLocalizable.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: &quot;de&quot;, withExtension: &quot;lproj&quot;)!)!
print(&quot;&#128193; \(bundle.bundleURL)&quot;)
for file in try! FileManager.default.contentsOfDirectory(at: bundle.bundleURL, includingPropertiesForKeys: []) {
    print(&quot;  &#128196; \(file.lastPathComponent)&quot;)
}
let done = bundle.localizedString(forKey: &quot;Done&quot;, value: &quot;_fallback_&quot;, table: &quot;Localizable&quot;)
print(&quot;Done in German: \(done)&quot;)

It was printing the following, just like expected:

<!-- language: lang-text -->

&#128193; 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/
  &#128196; Localizable.strings
  &#128196; Localizable.stringsdict
  &#128196; 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 -->

&#128193; file:///System/Library/PrivateFrameworks/UIKitCore.framework/de.lproj/
  &#128196; 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运行时的等效目录。你会看到熟悉的SystemLibraryDeveloperApplications等文件夹。

System/Library/Frameworks目录下,你会看到公共框架的列表。在UIKit.framework目录下,你会看到几乎完全为空的UIKit内容。那它们在哪里呢?

如果你用Hex Fiend等工具打开UIKit二进制文件,你可以查看文件的Mach-O配置结构:

最近的iOS版本中,设备上的`Localizable.strings`文件发生了什么变化?

看到有几个LC_REEXPORT_DYLIB命令吗?这是dyld使用的命令,用来“假装”所有来自指定框架(在截图中是UIKitCore.framework)的符号都应该被视为来自这个框架。

换句话说,这些符号都存在于私有的UIKitCore.framework中,但你的应用程序认为它们来自UIKit.framework

因此,当你向运行时请求包含UINavigationController类的Bundle时,它会报告它来自UIKit.framework,而不是实际的UIKitCore.framework

由于我们知道系统的运行时根目录在哪里,我们可以手动检查该框架中有什么:

最近的iOS版本中,设备上的`Localizable.strings`文件发生了什么变化?

你瞧……本地化文件就在那里!

(插入关于依赖私有内容不是一个好主意且可能不稳定的标准警告,正如你已经发现的那样)


简而言之:本地化文件位于私有的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 &#39;RuntimeRoot&#39;
./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:

最近的iOS版本中,设备上的`Localizable.strings`文件发生了什么变化?

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:

最近的iOS版本中,设备上的`Localizable.strings`文件发生了什么变化?

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) -&gt; 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: &quot;lproj&quot;), 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) -&gt; [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 ?? &quot;Localizable&quot;, withExtension: &quot;loctable&quot;)
        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((&quot;localizedStringsForTable:localization:&quot;))
        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: &quot;Done&quot;, localization: &quot;de&quot;)
print(&quot;Done in German: \(done)&quot;)

This prints Done in German: Fertig as expected.

huangapple
  • 本文由 发表于 2023年8月8日 21:56:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/76860253.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定