Home > Development, MacOSX > Automatically localize your nibs when building

Automatically localize your nibs when building

December 22nd, 2009

This post applies to all Cocoa apps, whether on the desktop or on the iPhone.

When you want to localize your application, you can take several routes.

No nibs, only .strings files

The first one is to not use nibs (or xibs, in the new parlance). If you build everything programmatically, +alloc and -init-ing your controls and your views, you can store all the strings in .strings files. These files are simple key-value pairs of Unicode-encoded strings. Call the files “Localizable.strings”, drop them in the appropriate language folders (English.lproj, French.lproj, etc…).

	English.lproj/
		Localizable.strings
			[file contents]
			"Dawn" = "Dawn";
			"Sunrise" = "Sunrise";
			"Sunset" = "Sunset";
			"Dusk" = "Dusk";
	French.lproj/
		Localizable.strings
			[file contents]
			"Dawn" = "Aube";
			"Sunrise" = "Lever";
			"Sunset" = "Coucher";
			"Dusk" = "Crépuscule";

To use, simply call call:

	NSString *str = NSLocalizedString(@"Dawn", nil);

This technique is important even if you have nib files, because most of your strings are probably in .strings files already. Don’t use hard-coded strings for anything that is user-visible. Only use hard-coded keys.

Use nibs

This looks easy: duplicate your English nib, move it to the appropriate folder, open it in Interface Builder and translate all the strings directly in IB.

This is very bad. Don’t do it.

  • It’s unmaintainable: as soon as you change one thing in the original nib, you have to do it in one or more other nibs.
  • It’s error-prone: you can easily disconnect a binding without realizing it, and Undo support in IB is spotty at best (I don’t rely on it). By the time you realize your mistake, it may be too late and you have to revert everything.
  • It’s not versionable: although xib files are XML files, they are not meant to be user-editable. Think of them as binary files. You wouldn’t merge binary files now, would you?
  • It’s hard to do: there are many nooks and crannies where localizable strings are found in IB (tooltips?), you’re bound to forget some of them in some language.
  • It needs a developer: you would not hand off a nib file to a translator and expect him to know all of the above: there is some training involved. And even if you have the Fear of God when editing nib files (because you’ve been burnt before by a disconnected IBOutlet), chances are your translator has no such qualms.

Translators want strings files, not nib files

You could use Wil Shipley’s approach of translating everything at run-time. That approach has one great advantage: everything is driven by strings files, which only contain strings (duh!). Your translator(s) will do a great job with them, and you can easily diff the results in your own version control system when the translations come back.

There are, however, drawbacks to this approach. Since everything happens at run-time:

  • You have to ship and execute more code than necessary (this is a minor point, but still valid for the iPhone since it is very resource-limited).
  • You can only see the results by launching your app in the appropriate language, switching using the International Settings panel, which is tedious.

A compile-time approach

My approach expands on Brian Dunagan’s use of ibtools and genstrings.

Xcode has excellent build scriptability due to Run Script build phases. In this example, we will use a script to generate a localized French xib from an English xib, using a strings file as a template for translation.

First, create a French version of your xib file by adding a French localization to your English xib file.
Assuming it is called “MainWindow.xib”, select the xib file, choose File > Get Info and in the “General” pane click “Add Localization”. Type “French”.

This will create the “French.lproj” folder, which will contain a copy of the English MainWindow.xib.

Next, add a Run Script phase to your application target:

Run Script build phase in Xcode

Finally, enter this as your script:

# Extract English strings (use this to check if you added new strings to your nibs)
ibtool --generate-strings-file Resources/English.lproj/MainWindow.strings Resources/English.lproj/MainWindow.xib
# Generate French interface
ibtool --strings-file Resources/French.lproj/MainWindow.strings --write Resources/French.lproj/MainWindow.xib Resources/English.lproj/MainWindow.xib

Repeat each pair of lines for each xib you need to localize, and adjust the names accordingly.

The first command extracts all the localizable strings from your English xib, and stores them in English.lproj/MainWindow.strings. This is your reference file: add it to version control, but you do not need to add it to your application. You can add it to your project, but make sure it is not included in your app bundle (it is useless at runtime).

The second command takes a French-localized version of the same strings file (French.lproj/MainWindow.strings) and, using the English xib as a template, generates the French xib.

Wait a moment…

If you followed so far, build your app. The script should fail, because French.lproj/MainWindow.strings does not exist yet. Just make a copy of English.lproj/MainWindow.strings and put it in the French folder. Just like the English MainWindow.strings, you want to add this file to version control and your project, but not to your app’s resources.

If you build again, everything should go fine and your French MainWindow.xib should be created… in English.

Translation

