String parameter in pluralized NSLocalizedString

As of macOS SDK 10.12/Xcode 8.3 this is still an issue, NSStringLocalizedFormatKey ignores the argument number and uses the first argument to determine the plurality for d_in_d_files_are_selected. BUT. The nested numbered references in the plural rule formats DO work, so e.g. "one" = "There is one file, and %2$#@it_is_selected@"; will correctly use the second argument to select the plural rule. This means that if you create a format proxy with a single other rule, you can achieve the behavior you wanted without rearranging the format string:

<key>%@ there are up to %i sun hours</key>
<dict>
    <key>NSStringLocalizedFormatKey</key>
    <string>%#@proxy@</string>
    <key>proxy</key>
    <dict>
        <key>NSStringFormatSpecTypeKey</key>
        <string>NSStringPluralRuleType</string>
        <key>NSStringFormatValueTypeKey</key>
        <string>i</string>
        <key>other</key>
        <string>%2$#@hours@</string>
    </dict>
    <key>hours</key>
    <dict>
        <key>NSStringFormatSpecTypeKey</key>
        <string>NSStringPluralRuleType</string>
        <key>NSStringFormatValueTypeKey</key>
        <string>i</string>
        <key>zero</key>
        <string>%1$@ there are no sun hours</string>
        <key>one</key>
        <string>There is one sun hour %1$@ </string>
        <key>other</key>
        <string>%1$@ there are up to %2$i sun hours</string>
    </dict>
</dict>

Note that for this to work, you have to be explicit with the argument numbers in the final format string ("%1$@ there are up to %2$i sun hours").


After solving my problem where I needed to pluralize based on second argument/variable, I have also found a solution to your problem without the need to change arguments' order or using proxy rule chaining. Thanks to the other posters for their answers and solutions which led me to experiments and better understanding of .stringsdict.

I think that my case is close to yours, but is a bit easier and may help you understand better how string format and rules are applied, so let me show starting with my example.

My case

I need to construct a string like 1 of 1 item selected or 1 of 5 items selected:

let countWithSelectionFormat = NSLocalizedString("%ld of %ld item(s) selected", comment: "Number of items with selection")
let countString = String.localizedStringWithFormat(countWithSelectionFormat, selectedCount, totalCount)

String format %ld of %ld item(s) selected is just a placeholder here, an alias. It is ignored in Localizable.strings, because it has been found first in Localizable.stringsdict. It is used only as a key to the rules in Localizable.stringsdict, but is not actually used to construct a string:

<?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>%ld of %ld item(s) selected</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%ld of %#@totalItems@ selected</string>
        <key>totalItems</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>ld</string>
            <key>one</key>
            <string>%ld item</string>
            <key>other</key>
            <string>%ld items</string>
        </dict>
    </dict>
</dict>
</plist>

The actual string format is listed in NSStringLocalizedFormatKey, it is %ld of %#@totalItems@ selected. Here the first argument selectedCount is passed through using %ld (without making it variable and listing rules for it). The second argument totalCount is parsed with %#@totalItems@ variable and its rules which return 1 item or, for example, 5 items, thus constructing correct strings.

If you need to change the order of arguments in output for some languages, you can use %2$#@totalItems@, %1$ld selected as NSStringLocalizedFormatKey.

You can also introduce second variable (and rules) if needed: %2$#@totalItems@, %1$#@selectedItems@.


Your case

My Swift code is essentially same as yours in Objective-C:

let stringFormat = NSLocalizedString("On %@ there are up to %ld sun hours", comment: "")
let string = String.localizedStringWithFormat(stringFormat, dayString, sunHours)

Localizable.stringsdict:

<?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>On %@ there are up to %ld sun hours</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@day@%#@hours@</string>
        <key>day</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>other</key>
            <string></string>
        </dict>
        <key>hours</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>ld</string>
            <key>zero</key>
            <string>On %1$@ there are no sun hours</string>
            <key>one</key>
            <string>There is one sun hour on %1$@</string>
            <key>other</key>
            <string>On %1$@ there are up to %2$ld sun hours</string>
        </dict>
    </dict>
</dict>
</plist>

In the NSStringLocalizedFormatKey I use 2 variables: %#@day@%#@hours@.

The day variable and rules are used to 'consume' the first argument dayString, we don't need any output here (fixed at the start of the sentence). I have used format value type d (as in %d for an integer) here as we don't need and we can't parse a string to choose an applicable plural rule. And we use only the required other rule, which returns an empty string. For the same reason there is no space between the day and hours variables in the string format.

The hours variable captures the second argument sunHours and its rules are in charge of constructing the actual output strings (whole sentences in this case). As I have to reference both arguments from inside the rules of second variable, I use %1$@ and %2$ld to refer to dayString and sunHours arguments respectively. Thus you can also use your variables in any combination and order.

This gives the desired results:

String.localizedStringWithFormat(stringFormat, dayString, 0) // On Monday there are no sun hours
String.localizedStringWithFormat(stringFormat, dayString, 1) // There is one sun hour on Monday
String.localizedStringWithFormat(stringFormat, dayString, 5) // On Monday there are up to 5 sun hours

References:

  • Internationalization and Localization Guide – Handling Noun Plurals and Units of Measurement
  • String Programming Guide – String Format Specifiers

Both examples were tested with Swift 5, Xcode 10.2.1, macOS 10.14.5, targeting macOS 10.12.


Based on the experimenting I've done, you need to change the order of your arguments. It seems that only the first can be used as the controlling value for the substitution rule.

This dictionary

<key>%i hours %@</key>
<dict>
    <key>NSStringLocalizedFormatKey</key>
    <string>%#@hours@</string>
    <key>hours</key>
    <dict>
        <key>NSStringFormatSpecTypeKey</key>
        <string>NSStringPluralRuleType</string>
        <key>NSStringFormatValueTypeKey</key>
        <string>i</string>
        <key>zero</key>
        <string>%2$@ there are no sun hours</string>
        <key>one</key>
        <string>There is one sun hour %2$@ </string>
        <key>other</key>
        <string>%2$@ there are up to %1$d sun hours</string>
    </dict>
</dict>

combined with

[NSString stringWithFormat:NSLocalizedString(@"%i hours %@", nil), sunHours, dayString];

produces the expected results for me. Note that I've added the argument indexes to the replacement strings so that the values are placed properly.

It looks like the docs are over-ambitious in their description of this functionality. The example given in the document titled "OS X 10.9 Release Notes" (though the link is for iOS) for example, implies that you should be able to switch on the second argument:

We're allowing the recursive formatting by applying the entire argument list to each substituted format specifier.

@"%d in %d files are selected" = @"%2$#@d_in_d_files_are_selected@"

The configuration dictionary can contain

"d_in_d_files_are_selected" = {

    "NSStringFormatSpecTypeKey" = "NSStringPluralRuleType"; // plural type
    "NSStringFormatValueTypeKey" = "d"; // int argument

    "zero" = "There is no file";
    "one" = "There is a file, and %1$#@it_is_selected@";
    "other" = "%1$d in %2$d files are selected"; 
};

But constructing a dictionary according to the guidelines there doesn't give the stated result. (And note the sample XML given immediately afterwards doesn't match this dictionary.)

There may be something I'm misreading, (or there may be a bug), but I haven't been able to suss out exactly what's going on. For now, I'm going to leave this as "changing the argument order will fix your problem".