Localizing NSPredicateEditor

Many people who know me personally (at least as a developer), know something interesting about me: I love NSPredicate. I love it so much that I stood up and blathered for an hour on how awesome it is. I won’t go into that here.

One of the things that make NSPredicate superbly awesome is the NSPredicateEditor. NSPredicateEditor, in a nutshell, is a way to visually build an NSPredicate, if you take the time to really dig in and understand what’s going on. Unfortunately that’s somewhat difficult, since the documentation on NSPredicateEditor is sparse, to say the least.

Customizing NSPredicateEditorRowTemplates is do-able, though you’re going to spend a few hours of trial-and-error programming getting it to work properly. However, if you want to localize the predicate editor, you are up a creek without a paddle. There is absolutely no documentation on it.

So, here’s a paddle.

First off, some introduction: localizing an NSPredicateEditor is a bit more complex than you would expect, because it works by displaying more-or-less natural-language phrases to the user. Localizing buttons and labels is easy, because we deal with isolated words such as “View file” or “Space used:” and so on. However, when you want to localize a whole sentence, suddenly you have to starting thinking about things like word order and the placement of prepositions in relation to nouns and verbs, and a whole host of other interesting challenges.

To illustrate: In English, when we meet someone new, we usually ask “Where are you from?” To analyze just the order of words, we have: Adverb verb pronoun preposition. In Spanish, we would ask “¿De donde es?”. Literally this means “From where are you?” (We could say this in English and people understand us, but it’s uncommon). In Spanish, the phrase is almost exactly backwards: Preposition adverb verb (with implicit pronoun). If we had a different view for displaying the adverb, and one for displaying the verb and pronoun, and one for displaying the preposition, then we would have to worry about rearranging our views to make the sentence readable. Saying “¿Donde es de?” (Literally: Where are you from?) may make sense if we were to literally translate this to English, but to a native Spanish speaker, you’re speaking nonsense.

Unfortunately, this is the precise problem we face with NSPredicateEditor. In English we might say “A is equal to B”, but it’s easy to imagine another language requiring something that’s more like “A and B are equal”.

To translate this discrepancy into NSPredicate-speak, we have to deal with “leftValue operation rightValue” versus “leftValue rightValue operation”.

Fortunately, there is a way to deal with this, and it relies on understanding three things:

  1. NSLocalizedString() and friends. I’m not going to cover this, since the documentation on how to localize is fairly straightforward
  2. printf-style positional specifiers. Here’s a basic example of what these are:

    NSLog(@"%2$@ %1$@", @"World", @"Hello"); //logs "Hello World"

    The “2$” and “1$” in the format specifier simply mean “use the second substitution value” and “use the first substitution value” (respectively). Easy.
  3. Using a special syntax for certain format specifiers (more on this later)

Now that we understand this, we can get on with localizing our NSPredicateEditor. We understand that we need NSLocalizedString() and that we can reposition substituted values using positional specifiers. Now let’s talk about the special format specifier syntax, in the context of another example.

For simplicity, let’s say we have an NSPredicateEditor with three row templates:

  • A compound row template ([Any, All, None] of the following are true)
  • A string row template (property [is, is not, contains, begins with, ends with, like, matches] a user-entered-string)
  • A selection row template (birthMonth is [January - December])

To properly localize these row templates, we simply need to produce some comments:

/**

NSLocalizedStringFromTable(@"%[Any, All, None]@ of the following are true", @"Predicate", @"localize the compound row template")
NSLocalizedStringFromTable(@"%[property]@ %[is, is not, contains, begins with, ends with, like, matches]@ %@", @"Predicate", @"localize the string row template")
NSLocalizedStringFromTable(@"%[birthMonth]@ %[is, is not]@ %[January, February, March, April, May, June, July, August, September, October, November, December]@", @"Predicate", @"localize the selection row template")

**/

Some explanation about what’s going on here:

  • Why are these in a comment?
    Because we don’t actually need this in the compiled code. We just need it in the source code so that the genstrings utility can find it. We won’t actually be using the NSLocalizedString() macros in our final, compiled version.
  • What’s with the square brackets?
    Because this is Objective-C, and we have to use square brackets everywhere, right? RIGHT???

In reality, the square brackets are what makes the magic happen. Each percent modifier (%@) represents a single view on the row template UI. Plain text (ex: “of the following are true”) is also represented as a view (specifically, an NSTextField).

For fields in a row template where you have multiple options, those options are comma-delimited within the square brackets. In the case of the compound row template, there are two fields: a popup that allows you to choose the kind of compound predicate (AND, OR, or NOT), and a field with some clarification text (“of the following are true”). The options for the template must match exactly (case, order, etc) as what you would see in the UI if you were to run your NSPredicateEditor without localization. If you mistype something, or mis-capitalize something else, that translation will not work.

Notice also that some fields don’t have anything to translate. The second row template shows “property”, some various string operators, and then %@. The %@ indicates that this will be replaced with something that cannot be translated. In this case, it would be an NSTextField, since we’re expecting the user to use this to enter some text.

When we run this code through the genstrings utility, we will get a file created (“Predicate.strings”) that contains this:

/* localize the compound row template */
"%[Any]@ of the following are true" = "%[Any]@ of the following are true";
"%[All]@ of the following are true" = "%[All]@ of the following are true";
"%[None]@ of the following are true" = "%[None]@ of the following are true";

