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!
Advertisement:
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
Thx for this great tutorial, but what is “rightScaledData” ?
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
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!
Only if you are using ARC, right? 😉
Yes, I’m using ARC! sorry…
Smart thinking – a clever way of lioonkg at it.
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.
Nice one
Weeeee, what a quick and easy soultion.
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
Hi there
Thanks for the compliment 🙂
It’s an easy-to-set-up Theme called zeeBizzCard, not developed by me!
Credits to: ThemeZee
Cheers
Interesting and informatory tutorial. :-). Is it possible for you to provide code sample or some thing of hat kind.
Hi Raghu
Thanks for the compliment!
I don’t know what you mean… there are code samples on this site regarding Localizable.strings…?
Regards
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.:-)
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.
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?
Please describe your problem better, I don’t understand what exactly is the problem.
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.
Can we have one language in iPad and a different language inside the app? Say for example I want my application contents to be in German but my iPad language I want still as English. If I give the user an option to change the application language alone, will it be possible?
can we replace AFNetworking by ASIHTTPRequest coz it’s make errors with Xcode 5; if Yes; please advice how?
Hi,
Does the file.string is saving only fon the detectes language?
Hi i am able to save my localised file de.lproj and create custom bundle also but when i am accessing localizedStringForKey i am not getting the value . Also in at my bundle when i print i am getting. full path with “(yet to load)”
I am able to save into folder and able to fetch the same folder, but unable to fetch value from the key. Can u please suggest me where i am going wrong.
Saving file–> (/Users/admin/Library/Developer/CoreSimulator/Devices/0F6819F1-6E06-42F5-BD23-381DE9F0CEF0/data/Containers/Data/Application/7D30E032-CCF0-4B7D-A25D-BAA2FF36BD4A/Documents/MYBUNDLE.bundle/de.lproj/Localizable.strings)
Fetching file–> (/Users/admin/Library/Developer/CoreSimulator/Devices/0F6819F1-6E06-42F5-BD23-381DE9F0CEF0/data/Containers/Data/Application/7D30E032-CCF0-4B7D-A25D-BAA2FF36BD4A/Documents/MYBUNDLE.bundle/de.lproj)