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".