
As an app developer, it can be quite effective to localize your apps (for this article, we focus on language), especially if you speak both English and your native tongue. At Japps, this is exactly what we do: localize at least to English and Dutch. Both iOS and Android provide means for this, although lozalizing the text on UI components is, frankly, a bit easier on Android. On iOS you are basically forced to duplicate your entire UI, which makes maintenance tricky (basically this violates the DRY principle).
Apple’s solution (prior to iOS 6)
Sadly, there are no real solutions available yet, although there are tools that speed up the process. The steps boil down to: duplicate Storyboard / nib files, extract strings from one language, translate strings into another and put them back (both using a command line tool). Any UI changes will require a repetition of these steps – however you can take a shortcut here by reusing most of the earlier translations. The great thing is that you can adjust your UI (sizes) to the different word lengths, but this is not always a big issue.
Alternative solution
Another solution that has been proposed is transferring the translation proces entirely to code by creating an outlet for each control that has text on it (see this tutorial). The downside is that that it requires quite some code for every new view. Furthermore, the texts in IB / storyboard will become totally meaningless, which may be confusing. To prevent this, they could be added just for clarity, but they’d still need to be defined in the strings file, which would again be non-DRY solution, and extra work.
Base Internationalization (iOS 6)
Luckily, Base Internationalization is coming, and it seems this will provide a KISS solution to localization on iOS. Auto layout could then ensure UI elements adapt their size and location to the length of the strings in the current language. This won’t work on iOS 5 though, so we’ll have to wait a bit before it is ‘acceptable’ to stop supporting the iOS 5 users.
Our solution (for the time being)
In the mean time, we crafted a bit of code that basically loops recursively over all views, checks the strings for square brackets. If anything is found like ‘[Blah]’ it is replaced by ‘BlahTranslated’, or ‘Blah’ if no translation was found. The latter means that for the ‘original’ language (ie the language that is used inside IB), no or only a few entries have to be added to Localizable.strings. For other (newly added) languages, everything can be added into one Localizable.strings file. Alongside, the UI in IB keeps being fairly understandable (since all texts are still relevant, albeit wrapped in square brackets).
This was implemented as ‘categories’ for NSString and UIView. In the latter case, subviews are checked recursively, and for different types of view (label, button, etc), slightly different steps are taken. The only thing that needs to be done is for every view that is loaded from IB (in view controller’s viewDidLoad, or after manually loading the view from a nib) lngfkt.h needs to be included and [theTopView lngfk] needs to be called. In views that are instantiated from code NSLocalizedString can be used like ‘normal’.
The code
lngfkt.h
1 2 3 4 5 6 7 8 9 10 |
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> @interface UIView (LngFkt) -(void)lngfk; @end @interface NSString (LngFkt) -(NSString*)lngfk; @end |
lngfkt.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
#import "lngfkt.h" // This enhancement enables one to do [@"[blah]" lngfk], which returns @"blah" or @"blahtranslated" @implementation NSString (LngFkt) /* Returns either self or the text between brackets, if there's any brackets. Text outside brackets is ignored */ -(NSString*) getkey { // Regular expression to match strings like @"[blah]" NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\[(.*)\\]" options:0 error:NULL]; NSTextCheckingResult *match = [regex firstMatchInString:self options:0 range:NSMakeRange(0, [self length])]; // No match? Return nil to indicate there's no match if ( match == nil ) return nil; // Else return first match return [self substringWithRange:[match rangeAtIndex:1]]; } /* Returns text between brackets translated. Or just text between brackets if not translatable. Or just the original text if no brackets were present. */ -(NSString*)lngfk { NSString* key = [self getkey]; return key ? NSLocalizedString(key, nil) : self; } @end // This enhancement adds a lngfk method to every UIView. This method is recursive. Endpoints are translated depending on their class types. NSString's lngfk category is used to do the actual string works. @implementation UIView (LngFkt) /* Checks every (sub)view recursively. When text labels, button titles, etc, contain text between brackets, this is translated whenever possible. */ -(void)lngfk { for (UIView* view in [self subviews]) { [view lngfk]; } // For UILabel, we try and translate the text value if ( [self isKindOfClass:[UILabel class]] ) { UILabel* me = (UILabel*)self; [me setText: [ me.text lngfk ] ]; } // For UIButton, we try and translate the title if ( [self isKindOfClass:[UIButton class]] ) { UIButton* me = (UIButton*)self; [me setTitle: [me.currentTitle lngfk] forState:UIControlStateNormal ]; } // For UINavigationBar, we loop over items // And try and translate their title if ( [self isKindOfClass:[UINavigationBar class]] ) { UINavigationBar* me = (UINavigationBar*)self; for ( UINavigationItem* item in me.items ) { [ item setTitle: [ item.title lngfk] ]; [ item.backBarButtonItem setTitle: [item.backBarButtonItem.title lngfk] ]; } } // For UITabBar, we loop over items // And try and translate their title if ( [self isKindOfClass:[UITabBar class]] ) { UITabBar* me = (UITabBar*)self; for ( UITabBarItem* item in me.items ) { [ item setTitle: [ item.title lngfk] ]; } } // For UITableViewHeaderFooterView (iOS 6 only), // we try and translate their txtLabel text value if ( [self isKindOfClass:[UITableViewHeaderFooterView class]] ) { UITableViewHeaderFooterView* me = (UITableViewHeaderFooterView*)self; [me.textLabel setText: [me.textLabel.text lngfk]]; } } @end |
There’s a fair amount of edge cases that are not covered by this approach. Some just didn’t pop up yet (the above series of if statements could probably cover more UIView subclasses), others need some more trickery.
For navigation bars, the lngfk method has to be called separtely: [self.navigationController.navigationBar lngfk]. For static table headers and cell views, a bit of code is required in the class that implements the UITableViewDelegate class (the UITableViewController for instance):
1 2 3 4 5 6 7 |
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { [cell lngfk]; } -(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return [ [super tableView:tableView titleForHeaderInSection:section] lngfk ]; } |
The pitfalls
It is a hack. I doesn’t even really adhere to any naming conventions. We used it and found it a fair alternative compared to the other options. The code is posted just in case others like this idea as well. Please use it at your own risk. Also, this happens on runtime, which costs time. Not notably, in our case, but it could be.