Multiple Localized .strings Files in iOS App Bundle

21,219

Solution 1

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?)

Solution 2

I was able to reproduce and fix the issue, though the solution does imply there's a bug in NSBundle.

I reproduced it with the following bundle structure:

MyApp.app/
 |
  - MyResources.bundle/
      |
       - en.lproj/
      |
       - fr.lproj/

and code:

  NSLog(@"A key: %@", NSLocalizedString(@"A key", nil));
  NSBundle *bundle = [NSBundle bundleWithPath: [[NSBundle mainBundle] pathForResource: @"MyResources" ofType: @"bundle"]];
  NSLog(@"Current locale: %@", [[NSLocale currentLocale] localeIdentifier]);
  NSLog(@"Bundle localizations: %@", [bundle localizations]);
  NSLog(@"Key from bundle: %@", [bundle localizedStringForKey: @"A key" value: @"Can't find it." table: nil]);
  NSLog(@"Key using bundle macro: %@", NSLocalizedStringFromTableInBundle(@"A key",
                                                                             nil,
                                                                             bundle,
                                                                             nil));

With the locale set to fr_FR (i.e. French) the bundle picked the string from the English strings table - even the string "can't find it" doesn't appear.

Without changing the code, I was able to get the French string using the following structure for the bundle instead:

MyApp.app/
 |
  - MyResources.bundle/
      |
       - Resources/
          |
           - en.lproj/
          |
           - fr.lproj/

It looks like NSBundle still expects the old Mac OS X bundle structure instead of what iOS is supposed to use. So a simple change of bundle structure should solve the problem...

Solution 3

To add to what David Doyle said.

Make sure you set the available languages in the info section of your project in both the bundle and the application itself. So for instance, if you support french and english in your app, make sure both your bundle and your app have the French and English languages defined in the available localizations for your projects.

Solution 4

Not clearly understand your question, but I use this macro to use multiple localized string files:

#define CustomLocalizedString(key, comment) \
  [[[NSBundle mainBundle] localizedStringForKey:(key) value:nil table:nil] isEqualToString:(key)] ? \
  [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:@"MyTable"] : \
  [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]

or you can try

