Localizable.strings – How to load translation files dynamically from a web server and use them it inside your iPhone App

We all know we can use “Localizable.strings” to translate our Apps into different languages like this in our code:

NSLocalizedString(@"myKey", @"");

… if we have created our “Localizable.strings” file first and put in some keys and values… but what if I want my translation to be dynamic, that is, download it from a webserver and use the key value pairs of that file instead of our “locked” app resources like Localizable.strings after deployment?

Fortunately there is a solution to this problem! I’m also going to show you how to download the file from a webserver using ASIHTTPRequest, a wrapper around the CFNetwork API that makes it very easy for us to communicate with our webserver even through https!

Localizable Strings

Advertisement:

white,pages,yellow,tracker,search,find,gps,mobile,411,ringtone,business,911,likes,follower,locator

The first iPhone App ever that finds out who’s calling you before you answer the phone.


 

Step 1: Download the translation file (built up like Localizable.strings) from the web server

Let’s jump right into the code

#import "ASIHTTPRequest.h"
#import "ASIFormDataRequest.h"

#define CUSTOM_BUNDLE_NAME @"MYBUNDLE.bundle"
...

#pragma mark Download Translation from server
-(void)downloadLocalizableStringsFromServer {

    NSURL *url = [NSURL URLWithString:@"HTTP://PATHTOYOURRESOURCE/"]; // adapt this

    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];

    [request setCompletionBlock:^(void) {

           NSData *responseData = [request responseData];

           // custom method to save persistent dat
           // I'm assuming here you are downloading the german translation file...
           // adapt and/or extend if you wish
           if(![self saveData:responseData toFolder:[NSString stringWithFormat:@"%@/%@", CUSTOM_BUNDLE_NAME, @"de.lproj"] usingFilename:@"Localizable.strings"]) { // adapt this

               updateWithNoError = NO;     // could not save data, maybe internet connection is broken. Implement your own handling code here if needed.
           }
     }];

     [request startAsynchronous];
}

#pragma mark saveData to folder
-(BOOL)saveData:(NSData *)data toFolder:(NSString *)folder usingFilename:(NSString *)filename {

    NSData *dataToSave = data;

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents folder
    NSString *dataPath = [documentsDirectory stringByAppendingPathComponent:folder];

    NSError *error;

    if (![[NSFileManager defaultManager] fileExistsAtPath:dataPath])
        [[NSFileManager defaultManager] createDirectoryAtPath:dataPath withIntermediateDirectories:YES attributes:nil error:&error]; //Create folder

    NSString *filePath = [NSString stringWithFormat:@"%@/%@", dataPath, filename];

    if(![dataToSave writeToFile:filePath atomically:YES]) {

        NSLog(@"filepath: %@", filePath);
        UIAlertView *error = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Error", @"") message:NSLocalizedString(@"couldNotSaveData", @"") delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [error show];
        [error release];

        return NO;
    }
    else {

        // DON'T forget to call the next function.
        // Since we do not want the downloaded data to be saved through iCloud
        // Otherwise App would be rejected by Apple. You can only save data in the 
        // iCloud if it is created by the user (not downloaded...)
        NSURL *backUrl = [NSURL fileURLWithPath:filePath];
        [self addSkipBackupAttributeToItemAtURL:backUrl];

        NSLog(@"DOWNLOADED AND SAVED: %@ to %@", filename, filePath);
    }

    return YES;
}

// needed, so that images are not stored in iCloud!
- (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL {

    assert([[NSFileManager defaultManager] fileExistsAtPath: [URL path]]);

    NSError *error = nil;

    BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES]
                                  forKey: NSURLIsExcludedFromBackupKey error: &error];

    if(!success){

        NSLog(@"Error excluding %@ from backup %@", [URL lastPathComponent], error);
    }

    return success;
}

As we can see, there are three methods:

  • downloadLocalizableStringsFromServer
  • saveData toFolder usingFilename
  • addSkipBackupAttributeToItemAtURL

The first one, downloadLocalizableStringsFromServer is responsible – as the name already says – for downloading the translation file from the web server. Just adapt the parameters to your needs (URL of the resource, filename and foldername). The second method, saveData toFolder usingFilename, is responsible for actually storing our data persistently on our device’s memory so that we can access it later on when it comes to step 2. The third method, addSkipBackupAttributeToItemAtURL, I put there just to make things right and complete. Since your App nowadays would be rejected if you don’t consider this. You may not store data into the iCloud if the data is not “produced” by the user himself – caution: deployment target of your App should be at least 5.1!

