Multiple Localized .strings Files in iOS App Bundle
I now have a hacky solution to this, but would appreciate if someone has a better answer (or explanation for why the above doesn't work).
I expanded my NSBundle category to include a preferred language resource:
Header
@interface NSBundle (MyBundle)
+ (NSBundle*) myResourcesBundle;
+ (NSBundle*) myPreferredLanguageResourcesBundle;
@end
Implementation
@implementation NSBundle (MyBundle)
+ (NSBundle*) myResourcesBundle
{
static dispatch_once_t onceToken;
static NSBundle *myLibraryResourcesBundle = nil;
dispatch_once(&onceToken, ^
{
myLibraryResourcesBundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:@"MyResources" withExtension:@"bundle"]];
});
return myLibraryResourcesBundle;
}
+ (NSBundle*) myPreferredLanguageResourcesBundle
{
static dispatch_once_t onceToken;
static NSBundle *myLanguageResourcesBundle = nil;
dispatch_once(&onceToken, ^
{
NSString *language = [[[NSBundle myResourcesBundle] preferredLocalizations] firstObject];
myLanguageResourcesBundle = [NSBundle bundleWithPath:[[NSBundle myResourcesBundle] pathForResource:language ofType:@"lproj"]];
if( myLanguageResourcesBundle == nil )
{
myLanguageResourcesBundle = [NSBundle myResourcesBundle];
}
});
return myLanguageResourcesBundle;
}
@end
I then have a simple macro for getting my localized strings:
#define MyLocalizedDocumentation(key, comment, chapter) \
NSLocalizedStringFromTableInBundle((key),(chapter),[NSBundle myPreferredLanguageResourcesBundle],(comment))
This solution simply gets the preferred language code from NSLocale
and then checks to see if a bundle exists for that language. If not, it falls back to the main resource bundle (perhaps it should iterate through the NSLocale preferredLanguage indices to check if a bundle exists? Does anyone know?)
Multiple Localizable.strings files in one iOS app
There is a way to do this that is quite simple.
There are several macros for localizing, one of which is NSLocalizedStringFromTable()
. This takes three parameters:
- The string to localize
- The table name, which is just a file name
- The comment, just like in
NSLocalizedString()
If you use a table name then you can have another strings file, i.e. if I do NSLocalizedStringFromTable(@"name", @"MyStrings", @"whatever");
then put my strings in MyStrings.strings
it will use that.
See Matt Gallagher's post about this.
IOS/iPhone: Nested Localizable.strings Files?
Two alternatives:
Use custom wrapper functions around
NSLocalizedString
that do your hierarchical lookup for you, and then fall back to theNSLocalizedString
mechanism for the default case.I previously wrote an answer about how to do this: Can .strings resource files be added at runtime?
Use multiple .strings files per language. By default things are looked up in "Localizable.strings", but the
NSLocalizedStringFromTable()
function allows you to specify a custom "table" name. So if you passed in "Configuration" as the table name, it would look things up in the "Configuration.strings" file.I worked on an app that did this. We had two different .strings files. One controlled the branding settings (what name to use, what images to use, what servers to use, etc), and the other contained all of the actual translated strings. There would only be a single copy of the branding information in the app bundle, and we chose the correct variant to use at compile time with some custom scripts. It worked really really well.
Get localized strings from main bundle
To use your localized strings in UITests you have to do two things:
1) Add your Localizable.strings file to your UITest target
2) Access the string via the UITest bundle:
func localizedText(_ key: String) -> String {
let uiTestBundle = Bundle(for: AClassFromYourUITests.self)
return NSLocalizedString(key, bundle: uiTestBundle, comment: "")
}
Just make sure that you use a class that is part of your UITest target to access the bundle.
Using Bundle.main
does not work when running UITests because it gives you the bundle of the UITest Runner App instead of the UITest bundle
Localized project with several targets with localized app names
I cracked this nut in an XCode project which, I believe, tackles the same issue as you have, so hopefully this will help you. The solution contains two targets built from the same codebase (with a different app name) and which are fully localized, including the app's names. (It builds my freemium app called Growth (French: Croissance, Spanish: Crecer) and the paid version called Growth+ (French: Croissance+, Spanish: Crecer+).
I also stumbled on the InfoPlist.string
files (which contain only the app's name). I got around the issue through the use of subfolders in my solution, and changing the targets to include the localized set of InfoPlist.strings from the relevant subfolder. Specifically, I have this structure:
Root
+-- en.lproj
¦ +-- Localizable.strings
¦ +-- SomeXib.xib
+-- es.lproj
¦ +-- Localizable.strings
¦ +-- SomeXib.xib
+-- fr.lproj
¦ +-- Localizable.strings
¦ +-- SomeXib.xib
+-- OnlyInAppA
¦ +-- en.lproj
¦ ¦ +-- InfoPlist.strings
¦ +-- es.lproj
¦ ¦ +-- InfoPlist.strings
¦ +-- fr.lproj
¦ +-- InfoPlist.strings
+-- OnlyInAppB
¦ +-- en.lproj
¦ ¦ +-- InfoPlist.strings
¦ +-- es.lproj
¦ ¦ +-- InfoPlist.strings
¦ +-- fr.lproj
¦ +-- InfoPlist.strings
+-- AppA.plist
+-- AppB.plist
And the only difference among my targets (other than different preprocessor symbols and different .plist file) is that AppA's build settings includes the InfoPlist.strings files under OnlyInAppA, whereas AppB's build settings includes the InfoPlist.strings files under OnlyInAppB.
The naming convention I used (OnlyInAppA/OnlyInAppB folders, AppB/AppB plist files) is obvious enough to make this a satisfactory approach, in my personal opinion.
Edit:
Please see these two XCode 4 screenshots to see where exactly to find the settings which are changed in the targets.
In the target build settings, a different plist file is specified (note: XCode automatically does this for you when you add a new target)
In the target build phases, in section Copy Bundle Resources, pick the version of InfoPlist.strings from the relevant OnlyInAppX subfolder (notice the gray text next to InfoPlist.strings on this screenshot--this will show a different location for the other target). You can achieve this by using the +/- buttons and replace the file with the intended one.
How to find all available Strings-Tables in iOS project?
This should do the trick:
func localizedString(forKey key: String) -> String {
guard let filesURL = Bundle.main.urls(forResourcesWithExtension: "strings", subdirectory: nil) else { return key }
let tablesName = filesURL.map { $0.deletingPathExtension().lastPathComponent }
for aTableName in tablesName {
var result = Bundle.main.localizedString(forKey: key, value: nil, table: aTableName)
if result != key {
return result
}
}
return key
}
How do I add a bundle with localized strings?
I was actually doing this correctly. The problem was that my Localizable.strings file was corrupted and, unlike app-level localizations, Xcode will not complain, it will just fail. In my case, I left out a semi-colon.
Can I load a localizable.strings file into an iOS app over-the-air
All localized string resources (plus many other kind of resources) are extracted from the bundle. Usually an app uses the "main bundle" which is the one that is created by XCode together with your app.
But you can create separately any other bundle, provided you make it with the correct structure, then you can download it in your app and finally extract a localized string using the NSLocalizedStringFromTableInBundle() function.
So let's say you extract for a key "KEY" the language translation, then the normal syntax would be:
NSString *translated = NSLocalizedStringFromTable(@"key",nil,nil);
but there is a variant of this option that allows you to specify the bundle:
NSString *translated = NSLocalizedStringFromTableInBundle(@"key",nil,myBundle,nil);
In the standard case you replace myBundle
with [NSBundle mainBundle]
but if you want to use another bundle you can specify it in this way:
NSString *myBundlePath = "the path to the downloaded bundle";
NSBundle *myBundle = [NSBundle bundleWithPath:myBundlePath];
NSString *translated = NSLocalizedStringFromTableInBundle(@"key",nil,myBundle,nil);
The full structure of a bundle can be seen in the "Bundle Programming Guide" in the Apple docs, but in your case you can simply create in this way:
- in your Mac create a directory, and call it "MyBundle"
- inside this directory move your localized string(s) (if you have multiple languages in the bundle then the localizable.strings file will be inside the lproj directories: en.lproj, it.lproj, fr.lproj, ...)
- then rename the directory to "MyBundle.bundle"
You will notice with the last operation that now this object is seen as a standalone object but in fact it is a directory.
Now you can decide to have a multiple-bundle approach or follow a single-bundle technique: in the latter case you can package all the languages and then use the unique updated bundle for language translation by using the automatic system localization rules; in the other case you can make a bundle for each language and then - based on the currently selected language - load the appropriate bundle and choose it for your translations.
Related Topics
Disable Warning Dialog If Bluetooth Is Powered Off iOS
How to Change Text Font in Uipickerview in iOS 7
App Is Not Showing in the Share Menu of Shared Options in Shared Extension in iOS8
Xcode5 Simulator: Unknown Option Character 'X' In: -Xlinker
Why Was Uitextfield's Text Property Changed to an Optional in Swift 2
How to Make a Conical Gradient in iOS Using Core Graphics/Quartz 2D
Store Time Efficiently in Firebase Database
iOS Memory Usage Increasing, Can't Find the Culprit
Is This a Bug with Mkmapkitdelegate Mapview:Didupdateuserlocation
Insert String at Cursor Position of Uitextfield
Module 'Googlemobileads' Not Found in iOS
Using Two Versions of a Cocoapod Dependency
Xcode 7 Beta 6, Dyld _Nsarray0_ Crash
Type 'Nsattributedstringkey' (Aka 'Nsstring') Has No Member 'Font'
How to Add a Custom Separator to Uitableviewcell