Convenient wrappers for localizedStringForKey:value:table:

Given how often one might need to return a localised string, the default method for it is not the most comfortable to use:

[NSBundle.mainBundle localizedStringForKey:@"translate_me" value:@"If not found, display me." table:nil];

And indeed Apple has provided a couple of macros that should give us shorthands for a few common localisation scenarios:

NSLocalizedString(key, comment)
NSLocalizedStringFromTable(key, tbl, comment)
NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment)
NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment)

But notably missing from these, at least in my opinion, is the macro where the only two parameters are the key and the fallback string. Something like this:

NSLocalizedString(key, val)

So you might have written a macro yourself for exactly this purpose. Maybe something like this:

#define OUNTranslate(string, fallback) [NSBundle.mainBundle localizedStringForKey:(string) value:(fallback) table:nil]

But macros have their downsides. Although Xcode (or, more precisely, Clang) gives us a warning if we make any of the common mistakes when using macros (e.g. mismatching types or redefining other macros), two problems still come to mind:

  • Xcode’s refactoring tools don’t work on macros. And Xcode’s refactoring tools are great, especially from Xcode 9 onward.
  • You can’t get a list of callers for a macro like you do for a method or a function.
  • So instead of a macro we will create a function or a method. And we will choose its syntax to be such that it will be easy to trigger auto-complete on it.

    So… How?

    Way 1: Use Swift

    In Swift, you can do just this:

    1. Create an empty Swift file, say, ‘Translate.swift’.
    2. Have this as its only content:
    import Foundation
    
    func translate(_ string: String, fallback: String?) -> String {
        return Bundle.main.localizedString(forKey: string, value: fallback, table: nil)
    }
    

    And that’s it. Now you have a convenient, short function that you can call like so:

    let t = translate("translate_me", fallback: "If not found, display me.")
    

    Way 2: Create a class method in Objective-C

    If you can’t go with Swift, you can create a short-ish class method in Objective-C like so:

    @interface OUNTranslate : NSObject
    
    + (NSString *)translate:(NSString *)string fallback:(NSString *)fallback;
    
    @end
    
    @implementation OUNTranslate
    
    + (NSString *)translate:(NSString *)string fallback:(NSString *)fallback {
        return [[NSBundle mainBundle] localizedStringForKey:string value:fallback table:nil];
    }
    
    @end
    

    Then you can use it like this:

    NSString *t = [OUNTranslate translate:@"translate_me" fallback:@"If not found, display me."];
    

    Way 3: Create a C function

    If the class method is still too unwieldy for our taste, we can achieve a shorter version by going with a C function instead. To go this route, do this:

    1. Create an empty header file, say, ‘OUNTransate.h’.
      • You can of course put the implementation in the .m file if you don’t want to make the implementation public.
    2. Have this as the header’s content:
    #import <Foundation/Foundation.h>
    
    NSString * OUNTranslate(NSString *string, NSString *fallback) {
        return [[NSBundle mainBundle] localizedStringForKey:string value:fallback table:nil];
    }

    We can then call the function like so:

    NSString *t = OUNTranslate(@"translate_me", @"If not found, display me.");</pre>
    It's concise, easy to trigger with auto-complete, and it doesn't have the same problems that a macro would have.

    Way 4: Create a category for (NS)String

    This one is arguably the least user-friendly of the four. We can create a category on NSString (or, using the equivalent Swift way, on String) like so:

    @interface NSString (OUNTranslation)

    - (NSString *)oun_translatedStringWithFallback:(NSString *)fallback;

    @end

    @implementation NSString (OUNTranslation)

    - (NSString *)oun_translatedStringWithFallback:(NSString *)fallback {
    return [[NSBundle mainBundle] localizedStringForKey:self value:fallback table:nil];
    }

    @end</pre>We can then use it like this:

    NSString *t = [@"translate_me" oun_translatedStringWithFallback:@"If not found, display me."];</pre>

    Accessing the wrapper immediately in all Objective-C classes

    To have the function or method available in all your Objective-C classes without having to #import it every time, you can import its header to your prefix header. The prefix header hasn't been created in the project by default though, so you need to create it yourself first if you want to make use of this feature.

    Code in GitHub

    You can pull the Xcode project with this code from GitHub here. All the methods and functions are called a couple of times in the unit test classes.