Painless localization of bundles with Xcode

In Cocoa world, NSLocalizedString() is the “official” way to localize a user-facing string data. It has been around for many years, and most of the time this mechanism works pretty well. Even better, it’s possible to use the XLIFF file format throughout a localization process since the release of Xcode 6. The IDE is smart enough to gather string resources defined in source files and to expose them via XLIFF. This support from IDE and external tools makes the localization process mostly smooth for small projects.

The process gets more clumsy when custom tables (other than “Localizable.strings”) or frameworks come into play. At that moment a relatively slender
NSLocalizedString(@”String”, “comment”)
transforms into a
NSLocalizedStringFromTable(@”String”, “Custom”, “comment”)
or, even worse, into a
NSLocalizedStringFromTableInBundle(@”String”, “Localizable”, [NSBundle bundleForClass:[self class]], “comment”).
With this amount of boilerplate code, an SNR drops below 25% and such inclusions make it hard to reason about the functional logic.

Some projects mitigate this issue by introducing another macro on top of existing, but this approach disables an ability of Xcode to gather localizable strings from the source code. During the refactoring of Nimble Commander’s file operations subsystem (i.e. moving a bunch of code into a separate framework), I found it relatively easy to deal with this issue within the Objective-C++ world.

Since NSLocalizedString is a macro, nobody can stop from removing it away via simple #undef. Next, NSLocalizedString can be defined again as a regular function without any preprocessor magic:

// Internal.h
#pragma once
#undef NSLocalizedString
namespace project {
...
NSString *NSLocalizedString(NSString *_key, const char *_comment);
...
}

This small workaround preserves Xcode’s ability to find and extract localizable strings and is syntactically identical to the macro definition, i.e. no changes are required within an existing code. An implementation of redefined NSLocalizedString is pretty straightforward:

// Internal.mm
#include "Internal.h"
namespace project {
...
static NSBundle *Bundle()
{
  static const auto bundle_id = @"com.company.project.framework";
  static const auto bundle = [NSBundle bundleWithIdentifier:bundle_id];
  return bundle;
}

NSString *NSLocalizedString(NSString *_key, const char *_comment)
{
  return [Bundle() localizedStringForKey:_key value:@"" table:@"Localizable"];
}
...
}

Obviously, “Internal.h” must be included only inside the framework’s source code.

 

Leave a Reply

Your email address will not be published. Required fields are marked *