Step 2: Tell NSBundle to use a different localization file

Now to make things comfortable for later usage we init a new cocoa bundle like this:

...
static NSBundle *staticCustomBundle = nil;
...

-(void)theMethodThatUsesOurDynamicTranslationFile {

    NSLog(@"The value for the key 'awesome' stands for: %@", [[THISCLASS getStaticCustomBundle] localizedStringForKey:@"awesome" value:nil table:nil]);
    // output in console: 'Gabe' ;)
}

...

-(void)dealloc {

    [staticCustomBundle release];
    [super dealloc];
}
+(void)createNewBundle {

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents folder
    NSString *dataPath = [documentsDirectory stringByAppendingPathComponent:CUSTOM_BUNDLE_NAME]; // adapt this

    staticCustomBundle = [NSBundle bundleWithPath:dataPath];
    [staticCustomBundle retain];
}
+(NSBundle *)getStaticCustomBundle {

    if(nil == staticCustomBundle) {

        [THISCLASS createNewBundle];
    }

    return staticCustomBundle;
}

Very easy, right? :)
I defined here the static variable “staticCustomBundle” and a static getter so that we can access the bundle from everywhere we need it, for example in another class, etc… everything in the code above is pretty self-explaining. I have a method (theMethodThatUsesOurDynamicTranslationFile) that wants to use the value of a key inside our downloaded “Localizable.strings”. To access the bundle I call [THISCLASS getStaticCustomBundle]. The first time the method “getStaticCustomBundle” is called, we create automatically our “dynamic” bundle and prepare it so that we can access the translation file(s) in it. then we call the method:

 - (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName {}

…on the bundle object to retrieve the value for a given key. That’s it!

I hope you enjoyed this tutorial. Feel free to comment below and ask if something is not clear.

-Gabe

19 comments

  1. mac says:

    Thx for this great tutorial, but what is “rightScaledData” ?

    • admin says:

      Thanks! You’re right, I just removed some code before posting as I was manipulating the data before saving…
      rightScaledData should be dataToSave

      I just updated the code above and now it should be right! Thanks for pointing that out!

      Regards

      • mac says:

        Thx!!

        Other point, XCode 4.5.2 show warning at downloadLocalizableStringsFromServer method.

        if you must replace, fix this:
        ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];

        for:
        ASIHTTPRequest *_request = [ASIHTTPRequest requestWithURL:url];
        __unsafe_unretained ASIHTTPRequest *request = _request;

        Regards!

    • Kameryn says:

      Smart thinking – a clever way of lioonkg at it.

  2. Andrew B. says:

    You can translate .strings files using localization tools, such as https://poeditor.com/, which works with pot, po, xls, xlsx, strings, xml, resx and properties files.

  3. Neat blog! Is your theme custom made or did you download it from somewhere? A design like yours with a few simple adjustements would really make my blog jump out. Please let me know where you got your theme. Bless you

    • admin says:

      Hi there

      Thanks for the compliment :)
      It’s an easy-to-set-up Theme called zeeBizzCard, not developed by me!
      Credits to: ThemeZee

      Cheers

  4. Raghu says:

    Interesting and informatory tutorial. :-). Is it possible for you to provide code sample or some thing of hat kind.

    • admin says:

      Hi Raghu
      Thanks for the compliment!
      I don’t know what you mean… there are code samples on this site regarding Localizable.strings…?

      Regards

      • Raghu says:

        Yes. I mean for the last module what ever you have mentioned in this blog. Downloading the localizable strings from the web server and making use of it.:-)

        • Raghu says:

          Hi,
          if we provide the complete path of the localizable.string file only, we are getting the value of the key. Simply mentioning the key and bundle name alone is not fetching the value.Please correct me if i am wrong.

  5. […] My problem is all the texts comes from webservices. So, at the beginning, I call the webservice and I have to set the texts in strings.xml. I know writing in strings.xml it’s impossible but we had the same problem on iOs and a solution has been found here http://iosapplove.com/archive/2013/01/localizable-strings-how-to-load-translations-dynamically-and-u…. […]

  6. himanshi goyal says:

    Hey with this i am having trouble with base language. If i save 2-3 folders for different languages. Then it takes first folder saved as base language. What is the problem?

  7. Anish says:

    Hi,
    Is it possible to dynamically localize the settings bundle. My requirement is that i have set of language which is dynamically fetched from server. I need to localize text of my app in iPhone Setting screen. is there any way to do this.

Leave a Reply

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


*