Swiftgen: Parse Localizable.stringsdict file to support plurals

Created on 5 Oct 2016  ·  10Comments  ·  Source: SwiftGen/SwiftGen

I checked the whole repository to find some documentation about how to handle plurals with Swiftgen, but I didn't find anything about this issue. I've just found this issue https://github.com/AliSoftware/SwiftGen/issues/91 where it's discussed about .stringsdict so apparently Swiftgen supports plurals.

Could anyone give some light about how to tell Swiftgen to parse the Localizable.stringsdict to get the right enums for plurals?

medium enhancement

Most helpful comment

Hi,

As of today, SwiftGen does not parse stringsdict files. But that does not mean you cannot use SwiftGen when you have plurals.

Because it parses the Localizable.strings file, this means that all you need to have is entries that match your plurals in it. The only requirement is to have a numeral info (e.g. %d) in the translation so that you can pass a numeral information.

All you need to have in your Localizable.strings file is a key with a matching value:

"ratings" = "(%ld Ratings)";

and the plural data in your Localizable.stringsdict file:

<dict>
    <key>ratings</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@elements@</string>
        <key>elements</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>one</key>
            <string>(%d  rating)</string>
            <key>other</key>
            <string>(%d  ratings)</string>
            <key>zero</key>
            <string>(%d  rating)</string>
        </dict>
    </dict>
</dict>

SwiftGen will generate an entry taking a Int parameter and the NSLocalizedString will do the heavy lifting behind the scene to use your plurals.

Hope this helps ☺️

All 10 comments

I've used pluralisation in a few projects, and used this for now:

String(format: NSLocalizedString("(%ld Ratings)", comment: "Ratings count"), data.ratingsCount)

It just works™. I don't think it matters what you use, the whole point of the built in pluralisation system is that __you don't do anything__. It is handled behind the scenes. Just translate a strings key, and depending on the values you pass to it, it will choose a specific translation.

For reference, the corresponding stringsdict entry:

