Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Change the iOS simulator's current locale at runtime

While developing a set of date calculations and language rules for converting numeric values and dates to strings, I'm writing tests that assert the outcome of the string formatting method. An imaginary assertion for it might looks like this:

NSAssert([dateString isEqualToString:@"Three days, until 6:00 PM"], @"Date string should match expectation");

However, because the app is localized for several languages, and my fellow developers are also from and in different locales than I, it can happen that your device or simulator is set to a different locale than the one that the tests are being written for. In a scenario like this, the contents of the dateString might be something like:

@"Drie dagen, tot 18:00" // the assertion fails
@"Drei Tage, bis 18 Uhr" // the assertion also fails

This may or may not be the correct date notation for these locales, but the part that my question is about, is how to be able to run tests to a specific locale, when the underlying code makes use of Apple API like this:

[NSDateFormatter localizedStringFromDate:date 
                 dateStyle:NSDateFormatterNoStyle 
                 timeStyle:NSDateFormatterShortStyle];

I would love to cover at two or more languages in my assertions, with something like this:

[NSSomething actionToSetTheLocaleTo:@"en_US"];
dateString = ...; // the formatting
NSAssert([dateString isEqualToString:@"Three days, until 6:00 PM"], @"match en_US");

[NSSomething actionToSetTheLocaleTo:@"nl_NL"];
dateString = ...; // the formatting
NSAssert([dateString isEqualToString:@"Drie dagen, tot 18:00"], @"match nl_NL");

Who knows how to achieve this effect?

Notes:

  • Changing the preferred language does not cut it, it also needs to influence the NSDateFormatter and NSNumberFormatter behavior.
  • Because this is for unit testing purposes only, I'd be content with using private API. However, for the benefit of other people stumbling on this post, public API is preferred.
  • Passing a custom locale to each and every date or number formatting API might be a final consideration, but I'm posting this question hoping to avoid falling back to those extreme measures. If you however know this to be the only solution, please provide some reference and I'll waste no more time

Links on the topic:

  • Nice article by Ray Lillywhite on i18n and l10n
  • NSHipster article on NSLocale
like image 570
epologee Avatar asked Oct 02 '13 18:10

epologee


3 Answers

@Desmond pointed out a working solution. Until he places an answer in here to put this info in, let me summarize what I ended up doing with a bit of code.

The solution, turns out, is "as easy" as swizzling the methods that the class methods use internally:

beforeEach(^{
    [NSBundle ttt_overrideLanguage:@"nl"];
    [NSLocale ttt_overrideRuntimeLocale:[NSLocale localeWithLocaleIdentifier:@"nl_NL"]];
});

afterEach(^{
    [NSLocale ttt_resetRuntimeLocale];
    [NSBundle ttt_resetLanguage];
});

The ttt_... methods you see above use the categories on NSObject, NSLocale and NSBundle to check at runtime whether it should use the original methods or return something else. This method works flawlessly when writing your tests, and although it doesn't technically use any private API, I would strongly suggest only to use this in your test setup, not for anything you submit to the App Store for review.

In this gist you'll find the Objective-C categories I added to my app's test target to achieve the required behavior.

like image 82
epologee Avatar answered Oct 16 '22 20:10

epologee


Since this is in a unit test, have you tried setting the locale on the NSDateFormatter? You should not have to set the locale on the whole simulator, you can make it a parameter of your tests. Most cocoa methods that are dependent on locale can take it as a parameter or property.

Something like:

NSLocale *locale = [NSLocale localeWithLocaleIdentifier:@"en-US"];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setLocale:locale];
[formatter setDateStyle: NSDateFormatterNoStyle];
[formatter setTimeStyle: NSDateFormatterShortStyle];
[formatter setFormatterBehavior:NSDateFormatterBehavior10_4];
thing = [formatter stringForObjectValue:date];

The localizedStringFromDate:dateStyle:timeStyle: method you're using is documented here, and it's pretty explicit about everything but the locale. So you can do the same steps, but set the locale to something other than the system's current locale using the steps outlined above.

like image 38
quellish Avatar answered Oct 16 '22 21:10

quellish


The only way I see is what quellish has mentioned. However, you mentioned it exists in a lot of places.

Instead of rewriting all your current code, in your pch you could do something fancy like

#import "UnitTestDateFormatter.h"
#define NSDateFormatter UnitTestDateFormatter

And then simply create a subclass to handle it:

@implementation UnitTestDateFormatter

- (id) init
{
    self = [super init];

    if(self != nil)
    {
        [self setLocale:...];
    }

    return self;
}

@end

At least then your code can remain unchanged.

like image 2
SomeGuy Avatar answered Oct 16 '22 22:10

SomeGuy