iOS:如何在不重启应用程序的情况下通过编程方式更改应用程序语言?

38

当我在设备语言独立于应用程序使用的语言时更改应用程序使用的语言,直到我关闭应用程序并重新启动它,更改才会生效。如何使应用程序不需要重新启动就能加载所有的nib文件和.strings文件,以便根据所选语言进行更新?

我使用以下代码在运行时更改语言:

NSArray* languages = [NSArray arrayWithObjects:@"ar", @"en", nil]; 
[[NSUserDefaults standardUserDefaults] setObject:languages forKey:@"AppleLanguages"];

请检查这个答案,它可以正常工作:https://dev59.com/2FbUa4cB1Zd3GeqPDOsi - Khaled Annajar
对于Swift,我建议使用Localize-Swift来实现多语言本地化:https://github.com/marmelroy/Localize-Swift - Tamás Sengel
嘿,你最终设法实现了吗? - Ankit Kumar Gupta
@TamásSengel,你有Objective-C相关的东西吗? - Vladyslav Panchenko
5个回答

23
这对我有用: Swift 4:
创建一个名为BundleExtension.swift的文件,并将以下代码添加到其中-
var bundleKey: UInt8 = 0

class AnyLanguageBundle: Bundle {

override func localizedString(forKey key: String,
                              value: String?,
                              table tableName: String?) -> String {
    
    guard let path = objc_getAssociatedObject(self, &bundleKey) as? String,
        let bundle = Bundle(path: path) else {
            
            return super.localizedString(forKey: key, value: value, table: tableName)
    }
    
    return bundle.localizedString(forKey: key, value: value, table: tableName)
  }
}

extension Bundle {
 
class func setLanguage(_ language: String) {
    
    defer {
        
        object_setClass(Bundle.main, AnyLanguageBundle.self)
    }
    