[[NSBundle mainBundle] localizedStringForKey:(key) value:[[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:@"MyTable"] table:nil]

This'll check Localizable.strings first, if the key does not exist, it'll return the key itself, then check and use MyTable.strings. Of course, you'd better name your key with a prefix. e.g. "KYName" = "Name";.

If it is what you want, feel free to check THIS question I've asked before. ;)

Solution 5

I have another solution.

#define localizedString(key)    [NSLocalizedString(key, nil) isEqualToString:key] ? \
                            [[NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"]]  localizedStringForKey:key value:@"" table:nil] : \
                            NSLocalizedString(key, nil)

so for instance if we're going to find a key "title" = "Manager"; in Localizable.strings (fr) and if not there then the result for the key "title" will be just same as the key.

In this case we can find the key "title" in localizable.string (en) but if can find in (fr) we can just use it.

Share:
21,219
simeon
Author by

simeon

Updated on July 19, 2022

Comments

  • simeon
    simeon almost 2 years

    I have a fairly complicated project, consisting of several large localized sub-projects.

    Most of my sub-projects are localized through a single Localizable.strings file. This file is copied into a SubProjectName.bundle target, which is used in conjunction with a SubProjectName.a static library in the main project. This works fine.

    However, one of my sub-projects contains many localized .strings files. This project fails to read strings in any language other than English, regardless of how the device (or simulator) is configured.

    For example, this line of code always returns the English string:

    [[NSBundle myResourcesBundle] localizedStringForKey:@"MY_TEST_STRING" value:@"" table:@"MyTable"]
    

    Where MyTable corresponds to a MyTable.strings file localized into several languages. When I peek into the .app package, all the localizations are there, sitting inside the "MyBundle.bundle" resource within the app.

    The following code, however, correctly finds the translations for a given string in all localizations:

    for (NSString *language in [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"])
    {
        NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle myResourcesBundle] pathForResource:language ofType:@"lproj"]];
        NSLog(@"%@: %@", language, NSLocalizedStringFromTableInBundle(@"MY_TEST_STRING", @"MyTable", bundle, nil));
    }
    

    So when the bundle is the actual MyBundle.bundle/<LanguageCode>.lproj folder, the string lookup works. But obviously this defeats the purpose of the automatic lookup provided by iOS.

    (Note that [NSBundle myResourcesBundle] above is simply a static convenience method to fetch my custom bundle for the sub-project).

    --

    Edit: I've been experimenting with this some more, and if I delete the en.lproj folder from my sub-project's bundle, then it correctly uses the locale of the device or simulator.

    For example, I have:

    MyApp.app/
     |
      - MyResources.bundle/
          |
           - en.lproj/
          |
           - zh-Hans.lproj/
    

    When I set the simulator (or device) to Chinese Simplified it looks up strings in en.lproj even though the locale is zh-Hans. If I delete the en.lproj folder and restart the app it correctly uses the zh-Hans localization.

  • simeon
    simeon over 11 years
    This answer does not allow for partial localizations (i.e. one translated .strings file out of ten).
  • simeon
    simeon over 11 years
    The problem occurs when I use: [MyCustomBundle localizedStringForKey:(key) value:@"" table:@"MyCustomStringsFile"] — I always get the English result (as in, from the en.lproj/ folder). Even when there are other languages and the device is correct configured in that language.
  • simeon
    simeon over 11 years
    Wow — great detective work! I'm trying to implement your answer. How did you maintain your Resources/ folder structure in the bundle while keeping project localisations? When I add my .strings into a Resources/ folder and drop it into Xcode as a folder reference I lose the ability to select the .strings files for localisation.
  • David Doyle
    David Doyle over 11 years
    I'll be honest, I cheated :). I added the bundle to the project as a .bundle folder, which works as expected, then re-organised the bundle folder in Finder. Xcode wasn't involved except to add the original bundle.
  • simeon
    simeon over 11 years
    Thanks David. I'll award the bounty to you as you seem to have figured out why it does this, even though there's no way to fix it. I'll accept my hack-workaround as the answer because it's a way to get around the problem.
  • LoriHC
    LoriHC about 11 years
    Not sure if this solves the problem posed above, but glad I found it, as it solves the problem I had!
  • Kjuly
    Kjuly about 11 years
    @LoriHC glad to hear that :)
  • Vladimir Grigorov
    Vladimir Grigorov over 10 years
    This should be the accepted answer. If the app does not support Fr locale, then that locale is not loaded for the custom bundle too, even though it may have fr.lproj directory in it. If the app supports Fr, then Fr is loaded from the custom bundle. There is no need to change bundle internal structure by adding 'Resources' dir.
  • some.birdie
    some.birdie over 10 years
    This helps, but why doesn't is work in normal way? Have you found the reason?
  • simeon
    simeon over 10 years
    @Anastasia see the answer by David Doyle, above. It appears to be a bug in NSBundle.
  • Alex Zavatone
    Alex Zavatone over 8 years
    As of 2015, that will no longer work. You will want to change the NSString *language = [NSLocale preferredLanguages][0]; to this: [[[NSBundle mainBundle] preferredLocalizations] firstObject];
  • simeon
    simeon over 8 years
    @AlexZavatone any idea why it stopped working? (It's not listed as deprecated). preferredLocalizations lists the localizations contained in the bundle in order of preference based on locale, while preferredLanguages lists all languages in order of preference regardless of what is in the bundle. I have edited my answer anyway as preferredLocalizations is better to use even if preferredLanguages still works.
  • Alex Zavatone
    Alex Zavatone over 8 years
    There are several reasons, but Apple states in the NSLocale header to use mainBundle. One of the BIG problems is that if you set your language to non English AND your region to not being in the US, NSLocale will return something like this: "ar-IT" if you were using Arabic in the Italian region. Here are the comments from the NSLocale header: + (NSArray<NSString *> *)preferredLanguages NS_AVAILABLE(10_5, 2_0); // note that this list does not indicate what language the app is actually running in; the [NSBundle mainBundle] object determines that at launch and knows that information
  • RamaKrishna Chunduri
    RamaKrishna Chunduri about 8 years
    The solution is just awesome for the apps that seeks the primary language from developer (or the user dynamically in a screen) i.e., forced localization. But if the app should work based on the users locale settings, then it fails to seek the correct bundle ex:indian english is EN_IN while us is EN_US.. dev might not have added the lproj for indian english but would have the lproj for en_us . Also date formatter might not seek the format from the bundle instead seeks from region settings MM/dd/yyyy in US & dd/MM/yyyy in India. But still solution is best among given.. Thanks for it..
  • Accid Bright
    Accid Bright about 6 years
    Also want to add, if you have multiple languages in your project config, but no files are localized really (there is 0 Files Localized message near the desired language), you won't get localized strings from your custom bundle (just checked by myself).