Of course, you have to translate the French MainWindow.strings. Mine looks a bit like this:

/* Class = "IBUIBarButtonItem"; title = "Done"; ObjectID = "138"; */
"138.title" = "Terminé";
/* Class = "IBUIBarButtonItem"; title = "Today"; ObjectID = "140"; */
"140.title" = "Aujourd'hui";
/* etc... */

You can send this file to your translator, and instruct her to only translate the words in quotes on the right-side of the equal (“=”) sign. Don’t touch anything else.

But… the Fear of God?

Everything else in this file is necessary for ibtool to do its job properly, and should not be touched. There are two safeguards against you (or your translator) accidentally touching this information:

  1. You have the original English file. When the file comes back from your translator, you can diff it against the original (which is in English, remember?) and see that she has followed your instructions (or not). It should be pretty easy to spot the differences with FileMerge.
  2. Every build, the script re-creates the English MainWindow.strings file. Your version control system should flag any differences immediately. For instance, if you added a label, you would see it in your new English file and you could apply the exact same change to your French file, and make a note to send it for translation again.

I found that these two safeguards more than compensated for the fact that the generated strings file are really source code, since they contain essential information for ibtool to translate properly.

Summary

Since everything happens at compile-time, my solution has none of the drawbacks of Wil’s solution:

  • No extra code needed.
  • You can look at a generated xib file in Interface Builder and immediately see any layout issues you might have. In this case, change the English xib, rebuild and check again.

Remember, these files should be added to version control, but not to your app bundle:

  • English.lproj/MainWindow.strings
  • English.lproj/MainWindow.xib
  • French.lproj/MainWindow.strings

And this file should be added to your app bundle, but not to version control (since it is generated each time):

  • French.lproj/MainWindow.xib

Now, every time you build, you will generate your French interface and it will be added to your app. Simple and efficient.