/* localize the selection row template */
"%[birthMonth]@ %[is]@ %[January]@" = "%1$[birthMonth]@ %2$[is]@ %3$[January]@";
"%[birthMonth]@ %[is]@ %[February]@" = "%1$[birthMonth]@ %2$[is]@ %3$[February]@";
"%[birthMonth]@ %[is]@ %[March]@" = "%1$[birthMonth]@ %2$[is]@ %3$[March]@";
"%[birthMonth]@ %[is]@ %[April]@" = "%1$[birthMonth]@ %2$[is]@ %3$[April]@";
"%[birthMonth]@ %[is]@ %[May]@" = "%1$[birthMonth]@ %2$[is]@ %3$[May]@";
"%[birthMonth]@ %[is]@ %[June]@" = "%1$[birthMonth]@ %2$[is]@ %3$[June]@";
"%[birthMonth]@ %[is]@ %[July]@" = "%1$[birthMonth]@ %2$[is]@ %3$[July]@";
"%[birthMonth]@ %[is]@ %[August]@" = "%1$[birthMonth]@ %2$[is]@ %3$[August]@";
"%[birthMonth]@ %[is]@ %[September]@" = "%1$[birthMonth]@ %2$[is]@ %3$[September]@";
"%[birthMonth]@ %[is]@ %[October]@" = "%1$[birthMonth]@ %2$[is]@ %3$[October]@";
"%[birthMonth]@ %[is]@ %[November]@" = "%1$[birthMonth]@ %2$[is]@ %3$[November]@";
"%[birthMonth]@ %[is]@ %[December]@" = "%1$[birthMonth]@ %2$[is]@ %3$[December]@";
"%[birthMonth]@ %[is not]@ %[January]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[January]@";
"%[birthMonth]@ %[is not]@ %[February]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[February]@";
"%[birthMonth]@ %[is not]@ %[March]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[March]@";
"%[birthMonth]@ %[is not]@ %[April]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[April]@";
"%[birthMonth]@ %[is not]@ %[May]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[May]@";
"%[birthMonth]@ %[is not]@ %[June]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[June]@";
"%[birthMonth]@ %[is not]@ %[July]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[July]@";
"%[birthMonth]@ %[is not]@ %[August]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[August]@";
"%[birthMonth]@ %[is not]@ %[September]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[September]@";
"%[birthMonth]@ %[is not]@ %[October]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[October]@";
"%[birthMonth]@ %[is not]@ %[November]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[November]@";
"%[birthMonth]@ %[is not]@ %[December]@" = "%1$[birthMonth]@ %2$[is not]@ %3$[December]@";

/* localize the string row template */
"%[property]@ %[is]@ %@" = "%1$[property]@ %2$[is]@ %3$@";
"%[property]@ %[is not]@ %@" = "%1$[property]@ %2$[is not]@ %3$@";
"%[property]@ %[contains]@ %@" = "%1$[property]@ %2$[contains]@ %3$@";
"%[property]@ %[begins with]@ %@" = "%1$[property]@ %2$[begins with]@ %3$@";
"%[property]@ %[ends with]@ %@" = "%1$[property]@ %2$[ends with]@ %3$@";
"%[property]@ %[like]@ %@" = "%1$[property]@ %2$[like]@ %3$@";
"%[property]@ %[matches]@ %@" = "%1$[property]@ %2$[matches]@ %3$@";

Man, that’s a lot of stuff to localize! If you look closely, you can see that every occurrence of NSLocalizedString has been expanded for all possible combinations. So if you have a row template that supports 3 different left expressions, 6 different operators, and 7 different right expressions, then you have to translate 126 different things (3 * 6 * 7 = 126).

One thing that genstrings has conveniently done for us is insert positional specifiers! This allows us to rearrange the order of the string in a translated version, but still get the appropriate value substituted in.

Now that we have this .strings file, we can translate it just like any other strings file, and we can also rearrange fields as necessary:

"%[Any]@ of the following are true" = "%[Cualquiera]@ de las siguientes son verdaderas";
"%[All]@ of the following are true" = "%[Todas]@ de las siguientes son verdaderas";
"%[None]@ of the following are true" = "%[Ningún]@ de las siguientes son verdaderas";

...

"%[property]@ %[is]@ %@" = "%1$[propiedad]@ y %3$@ %2$[son iguales]@"; //literally "property and [value] are equal"
"%[property]@ %[is not]@ %@" = "%1$[propiedad]@ y %3$@ %2$[no son iguales]@";
"%[property]@ %[contains]@ %@" = "%1$[propiedad]@ %2$[contiene]@ %3$@";

...

The final step (beyond including the translated strings files as resources in your application) is that you need to tell your NSPredicateEditor where to find this information. There are two ways to do this:

  • -[NSPredicateEditor setFormattingStringsFilename:]
  • -[NSPredicateEditor setFormattingDictionary:]

The first option is usually the way to go. You simply give it the name of your .strings file sans extension (in this case, @"Predicate") and you’re good to go. If it comes across a syntax error, it’ll log it so you can debug it. The problem with this approach is that it only looks inside [NSBundle mainBundle] for the resource. So if you’re writing a plugin (such as a system preference pane), this won’t work.

Fortunately, there’s the second option, and it works like this:

NSString * stringsFile = [[NSBundle bundleForClass:[self class]] pathForResource:@"Predicate" ofType:@"strings"];
NSString * strings = [NSString stringWithContentsOfFile:stringsFile encoding:NSUTF16StringEncoding error:nil];
NSDictionary * formattingDictionary = [strings propertyListFromStringsFileFormat];
[predicateEditor setFormattingDictionary:formattingDictionary];

This is pretty straightforward: load the strings file into memory, use a built-in method to convert it to an NSDictionary, and then give that dictionary to the predicate editor.

So there you have it! An actual explanation on how to localize an NSPredicateEditor! Hopefully anyone who comes across this will find it useful, since it was a total pain to figure out. :)

Enjoy!

  1. do-nothing reblogged this from funwithobjc
  2. funwithobjc posted this
Short URL for this post: http://tmblr.co/Zt522y1OOtv6
blog comments powered by Disqus