    objc_setAssociatedObject(Bundle.main, &bundleKey,    Bundle.main.path(forResource: language, ofType: "lproj"), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  }
}

现在每当您需要更改语言时,请调用此方法:
func languageButtonAction() {
    // This is done so that network calls now have the Accept-Language as "hi" (Using Alamofire) Check if you can remove these
    UserDefaults.standard.set(["hi"], forKey: "AppleLanguages")
    UserDefaults.standard.synchronize()
    
    // Update the language by swapping bundle
    Bundle.setLanguage("hi")
    
    // Done to reinstantiate the storyboards instantly
    let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
    UIApplication.shared.keyWindow?.rootViewController = storyboard.instantiateInitialViewController()
}

1
干净利落! - Suresh Mopidevi
1
如何使当前视图控制器重新加载以查看语言更改,而不是导航到根视图控制器?@ankit - Suresh Mopidevi
1
完美的答案! - Chowdhury Md Rajib Sarwar
1
键盘工具栏本地化未更改。 - Dhiru
1
老兄,这是唯一一个完整的答案,在我们苦苦挣扎中帮助了我们,让我们不必花费数百万小时重新定位大量的Storyboard生成的键值对和/或完全重启应用程序。非常感谢! - kkaun
显示剩余3条评论

11
我曾有一个类似的需求,需要开发一款使用选项卡导航的Kiosk模式iPad应用程序。该应用程序不仅需要支持即时语言更改,而且还必须知道大多数选项卡已从nibs中加载,因为该应用程序只在每周平均重新加载一次新版本时重启。
我尝试了几种建议来利用现有的Apple本地化机制,但它们都有严重的缺点,包括XCode 4.2对本地化nib的支持有问题 - 我在IB中设置的IBoutlet连接变量看起来是正确的,但在运行时它们经常为null!?
我最终实现了一个类,模仿了Apple NSLocalizedString类,但它可以处理运行时更改,并且每当用户进行语言更改时,我的类就会发布一个通知。需要更改本地化字符串(和图像)的屏幕声明了一个handleLocaleChange方法,在viewDidLoad时调用,以及每当LocaleChangedNotification被发布时调用。
所有按钮和图形都被设计成与语言无关,尽管标题文本和标签文本通常会根据语言环境更改。如果我必须更改图像,我可能会在每个屏幕的handleLocaleChange方法中这样做。
以下是代码。它包括一些nib / bundle路径的支持,但我实际上在最终项目中没有使用它。
MyLanguage.h // // MyLanguage.h //
#import <Foundation/Foundation.h>

#define DEFAULT_DICTIONARY_FOR_STRINGS                      @""
#define ACCESSING_ALTERNATE_DICTIONARY_SETS_DEFAULT         1

#define LANGUAGE_ENGLISH_INT  0
#define LANGUAGE_SPANISH_INT  1
#define LANGUAGE_ENGLISH_SHORT_ID  @"en"
#define LANGUAGE_SPANISH_SHORT_ID  @"es"

#define LANGUAGE_CHANGED_NOTIFICATION   @"LANGUAGE_CHANGED"


@interface MyLanguage : NSObject
{
    NSString        *currentLanguage;    
    NSDictionary    *currentDictionary;
    NSBundle        *currentLanguageBundle;
}

+(void) setLanguage:(NSString *)languageName;


+(NSString *)stringFor:(NSString *)srcString forLanguage:(NSString *)languageName;
+(NSString *)stringFor:(NSString *)srcString;

+ (MyLanguage *)singleton;

@property (nonatomic, retain) NSBundle        *currentLanguageBundle;
@property (nonatomic, retain) NSString        *currentLanguage;    
@property (nonatomic, retain) NSDictionary    *currentDictionary;

@end

MyLanguage.m: // // MyLanguage.m

#import "MyLanguage.h"
#import "Valet.h"

#define GUI_STRING_FILE_POSTFIX   @"GUIStrings.plist"

@implementation MyLanguage

@synthesize currentLanguage;   
@synthesize currentDictionary;
@synthesize currentLanguageBundle;

+(NSDictionary *)getDictionaryNamed:(NSString *)languageName
{
    NSDictionary *results = nil;

    // for now, we store dictionaries in a PLIST with the same name.
    NSString *dictionaryPlistFile = [languageName stringByAppendingString:GUI_STRING_FILE_POSTFIX];

    NSString *plistBundlePath = [Valet getBundlePathForFileName:dictionaryPlistFile];

    if ( [[NSFileManager defaultManager] fileExistsAtPath:plistBundlePath] )
    {
        // read it into a dictionary
        NSDictionary *newDict = [NSDictionary dictionaryWithContentsOfFile:plistBundlePath]; 
        results = [newDict valueForKey:@"languageDictionary"];

    }// end if

    return results;
}

+(NSString *)stringFor:(NSString *)srcString forDictionary:(NSString *)languageName;
{
    MyLanguage *gsObject = [MyLanguage singleton];

    // if default dictionary matches the requested one, use it.
    if ([gsObject.currentLanguage isEqualToString:languageName])
    {
        // use default
        return [MyLanguage stringFor:srcString];
    }// end if
    else
    {
        // get the desired dictionary
        NSDictionary *newDict = [MyLanguage getDictionaryNamed:languageName];

        // default is not desired!
        if (ACCESSING_ALTERNATE_DICTIONARY_SETS_DEFAULT)
        {
            gsObject.currentDictionary = newDict;
            gsObject.currentLanguage = languageName;
            return [MyLanguage stringFor:srcString];
        }// end if
        else
        {
            // use current dictionary for translation.
            NSString *results = [gsObject.currentDictionary valueForKey:srcString];

            if (results == nil)
            {
                return srcString;
            }// end if

            return results;
        }
    }

}

+(void) setLanguage:(NSString *)languageName;
{
    MyLanguage *gsObject = [MyLanguage singleton];

    // for now, we store dictionaries in a PLIST with the same name.
    // get the desired dictionary
    NSDictionary *newDict = [MyLanguage getDictionaryNamed:languageName];

    gsObject.currentDictionary = newDict;
    gsObject.currentLanguage = languageName;   


    // now set up the bundle for nibs
    NSString *shortLanguageIdentifier = @"en";
    if ([languageName contains:@"spanish"] || [languageName contains:@"espanol"] || [languageName isEqualToString:LANGUAGE_SPANISH_SHORT_ID])
    {
        shortLanguageIdentifier = LANGUAGE_SPANISH_SHORT_ID;
    }// end if
    else
        shortLanguageIdentifier = LANGUAGE_ENGLISH_SHORT_ID;

//    NSArray *languages = [NSArray arrayWithObject:shortLanguageIdentifier];
//    [[NSUserDefaults standardUserDefaults] setObject:languages forKey:@"AppleLanguages"]; 
//    
    NSString *path= [[NSBundle mainBundle] pathForResource:shortLanguageIdentifier ofType:@"lproj"];
    NSBundle *languageBundle = [NSBundle bundleWithPath:path];
    gsObject.currentLanguageBundle = languageBundle;


    [[NSNotificationCenter defaultCenter] postNotificationName:LANGUAGE_CHANGED_NOTIFICATION object:nil];

}


+(NSString *)stringFor:(NSString *)srcString;
{
    MyLanguage *gsObject = [MyLanguage singleton];
    // default is to do nothing.
    if (gsObject.currentDictionary == nil || gsObject.currentLanguage == nil || [gsObject.currentLanguage isEqualToString:DEFAULT_DICTIONARY_FOR_STRINGS] )
    {
        return srcString;
    }// end if

    // use current dictionary for translation.
    NSString *results = [gsObject.currentDictionary valueForKey:srcString];

    if (results == nil)
    {
        return srcString;
    }// end if


    return results;
}



#pragma mark -
#pragma mark Singleton methods

static MyLanguage *mySharedSingleton = nil;

-(void) lateInit;
{

}

// PUT THIS METHOD DECLARATION INTO THE HEADER
+ (MyLanguage *)singleton;
{
    if (mySharedSingleton == nil) {
        mySharedSingleton = [[super allocWithZone:NULL] init];
        [mySharedSingleton lateInit];
    }
    return mySharedSingleton;
}

+ (id)allocWithZone:(NSZone *)zone
{    return [[self singleton] retain]; }

- (id)copyWithZone:(NSZone *)zone
{    return self; }

- (id)retain
{    return self; }

- (NSUInteger)retainCount //denotes an object that cannot be released
{    return NSUIntegerMax;  }

- (oneway void)release    //do nothing
{   }

- (id)autorelease
{     return self; }


@end

如果有人想重用我的代码,请注意以下一些注释(可能不太明显):locale PLIST文件是语言短ID,后跟GUIStings.plist,例如esGUIStrings.plist。plist中的根对象是名为“languageDictionary”的字典。字典中的条目由要翻译的字符串作为键(例如“Unlock”和“Log in”),值是已翻译的字符串(例如“Desbloquear”和“Iniciar la sesion”)。 - software evolved
Valet是我创建的一个辅助类,它作为比NSFileManager更高级的文件系统接口。在发布代码之前,我尝试删除所有对它的引用,但看起来我错过了一个。您可以在代码后面看到一个不使用Valet的类似行:NSString *path= [[NSBundle mainBundle] pathForResource:shortLanguageIdentifier ofType:@"lproj"]; - software evolved

8
不要依赖于在nib文件中设置的字符串。将nib仅用于视图的布局和设置。向用户显示的任何字符串(按钮文本等)都需要在您的Localizable.strings文件中,并且在加载nib时,您需要相应地设置视图/控件上的文本。
获取当前语言的bundle:
NSString *path = [[NSBundle mainBundle] pathForResource:currentLanguage ofType:@"lproj"];
if (path) {
    NSBundle *localeBundle = [NSBundle bundleWithPath:path];
}

使用bundle获取本地化字符串:

NSLocalizedStringFromTableInBundle(stringThatNeedsToBeLocalized, nil, localeBundle, nil);

此外,对于日期格式化,您可能需要查看。
[NSDateFormatter dateFormatFromTemplate:@"HH:mm:ss"" options:0 locale:locale];

要使用它,您需要为所需使用的语言/国家创建一个对应的NSLocale。

但是xib文件中的本地化图像怎么办?例如:按钮图像。而且,对于不同的本地化,在xib文件中有不同的布局和标签大小,这又该怎么处理呢? - Ahmed Said
1
请参考https://dev59.com/ilDTa4cB1Zd3GeqPLbM6来了解如何加载图片的示例。 - mamills
你可能需要动态调整标签的大小等。例如,您可能需要使用 -[NSString sizeWithFont: constrainedToSize: lineBreakMode:] 来确定某些文本所需的高度(或宽度),然后相应地设置框架。 - mamills
话虽如此,使用与设备不同的语言/区域设置并不简单。即使您完成了上述所有操作,[NSError localizedErrorDescription] 将根据设备设置返回文本(或者可能根据 NSUserDefaults 的“AppleLanguages”返回)。但是从我在其他问题和答案中看到的情况来看,您必须在启动 UIApplication 之前在主要代码中设置它,因此在应用程序运行时无法动态更改它而不重新启动应用程序。 - mamills

7
这是我的做法。我猜窍门在于使用 NSLocalizedStringFromTableInBundle 而不是 NSLocalizedString。
对于所有的字符串,使用以下方法:
someLabel.text = NSLocalizedStringFromTableInBundle(@"Your String to be localized, %@",nil,self.localeBundle,@"some context for translators");

要更改语言,请运行此代码

    NSString * language = @"zh-Hans"; //or whatever language you want
    NSString *path = [[NSBundle mainBundle] pathForResource:language ofType:@"lproj"];
    if (path) {
        self.localeBundle = [NSBundle bundleWithPath:path];
    }
    else {
        self.localeBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"] ];
    }

在这之后,您可能希望调用任何更新代码以将字符串更新为新的语言,例如再次运行此操作。

someLabel.text = NSLocalizedStringFromTableInBundle(@"Your String to be localized, %@",nil,self.localeBundle,@"some context for translators");

就这样了,不需要重新启动应用程序。与系统设置兼容(如果您通过iOS设置设置语言,也将起作用)。不需要外部库。无需越狱。它还可以与genstrings一起使用。

当然,您仍然应该像往常一样使您的应用程序设置持久化:

[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObjects:@"zh-Hans", nil] forKey:@"AppleLanguages"];
[[NSUserDefaults standardUserDefaults] synchronize];

(并在viewDidLoad或其他地方进行检查)
注:本文为IT技术相关内容,涉及代码编写,请谨慎操作。
NSString * language = [[NSLocale preferredLanguages] objectAtIndex:0];
    NSString *path = [[NSBundle mainBundle] pathForResource:language ofType:@"lproj"];
    if (path) {
        self.localeBundle = [NSBundle bundleWithPath:path];
    }
    else {
        self.localeBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"en" ofType:@"lproj"] ];
    }

这与我的2012年的解决方案有何不同? - mamills

2

您应该创建自己的宏,类似于NSLocalizedString,但基于您设置的NSUserDefaults值选择字符串的包(即不用担心苹果语言默认值是什么)

当您更改语言时,应发送通知,视图控制器、视图等应侦听并刷新自己


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接