<dict>
    <key>NSStringLocalizedFormatKey</key>
    <string>(%#@ld_ratings@)</string>
    <key>ld_ratings</key>
    <dict>
        <key>NSStringFormatSpecTypeKey</key>
        <string>NSStringPluralRuleType</string>
        <key>NSStringFormatValueTypeKey</key>
        <string>ld</string>
        <key>zero</key>
        <string>No ratings yet</string>
        <key>one</key>
        <string>%ld Rating</string>
        <key>other</key>
        <string>%ld Ratings</string>
    </dict>
</dict>

PS: Yeah don't shoot me, I don't use swiftgen for strings yet 😞

Hi,

As of today, SwiftGen does not parse stringsdict files. But that does not mean you cannot use SwiftGen when you have plurals.

Because it parses the Localizable.strings file, this means that all you need to have is entries that match your plurals in it. The only requirement is to have a numeral info (e.g. %d) in the translation so that you can pass a numeral information.

All you need to have in your Localizable.strings file is a key with a matching value:

"ratings" = "(%ld Ratings)";

and the plural data in your Localizable.stringsdict file:

<dict>
    <key>ratings</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@elements@</string>
        <key>elements</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>one</key>
            <string>(%d  rating)</string>
            <key>other</key>
            <string>(%d  ratings)</string>
            <key>zero</key>
            <string>(%d  rating)</string>
        </dict>
    </dict>
</dict>

SwiftGen will generate an entry taking a Int parameter and the NSLocalizedString will do the heavy lifting behind the scene to use your plurals.

Hope this helps ☺️

Just to clarify:

  • I definitely want to support .stringsdict one day properly.
  • What @Liquidsoul explains is the right solution for now to overcome the fact that SwiftGen doesn't parse .stringsdict files _yet_. That doesn't mean it's the official solution that we plan keeping, just that it's the official way to workaround it in minimal effort so far
  • I'd love to validate a PR to add support for parsing .stringsdict files (in addition of .strings as we do now). But just be aware that the problem isn't that simple (and that's also one reason why we haven't have time to implement it yet)

The problem arise mainly when parsing which arguments to propose to your enum case, so when parsing those %d/%@/… placeholders. Because for each pluralisation variant there might be different placeholders, so what rule should we use in those case?


Some tricky examples:

<dict>
    <key>ratings</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@elements@</string>
        <key>elements</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>one</key>
            <string>%@ has one rating</string>
            <key>other</key>
            <string>%@ has %d ratings</string>
            <key>zero</key>
            <string>%@ has no rating</string>
        </dict>
    </dict>
</dict>

There you can see that one and zero have only one %@ placeholder but other exposes two (%@ and %d). So we probably want our case to expose %@ AND %d. This means we'll have to parse all variants, ensure consistency, and find the larger ordered set of the parameters we need.

This can become even more complex if the NSStringLocalizedFormatKey is composed of multiple ones… or if some variants reference other keys!

<key>geese.landed.ct</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%1$#@geese@</string>
        <key>geese</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>one</key>
            <string>A goose landed on %2$#@goose_fields@</string>
            <key>other</key>
            <string>%1$d geese landed on %2$#@geese_fields@</string>
        </dict>
        <key>goose_fields</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>one</key>
            <string>its own field</string>
            <key>other</key>
            <string>its own %3$d fields</string>
        </dict>
    </dict>
        <key>geese_fields</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>d</string>
            <key>one</key>
            <string>their shared field</string>
            <key>other</key>
            <string>their %3$d fields</string>
        </dict>
    </dict>

That would be quite fun to resolve the tree of possibilities when parsing that to determine the list of parameter types to pass to our case in such… case!

So I definitely would love to have that support and parsing of .stringsdict for sure, but that might not be as trivial as we can think. But I'm sure with some recursion and a good set of unit tests we can figure this out. Don't hesitate to propose a PR some day, I'd love to integrate it! And in the meantime, @Liquidsoul 's solution is the best one I can suggest.

Hi, maybe one way to go might be to simplify a bit. IMO for most cases are .stringsdicts too complex and very often plurals can be handled in an easier way. On many projects I use the ios-i18n library which just adds the ##{zero|one|few|...} and that's it.

This simpler format is also more understandable for my clients so they can localize it in an easier way.

Right now I use separate .strings file to separate my plurals because SwiftGen generated API is a bit messy which is of course understandable because of the way it works.

static let outOfStockPiecesFew = L10n.tr("product.out_of_stock_pieces##{few}")
 /// kus
static let outOfStockPiecesOne = L10n.tr("product.out_of_stock_pieces##{one}")
/// kusů
static let outOfStockPiecesOther = L10n.tr("product.out_of_stock_pieces##{other}")

I understand if this isn't the way SwiftGen wants to go, but I think it might be useful to discuss this option.

I think that if we write a parser, might as well write a full featured one and handle all the cases. The thing is that we need a good documentation of all the cases/possibilities, so some good files to test on would definitely be welcome. I have some stringdict's in my projects, but they're really basic.

Hi 👋🏻 I'd like to tackle this problem, but I will need a bit of support on this. I looked at the current implementation of the Strings.Parser and was debating whether to create a DictParser in the Strings namespace or a new StringsDict namespace with its own Parser.

Apart from that, I think I'd go a different route with parsing .stringsdict files. The Strings.Parser directly leverages the PropertyListSerialization and asserts to get a [String: String] result. With the stringsdict however, I think we can leverage the PropertyListDecoder/PropertyListEncoder and create a struct PluralEntry: Codable type to be able to handle most of the above mentioned edge cases.

Since Apple's documentation on .stringsdict files feels not very comprehensive, does any of you have (more advanced) example files that could be used as fixtures?

Hi @ffittschen and thanks for proposing to tackle this long-awaiting feature!

The Strings namespace is meant to host the parsers for the strings command, that's why it's called that way (and not because of the file extension of Localizable.strings). I think the goal would be to be able to parse both the .strings and .stringsdict file with the same unique subcommand, swiftgen strings (aka strings: key in the config file), so it would make more sense to host the DictParser in the Strings namespace to me.

Maybe you can get some inspiration by also looking how it's handled for the Colors parser: the swiftgen colors command is a single command but is able to parse inputs that could either be .txt files, Android's colors.xml files, .clr macOS color palettes, ... but they are all under the same namespace. Not sure if for stringsdict you'd adopt the _exact_ same model as for colors, but that could give you some ideas maybe.

I'm also OK to use Codable and the PropertyListDecoder protocol for this parser. The .strings parser was implemented before Swift's Codable so that's why we used the PropertyListSerialization I guess. Could be nice to rewrite this one too using Codable one day.

Regarding fixtures I don't have many .stringsdict files laying around on that computer, only the ones that got posted above, but hopefully more people would be able to provide some real-world examples? (call for contribution, anyone reading!)

In any way, even if the format can get quite complex because it can be recursive (second example in my last comment on this thread), since those complex cases are rare, even having an initial implementation supporting the more common case of non-recursive entries (the ones with only one level) could be an improvement towards the final solution and would be worth having at that stage 😉

Here's an example from a recent project I'm working on. Note that it's not only pluralization, but also "adaptive width strings" (NSStringVariableWidthRuleType).

Note: changed the extension to txt 'cause GitHub is being annoying about it.
Localizable.stringsdict.txt

Thanks for the fast answers!

I think the goal would be to be able to parse both the .strings and .stringsdict file with the same unique subcommand, swiftgen strings (aka strings: key in the config file), so it would make more sense to host the DictParser in the Strings namespace to me.

I haven't worked with the swiftgen colors command before, but looking at the Colors namespace I definitely agree to create subparsers for Strings as well!

Here's an example from a recent project I'm working on. Note that it's not only pluralization, but also "adaptive width strings" (NSStringVariableWidthRuleType).

This makes things a little more complicated 😄
It could be solved by creating a StringsDict enum with the cases pluralEntry and variableWidthRule. I would then filter for the pluralEntry cases. The variableWidthRule could then be used in some later SwiftGen releases.
The other solution would be to create the StringsDict as struct and only take elements of the dictionary into account that contain NSStringPluralRuleType entries.
The first solution would be more flexible but also more complicated to maintain.

I'll create a WIP PR soon so that you can take a look.

Implemented in SwiftGen 6.3.0, which has been released quite while ago (we're on 6.4.0 now). The remaining task of supporting adaptive strings is a separate issue, which can be found here: #762.

Was this page helpful?
0 / 5 - 0 ratings