Categories: Development, MacOSX Tags:
  1. December 22nd, 2009 at 23:10 | #1

    This could get rid of so much maintenance time! My one question: if in German, a button’s title is twice as long as in English, how does ibtool handle the layout? That seems like a tough problem.

  2. December 22nd, 2009 at 23:23 | #2

    An excellent point, which this method does not address.

    In my previous job, experience showed that when designing for English, you needed to account for an extra 30% in space for “verbose” languages (notably French and German). This usually worked well enough, and is why I like to be able to see the localized nib in IB as opposed to always needing the running app.

    In my previous job (again) we developed a UI framework (hi, Dan!) that dynamically resized and re-arranged UI elements based on an XML layout with anchors and everything. It was pretty advanced, and shipped in at least two apps (Sketch Pad and Paint It! Touch, the latter being Win7-only).

    The downside to the programmatic layout is that they looked “plain”, almost boring. Very utilitarian. It was not easy to write the layout code; our QA kept finding edge cases. But they did work! And the apps shipped in 8 languages (EN, FR, DE, IT, JP, KR, CN, CT) with only UTF8-encoded XML files as translation matrices.

  3. December 23rd, 2009 at 03:42 | #3

    I think this is a really good solution, and it’s a good idea to have it run as part of your build. Thanks for sharing!

    A small note is that I’d put the new Run Script build phase *after* the Compile Sources build phase so that you fail early if you have any compile errors, rather than having to wait around for ibtool to do its thing before finding out you’re missing a semicolon. (Pro-tip: While you’re at it, move the “Copy Bundle Resources” build phase to be later also, for the same reason. It’s silly that Xcode’s default is to have it first.)

    At my job we have a similar approach, where we have a single xib layout that is copied to other languages and just the strings are substituted (though, the process is slightly different), and it works pretty well. It’s true that you have to leave extra space for some languages (German, I’m looking in your direction), but it usually still looks fine in the more conservatively-lengthed languages.

    I do, however, wish there was a solution for automatically checking if the translated strings all fit in the space given in their UI element, or if they are truncated. We end up having to do this manually, which is tedious and error-prone. If you have any suggestions about that, I’d love to hear them!

    Also, one catch to be aware of is that `ibtool –generate-strings-file` does not extract text out of NSTextViews, so you have to do that manually. I think the best solution is to not put in any text directly in the NSTextViews in the xibs. Instead, keep it in Localizable.strings (or separate .rtf files if you need rich-text), and fill them all in at run-time.

    Lastly, a small question/correction: Shouldn’t English.lproj/MainWindow.xib be added to both version control AND your app bundle?

  4. December 23rd, 2009 at 05:28 | #4

    Interesting idea. Myself I implemented something similar to Wil’s solution.
    One possible problem that I see with this is that the translations are done on strings that look like:
    “objectid.title” = “Localized string”.

    Sometimes, if you make some heavy changes to objects in XIBs, their IDs can change, then you French translation will miss.

    A better solution would be to use an intermediate file and will only include to translations and then add a script that will substitute the English text itself, irregardless of the object id.

    The one downside to this that I found is similar to Wil’s solution downside, mainly the case where one English strings need to be translated to different French strings because of the context.

    Still, I think it’s a safer approach.

  5. December 23rd, 2009 at 07:15 | #5

    Kelan Champagne :
    A small note is that I’d put the new Run Script build phase *after* the Compile Sources build phase so that you fail early if you have any compile errors, rather than having to wait around for ibtool to do its thing before finding out you’re missing a semicolon. (Pro-tip: While you’re at it, move the “Copy Bundle Resources” build phase to be later also, for the same reason. It’s silly that Xcode’s default is to have it first.)

    An excellent point which I do myself but forgot to mention. Thanks for reminding!

    I do, however, wish there was a solution for automatically checking if the translated strings all fit in the space given in their UI element, or if they are truncated. We end up having to do this manually, which is tedious and error-prone. If you have any suggestions about that, I’d love to hear them!

    It sounds like a good idea, and I even got to write some unit tests to check this. But in practice, like a lot of UI-related unit tests, they were very costly to maintain vs the percieved benefits. You want to look at your strings in the running app eventually (that should be part of your QA) and that is easier / more valuable than automated tests.

    Also, one catch to be aware of is that `ibtool –generate-strings-file` does not extract text out of NSTextViews, so you have to do that manually. I think the best solution is to not put in any text directly in the NSTextViews in the xibs. Instead, keep it in Localizable.strings (or separate .rtf files if you need rich-text), and fill them all in at run-time.

    Agreed. Again, thanks for reminding me.

    Lastly, a small question/correction: Shouldn’t English.lproj/MainWindow.xib be added to both version control AND your app bundle?

    Of course! Good catch. I have amended the post.

  6. December 23rd, 2009 at 07:26 | #6

    Jacob Gorban :

    Sometimes, if you make some heavy changes to objects in XIBs, their IDs can change, then you French translation will miss.

    You are technically correct (the best kind of correct!). I wondered about that too. But in my experience (albeit with small apps), I found that if I kept my refactorings small between each build or better, between each check-in, that problem never appeared.

    In my opinion, if you are still making heavy changes to your xibs, you are not ready to localize. You should have some kind of “UI freeze” milestone in your development after which any UI change needs to be carefully reviewed because of dependencies to localization, documentation, etc.

    A better solution would be to use an intermediate file and will only include to translations and then add a script that will substitute the English text itself, irregardless of the object id.

    In my experience that has not been necessary, and this may be a premature optimization.

    And for the record, I don’t think that Wil’s solution is bad. It has some significant advantages: only one nib vs one per language, only strings files for translators. I just wanted an alternative :-)

  7. December 23rd, 2009 at 11:28 | #7

    @Philippe
    Yes, that’s very tricky! I think that nice UIs must have the localization layouts done by hand. I just got a patch for Sparkle which fixed some localizations but made all buttons equal to the maximum length of that string across all languages. It looks awful! I wonder how Apple does it.

  8. December 23rd, 2009 at 16:17 | #8

    Well Andy why don’t you find out in … what … January or something? And tell us! :)

  9. February 3rd, 2010 at 04:29 | #9

    Excellent technique, thanks Philippe. I have just implemented this idea in DateLine, and it is working nicely. Whew, no more maintining 16 separate nib files!

  10. nobre
    March 22nd, 2010 at 15:14 | #10

    Hello, I just implemented this on an pretty big project, and it will save us TONS of time doing the localization for the three primary languages we target. Is there a way to identify when the original XIB files are unchanged ? Or even a way to do a “localized build” that will trigger the script, rather than always generate the strings file from the original xib and build a localized xib for each strings file ? My build times increased a lot… I’m currently deploying 20 xib files in 3 languages.
    Thanks for the nice article!

  11. March 24th, 2010 at 17:58 | #11

    I implemented this technique in the open source Murky http://bitbucket.org/snej/murky and David Keegan improved it with a Python script that basically creates an md5 hash of your original files, and quickly computes that to see if they have changed before unleashing the translation scripts. Speeds up builds significantly, as the md5-hashing is very quick. Check it out here: http://bitbucket.org/snej/murky/src/tip/Localize.py

  12. Christiaan Hofman
    March 25th, 2010 at 07:24 | #12

    @nobre
    To get an implicit check when .xib files are changed, add them to the list of Input/Output Files for the Run Script build phase. (I wish Xcode would make that easier by allowing drag&drop.) I personally have an extra check at the beginning of these scripts to only run during a Release build (if [ "$BUILD_STYLE" = Debug ]; then exit 0; fi), to save a lot of time during the much more regularly used Debug build. I just need to do at least one Release build after changing or adding a .xib.

  13. nobre
    March 25th, 2010 at 14:01 | #13

    @Philippe
    That’s a really nice script, it works flawlessly, just what I was looking for. Thank you!
    I still haven’t got the hang of the genstrings feature yet, it seems very handy as well.

  14. nobre
    March 25th, 2010 at 14:06 | #14

    @Christiaan Hofman
    As I understood it, should I need to add one Run Script phase for each XIB ? I didn’t exactly understand how to make they all run in a single script together with the Input/Output change, the feature says that if any of the input files differ from their output counterparts, the script will run.

  15. May 17th, 2010 at 21:20 | #15

    Can you expand on this statement: “This technique is important even if you have nib files, because most of your strings are probably in .strings files already. Don’t use hard-coded strings for anything that is user-visible. Only use hard-coded keys.”

    I do have XIBs, but would like to only use Localizable.strings files using genstrings if possible, and more importantly, only one XIB for all languages (as apposed to one for each). What do you mean by hard-coded keys? I am assuming you mean that all user-visible labels, should be programmatically set rather than with IB, correct? If that’s do-able I’d like to use this approach and avoid using IBTOOL altogether. Am I missing anything? Are their drawbacks to this approach?

  16. May 17th, 2010 at 22:32 | #16

    Ferris Gabriel :

    What do you mean by hard-coded keys? I am assuming you mean that all user-visible labels, should be programmatically set rather than with IB, correct?

    What I mean is, don’t depend on the return value of NSLocalizedString. It is common to see “NSLocalizedString(@”Some sentence that needs to be localized”, nil)” where you should be using “NSLocalizedString(@”SentenceLabel”, nil)” and in your strings file, you have the translation between “SentenceLabel” = “Some sentence that needs to be localized” for every supported language. That way, English is just a language, not a key.

  17. May 18th, 2010 at 10:46 | #17

    @Philippe
    Oh perfect, I see what you’re saying.
    Now, with that, and if all UI texts are set programmatically, then we should be OK just using genstrings (and not have to use IBTOOL), is this correct? For a small app, do you know if there’ll be any noticeable runtime performance degradation for the app always having to translate from string files, as oppose to pre-localizing the XIBs with IBTOOL). I’m trying to weigh the effort with IBTOOL and increase in bundle size incurred with additional XIBs (one for each language), against using genstrings alone? (sorry if this is too simple, this is our first iPhone project and therefore our first localization.)

  18. May 21st, 2010 at 19:23 | #18

    @Ferris Gabriel

    Sure, you could only have one nib and translate everything in strings. That’s kind of what Wil Shipley does.

    Or you could have no nib too. It’s really up to you to weigh the pros and the cons. I like nibs, other people, not so much.

  19. February 1st, 2011 at 07:27 | #19

    Great article. Yeah I’m a bit late finding it, but nonetheless… I’ve sort of adopted a project, and since I started on it, it’s grown to 5 languages, all maintained in separate nib files. I’ve found that (as highlighted by Andy above) I have to re-layout each language due to the different degree of verbosity in each language. Automatically generating the nib/xib files sounds great. It would neatly correct the fact that the project has already progressed to the point where interface builder has changed object-ids, etc in the nib files. Unfortunately, though I’m of the opinion that it is probably now more work to fix the layout each time than it is to ‘cope’ with supporting multiple nib files. to that end, I’ve taken some suggestions here, and now, each release will generate an updated strings file for each localized nib that will be put in SVN. this way I can get delta’s easily, send them to translators, and integrate their changes. I’ve also knocked up a small java app to automatically generate files for each language that contain only those strings from Localizable.strings that haven’t yet been translated. All in all, it’s still a bit manual, but I’ve got xcode doing some of the work, and I don’t have to redo the layouts every time.

  20. June 3rd, 2011 at 12:32 | #20

    You can localize automatically your nibs without duplicating them also following this simple guide:

    http://programminghacks.net/2011/06/03/ios-nib-localization-in-localizable-strings/

  21. rick
    July 1st, 2011 at 18:50 | #21

    This is a great tutorial here and since I’ve already been using ibtool this is probably the way to go for me. Question though…I have noticed that when I build my project these scripts update the nibs in my project, but I must build twice before the nibs in my binary are updated. I have tried moving the run script phase to the top and same result. Ok it’s not a huge issue since a clean and build would be done before actually releasing an app, but it would be nice to have one build update the nibs in my binary. Am I doing something wrong? Thanks!

    rc

Comments are closed.