【Forwarded】How stringsdict and NSLocalizedString Works

Original url: How stringsdict and NSLocalizedString Works

When I wrote everything about iOS localization, I covered on plural support, but was brief.

This post I will explain the use of the powerful .stringsdict with a simple use, explaining the basics, then an advanced use. Lastly I will try to explain some magic under the hood.

Simple Case

It is easier to use an example and explain.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>%d file(s) are selected</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@[email protected] selected</string>
        <key>num_files_are</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>zero</key>
            <string>No file is</string>
            <key>one</key>
            <string>A file is</string>
            <key>other</key>
            <string>%d files are</string>
        </dict>
    </dict>
</dict>
</plist>

The code to use:

let numberOfFiles = 1
let format = NSLocalizedString("%d file(s) are selected", comment: "")
let localizedString = String(format: format, numberOfFiles)

There are 2 distinct calls here:

  1. You need to get the “format” using the macro NSLocalizedString. In the code above, the variable format will be %#@[[email protected]](/cdn-cgi/l/email-protection) selected. Note: This is not yet localized, unlike the usual localization!
  2. Init String with this format and the actual arguments, and you will get the localized string.

Basics of the XML:

  • %d file(s) are selected is simply a key to refer to this 1 string to localize. But it is NOT necessary to have the %d in the key. You can rename the key as eg files and it will work. Having the %d is more of a good convention, telling us 1 number should be provided as an argument.
  • %#@[[email protected]](/cdn-cgi/l/email-protection) selected is known as a format, which is made up of 1 variable num_files_are and the text “ selected”.
  • num_files_are is a key (to the variable) with a dict to explain the rules:
    • NSStringPluralRuleType is plural rules (there could be others in the future)
    • NSStringFormatValueTypeKey describe what type of argument works with this variable eg. d for Int
    • zero is a rule. When the argument is 0, it will use “No file is” to replace the variable num_files_are, therefore finally forming “No file is selected”
    • one is another rule
    • other is another rule. This time, it will use “%d files are”, and the argument is used here, finally forming eg. “2 files are selected”

[圖片上傳失敗...(image-620bea-1555568209760)]

Advanced Case

I like the example used in objc.io:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>scope.%lu out of %lu runs</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%1$#@[email protected]</string>
        <key>lu_completed_runs</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>lu</string>
            <key>zero</key>
            <string>No runs completed yet</string>
            <key>one</key>
            <string>One %2$#@[email protected]</string>
            <key>other</key>
            <string>%lu %2$#@[email protected]</string>
        </dict>
        <key>lu_total_runs</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>lu</string>
            <key>one</key>
            <string>run completed</string>
            <key>other</key>
            <string>of %lu runs completed</string>
        </dict>
    </dict>
</dict>
</plist>

The localized string requires 2 arguments:

let completedRuns = 1
let totalRuns = 2
let format = NSLocalizedString("scope.%lu out of %lu runs", comment: "")
let str = String(format: format, completedRuns, totalRuns)

Let’s look at the output first.

Completed runs    Total Runs    Output
------------------------------------------------------------------
0                 0+            No runs completed yet
1                 1             One run completed
1                 2+            One of x runs completed
2+                2+            x of y runs completed

It is an advanced use because the output depends on both the arguments.

The biggest difference is that this time, there are 2 variables, each with their set of rules, and one variable refer to another!

[圖片上傳失敗...(image-7c7029-1555568209760)]

  • The format is %1$#@[[email protected]](/cdn-cgi/l/email-protection), which uses 1 variable lu_completed_runs (yes, just 1 variable in the format is okay), and that it is the first argument as specified by the 1$
  • In some of the rules for lu_completed_runs, it uses a format %2$#@[[email protected]](/cdn-cgi/l/email-protection), which uses another variable lu_total_runs. This variable is the 2nd argument as specified by the 2$.
  • The rules in lu_total_runs will produce part of the text for lu_completed_runs

There is some recursive lookup going on :)

The Puzzling String

Let’s look at the simple case again to explain something strange that goes on.

let format = NSLocalizedString("%d file(s) are selected", comment: "")
// format is "%#@[email protected] selected"
let str = String(format: format, 1)
// str is "1 file is selected"

If you have been using String(format:_:), you know it can format the string and replace with the arguments.

But it is hard to digest for the format %#@[[email protected]](/cdn-cgi/l/email-protection) selected, a mere string.. How does a mere string have access to the rules in stringsdict?

The string %#@[[email protected]](/cdn-cgi/l/email-protection) selected is not a mere string. It knows the rules, for a particular locale.

Try this and it will NOT work:

// let format = NSLocalizedString("%d file(s) are selected", comment: "")
let format = "%#@[email protected] selected"
let str = String(format: format, 1)

This proves that the macro NSLocalizedString, which return a String via Bundle.localizedString, is not a pure string.

I am puzzled and I hope someone can explain.

I can see NSString use CFStringCreateWithFormatAndArguments. In CFString, it mentions stringsDictConfig. It seems like the system refer to this dictionary to look up the rules and format the final string.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章