/* Melbourne Cocoheads August 2012 presentation title */
"talk-title" = "Localising your apps.";
/* presentation author */
"presentation-by" = "Jesse Collis";
/* presentation author email address */
"author-email" = "jesse@jcmultimedia.com.au";
.
- Initial development
- Initial localisation effort
- Ongoing updates
/* No comment provided by engineer. */
"Next" = "Next";
NSLocalizedString(@"Next", @"");
NSLocalizedString(@"Next", nil);
NSLocalizedString(@"Next", @"Next");
#define MYBadCompanyLocalizedString(key) NSLocalizedString((key), @"")
NSLocalizedString(@"In order to determine your location, location services must be turned on in settings", @"In order to determine your location, location services must be turned on in settings");
NSString *aStringFromAPI = [response objectForKey:@"next-title"];
NSLocalizedString(aStringFromAPI, @"api resposne for `next`");
In this case Apple's tools can't help you;
genstrings
will throw warnings and ignore yourNSLocalizedString()
all together.
This is a grey area; good comments can make duplicating keys manageable
NSLocalizedString(@"Next", @"RootViewController 'next' button title");
NSLocalizedString(@"Next", @"DetailViewController 'next page' button title");
Good comments show you the key
's appearances in your app in the .strings file
/* DetailViewController 'next page' button title
RootViewController 'next' button title */
"Next" = "Next";
genstrings
will warn you about duplicate keys- Bad/stupid/empty comments make this a bad offender
- keys with
nil
or@""
comments aren't picked up as duplicate
button.titleLabel.text = [NSString stringWithFormat:@"4 %@, 3 %@",
NSLocalizedString(@"pineapples",@"plural pineapples"),
NSLocalizedString(@"pears",@"plural pears")];
This is grey area because it's inflexible and implies a structure, but can be okay for really small strings, but often you should consider NSNumberFormatter
for numbers. (That's another talk)
button.titleLabel.text = [NSLocalizedString stringWithFormat:
NSLocalizedString(@"4 %@, 3 %@", @"fruit quantities format string"),
NSLocalizedString(@"pineapples",@"plural pineapples"),
NSLocalizedString(@"pears",@"plural pears")];
But at this point we've got localised pieces everywhere. But it's good that you're trying.
//NSBundle.h
#define NSLocalizedString(key, comment) \
[[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]
#define NSLocalizedStringFromTable(key, tbl, comment) \
[[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \
[bundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \
[bundle localizedStringForKey:(key) value:(val) table:(tbl)]
- (NSString *)localizedStringForKey:(NSString *) value:(NSString *) table:(NSString *)
localizedStringForKey:value:table:
has defaults
- default
table
is Localizable - default
value
isnil
or@""
- unlocalized keys return
vale
or the key ifvalue
isnil
You can localise resources by duplicating them in *.lproj
directories. You can see the supported localisations as an array of string in your NSBundle instance has with:
NSArray *localisations = [bundle localizations];
po [[NSBundle mainBundle] localizations]
(id) $4 = 0x08356290 <__NSCFArray 0x8356290>(en,ko)
These folders must be of a particular format. en
/ english, ko
/ Korean. You can't just give it random .lproj folder names. .lproj directory names` should be canonicalized IETF BCP 47 language identifier strings. Here's a list
NSBundle then references the @"AppleLanguages"
key from [NSUserDefaults standardUserDefaults] in combination with it's - localizations
to work out what .lproj directory it will look for key:
and table:
.
My educated guess
is the @"AppleLanguages" key in NSUserDefaults is derived from [NSLocale preferredLocalizations]
which represents your system's preferred localisations in order of preference.
po [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"];
(id) $1 = 0x0754e790 <__NSCFArray 0x754e790>(
en,fr,de,ja,nl,it,es,pt,pt-PT,da,fi,nb,sv,ko,zh-Hans,zh-Hant,ru,pl,tr,uk,ar,hr,cs,el,
he,ro,sk,th,id,ms,en-GB,ca,hu,vi)
There is a number of Stack Overflow posts on how to force NSLocalizedString() to use a language by changing this value in NSUserDefaults early in the app lifecycle, even changing it, syncing it and force quitting the app.
There's also some smarts here about what to look for if nothing is localised, but it's not that important here.
At this point we're looking for a stringsTable
based on the set localisation
which we know already is just an IETF BCP 47 NSString
.
To find our strings table path we use another NSBundle method:
NSString *tablePath = [bundle pathForResource:tableName
ofType:@"strings"
inDirectory:nil
forLocalization:localization];
To translate this path from a .strings
file into something we can use, we use:
NSString *stringsFile = [NSString stringWithContentsOfFile:stringsFilePath
encoding:NSUTF8StringEncoding
error:nil];
NSDictionary *stringsDict = [stringsFile propertyListFromStringsFileFormat];
And finally, where key
is the original key we passed to NSLocalizedString():
return [stringsDict objectForKey:key];
From NSString.h regarding propertyListFromStringsFileFormat
These methods are no longer recommended since they do not work with property lists and strings files in binary plist format. Please use the APIs in NSPropertyList.h instead.
self.title = NSLocalizedStringFromTable(@"nav-title",@"StationDetailController",@"NavigationItem title (Station Detail)");
Provide a tableName
to each string.
This splits your .strings
files by logical sections making localisation easier and implies a context to your keys and comments.
Even with the extra context provided by the different table names, I'm an advocate for unique keys across everything.
Provide useful comments with the suggested English (en) localisation included
@"NavigationItem title (Station Detail)"
@"TableView section 2 ('Other City Metro apps')"
Here's how not to do it
overviewCell.detail = [NSString stringWithFormat:
NSLocalizedString(@"stops and transfers format",@"Format for saying (1- 15) (3- stops) and (2- 4) (4-Transfers)"),
[NSNumber numberWithInt:[self.route.routePath count] - 1],
[NSNumber numberWithInt:[self.route.transfers count] - 1],
NSLocalizedString(@"stops",@"stops"),
[self.route.transfers count] > 1 ?
NSLocalizedString(@"Transfers",@"Plural transfers") :
NSLocalizedString(@"Transfer",@"Singular Transfers")];
Don't do this... localise each case
if (stops > 1){
if (transfers > 1){
detailString = [NSString stringWithFormat:NSLocalizedStringFromTable(@"plural-stops-plural-transfers",@"RouteDetail",@"Overview Header - 2 (plural) stops and 2 (plural) transfers ('%@ stops and %@ transfers')"), stopsNumber, transfersNumber];
}else if (transfers == 1){
detailString = [NSString stringWithFormat:NSLocalizedStringFromTable(@"plural-stops-single-transfer",@"RouteDetail",@"Overview Header - 2 (plural) stops and 1 (singular) transfer ('%@ stops and %@ transfer')"), stopsNumber, transfersNumber];
}else{
detailString = [NSString stringWithFormat:NSLocalizedStringFromTable(@"plural-stops",@"RouteDetail",@"Overview Header - 2 (plural) stops ('%@ stops')"), stopsNumber];
}
}else{
detailString = JCLocalizedStringFromTable(@"one-stop",@"RouteDetail",@"Overview Header - 1 (singular) stop ('only one stop!')");
}
If you've got strings from API or external source that aren't statically defined, genstrings
complains, as it can't read your keys.
My solution is to use a specific table and use genstrings -skipTable
to skip it so it won't error, and will let me create my own .strings file.
NSLocalizedStringFromTable(apiValue, @"ApiTranslations", @"apiValue for such and such");
- Use
genstring -s
to define your own macros. Make sure their name doesn't clash with other class names. - If you use *stringFromTable: it will create the appropriate .strings file for you
- It will read
NSLocalizedString*
elements in comments - Output your files to any directory, like en.lproj
- It gives good good warnings
- suppress multiple key warnings with
-q
- It will add numbers to format string parameters
@1$%
etc - You can skip certain
tableName
s with-skipTable
(see localising non-literals)
.
genstrings -s JCLocalizedString ./*.m -o ./en.lproj
generate your strings
ibtool --generate-strings-file MainStoryboard.strings MainStoryboard.storyboard
write your strings
ibtool --strings-file ~/MainStoryboard.strings
--write ../ko.lproj/MainStoryboard.storyboard MainStoryboard.storyboard
Xcode 4.4 , Mountain Lion, iOS 6 lets you localise a xib file with a .strings file named the same.
Pass -NSShowNonLocalizedStrings YES
as a launch argument in your scheme and see the unlocalised strings come to life in ALLCAPS
The previous info is great when you build an app from scratch, but updating an app months or years after initial release means your strings change, contexts change and this is hard to manage, especially if you use some of the bad methods above.
- Manages your strings files, and changes.
- A smarter wrapper around
genstrings
andlibtool
AppCode has blazed ahead in this space
Their latest blog post details a lot of new features
- Rename localisation keys
- Provide missing localisations
- Highlight unused keys
- Find usage of a key
- International friends
- crowdin.net
- applingua.com
- Greenwich
- Auto Layout OSX Lion, iOS 6
- Internationalisation with NSLocale, NSDateFormatter, NSNumberFormatter
WWDC Session 244 'Internationalization tips and tricks' presented by Dave De Long
http://www.albertmata.net/articles/introduction-to-internationalization-using-storyboards-on-ios-5.html https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/LoadingResources/Strings/Strings.html#//apple_ref/doc/uid/10000051i-CH6-SW1