How (Not) to Create a Spell Server for Mac OS X

bugs

A while ago, I was asked to figure out how to implement a custom spell-checking service for Mac OS X. The service was commissioned by the people behind Remember, a remarkable project intended to prevent further use of the incorrect phrase “Polish death camps” by journalists and the general public. The task seemed trivial and a cursory search on the Web yielded a result I expected: the Foundation Framework contains the NSSpellServer class which would allow me to implement what I wanted in a fairly straightforward manner.

“Easy,” I thought, “what can go wrong?” And oh boy, if I only knew how wrong I was. Let me tell you a short story about the time I almost pulled half of my hair out.

I hope it will also serve as a good documentation of the current state of spell checking in Mac OS X, something I wish I have found when doing my own research.

Almost all of the tests were made on OS X 10.11 El Capitan.

The Documentation

Obviously, the first thing to check when doing research for a new project is documentation. Let’s forget for a moment about the above-mentioned class reference, and focus on ’Creating a Spelling Server.

Here’s a line from an example:

if ([aServer registerLanguage:"English" byVendor:"Acme"]) {

So far, so good. You just need to register a language name and vendor. But what about other languages? For Polish, should it be ’Polish’ or ’Polski’? Or what about British English, American English, etc.? The answer, which I discovered later, is: don’t follow the example. Instead, provide names using ISO codes: ’en_GB’, ’en_US’, ’pl’.

Let’s continue. Now, we will need a little something called Service Availability Notice. A couple of hints about what it is exactly can be found in the documentation.

A service descriptor is an entry in a text file called services. Usually it’s located within the bundle that also contains your spelling checker’s executable file. The bundle (or directory) that contains the services file must have a name ending in “.service” or “.app”. The system looks for service bundles in a standard set of directories. A spell checker service availability notice has a standard format, illustrated in the following example for the Acme spelling checker:

Spell Checker: Acme
Language: French
Language: English
Executable: franglais.daemon

The documentation mentions some ‘standard set of directories’, and as I later discovered these directories are ‘~/Library/Services’ and ‘/Library/Services’. The text file called ‘services’ is more intriguing. It is written in some kind of standard format, but it does not look like any standard familiar to me. And it is not a .plist file for sure.

A quick search for the system spelling server, called AppleSpell, gives us its location in /System/Library/Services. No ‘services’ file there, but a peek into its Info.plist makes everything clear:

NSServices NSExecutable AppleSpell NSLanguages da de en en_US

So, as it turns out, the documentation is outdated. Very outdated. So much that is not even useful anymore. By this point, I had no idea how to write an application using ‘NSSpellServer’, where to place it, and how to properly integrate it with the system. Fortunately, there was a GitHub project that helped me immensely with setting up a correct Xcode project: MacVoikko.

Building a ‘Hello world’ Spell Server

Naturally, the next step was to create a proof of concept, a simple spell server that would correct just one word. You can find our example project here.

It seems easy enough at first glance: just provide the range of the first misspelled word found, and in another method provide suggestions. Easy peasy. Naturally, in real life, the implementation would probably be a bit more complicated and would likely require additional tests in order to make sure that everything works correctly. Unfortunately, you would also need to test against your implementation of NSSpellServerDelegate, because NSSpellChecker would not be able to find your spell server if you just ran it from Xcode.

The Readme in the MacVoikko project was very helpful in that regard, stating that the service needs to be placed in ‘~/Library/Services’ or ‘/Library/Services’ folder. But placing it there didn’t seem to solve anything, because the Keyboard settings were not updated. One option that was working was to log out and then log back in. After searching for a while, I finally found a post wherein someone mentioned a tool called pbs.

If you want to refresh the list of spell servers, you need to close System Preferences, run:

pbs -flush && pbs -update and reopen the Keyboard settings. The list of spell servers should be updated by now. If you want to test it in a text editor, you probably need to reopen it, too.

How About ‘Automatic by Language’?

In Mac OS X 10.6, Apple introduced a new option for spell checking: Automatic by Language. When selecting that option, the system would automatically recognize the language that was used in a given sentence or paragraph. And, for AppleSpell, it works. If you are using multiple languages in your e-mails or documents, you may have noticed that particlar feature in action: using different languages in a single document causes it no trouble. But what if you wanted to implement a special spell server and still have AppleSpell working? Well, there is an option for that, you can check multiple spell servers for the same language under ‘Set up’ in the spell servers list.

If you now open a text editor, like TextEdit, and start writing, everything will work fine with AppleSpell. But your spell server will not be called at all. But what I have found, through trial and error, is that if you activate the spell server you set up as the main one, write some text in an editor, and then switch back to Automatic by Language, it just may work. Sort of, anyway: I discovered that AppleSpell will work until you write a word that your spell server will consider misspelled, and then will just stop working for that particular paragraph.

It turns out that I wasn’t the only one having problems: in 2009, someone trying to maintain a Latvian spell server had run into the same issues with ‘Automatic by Language’. See here for the e-mails.

Maybe I Will Try the New API.

Ok, that’s not all good news. It seems like the bugs from 2009 still have not been fixed. The e-mails also bring up another problems, the one with the method introduced in 10.5 to NSSpellServerDelegate: spellServer:checkString:offset:types:options:orthography:wordCount: In theory, this method should allow us to replace all other methods used to find misspelled words, suggest autocorrections, and completions. Alas, only in theory. In practice we will quickly come up against pretty serious problems.

  1. The method needs to return all results for misspelled words for the entire ‘checkString’ parameter. Setting ‘wordCount’ and returning partial results does not work. It is a step back from the old API, because now after every minor change in the paragraph the spell server needs to analyze whole text again.
  2. The system does not call for corrections in options, so you need to provide all of them. Combined with problem no. 1, it may slow down both the spell checking and text editor itself when a paragraph gets overly long.
  3. If you implement this new method, the system should not call the old ones. But it does. It sometimes calls spellServer:suggestGuessesForWord:inLanguage: for guesses (even if you provided corrections in the new method). But for TextEdit it also calls spellServer:findMisspelledWordInString:language:wordCount:countOnly: when you right-click on misspelled word.
    It all depends on the text editor you use: for example, Pages behaves different than TextEdit.

What it ultimately means is that if you want to use it and still provide corrections, you need to implement the older methods anyway.

What If I Want to Have AppleSpell Working Too?

Over the course of my previous experiments, I have learned that there was no easy way to create a spell server that could work alongside the operating system’s own AppleSpell. At one point I wanted to reverse engineer how OS X calls AppleSpell via the NSConnection class, but I did not have enough time to do it.

When I was close to finally give up, I received a great deal of help from my colleague and boss, Wojtek. He asked me what will happen if we try to register a spell server with a suffix, in the way ‘en_GB’ or ‘en_US’ is written. I didn’t follow up on the idea back then, mostly because it seemed plain wrong. How could the system correctly manage the list of languages when some of them are made up? A few days later, however, I gave it a try, and added a suffix to each of the names, like that: ‘pl_customname’, ‘en_US_customname’, ‘en_GB_customname’. To my surprise, it did work. The system picked up all of the languages correctly and, most importantly, was that it called my custom spell server with those names. And I could use those names to contact AppleSpell via the NSSpellChecker class, after just stripping out the ‘_customname’ suffix. That was great news and just after one day of working with code, the spell server started working as intended. It is a hack, I admit, and it works fine only on El Capitan. In previous versions of OS X, the system is not able to differentiate between versions of English in the keyboard settings. And, obviously, the ‘Automatic by Language’ option does not work in that instance, because we need to select one language from the spelling list.

It’s Great, Let Me Make an Installer. With Images, Please.

After all that hard work I felt great: I had my new spell server up and running, and I felt that everything will be fairly easy from now on. And it would have been, had I not wanted to provide images in the installer on the Introduction or Summary pages.

There are two ways to do it: via HTML or RTF files. I had some prior experience with HTML, so I thought it will be easy. Except the HTML cannot display images in the installer, either provided images or embedded inside HTML with Base64. It seems that it’s a known bug, images in HTML haven’t been working since OS X 10.9, as mentioned here on StackOverflow.

That left me with RTF files. Or, more exactly, with RTFD folders, as we’re talking about providing images. But what I have found was another well-known bug. productbuild used to create installers can’t handle .rtfd files properly, and finishes with a message about permission errors. Don’t try to check what is wrong with the permissions, because there is nothing wrong. One workaround is to use dummy .rtf files to create an installer, expand its contents, replace .rtf files with correct .rtfd ones, and repack everything again.

You can find the script I used for that particular purpose below; you may need to change paths and names, but you get the idea. The script assumes that the Distribution file and the Resources folder are located in the ./installer folder.

readonly EXP_PATH=/tmp/target_name.expanded
readonly TMP_PKG=/tmp/target_name.pkg
readonly INSTALLER_SIGN_ID="Developer ID Installer: your id (id)"
readonly CODE_SIGN_ID="Developer ID Application: your id (id)"
readonly CONFIGURATION="Release"

xcodebuild install -configuration "$CONFIGURATION" CODE_SIGN_IDENTITY="$CODE_SIGN_ID" &&  
pkgbuild --root /tmp/target_name.dst ./installer/target_name_unsigned.pkg &&  productsign --sign "$INSTALLER_SIGN_ID" ./installer/target_name_unsigned.pkg installer/target_name.pkg &&  
rm installer/target_name_unsigned.pkg &&  
productbuild --distribution installer/Distribution --resources installer/Resources --package-path ./installer $TMP_PKG &&  
rm -rf $EXP_PATH &&  pkgutil --expand $TMP_PKG $EXP_PATH &&  
rm -rf $EXP_PATH/Resources && cp -r installer/Resources $EXP_PATH &&  sed -i bak -e 's/-replace.rtf/.rtfd/' $EXP_PATH/Distribution &&  
pkgutil --flatten $EXP_PATH target_name_unsigned.pkg &&  
productsign --sign "$INSTALLER_SIGN_ID" target_name_unsigned.pkg target_name.pkg &&  
rm target_name_unsigned.pkg &&  
echo "Created target_name.pkg"

To Sum Up

It was my first project for Mac OS X and given all the problems I encountered, I hope that all other parts of the system are written in a much better way and I will not have to write a spell server again soon.

I also hope that Apple will fix all those bugs in the next release, but the fact that they are still there after all these years and that almost no one wants to implement their own spell checking servers makes me think that it’s not likely.

Most of the the bugs were reported recently, you can find them here on OpenRadar:
rdar://24710963 rdar://24710421 rdar://24710069

How to Work Effectively on Your Mobile App? Part 1: The Team
Our Farewell to an Eventful 2015