Advanced programming tips, tricks and hacks for Mac development in C/Objective-C and Cocoa.

Background audio through an iOS movie player

Background audio in iOS is supposed to be as simple as entering a setting in your Info.plist and making sure your kAudioSessionProperty_AudioCategory is appropriate. This is true unless your audio is part of a movie file or is played in a movie player that has just played video — suddenly it becomes fiddly, hard to test, unreliable and changeable from version to version of iOS.

Introduction

I was not sure I wanted to write this post. It runs the risk of pointing out that I'm not perfect. But all programs have bugs and my programs are no different.

And anyway, as both Han Solo and Lando Calrissian validly said of the Millenium Falcon's failure to reach light speed, "it's not my fault". Of course, as it was in Star Wars, so it is in real life: your users don't care whose fault it is, they just want it fixed.

Obviously, I develop and sell a product named StreamToMe, available through the iOS App Store, that plays video and music and lists "Background audio" as one of its features. In this post, I'm going to talk about why background audio has worked and then not worked, been fixed and then not worked again only to be mostly fixed with some issues outstanding.

How can a feature that is simple, according to Apple's documentation, cause such a quality headache in a program?

In this post I'll be looking at playing background audio through the iOS movie playing APIs (either MPMoviePlayerController or AVPlayer/AVQueuePlayer). I've recently written a post on the history of iOS media APIs but as you'll see in this post, background audio is functionality that relates to the implications of the APIs, not the APIs themselves. You need to discover the "de facto" behavior yourself and hope you're correct.

Specific points will include:

  • why an application that also plays video has so much more difficulty with background audio than other kinds of applications
  • why background audio has broken multiple times in StreamToMe since iOS 4 was released, despite using no undocumented functionality and despite the documented API remaining nominally unchanged
  • why background audio is affected by seemingly unrelated choices like Apple's HTTP live streaming and 3G network

I'll also briefly look at quality management on a complicated program and how the largely undocumented behaviors of Apple's video APIs make perfect testing impossible.

Apple's documentation for background audio in iOS

Apple's documentation for background audio makes it sound very simple. It is 4 paragraphs long under the heading "Playing Background Audio" on the Executing Code in the Background page.

Additionally, Technical Q&A QA1668 discusses "How to play audio in the background with MPMoviePlayerController" by ensuring the Audio Session Category is correct.

Background audio is mentioned in a few other pages but it mostly repeats the information found in these two locations.

It all sounds pretty simple: it seems like background audio should "just work".

What happens to a file that contains video?

The movie players in iOS are explicitly capable of working in the background

But in the above linked Technical Q&A QA1668, the question explicitly mentions "audio-only movies and other audio files". There is no mention of what happens to files that have a video track.

In fact, there is no mention anywhere in the iOS documentation that I could find about what happens to a video file when you switch into the background.

All we can do is examine the behaviors experimentally. The following are the behaviors I've noticed in iOS 4.3 when switching video into the background.

Any file that contains a video track of any sort will be paused if the application switches into the background.

This pause is sent from the CALayer displaying the video frames. This is a private class for an MPMoviePlayerController and is your own AVPlayerLayer for an AVPlayer.

You can't really control this — even in the situation where it's your own AVPlayerLayer — the pause is sent from private methods (so you can't legally override them), during a private "UIApplicationDidSuspendNotification" (so you can't legally block or intercept this). This notification occurs between the UIApplicationWillResignActiveNotification and the UIApplicationDidEnterBackgroundNotification.

