On iPhone NSLocalizedString returns the string in the language of the iPhone.
Is it possible to force NSLocalizedString to use a specific language to have the app
in a different language than the device ?
NSLocalizedString() (and variants thereof) access the "AppleLanguages" key in NSUserDefaults to determine what the user's settings for preferred languages are. This returns an array of language codes, with the first one being the one set by the user for their phone, and the subsequent ones used as fallbacks if a resource is not available in the preferred language. (on the desktop, the user can specify multiple languages with a custom ordering in System Preferences)
You can override the global setting for your own application if you wish by using the setObject:forKey: method to set your own language list. This will take precedence over the globally set value and be returned to any code in your application that is performing localization. The code for this would look something like:
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObjects:#"de", #"en", #"fr", nil] forKey:#"AppleLanguages"];
[[NSUserDefaults standardUserDefaults] synchronize]; //to make the change immediate
This would make German the preferred language for your application, with English and French as fallbacks. You would want to call this sometime early in your application's startup. You can read more about language/locale preferences here: Internationalization Programming Topics: Getting the Current Language and Locale
I had the same problem recently and I didn't want to start and patch my entire NSLocalizedString nor force the app to restart for the new language to work. I wanted everything to work as-is.
My solution was to dynamically change the main bundle's class and load the appropriate bundle there:
Header file
#interface NSBundle (Language)
+(void)setLanguage:(NSString*)language;
#end
Implementation
#import <objc/runtime.h>
static const char _bundle=0;
#interface BundleEx : NSBundle
#end
#implementation BundleEx
-(NSString*)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName
{
NSBundle* bundle=objc_getAssociatedObject(self, &_bundle);
return bundle ? [bundle localizedStringForKey:key value:value table:tableName] : [super localizedStringForKey:key value:value table:tableName];
}
#end
#implementation NSBundle (Language)
+(void)setLanguage:(NSString*)language
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
object_setClass([NSBundle mainBundle],[BundleEx class]);
});
objc_setAssociatedObject([NSBundle mainBundle], &_bundle, language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:#"lproj"]] : nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#end
So basically, when your app starts and before you load your first controller, simply call:
[NSBundle setLanguage:#"en"];
When your user changes his preferred language in your setting screen, simply call it again:
[NSBundle setLanguage:#"fr"];
To reset back to system defaults, simply pass nil:
[NSBundle setLanguage:nil];
Enjoy...
For those who need a Swift version:
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)
}
}
I usually do this in this way, but you MUST have all localization files in your project.
#implementation Language
static NSBundle *bundle = nil;
+(void)initialize
{
NSUserDefaults* defs = [NSUserDefaults standardUserDefaults];
NSArray* languages = [defs objectForKey:#"AppleLanguages"];
NSString *current = [[languages objectAtIndex:0] retain];
[self setLanguage:current];
}
/*
example calls:
[Language setLanguage:#"it"];
[Language setLanguage:#"de"];
*/
+(void)setLanguage:(NSString *)l
{
NSLog(#"preferredLang: %#", l);
NSString *path = [[ NSBundle mainBundle ] pathForResource:l ofType:#"lproj" ];
bundle = [[NSBundle bundleWithPath:path] retain];
}
+(NSString *)get:(NSString *)key alter:(NSString *)alternate
{
return [bundle localizedStringForKey:key value:alternate table:nil];
}
#end
Do not use on iOS 9. This returns nil for all strings passed through it.
I have found another solution that allows you to update the language strings, w/o restarting the app and compatible with genstrings:
Put this macro in the Prefix.pch:
#define currentLanguageBundle [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:[[NSLocale preferredLanguages] objectAtIndex:0] ofType:#"lproj"]]
and where ever you need a localized string use:
NSLocalizedStringFromTableInBundle(#"GalleryTitleKey", nil, currentLanguageBundle, #"")
To set the language use:
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObject:#"de"] forKey:#"AppleLanguages"];
Works even with consecutive language hopping like:
NSLog(#"test %#", NSLocalizedStringFromTableInBundle(#"NewKey", nil, currentLanguageBundle, #""));
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObject:#"fr"] forKey:#"AppleLanguages"];
NSLog(#"test %#", NSLocalizedStringFromTableInBundle(#"NewKey", nil, currentLanguageBundle, #""));
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObject:#"it"] forKey:#"AppleLanguages"];
NSLog(#"test %#", NSLocalizedStringFromTableInBundle(#"NewKey", nil, currentLanguageBundle, #""));
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObject:#"de"] forKey:#"AppleLanguages"];
NSLog(#"test %#", NSLocalizedStringFromTableInBundle(#"NewKey", nil, currentLanguageBundle, #""));
As said earlier, just do:
[[NSUserDefaults standardUserDefaults] setObject: [NSArray arrayWithObjects:#"el", nil] forKey:#"AppleLanguages"];
But to avoid having to restart the app, put the line in the main method of main.m, just before UIApplicationMain(...).
The trick to use specific language by selecting it from the app is to force the NSLocalizedString to use specific bundle depending on the selected language ,
here is the post i have written for this
learning advance localization in ios apps
and here is the code of one sample app advance localization in ios apps
What do you think about this solution for Swift 3?
extension String {
func localized(forLanguage language: String = Locale.preferredLanguages.first!.components(separatedBy: "-").first!) -> String {
guard let path = Bundle.main.path(forResource: language == "en" ? "Base" : language, ofType: "lproj") else {
let basePath = Bundle.main.path(forResource: "Base", ofType: "lproj")!
return Bundle(path: basePath)!.localizedString(forKey: self, value: "", table: nil)
}
return Bundle(path: path)!.localizedString(forKey: self, value: "", table: nil)
}
}
Simple usage:
"report".localized(forLanguage: "pl") //forced language
"report".localized() //default language selected by user in settings, in case when your app doesnt support selected lanaguage, the default one is selected, here is an english.
As Brian Webster mentions, the language needs to be set "sometime early in your application's startup". I thought applicationDidFinishLaunching: of the AppDelegate should be a suitable place to do it, since it's where I do all other initialization.
But as William Denniss mentions, that seems to have an effect only after the app is restarted, which is kind of useless.
It seems to work fine if I put the code in the main function, though:
int main(int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
// Force language to Swedish.
[[NSUserDefaults standardUserDefaults]
setObject:[NSArray arrayWithObject:#"sv"]
forKey:#"AppleLanguages"];
int retVal = UIApplicationMain(argc, argv, nil, nil);
[pool release];
return retVal;
}
I'd appreciate any comments on this.
NSLocalizedString() reads the value for the key AppleLanguages from the standard user defaults ([NSUserDefaults standardUserDefaults]). It uses that value to choose an appropriate localization among all existing localizations at runtime. When Apple builds the user defaults dictionary at app launch, they look up the preferred language(s) key in the system preferences and copy the value from there. This also explains for example why changing the language settings in OS X has no effect on running apps, only on apps started thereafter. Once copied, the value is not updated just because the settings change. That's why iOS restarts all apps if you change then language.
However, all values of the user defaults dictionary can be overwritten by command line arguments. See NSUserDefaults documentation on the NSArgumentDomain. This even includes those values that are loaded from the app preferences (.plist) file. This is really good to know if you want to change a value just once for testing.
So if you want to change the language just for testing, you probably don't want to alter your code (if you forget to remove this code later on ...), instead tell Xcode to start your app with a command line parameters (e.g. use Spanish localization):
No need to touch your code at all. Just create different schemes for different languages and you can quickly start the app once in one language and once in another one by just switching the scheme.
I like best Mauro Delrio's method.
I also have added the following in my Project_Prefix.pch
#import "Language.h"
#define MyLocalizedString(key, alt) [Language get:key alter:alt]
So if you ever want to use the standard method (that uses NSLocalizedString) you can make a quick syntax substitution in all files.
I came up with a solution that allows you to use NSLocalizedString. I create a category of NSBundle call NSBundle+RunTimeLanguage. The interface is like this.
// NSBundle+RunTimeLanguage.h
#import <Foundation/Foundation.h>
#interface NSBundle (RunTimeLanguage)
#define NSLocalizedString(key, comment) [[NSBundle mainBundle] runTimeLocalizedStringForKey:(key) value:#"" table:nil]
- (NSString *)runTimeLocalizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName;
#end
The implementation is like this.
// NSBundle+RunTimeLanguage.m
#import "NSBundle+RunTimeLanguage.h"
#import "AppDelegate.h"
#implementation NSBundle (RunTimeLanguage)
- (NSString *)runTimeLocalizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName
{
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
NSString *path= [[NSBundle mainBundle] pathForResource:[appDelegate languageCode] ofType:#"lproj"];
NSBundle *languageBundle = [NSBundle bundleWithPath:path];
NSString *localizedString=[languageBundle localizedStringForKey:key value:key table:nil];
return localizedString;
}
#end
Than just add import NSBundle+RunTimeLanguage.h into the files that use NSLocalizedString.
As you can see I store my languageCode in a property of AppDelegate. This could be stored anywhere you'd like.
This only thing I don't like about it is a Warning that NSLocalizedString marco redefined. Perhaps someone could help me fix this part.
Swift 3 extensions:
extension Locale {
static var preferredLanguage: String {
get {
return self.preferredLanguages.first ?? "en"
}
set {
UserDefaults.standard.set([newValue], forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
}
}
}
extension String {
var localized: String {
var result: String
let languageCode = Locale.preferredLanguage //en-US
var path = Bundle.main.path(forResource: languageCode, ofType: "lproj")
if path == nil, let hyphenRange = languageCode.range(of: "-") {
let languageCodeShort = languageCode.substring(to: hyphenRange.lowerBound) // en
path = Bundle.main.path(forResource: languageCodeShort, ofType: "lproj")
}
if let path = path, let locBundle = Bundle(path: path) {
result = locBundle.localizedString(forKey: self, value: nil, table: nil)
} else {
result = NSLocalizedString(self, comment: "")
}
return result
}
}
Usage:
Locale.preferredLanguage = "uk"
label.text = "localizedKey".localized
Swift Version:
NSUserDefaults.standardUserDefaults().setObject(["fr"], forKey: "AppleLanguages")
NSUserDefaults.standardUserDefaults().synchronize()
In swift 4, I have solved it without needing to restart or use libraries.
After trying many options, I found this function, where you pass the stringToLocalize (of Localizable.String, the strings file) that you want to translate, and the language in which you want to translate it, and what it returns is the value for that String that you have in Strings file:
func localizeString (stringToLocalize: String, language: String) -> String
{
let path = Bundle.main.path (forResource: language, ofType: "lproj")
let languageBundle = Bundle (path: path!)
return languageBundle! .localizedString (forKey: stringToLocalize, value: "", table: nil)
}
Taking into account this function, I created this function in a Swift file:
struct CustomLanguage {
func createBundlePath () -> Bundle {
let selectedLanguage = //recover the language chosen by the user (in my case, from UserDefaults)
let path = Bundle.main.path(forResource: selectedLanguage, ofType: "lproj")
return Bundle(path: path!)!
}
}
To access from the whole app, and in each string of the rest of ViewControllers, instead of putting:
NSLocalizedString ("StringToLocalize", comment: “")
I have replaced it with
let customLang = CustomLanguage() //declare at top
let bundleLanguage = customLang.createBundle()
NSLocalizedString("StringToLocalize", tableName: nil, bundle: bundleLanguage, value: "", comment: “”) //use in each String
I do not know if it's the best way, but I found it very simple, and it works for me, I hope it helps you!
In a nutshell :
Localize your application
It's the first thing you have to do is to localise your app with at least two languages (english and french in this example).
Override NSLocalizedString
In your code, instead of using NSLocalizedString(key, comment), use a macro MYLocalizedString(key, comment) defined like this :
#define MYLocalizedString(key, comment) [[MYLocalizationSystem sharedInstance] localizedStringForKey:(key) value:(comment)];
This MYLocalizationSystem singleton will :
Set langage by setting the right localized NSBundle user asks for
Returns the localized NSString according to this previously set language
Set user language
When user changed application language in french, call [[MYLocalizationSystem sharedInstance] setLanguage:#"fr"];
- (void)setLanguage:(NSString *)lang
{
NSString *path = [[NSBundle mainBundle] pathForResource:lang ofType:#"lproj"];
if (!path)
{
_bundle = [NSBundle mainBundle];
NSLog(#"Warning: No lproj for %#, system default set instead !", lang);
return;
}
_bundle = [NSBundle bundleWithPath:path];
}
In this example this method set localized bundle to fr.lproj
Return localized string
Once you've set the localized bundle, you'll be able to get the right localised string from him with this method :
- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value
{
// bundle was initialized with [NSBundle mainBundle] as default and modified in setLanguage method
return [self.bundle localizedStringForKey:key value:value table:nil];
}
Hope this will help you.
You'll find more details in this article from NSWinery.io
In file .pch to define:
#define currentLanguageBundle [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:[[NSLocale preferredLanguages] objectAtIndex:0] ofType:#"lproj"]]
#define NSLocalizedString(str,nil) NSLocalizedStringFromTableInBundle(str, nil, currentLanguageBundle, #"")
Maybe you should complement with this (on .pch file after #import ):
extern NSBundle* bundle; // Declared on Language.m
#ifdef NSLocalizedString
#undef NSLocalizedString
// Delete this line to avoid warning
#warning "Undefining NSLocalizedString"
#endif
#define NSLocalizedString(key, comment) \
[bundle localizedStringForKey:(key) value:#"" table:nil]
Swift 3 solution:
let languages = ["bs", "zh-Hant", "en", "fi", "ko", "lv", "ms", "pl", "pt-BR", "ru", "sr-Latn", "sk", "es", "tr"]
UserDefaults.standard.set([languages[0]], forKey: "AppleLanguages")
Gave some examples of language codes that can be used. Hope this helps
You could build a sub-bundle with the set of localized strings that you want to do this with, and then use NSLocalizedStringFromTableInBundle() to load them. (I'm assuming that this is content separate from the normal UI localization you might be doing on the app.)
for my case i have two localized file , ja and en
and i would like to force it to en if the preferred language in the system neither en or ja
i'm going to edit the main.m file
i 'll check whether the first preferred is en or ja , if not then i 'll change the second preferred language to en.
int main(int argc, char *argv[])
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"AppleLanguages"];
[[NSUserDefaults standardUserDefaults] synchronize];
NSString *lang = [[NSLocale preferredLanguages] objectAtIndex:0];
if (![lang isEqualToString:#"en"] && ![lang isEqualToString:#"ja"]){
NSMutableArray *array = [[NSMutableArray alloc] initWithArray:[NSLocale preferredLanguages]];
[array replaceObjectAtIndex:1 withObject:#"en"];
[[NSUserDefaults standardUserDefaults] setObject:array forKey:#"AppleLanguages"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
#autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
You can do something like this:
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:#"Localizable" ofType:#"strings" inDirectory:nil forLocalization:#"es"];
NSBundle *spanishBundle = [[NSBundle alloc] initWithPath:[bundlePath stringByDeletingLastPathComponent]];
NSLocalizedStringFromTableInBundle(#"House", nil, spanishBundle, nil):
Here is a decent solution for this problem, and it does not require application restart.
https://github.com/cmaftuleac/BundleLocalization
This implementation works by tweaking inside NSBundle. The idea is that you override the method localizedStringForKey on the instance of NSBundle object, and then call this method on a different bundle with a different language. Simple and elegant fully compatible with all types of resources.
Based on Tudorizer's answer to change language without leaving or restarting the application.
Instead of a macro, use a class for accessing the preferred language in order to check if a specific language code is present.
Below is a class used to obtain the current language bundle that is working for iOS 9:
#implementation OSLocalization
+ (NSBundle *)currentLanguageBundle
{
// Default language incase an unsupported language is found
NSString *language = #"en";
if ([NSLocale preferredLanguages].count) {
// Check first object to be of type "en","es" etc
// Codes seen by my eyes: "en-US","en","es-US","es" etc
NSString *letterCode = [[NSLocale preferredLanguages] objectAtIndex:0];
if ([letterCode rangeOfString:#"en"].location != NSNotFound) {
// English
language = #"en";
} else if ([letterCode rangeOfString:#"es"].location != NSNotFound) {
// Spanish
language = #"es";
} else if ([letterCode rangeOfString:#"fr"].location != NSNotFound) {
// French
language = #"fr";
} // Add more if needed
}
return [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:language ofType:#"lproj"]];
}
/// Check if preferred language is English
+ (BOOL)isCurrentLanguageEnglish
{
if (![NSLocale preferredLanguages].count) {
// Just incase check for no items in array
return YES;
}
if ([[[NSLocale preferredLanguages] objectAtIndex:0] rangeOfString:#"en"].location == NSNotFound) {
// No letter code for english found
return NO;
} else {
// Tis English
return YES;
}
}
/* Swap language between English & Spanish
* Could send a string argument to directly pass the new language
*/
+ (void)changeCurrentLanguage
{
if ([self isCurrentLanguageEnglish]) {
[[NSUserDefaults standardUserDefaults] setObject:#[#"es"] forKey:#"AppleLanguages"];
} else {
[[NSUserDefaults standardUserDefaults] setObject:#[#"en"] forKey:#"AppleLanguages"];
}
}
#end
Use the class above to reference a string file / image / video / etc:
// Access a localized image
[[OSLocalization currentLanguageBundle] pathForResource:#"my_image_name.png" ofType:nil]
// Access a localized string from Localizable.strings file
NSLocalizedStringFromTableInBundle(#"StringKey", nil, [OSLocalization currentLanguageBundle], #"comment")
Change language in-line like below or update the "changeCurrentLanguage" method in the class above to take a string parameter referencing the new language.
[[NSUserDefaults standardUserDefaults] setObject:#[#"es"] forKey:#"AppleLanguages"];
This function will try to get localized string for current language and if it's not found it will get it using english language.
- (NSString*)L:(NSString*)key
{
static NSString* valueNotFound = #"VALUE_NOT_FOUND";
static NSBundle* enBundle = nil;
NSString* pl = [NSLocale preferredLanguages][0];
NSString* bp = [[NSBundle mainBundle] pathForResource:pl ofType:#"lproj"];
NSBundle* b = [NSBundle bundleWithPath:bp];
NSString* s = [b localizedStringForKey:key value:valueNotFound table:nil];
if ( [s isEqualToString:valueNotFound] ) {
if ( !enBundle ) {
bp = [[NSBundle mainBundle] pathForResource:#"en" ofType:#"lproj"];
enBundle = [NSBundle bundleWithPath:bp];
}
s = [enBundle localizedStringForKey:key value:key table:nil];
}
return s;
}
I wanted to add support for a language that isn't officially supported by iOS (not listed in Language section under system settings). By following the Apple's Internationalization Tutorial and few hints here by Brian Webster and geon, I came up with this piece of code (put it in main.m):
int main(int argc, char * argv[]) {
#autoreleasepool {
// Grab regional settings locale, for Slovenian this is either sl_SI or en_SI
NSLocale *locale = [NSLocale currentLocale];
NSString *ll = [locale localeIdentifier]; // sl_SI
// Grab the first part of language identifier
NSArray *comp = [ll componentsSeparatedByString:#"_"];
NSString *ll1 = #"en";
if (comp.count > 0) {
ll1 = comp[0]; // sl, en, ...
}
// Check if we already saved language (user can manually change it inside app for example)
if (![[NSUserDefaults standardUserDefaults] objectForKey:#"SelectedLanguage"]) {
// Slovenian (Slovenia), Slovenia
if ([ll isEqualToString:#"sl_SI"] || [ll isEqualToString:#"en_SI"]) {
ll1 = #"sl-SI"; // This is the part of localized path for Slovenian language that Xcode generates
}
// Add more unsupported languages here...
[[NSUserDefaults standardUserDefaults] setObject:ll1 forKey:#"SelectedLanguage"]; // Save language
}
else {
// Restore language as we have previously saved it
ll1 = [[NSUserDefaults standardUserDefaults] objectForKey:#"SelectedLanguage"];
}
// Overwrite NSLocalizedString and StoryBoard language preference
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObjects:ll1, #"en", #"fr", nil] forKey:#"AppleLanguages"];
// Make sure settings are stored to disk
[[NSUserDefaults standardUserDefaults] synchronize];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
This works well for both Storyboard and NSLocalizedString code. The code assumes that user will have an option to manually change language inside app later on.
Of course, don't forget to add proper Storyboard translations and Localizable.strings translations (see link to Apple page above for how to do that).
For Swift you can override the main.swift file and set the UserDefaults string there before you app runs. This way you do not have to restart the App to see the desired effect.
import Foundation
import UIKit
// Your initialisation code here
let langCultureCode: String = "LANGUAGE_CODE"
UserDefaults.standard.set([langCultureCode], forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))
paired together with the removal of #UIApplicationMain in your AppDelegate.swift file.
For me, Test-Plan user, specifying -AppleLanguages (en_BZ) (English Belize) in the TestPlan's Arguments Passed On Launch is working.
Since we use en_GB and en_BZ, iOS always preferred en_GB. Even so when I switched the devices language to EN and the region to BZ and when setting these values in the Test Plans Application Language and Application Region settings . Nothing helped except the good old arguments approach above :)
whatever you all do, the best way is to take the short_name for the specified language, i.e.: fr, en, nl, de, it, etc... and assign the same to a global value.
make a picker view to pop up like a drop down menu (combination of a button on click of which a picker view appears from below with a list of languages) and select the language you desire. let the short name be stored internally.
make a .h + .m file named LocalisedString.
Set the global value of short_name to be equal to the obtained value in LocalisedString.m
When the required language is selected assign the NSBundlePath to create project sub-directories for the needed language. for eg, nl.proj, en.proj.
When the particular proj folder is selected call the localised string for the respective language and change the language dynamically.
no rules broken.
Related
i am building up a project in ios platform and want to use two language in this application
and also want . the user can change the language on Runtime suppose we can take two language button button 1 for english and button 2 for german and the user can change the application language at any time by using these button . any help or tutorials
Thanks in advance
Assuming that you have created localizations for English and German, your app will use the language selected in Settings (this is the preferred option).
If you want to set the language directly from your application:
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObjects:#"en", nil] forKey:#"AppleLanguages"];
will set the language for your application only to English (for German, I assume the key is "de"). You should also:
[[NSUserDefaults standardUserDefaults] synchronize];
This will not take effect until your application restarts.
Have a class "LanguageUtils" with that method :
- (NSString *) localizedString:(NSString *)key
{
if (self.currentLanguage == nil) {
self.currentLanguage = #"en";
}
NSString* path = [[NSBundle mainBundle] pathForResource:[self.currentLanguage lowercaseString] ofType:#"lproj"];
NSBundle* languageBundle = [NSBundle bundleWithPath:path];
return [languageBundle localizedStringForKey:key value:#"" table:nil];
}
And the property NSString currentLanguage.
Then in your .pch do :
#undef NSLocalizedString
#define NSLocalizedString(key, _comment) [[Language sharedInstance] localizedString:key]
Works like a charm and you can redefine the current language at runtime.
Just keep in mind that your view should be refreshed by yourself. It won't automatically do it for you.
I came up with a solution that allows you to use NSLocalizedString. I create a category of NSBundle call NSBundle+RunTimeLanguage. The interface is like this.
// NSBundle+RunTimeLanguage.h
#import <Foundation/Foundation.h>
#interface NSBundle (RunTimeLanguage)
#define NSLocalizedString(key, comment) [[NSBundle mainBundle] runTimeLocalizedStringForKey:(key) value:#"" table:nil]
- (NSString *)runTimeLocalizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName;
#end
The implementation is like this.
// NSBundle+RunTimeLanguage.m
#import "NSBundle+RunTimeLanguage.h"
#import "AppDelegate.h"
#implementation NSBundle (RunTimeLanguage)
- (NSString *)runTimeLocalizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName
{
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
NSString *path= [[NSBundle mainBundle] pathForResource:[appDelegate languageCode] ofType:#"lproj"];
NSBundle *languageBundle = [NSBundle bundleWithPath:path];
NSString *localizedString=[languageBundle localizedStringForKey:key value:key table:nil];
return localizedString;
}
#end
Than just add import NSBundle+RunTimeLanguage.h into the files that use NSLocalizedString.
As you can see I store my languageCode in a property of AppDelegate. This could be stored anywhere you'd like.
This only thing I don't like about it is a Warning that NSLocalizedString marco redefined. Perhaps someone could help me fix this part.
This is very good and the language is changed within the app and also when device language changes. I used this in many apps.
For localization we generally use
NSLocalizedString(#"hello",#"Hello World");
in the custom implementation they have something similar like this
AMLocalizedString(#"hello",#"Hello World");
Here is demo tutorial
https://github.com/object2dot0/Advance-Localization-in-ios-apps
Just remove
[self dealloc];
from
-(IBAction) languageSelection:(id)sender
so it wont be crash!!! enjoy!
You have to use localization for this . Just store the key and value in side localizable.string and load the appropriate .string file as per your requirement .
You can refer following tutorial for this :
http://www.raywenderlich.com/2876/how-to-localize-an-iphone-app-tutorial
SWIFT 2.0 version
We can do this on run time without restarting the app in Swift 2.0 as follows
Make a function within a class, lets say LanguageManager is the name of the class
class LanguageManager
{
static let sharedInstance = LanguageManager()
//Make a function for Localization. Language Code is the one which we will be deciding on button click.
func LocalizedLanguage(key:String,languageCode:String)->String{
//Make sure you have Localizable bundles for specific languages.
var path = NSBundle.mainBundle().pathForResource(languageCode, ofType: "lproj")
//Check if the path is nil, make it as "" (empty path)
path = (path != nil ? path:"")
let languageBundle:NSBundle!
if(NSFileManager.defaultManager().fileExistsAtPath(path!)){
languageBundle = NSBundle(path: path!)
return languageBundle!.localizedStringForKey(key, value: "", table: nil)
}else{
// If path not found, make English as default
path = NSBundle.mainBundle().pathForResource("en", ofType: "lproj")
languageBundle = NSBundle(path: path!)
return languageBundle!.localizedStringForKey(key, value: "", table: nil)
}
}
}
How to Use this.
Now Assume you have two Languages for the application (English and Spanish)
English - en
Spanish - es
Suppose you have to place something for a label
//LOGIN_LABEL_KEY is the one that you will define in Localizable.strings for Login label text
logInLabel.text = LanguageManager.sharedInstance.LocalizedLanguage("LOGIN_LABEL_KEY", languageCode: "es")
//This will use the Spanish version of the text.
//"es" is the language code which I have hardcoded here. We can use a variable or some stored value in UserDefaults which will change on Button Click and will be passed in loginLabel
For more info on language codes see this link below
https://developer.apple.com/library/ios/documentation/MacOSX/Conceptual/BPInternational/LanguageandLocaleIDs/LanguageandLocaleIDs.html
or
http://www.ibabbleon.com/iOS-Language-Codes-ISO-639.html
Currently, I have three separate .strings files - for English, French and German respectively.
I have a UISegmentedControl in the preferences section, where I would like to change the default .strings file used depending on which segment is selected.
Localisation is already working outside the application when I test it, and I've even managed to localise individual objects depending on which segment is selected (If france is selected, the label text below changes from "South" to "Sud", and then back again if English is re-selected) using this:
if(German segment) {
NSString *path= [[NSBundle mainBundle] pathForResource:#"de" ofType:#"lproj"];
NSBundle* languageBundle = [NSBundle bundleWithPath:path];
// Returns german version of localised string as label text.
self.defaultName.titleLabel.text = [languageBundle localizedStringForKey:#"PREF_WEATHER_NORTH" value:#"" table:nil];
// I would like to set the default .strings file to 'de' here.
}
else if(French segment) {
NSString *path= [[NSBundle mainBundle] pathForResource:#"fr" ofType:#"lproj"];
NSBundle* languageBundle = [NSBundle bundleWithPath:path];
// Returns french version of localised string as label text
self.defaultName.titleLabel.text = [languageBundle localizedStringForKey:#"PREF_WEATHER_NORTH" value:#"" table:nil];
// I would like to set the default .strings file to 'fr' here
}
else if(British segment) {
NSString *path= [[NSBundle mainBundle] pathForResource:#"en" ofType:#"lproj"];
NSBundle* languageBundle = [NSBundle bundleWithPath:path];
// Returns english version of localised string as label text
self.defaultName.titleLabel.text = [languageBundle localizedStringForKey:#"PREF_WEATHER_NORTH" value:#"" table:nil];
// I would like to set the default .strings file to 'en' here.
}
Now, as I've already got individual strings being localised on button click, I was wondering if I could localise ALL strings in each of the .strings files in one go (so that I'm changing the default .strings used depending on which segment is selected) or at least target as many as possible in an acceptable amount of code.
Obviously, at the moment I could type them one-by-one, but that isn't really an option due to the amount of localised strings used in an application.
Any help would be appreciated.
You can wrap this localization approach in LocalizationSystem class:
#interface PLLocalizationSystem : NSObject
#property (strong, nonatomic) NSString *language;
+ (PLLocalizationSystem *) sharedLocalizationSystem;
- (NSString *) localizedStringForKey:(NSString *)key value:(NSString *)comment;
#end
#implementation PLLocalizationSystem {
NSBundle *_bundle;
}
- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)comment {
return [_bundle localizedStringForKey:key value:comment table:nil];
}
- (void)setLanguage:(NSString *)newLanguage {
...
NSString *path = [[NSBundle mainBundle] pathForResource:newLanguage ofType:#"lproj"];
if (path) {
_bundle = [NSBundle bundleWithPath:path];
_language = newLanguage;
} else {
[self resetLocalization];
}
...
[[NSNotificationCenter defaultCenter] postNotificationName:kLocalizationChangedNotification object:nil];
}
And make some defines for more simpler access (define in LocalizationSystem header file):
#define PLLocalizedString(key, comment) \
[[PLLocalizationSystem sharedLocalizationSystem] localizedStringForKey:(key) value:(comment)]
#define PLLocalizationSetLanguage(language) \
[[PLLocalizationSystem sharedLocalizationSystem] setLanguage:(language)]
#define PLLocalizationGetLanguage \
[[PLLocalizationSystem sharedLocalizationSystem] language]
Also you can import this file from your AppName-Prefix.pch file to get access from anywhere.
Next in your controllers you can observe for localization changes and call appropriate method.
So for now, you can use it in the same way as NSLocalizedString.
P.S. i'm not write here all implementation, but if somebody wants i may later share it on githab
Update. Usage sample.
For example, you have UIViewController which you want to localize dynamically:
#implementation MyCustomViewController
- (void)viewDidLoad {
[super viewDidLoad];
//first of all - register for localization changes
//i made for this purposes category for UIViewController(localizable)
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(localize)
name:kLocalizationChangedNotification
object:nil];
}
//here you should apply all localization
- (void)localize {
[self setTitle:PLLocalizedString(#"myTitle", nil);
[label setText:PLLocalizedString(#"labelTitle", nil)];
....
}
#end
And in your preferences, when you change language call PLLocalizationSetLanguage(language),
and all controllers registered for this event will be localized automatically. Of course, this approach needs that all localization applies in localize method.
And don't forgot to remove observer when controller dealloced or unloaded
You can read: How to force NSLocalizedString to use a specific language and http://aggressive-mediocrity.blogspot.nl/2010/03/custom-localization-system-for-your.html to see how you could do it (the 2nd one is more suited for what you want).
But read this (also from the second link):
In general, you should not change the iOS system language (via use of the AppleLanguages pref key) from within your application. This goes against the basic iOS user model for switching languages in the Settings app, and also uses a preference key that is not documented, meaning that at some point in the future, the key name could change, which would break your application.
If you want to switch languages in your application, you can do so via manually loading resource files in your bundle. You can use NSBundle:pathForResource:ofType:inDirectory:forLocalization: for this purpose, but keep in mind that your application would be responsible for all loading of localized data.
I have added this file in my project with the code:
Language.h:
#interface Language : UIViewController
#end
#implementation Language
static NSBundle *bundle = nil;
+(void)setLanguage:(NSString *)l {
NSLog(#"preferredLang: %#", l);
NSString *path = [[ NSBundle mainBundle ] pathForResource:l ofType:#"lproj" ];
bundle = [[NSBundle bundleWithPath:path] retain];
}
+(void)initialize {
NSUserDefaults* defs = [NSUserDefaults standardUserDefaults];
NSArray* languages = [defs objectForKey:#"AppleLanguages"];
NSString *current = [[languages objectAtIndex:0] retain];
[self setLanguage:current];
}
Here this method +(NSString *)get: is not getting called
+(NSString *)get:(NSString *)key alter:(NSString *)alternate {
NSLog(#"Bundle path: %#", [bundle bundlePath]);
return [bundle localizedStringForKey:key value:alternate table:nil];
}
#end
And in my viewController.m file where I have two buttons for change of language Turkish and English Language:
-(IBAction)englishLang:(id)sender{
[Language setLanguage:#"en"];
NSLog(#"language to english");
}
-(IBAction)turkishLang:(id)sender{
[Language setLanguage:#"tr"];
NSLog(#"language to turkish");
}
The issue is when I click on turkish lang button, the language should be changed and the next nib file should be turkish localized, picked up by the application. But I am not getting that behavior from my application.
It only loads the english localized nib file.
What can be the issue ?
If you want to use an app language that isnt based on the phones language you need to do a couple of things:
make your own version of NSLocalizedString which will return the
string based on your language, not the phones (it sounds like this is the main problem, if you have NSLocalizedStrings through the app, the value returned will only ever be the same as the language of the phone)
post notifications when the language is changed so that your views can refresh with the new content
Okay a few things here:
1) Why do you retain in +initialize the current string? That's going to leak.
2) What did you expect? You're only changing the language on a very high level. The app definitely doesn't observe these settings to instantly adjust the language. It might work after relaunching the app but it's not the way to solve this.
3) Why would you want to change the language within an app? Nobody will ever expect this feature in your app. So I'd just ditch this feature because it's unnecessary.
Your issue is that the nib loading doesn't know that it should use your own i18n method. So you need to fill labels etc programmatically in the viewDidLoad method
How can I do I18N within my app leaving the language setting of the device out?
I want to have a screen after the splash screen asking the user for the language he want to use the app in. This should be independent from the system language.
Is there an easy way to just set the language setting in the apps context? Something like:
[[UIApplication sharedApplication] setLocale/setBundle/setLanguage]
Thanks for any advice.
EDIT
Ok, I made some progress. Is there a way to override NSLocalizedString()? The problem is that setting the NSUserDefaults to another language code will only be recognized after restarting the app. That is of course no option for me.
I ended up with this solution. I first define a static class to set my In-App language on:
#implementation I18NUtil
static NSBundle *_bundle;
static NSString *_language;
+ (id)alloc
{
[NSException raise:#"Cannot be instantiated!" format:#"Static class 'ClassName' cannot be instantiated!"];
return nil;
}
+ (void)setLanguage:(NSString *)languageCode
{
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithObjects:languageCode, nil] forKey:#"AppleLanguages"];
NSString *path = [[NSBundle mainBundle] pathForResource:languageCode ofType:#"lproj"];
_bundle = [NSBundle bundleWithPath:path];
_language = languageCode;
}
+ (NSString *)localizedString:(NSString *)key
{
return [_bundle localizedStringForKey:key value:#"" table:nil];
}
#end
You could easily use:
[I18NUtil localizedString:#"myLocalizedString"]
in your whole project now.
But I found it a lot easier to override the NSLocalizedString macro and use the i18n like I am used to it. You have to include this into your AppName-Prefix.pch:
#ifdef __OBJC__
...
// Override native NSLocalizedString(key,message) to achieve in-app and on-the-fly i18n
// Needs to be defined AFTER importing Foundation.h
#import "I18NUtil.h"
#ifdef NSLocalizedString
#undef NSLocalizedString
#endif
#define NSLocalizedString(key, message) \
[I18NUtil localizedString:(key)]
#endif
Have fun!
You can use an arbitrary strings file with NSLocalizedStringFromTable.
For instance, you can have all your strings file organized like this:
localizable_buttons_english.strings
localizable_buttons_spanish.strings
...
localizable_errors_english.strings
localizable_errors_spanish.strings
...
Then, using user's chosen language in your app preferences to obtain the translation in any language:
NSString *lang = <# read user prefs #> // Example: spanish
NSString *table = [NSString stringWithFormat:#"localizable_buttons_%#", lang];
NSString *text = NSLocalizedStringFromTable(#"SendButtonLabel", table, nil);
sendButton.titleLabel.text = text;
// localizable_buttons_english.strings
"SendButtonLabel" = "Send";
// localizable_buttons_spanish.strings
"SendButtonLabel" = "Enviar";
Localizing NIB files is just a matter of loading the NIBs programmatically using the user chosen language to build the NIB name.
I will be sending out an update to my app with a new data structure, therefore if a user is updating my app I need to update their current data. So I was wondering how can I programatically tell if the user updated my app or installed a new copy (if a new copy is installed I don't need to update anything) ?
Checking the data structure is a solid solution. I began to worry in my own apps about folks who don't upgrade for several versions. I felt this would lead to a myriad of structure checks. The code I show below determines and stores the version and previous version in the NSUserDefaults. You could code for those varying version difference scenarios if needed.
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
BOOL versionUpgraded;
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"];
NSString *preVersion = [prefs stringForKey:#"appVersion"];
if ([prefs stringForKey:#"appVersion"] != nil) {
//see if version is the same as prior
//if not it is an Upgraded
versionUpgraded = !([preVersion isEqualToString: version]);
} else {
//nil means new install
//This needs to be YES for the case that
//"appVersion" is not set anywhere else.
versionUpgraded = YES;
}
if (versionUpgraded) {
[prefs setObject:version forKey:#"appVersion"];
[prefs setObject:preVersion forKey:#"prevAppVersion"];
[prefs synchronize];
}
That depends on the kind of data structure you're using.
In general, I would advise you against relying on checking your application version: a user using 2.0 might have just upgraded or it might be a new user.
I'd rather check if there's a data structure already, and act accordingly. Assuming that you're using a Sqlite-backed Core Data storage, you can either check whether the .sqlite file exists, or check if there are objects in your storage.
Just save the bundle version somewhere and check if it differs from
[[NSBundle mainBundle] objectForInfoDictionaryKey:#"CFBundleVersion"]]
on each app startup.
I have created a category for this. Just implement the two new delegate calls found in the header. It relies quite heavily on the obj-c runtime libraries, so make sure you are confident with them before using this.
.h
#import <UIKit/UIKit.h>
#protocol UIApplicationDelegate <UIApplicationDelegate>
#optional
- (void) application:(UIApplication *)application willUpdateToVersion: (NSString*) newVersion fromVersion: (NSString*) previousVersion;
- (void) application:(UIApplication *)application didUpdateToVersion: (NSString*) newVersion fromVersion: (NSString*) previousVersion;
#end
#interface UIApplication (Versioning)
#end
.m
#import "UIApplication+Versioning.h"
#import <objc/message.h>
#import <objc/runtime.h>
static NSString* UIApplicationVersionFileName = #"app.ver";
#implementation UIApplication (Versioning)
+ (void) load {
Method original, swizzled;
original = class_getInstanceMethod(self, #selector(setDelegate:));
swizzled = class_getInstanceMethod(self, #selector(swizzled_setDelegate:));
method_exchangeImplementations(original, swizzled);
}
- (void) swizzled_setDelegate: (id<UIApplicationDelegate>) delegate {
IMP implementation = class_getMethodImplementation([self class], #selector(swizzled_application:didFinishLaunchingWithOptions:));
class_addMethod([delegate class], #selector(swizzled_application:didFinishLaunchingWithOptions:), implementation, "B#:##");
Method original, swizzled;
original = class_getInstanceMethod([delegate class], #selector(application:didFinishLaunchingWithOptions:));
swizzled = class_getInstanceMethod([delegate class], #selector(swizzled_application:didFinishLaunchingWithOptions:));
method_exchangeImplementations(original, swizzled);
[self swizzled_setDelegate: delegate];
}
- (BOOL)swizzled_application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//Check for a version change
NSError* error;
NSArray* directories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* versionFilePath = [[directories objectAtIndex: 0] stringByAppendingPathComponent: UIApplicationVersionFileName];
NSString* oldVersion = [NSString stringWithContentsOfFile: versionFilePath
encoding: NSUTF8StringEncoding
error: &error];
NSString* currentVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey: #"CFBundleVersion"];
switch (error.code) {
case NSFileReadNoSuchFileError:
{
//Delegate methods will not be called first time
oldVersion = [currentVersion copy];
[currentVersion writeToFile: versionFilePath
atomically: YES
encoding: NSUTF8StringEncoding
error: &error];
break;
}
default:
{
NSLog(#"Warning: An error occured will loading the application version file -> Recreating file");
[[NSFileManager defaultManager] removeItemAtPath: versionFilePath
error: nil];
oldVersion = [currentVersion copy];
[currentVersion writeToFile: versionFilePath
atomically: YES
encoding: NSUTF8StringEncoding
error: &error];
break;
}
}
if( ![oldVersion isEqualToString: currentVersion] ) {
if ([[application delegate] respondsToSelector: #selector(application:willUpdateToVersion:fromVersion:)]) {
objc_msgSend([application delegate], #selector(application:willUpdateToVersion:fromVersion:), currentVersion, oldVersion);
}
[currentVersion writeToFile: versionFilePath
atomically: YES
encoding: NSUTF8StringEncoding
error: &error];
if ([[application delegate] respondsToSelector: #selector(application:didUpdateToVersion:fromVersion:)]) {
objc_msgSend([application delegate], #selector(application:willUpdateToVersion:fromVersion:), currentVersion, oldVersion);
}
}
SEL realSelector = #selector(swizzled_application:didFinishLaunchingWithOptions:);
return (BOOL) objc_msgSend([application delegate], realSelector, application, launchOptions);
}
#end