Nor can you simply disconnect the AVPlayerLayer of an AVPlayer to avoid the pause being sent — this actually leads to a crash if the file is still playing for reasons that are not explained and could be either a bug in iOS or expected behavior (it's not at all clear).

If you attempt to start a file playing video in the background it will fail with an error

While a video file started in the foreground will simply pause, a video file started in the background will actually give an error abort playback entirely.

This can even occur for a file that was pausing on entering the background but which you attempt to resume.

If you attempt to play a file without video but the previous file contained video, the new file will also fail in many cases

The video system in iOS has a degree of latency between commands you request and actual changes in playback.

My guess (again, none of this is explained in the documentation) is that this latency occurs because your video commands need to be sent to the separate mediaserverd process in iOS that handles all media playback. This process then makes the required changes and sends back response notifications.

This seems to create a situation where if you cancel the playback of a file and immediately start a new file, some of the properties of the old file will remain for a time.

In the case of playing an audio-only file immediately after a video file, this latency appears to be long enough for the audio-only file to be rejected with an error as though it was a file with video.

Even a file with the video tracks disabled will still fail

If you're using an AVPlayer or AVQueuePlayer, you can disable all the video tracks any time after the AVPlayerItemStatusReadyToPlay notification is sent using the following code:

for (AVPlayerItemTrack *track in player.currentItem.tracks)
{
    if ([track.assetTrack.mediaType isEqual:AVMediaTypeVideo])
    {
        track.enabled = NO;
    }
}

This will stop the tracks playing but despite the tracks being disabled, the effect on background play remains the same: presence of video in the file still causes the player to pause.

How StreamToMe has handled video in the background

As you can tell by the summary of experimentally determined functionality above, iOS really strongly doesn't want you to play video in the background.

Frankly, iOS's restrictions in this area are contrary to what people want.

Where iOS makes a huge distinction between audio-only media and media with both video and audio, many users do not. We are accustomed to Quicktime and iTunes and VLC and MPlayer and most other media applications being able to perform all the same tasks with either video or audio.

Even for users who only use StreamToMe to play music, it's hard to avoid video in StreamToMe because StreamToMe puts a still image for the album artwork into a video track to display artwork for music files — in the eyes of iOS, basically every files StreamToMe plays counts as video.

It was necessary to find a way around these restrictions. And so begins the story of half a dozen application updates over 3 major iOS updates.

iOS 4.0

In iOS 4.0, StreamToMe used MPMoviePlayerController and was able, through a bizarre sequence of layer manipulation operations in the MPMovieMediaTypesAvailableNotification method (basically removing the video render layer and reinserting at the right time), to convince the MPMoviePlayerController to proceed, even when it was playing video in the background.

Technically, you didn't need to remove the layer to get it to play in the background (all you needed was to resume after the "UIApplicationDidSuspendNotification" pause) but if you didn't remove the video layer, video frames would still be rendered and queued for display, leading to out of memory problems or weird speedy video quirks when the video came back to the foreground.

I'm not going to share the code that did this: it was messy, not advisable and doesn't work anymore. I was fully aware that this was a bizarre thing to do and that I would need to keep a really close eye on iOS updates to ensure that it kept working.

iOS 4.2

From the betas of iOS 4.2, it became apparent that the layer manipulation would no longer work to allow background video to work smoothly and no combination of actions I could find would make it work again. Playing the audio from a file also containing video looking like it would be impossible.

Fortunately, with StreamToMe, I control both ends of the client-server communication and there was another solution: upon entering the background, StreamToMe could reload the stream from the server with the video track stripped off by the server.

This server reconnection results in a second pause or so (more over 3G) while the new stream was started and sometimes a jump back to the start of the previous HTTP live stream segment but otherwise the experience is tolerable.

However, there was a catch: MPMoviePlayerController didn't like being torn down and recreated in a short space of time. In iOS 4.2, doing this would actually result in an error.

But the new AVQueuePlayer API introduced in iOS 4.1 did support queuing a new stream and then switching to it. In fact, it did it pretty well (after all, that's what the whole "queue" is about). Unfortunately, switching to AVQueuePlayer from MPMoviePlayerController is not a small task: AVQueuePlayer offers no user interface (you have to implement one entirely for yourself) and the entire property observation model is completely different.

The following code sample shows how a switch to a background track was managed in the UIApplicationDidEnterBackgroundNotification. A new "background" variant of the URL for the current item is generated by the STMQueuePlayerController (the StreamToMe class that relates the StreamToMe representation of files to the AVQueuePlayer represenation) is generated, seeked to the same point as the current file, inserted into the queue and played.

if (resyncTask)
{
    [[UIApplication sharedApplication] endBackgroundTask:resyncTask];
}
resyncTask = [[UIApplication sharedApplication]
    beginBackgroundTaskWithExpirationHandler:^{resyncTask = 0;}];

AVPlayerItem *backgroundItem =
    [[AVPlayerItem alloc]
        initWithURL:[[STMQueuePlayerController sharedPlayerController]
            urlForFile:[[STMQueuePlayerController sharedPlayerController] currentFile]
            inBackground:YES
            offset:CMTimeGetSeconds(player.currentTime)]];
[backgroundItem                              // seek the item, not the player
    seekToTime:player.currentTime
    toleranceBefore:kCMTimeZero
    toleranceAfter:kCMTimeZero];
[player insertItem:backgroundItem afterItem:currentItem];

[self stopObservingPlayerItem:currentItem];  // stop observing the old AVPlayerItem
[currentItem release];
currentItem = [backgroundItem retain];
[self startObservingPlayerItem:currentItem]; // begin observing the new AVPlayerItem

[player advanceToNextItem];
[player play];

The resyncTask is ended when this new file sends an AVPlayerItemStatusReadyToPlay and is used to ensure that we don't get suspended while restarting the playback.

Needing to rewrite the code for AVQueuePlayer left a brief gap at the start of iOS 4.2 until StreamToMe 3.3 was released, where background audio was broken in StreamToMe.

iOS 4.3

But iOS 4.3 turned out to be a bit of a one-two punch. On paper, the big change was AirPlay video — the new feature in iOS 4.3 that didn't work with AVQueuePlayer (seriously) — but it turns out that iOS 4.3 also changed how movie players were paused when going into the background. This change to pausing behavior was not clear to me until after iOS 4.3 was released, so StreamToMe's background behavior broke again.

What happened is that StreamToMe used to read whether the stream was currently playing (i.e. not paused) and only transition to the background version of the stream if it was actively playing. Unfortunately, the -[AVPlayer rate] which previously returned 1.0 for a previously playing video stream during the UIApplicationDidEnterBackgroundNotification would now return 0.0 (i.e. reporting that the stream was paused).

The fix is pretty simple: when we receive UIApplicationWillResignActiveNotification we needed to record whether the current file was playing or paused and use that information later in the UIApplicationDidEnterBackgroundNotification (the private "UIApplicationDidSuspendNotification" that pauses the file occurs between these two notifications).

Unfortunately, I didn't realize until the last moment on an update that the AVPlayerLayer had also started pausing audio-only files, not just files with video. To me, this seems like a significant change in behavior; why should an audio-only file suddenly start getting paused when the application enters the background? It's not my fault but I need to fix it anyway — unfortuntely due to the slowness in realizing this problem, this separate fix for audio-only files in StreamToMe (files with neither video nor album artwork in a video track) had to be held over until the 3.5.4 update.

More than StreamToMe was affected: iOS 4.3 actually broke background video for Apple's apps too. While Apple's apps (iPod, Movies, Safari, YouTube) have always paused the current video when switching into the background, you used to be able to resume the video from the multitasking bar, lock screen or headphones. From iOS 4.3, this behavior has been blocked; the video may play for a fraction of a second but then will immediately stop again.
3G and slow WiFi affecting background audio?

Even after fixing these problems it turns out iOS 4.3 had one more suprise. It now appears that the code I showed above that handles the track change:

[player insertItem:backgroundItem afterItem:currentItem];
[player advanceToNextItem];
[player play];

will work on a local WiFi network but on a high latency WiFi or 3G connection can cause the proper, background-safe version of the file (which is the "next item" loaded here) to be rejected.

Why on earth would the speed of the network affect this?

I'm not entirely certain but it appears that when the network is fast enough, the command:

[player insertItem:backgroundItem afterItem:currentItem];

actually fetches the first segment of the stream and updates all the track information, so it correctly realizes that there is no video track.

But on a slower network, this first segment of the stream is not loaded so the call to [player play]; immediately results in an error and the file being rejected from the stream.

The fix for this is that you need to defer the call to [player play]; until after a AVPlayerItemStatusReadyToPlay notification is sent for the new file.

Yuck.

Why was this not caught in testing?

As I write this, the current version of StreamToMe is 3.5.4 and it still contains this 3G/slow WiFi problem.

Yes, I know what the cause of the bug is. Yes, I already have a fix for it. Unfortunately, the agony of release cycles and the nuisance of the App Store approval process means that I'm going to sit on this fix until I've finished the other features I wanted to include in the next update — the background audio over 3G/slow WiFi is simply too narrow a niche to justify an update right now.

However, there's one thing I've noticed about media application users: people seem to use their media within specific niches and if their specific niche isn't working, they're prepared to eviscerate you.

How StreamToMe and ServeToMe are tested

As an independent developer, it is very difficult to handle quality assurance. I don't have a dedicated tester; I have a few people who help me test but they're all volunteers and tend to use the application however they feel. They're not really robust testers. While I use the application all day, I don't really exercise the whole application: on any given day, I focus on pretty specific issues.

Despite these resource limitations, I do have a pretty extensive set of tests. Unfortunately, the scope of the application means that the tests are arguably too extensive for my ability to run them all.

For file compatibility, I have 280 different test files in a regression suite that I run (literally media files in a folder that I run through the program and process the log file to ensure no unexpected errors). This takes 8 hours.

For server functionality, I have a test harness that tests every server command (fortunately, this takes just 30 seconds).

For client functionality, I have a 166 step, user-operated test script. This takes about an hour to perform, sitting in front of the application, pressing buttons in order.

Just 10 hours for these steps but it only tests 1 version of the program.

If you include all the different platforms for which there is platform-specific code, there are 4 versions of the server that need testing (Windows XP, Windows 7, Mac OS X 10.5, Mac OS X 10.6) and 6 versions of the client (iOS 3 on any iOS device, iOS 3.2 iPad, iOS 4 on iPhone 3G, iOS 4 on iPhone 3Gs, iOS 4 on iPhone 4, iOS 4 on iPad).

You should realize that just running this suite in these testing environments would take me about a week. And that's if I worked non-stop on StreamToMe, which I don't.

But the bug slips through: how do you fix it?

It is unreasonable for me to fully test minor releases and sometimes minor issues slip through. Needing to limit testing so that it is manageable has resulted in some minor bugs but it does not describe why this latest 3G/slow-WiFi problem escaped testing.

Even if I had run my full test suite on version 3.5.4 of StreamToMe, it would not have detected the problem between 3G and background audio. This is because the test script tested background audio on local WiFi — you don't generally insert repeats of tests into your script unless you suspect something about the repeat in a new context will actually affect the test. In this case, I had no reason to suspect that the two ideas would be connected.

An interesting thought to consider here: the code coverage through my program is identical on local WiFi and 3G. The difference is either somewhere in Apple's code or purely a timing problem.

All you can do in these situations is add the scenario to your test cases, fix the bug and makes sure it keeps getting tested in new releases.

Conclusion

I hope you can see that even when APIs are documented, the usage and implications of the API can be unknown and subject to misunderstandings and change over time. The fact that video cannot play in the background is barely mentioned by the documentation but the details of video being paused, stopped or rejected with an error is completely absent from documentation (you will only discover this by experimentation).

Lack of information is always hard to deal with in testing. You can only exercise documented or otherwise suspected behavior, and even so, you need to be practical. You can't simply say: test everything about background audio. You need to formulate your tests based on what you think is likely to have different effects.

The decision by Apple to forbid video in the background is frustrating and puzzling from my perspective. Why can't iOS simply ignore video packets in the background — particularly for disabled tracks? I can only presume that there's a technical reason for this behavior but since we haven't been informed of the boundaries, it remains frustrating.

Additionally, the entire iOS environment makes this type of problem exceptionally difficult to characterize and test. UI automation is insanely difficult in iOS and even if it improved (I'm keeping my eye on Cucumber+Frank) it probably wouldn't be able to exercise background switches and realtime and network issues easily.

User interface strings in Cocoa

In this post, I'll look at best practice for using and managing text strings in your user interface. This is a fairly simple topic but Cocoa has established "best practices" for handling user interface strings that new Cocoa developers should be aware of. Since it is inevitably related, I'll also look at the steps involved in localizing the strings in your applications but remember: you should follow good practice for string handling, even if you have no intention of ever translating your application.

Introduction (the wrong way)

Putting a text string in your user interface is not a difficult thing to do on a technical level. In code, filling in text can be as simple as setting the text property of a UILabel to a literal string:

someUserInterfaceLabel.text = @"Text to display";

(This code is for an iOS UILabel. On Mac OS X, you would set the stringValue property of an NSTextField but otherwise the step is the same.)

While this will work, you should never set a user interface string this way.

Setting labels with literal strings (the right way)

The most thorough way to put a literal string into your Cocoa application's user interface is:

someUserInterfaceLabel.text =
    NSLocalizedStringFromTable(
        @"Text to display",       // the native language string
        @"SomePageLabels",        // the category
        @"Label display string"); // a comment describing context

This is pretty verbose though. It is often okay to just use:

someUserInterfaceLabel.text = NSLocalizedString(@"Text to display", nil);

If you take no other steps, this will produce exact the same output as the "wrong way" example.

You should always use the NSLocalizedString[...] macros for every user interface string in your code.

But wait... this NSLocalizedString[...] stuff requires more typing and unless you take yet more additional steps, it won't have any functional difference? If I'm not planning to translate my program right now, aren't they a complete waste of time?

Why NSLocalizedString is important, even if you don't intend to translate

Obviously, the NSLocalizedString[...] functions (and the less common CFCopyLocalizedString[...] variants) are functions that exist to enable localization (i.e. letting you translate your application into different languages).

Technically, they're not even functions — they're just macros that invoke the -[NSBundle localizedStringForKey:value:table:] method — but you should always use the macro and not the underlying method for reasons I'll discuss in the "Mechanics of Translation" section below.

However, even if you're not intending to ever localize your application, you should always use NSLocalizedString.

There are a few reasons for this:

  1. Futureproofing: The future is hard to predict: you never know if you'll want to translate in the future. Needing to go through your code and find rogue literal strings is time consuming and prone to mistakes. Instead, everything should always have NSLocalizedString from the beginning.
  2. MVC practices: It keeps the exact details of your model/presentation layer at least one level of indirection removed from your controller code. In some cases, you can simply change the .strings files for your program to update the user interface and not need to change code due to this separation.
  3. Separation of concerns: It clearly identifies text strings intended for user presentation as opposed to text strings used as keys for programming use only.
  4. Discourages other bad practices: with your user interface strings detached from your controller, you'll be less likely to try to read static strings back from the user interface (a very bad idea) or place programmer-targetted strings in the user-interface.

Get into the habit of using NSLocalizedString. It's really simple to do — even when you're hacking code together quickly, you should be able to use it.

The first two points in the previous list are self-explanatory but the second two merit further explanation.

Separation of concerns

It is always helpful in programming to be able to glance at code and understand the intent. Consider the following piece of code in isolation:

[someDictionary setObject:@"value" forKey:SomeKeyNameString];
[someDictionary setObject:NSLocalizedString(@"value", nil) forKey:SomeOtherKeyNameString];

Without knowing what someDictionary is for or what the purpose of the SomeKeyNameString and SomeOtherKeyNameString values are, we know that the second string is intended for display in the user interface at some point whereas the first string is purely for tracking a value internally.

This clear labelling of intent is helpful as strings for user display have a very different role in a program compared to strings for internal use.

Discourages other bad practices

If you treat NSLocalizedString in your mind as though its output is a black box, this can help you avoid poor controller design when managing user interface elements. It can act as a conceptual tool to encourage you to design things the right way, instead of a lazy way.

Your controller code should treat user interface strings as something that can be written but not read. Reading static strings back from the user interface is always bad (it ends up being a form of "common or data coupling" — a bad design practice).

In the "Separation of concerns" example above, you might consider that since the keys SomeKeyNameString and SomeOtherKeyNameString are defined in global variables in this example, that perhaps you'd want to define your localized strings in global variables. In most cases a global variable for a user string is actually a bad idea.

We define dictionary keys in global variables because more than one location in the program may need to use exact the same value or the exchange of information between the two points will fail. But with user interface strings, you should never have a second piece of code that requires the exact same value: you should never read back from the user interface or require user interface collusion. Generally, the only situation where the same string should appear multiple times is if the same user interface code is displaying it (i.e. you're drawing the same object) but in this case, the code is common and the string should only need to appear once in the code.

If you need to uniquely identify a label or the state of a text displaying item, testing the text it contains is the wrong way to do that. A far better way is to use the tag value of any UIView/NSActionCell and then map the tag value onto the object's role or function (tag is a pointer sized value so you can store a non-retained object reference here if needed, not just an integer). The tag property is not reserved for any other purpose; it is intended for the controller to track user interface items and their state.

Mechanics of translation (when you're ready)

Eventually, you may actually have to translate your program. Let's look at the steps involved.

Create your ".strings" files

The files you need to translate are the ".strings" files in your application. By default though, your project probably won't have any ".strings" files (except possibly an InfoPlist.strings file which is for translating your Info.plist file's strings).

The first step is to make sure you have a localized directory somewhere (probably in the Resources subdirectory of your project's folder). The localized directory should be named "en.lproj" if you're starting with English strings, otherwise you'll want to replace "en" with the appropriate ISO 639-1 or ISO 639-2 designators. If needed you can use the script and region identifiers too as described in Apple's Language and Locale Designations.

A note on folder names: it is common to see "English.lproj" used as the name for English localization instead of "en.lproj" — in fact, Xcode 3 still generates folders with this name if you Get Info on a file and select "Make file localizable". Apple have stated that these old, "full" names are deprecated from Mac OS X 10.4 onwards in favor of ISO 639-1 or ISO 639-2 designators. Don't use the old "English.proj" style names anymore and replace with "en.lproj" if it is autocreated (yes, you might need to update your Xcode paths if you change the folder name).

Now we can create ".strings" files automatically from all the NSLocalizedString references in your program. To do this, open a Terminal in your Project's root directory and run the following command:

find -E . -iregex '.*\.(m|h|mm)$' -print0 | xargs -0 genstrings -a -o Resources/en.lproj

This will process all .m, .h and .mm files in your directory hierarchy and create ".strings" files for them in the en.lproj directory (note, the en.lproj directory must already exist). This assumes that the localized resources directory you created is located at "Resources/en.proj", relative to your Project's root directory; obviously, you'll need to change this if your localized resources are elsewhere.

The ".strings" file will be filled with entries that look like this:

/* This comment provided comes from the last parameter of the NSLocalizedString macro. */
"Some UI string %@ to translate %@" = "Some UI string %1$@ to translate %2$@";

Your translator just needs to translate the right-hand side of the equality statement. Notice that placeholders in your strings are given ordinal positions (1 and 2 in this case) so that the translation can change the order of placeholders if necessary (obviously, if you use placeholders, you should include a comment that explains what they're going to be).

Localization versus Internationalization: generally, the whole process of creating new language variants is referred to as localization. In reality though, it comprises two steps:
  1. Internationalization: where you decouple the program from the original locale
  2. Localization: where you add translations and behaviors for each new locale
By that terminology, the inclusion of NSLocalizedString wrappers and the creation of ".strings" files is the "Internationalizing" phase.
genstrings will only handle static NSLocalizedString and CFCopyLocalizedString strings

The only strings that will be automatically extracted are those wrapped in NSLocalizedString[...] and CFCopyLocalizedString[...] macros. Obviously, all your user interface text needs to be wrapped in these but also remember that the underlying -[NSBundle localizedStringForKey:value:table:] method will not be automatically extracted.

Why would you ever use -[NSBundle localizedStringForKey:value:table:] directly then? The answer is for dynamically generated strings.

The genstrings command will raise an error if it detects anything other than a static string in the localization macros. This is appropriate because you don't want your translators translating variable names and function calls (they only need to translate the results of those calls).

The answer to why you would use -[NSBundle localizedStringForKey:value:table:] is then: the actual strings to be translated are located elsewhere in the code (or are in a ".strings" file that was not generated from code) and you are simply looking them up dynamically.

Encoding problems

From Mac OS X 10.5 onwards, you can put any UTF-8 characters in your NSLocalizedString constants. Prior to this, they were required to be pure 7-bit ASCII with all Unicode escaped with \\Uxxxx style escaping or you could use MacRoman with the -macRoman command-line option to use MacRoman high-ASCII characters.

A quick swipe at almost everybody: UTF-8 has been around since 1993 and Unicode 2.0 since 1996; if you have created any 8-bit character content since 1996 in anything other than UTF-8, then I hate you.

I weep to think of the years of programmer time that are still wasted attempting to support non-Unicode formats without characters getting garbled because people are still creating content using ancient encodings without useful identifiers to indicate what nonsense encoding they're using (or worse, people creating content that explicitly uses the wrong encoding for an encoding-specific text field).

MacRoman? Atrocious. Big-5? I hope you want to see garbage output. Windows Latin? You suck. If you're creating new content using anything other than UTF-8, UTF-16 or UTF-32 then you should be forced to serve prison time with whatever idiot monkey decided that UTF-16 should be allowed little-endian and big-endian variants instead of a single authoritative encoding.

The actual text files generated by genstrings are UTF-16 in whatever byte order your system happens to use. i.e. UTF-16BE on PowerPC and UTF-16LE on Intel Macs.

Grumble.

Translating XIB files

Not all your strings will come from your code. The other common text location is in XIB files. XIB files can be a little bit trickier than strings in code due to two factors:

  1. While you can extract the strings from a XIB file easily, you also have to merge them back in once the translation is complete — basically another step that can go wrong
  2. The ".strings" file format extracted from XIB files is uglier and doesn't have easy room for comments to send to the translator

For these two reasons, I generally avoid putting text in XIB files if reasonably possible — it is normally easier to have text inserted at NIB load time by the code. Of course, menus, button labels and automator labels can't reasonably be moved into code so you're still likely to need a number of XIB files translated.

You extract the .strings from XIB files in a similar way to extracting the strings from code. However, first we must make all of our XIB files localizable (if they aren't already).

To localize your XIB files, select them all in the Xcode project Group Tree, Get Info on them and then from the first tab, select "Make file localizable".

Then, go to the localized directory where your files all ended up (if there's multiple, you'll need to do this for each one) and run the following in Terminal:

for file in *.xib; do
    ibtool --export-strings-file "$file".strings "$file"
done

This will generate all the ".strings" files for your XIB files.

Once the ".strings" files are localized, create a new ".lproj" directory with the appropriate language name for the new translations and put all the ".strings" files in it. Then open a Terminal in this new folder and run:

for file in *.xib.strings; do
    basename=`basename "$file" .strings`
    ibtool --strings-file "$file" --write "$basename" "../en.lproj/$basename"
done

This will merge all the ".xib.strings" files in the current directory with the XIB files from the en.lproj directory, creating the translated XIB files.

Translating other resources

The same "Make file localizable" step that we used for the XIB files in the previous section can be applied to any resource file in your Xcode group tree so you can localize other resources in whatever way is apppropriate.

Here's a tip though: avoid localizing anything other than strings and XIB files by whatever means possible. Having non-strings files for translation will cause you nothing but pain and suffering.

In particular: avoid localizing images. Work as hard as you can to keep all text out of images (except in logos that don't require translation). You can perform quite sophisticated drawing and text handling in Cocoa code if needed and this will almost always be easier than localizing images.

I haven't really touched on non-string code localization topics in this post. There's date, time, numbers, error descriptions and other stuff — most of the time, the classes and APIs for these make it clear what you need to do. Just read Apple's Internationalization documentation.

Conclusion

Most programmers should already know the information in this post. Numerous other Mac programming blogs have discussed the topic:

See how anciently old those second two links are? I'm not telling you new information. The advice remains the same: always, ALWAYS use NSLocalizedString for your user interface strings.

Actual translation can happen later or not at all — my point is that your need for translation (or lack of need) should not determine whether you use NSLocalizedString as it is best practice in any case. Of course you can rest assured that translation will all work out if your code is already NSLocalizedString-compliant.