Adding custom language dialect in iOS App - ios

I was doing localization of an app in which I face an issue regarding dialects of a country's language. My main question is, is there any provision of adding custom language.
Eg:
Suppose there are two languages:
PL for Poland
UK for Ukraine
I need to support pl-uk i.e Poland Ukraini

Adding a pl-UK.lproj would have made sense if this dialect could be chosen from the system preferences, which is not the case. If you have a local setting, I'm afraid there's no other solution than managing the localisations yourself - and it won't work for Interface Builder files.
The simplest is to store all the pl-UK differences in a separate file (it can be a .strings that you store into the pl.lproj folder (that you localise in Polish Polish - to respect the semantics of the system). Then in a custom function, you load those strings:
func localize(_ string : String, comment: comment) {
guard !isUkrainianPolish else {
return NSLocalizedString(string, comment: comment)
}
// retrieve the cache and check if a key with string exists
if let url = Bundle.main.url(forResource: "localizable_pl_UK" /* or any other name*/, withExtension: "strings", subdirectory: nil, localization:"pl"),
let data = try? Data(contentsOf: url),
let plist = (try? PropertyListSerialization.propertyList(from: data, options: [], format: nil)) as? [String:String] {
// cache the dictionary where you want
return plist[string] ?? NSLocalizedString(string, comment: comment)
}
}
Depending on the organisation of your code, you can implement the function in a singleton or the class that handle localizations.

Related

How to get Localization languages from strings file programmatically in Swift?

How do I grab all of the checked languages for Localization programmatically in Swift with Xcode 12?
I want to have these languages in an array that I can display as a picker view so the user can seamlessly change the language without having to change the language of their device. I want to avoid hard coding this.
Screenshot of the Localization check boxes in Xcode from my .strings file
This might do the trick:
func localizations() -> [String] {
guard let contentsURLs = try? FileManager.default.contentsOfDirectory(at: Bundle.main.bundleURL, includingPropertiesForKeys: nil) else { return [] }
let identifiers = contentsURLs.compactMap { anURL -> String? in
guard anURL.pathExtension == "lproj" else { return nil }
return anURL.deletingPathExtension().lastPathComponent
}
let humanReadableNames = identifiers.compactMap { anIdentifier in
return Locale.current.localizedString(forIdentifier: anIdentifier)?.localizedCapitalized
}
return humanReadableNames
}
I even transformed them into human readable languages names, instead of having for instance "zh-Hans" for "Chinese (Simplified)".
Of course, if you handle yourself your locale, your might want to use a different Locale.current, but you get the idea...

SCNParticleSystem load from document Directory

I am trying load SCNParticleSystem from download bundle which i am not able to load.
Path for the resource.
file:///var/mobile/Containers/Data/Application/A91E9970-CDE1-43D8-B822-4B61EFC6149B/Documents/so/solarsystem.bundle/Contents/Resources/
let objScene = SCNParticleSystem(named: "stars", inDirectory: directory)
This object is nil.
This is a legitimate problem since SceneKit does not provide an out-of-the-box solution for initializing particle systems from files that are outside of the main bundle (the only init method SCNParticleSystem.init(named:inDirectory:) implies that SCNParticleSystem.scnp files are in the main bundle).
Luckily for us .scnp files are just encoded/archived SCNParticleSystem instances that we can easily decode/unarchive using NSKeyedUnarchiver:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
return system
}
}
If you do not need to support iOS 9 and iOS 10 you can use NSKeyedUnarchiver.unarchivedObject(ofClass: SCNParticleSystem.self, from: data) instead of NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(_:) and type casting, which was introduced in iOS 11.0.
Another issue that you're most likely to encounter is missing particle images. That is because by default SceneKit will look for them in the main bundle. As of current versions of iOS (which is iOS 12) and Xcode (Xcode 10) particle images in .scnp files (particleImage property) are String values which are texture filenames in the main bundle (that might change, but probably won't, however there's not much else we could use).
So my suggestion is to take that filename and look for the texture file with the same name in the same directory where the .scnp file is:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
system.particleImage = particleImageURL
}
}
return system
}
}
You can just set the URL of the image file and SceneKit will handle it from there.
As a little side-note, the recommended directory for downloadable content is Application Support directory, not Documents.
Application Support: Use this directory to store all app data files except those associated with the user’s documents. For example, you might use this directory to store app-created data files, configuration files, templates, or other fixed or modifiable resources that are managed by the app. An app might use this directory to store a modifiable copy of resources contained initially in the app’s bundle. A game might use this directory to store new levels purchased by the user and downloaded from a server.
(from File System Basics)
Don't have enough reps to add the comment so adding it as the answer.
The answer by Lësha Turkowski works for sure but was had issues with loading the particle images using only NSURL.
All particles were appearing square which meant,
If the value is nil (the default), SceneKit renders each particle as a
small white square (colorized by the particleColor property).
SCNParticleSystem particleImage
In the documentation it says You may specify an image using an
NSImage (in macOS) or UIImage (in iOS) instance, or an NSString or
NSURL instance containing the path or URL to an image file.
Instead of using the NSURL, ended up using the UIImage and it loaded up fine.
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
// load up the NSURL contents in UIImage
let particleUIImage = UIImage(contentsOfFile: particleImageURL.path)
system.particleImage = particleUIImage
}
}
return system
}
}
I found out, that sometimes when dragging a SCNParticleSystem file into your project (probably form a different project) a silent error can happen due to some bugs in Xcode. As a result you can't get a reference to an instance of your SCNParticleSystem.
Solution: Check your BuildSettings in your target. The SCNPaticleSystem AND the associated ImageFile should be listed there and then you should get it right. (see screenShot below)

iOS Localization, simulator and device always uses Base.lproj instead of users preferred locale

I am creating an app that for now I would like to offer in English and German. I checked the Base localization mark in the project configuration and added German. I left English as the development language.
Then I created a file Translation.plist which basically consists of dictionaries that I call categories, e.g. I have one dictionary for button texts, label texts etc. Each of the category dictionaries again consists of dictionaries that contain two Strings: value and comment. Translation.plist is localized via XCode. The folders Base.lproj, en.lproj and de.lproj exist and contain a copy of the plist-file as expected.
Then I created a class Translator.swift that is supposed to load the Translation.plist file as an NSDictionary depending on the user's preferred locale. The code looks like this:
func relevantDictionary(category: String) -> NSDictionary {
let preferredLocale = Bundle.main.preferredLocalizations.first ?? "Base"
NSLog("User's preferred locale is \(preferredLocale)")
guard let url = Bundle.main.url(forResource: "Translation", withExtension: "plist") else {
fatalError("Could not find Translation.plist")
}
NSLog("Using \(url.absoluteURL) for translation")
guard let root = NSDictionary(contentsOf: url) else {
fatalError("Could not find dictionary for category (locale=\(preferredLocale)")
}
guard let relevant = root.value(forKey: category) as? NSDictionary else {
fatalError("Could not create dictionary from Translation.plist")
}
return relevant
}
Then I created a String extension that uses the Translator as follows:
func localize(category: String) -> String {
return Translator.instance.translate(category: category, string: self)
}
With this I call the Translator by "yes".localize("button"). In English I would expect "Yes", in German I would expect "Ja". The log says the following:
2017-07-05 08:45:24.728 myApp[13598:35048360] User's preferred locale is de_DE
2017-07-05 08:45:24.728 myApp[13598:35048360] Using file:///Users/me/Library/Developer/CoreSimulator/Devices/A39D3318-943D-4EFE-BB97-5C2218279132/data/Containers/Bundle/Application/4614E696-B52E-4C30-BBE8-3C76F6392413/myApp.app/Base.lproj/Translation.plist for translation
I wonder why this happens and what I have missed. I would have expected that de.lproj/Translation.plist is loaded instead of Base.lproj/Translation.plist.
Any help is highly appreciated.
You can do it with single .plist file. You don't need to create different .plist files for it.
Firstly, Add English and German countries with locale in Project -> info
https://i.stack.imgur.com/M4QIY.png
Once, you added countries with locale in Project -> info then add localizable.strings file in your bundle.
https://i.stack.imgur.com/lnjgL.png
At the end, just add country's locale in your language support class.
NSArray* languages = #[#"en", #"de"];`enter code here`
NSString *current = [languages objectAtIndex:0];
[self setLanguage:current];
Hope it would help you.

Force NSLocalizedString to use a specific language using Swift

With swift, how can I force my app to read data from a specific Localizable.strings.
I put this in didFinishLaunchingWithOptions before instantiate the ViewController but it still show me the App in English.
NSUserDefaults.standardUserDefaults().removeObjectForKey("AppleLanguages")
NSUserDefaults.standardUserDefaults().setObject("fr", forKey: "AppleLanguages"
NSUserDefaults.standardUserDefaults().synchronize()
And I tried to pass an Array for the "AppleLanguages" key like this but it still doesn't work:
NSUserDefaults.standardUserDefaults().setObject(["fr"], forKey: "AppleLanguages"
And once this is done, can I call this inside the App and take the changes in consideration without restarting the App?
It's not possible to change app's language immediately by changing the value of AppleLanguages. It requires restarting the app before the change takes effect.
It seems that your problem is accessing the localization strings of different languages rather than changing the app's language, right? If you want your app to support multiple languages, you can just provide the translations and rely on settings.app for the actual change.
If you want to access the localization strings from other than currently used localization, you need to get access to the proper translations bundle. And then just query that bundle for the translations. The following piece of code should do the trick.
let language = "en"
let path = Bundle.main.path(forResource: language, ofType: "lproj")
let bundle = Bundle(path: path!)
let string = bundle?.localizedStringForKey("key", value: nil, table: nil)
With NSLocalizedString you can specify the bundle.
let language = "fr"
let path = Bundle.main.path(forResource: language, ofType: "lproj")!
let bundle = Bundle(path: path)!
let localizedString = NSLocalizedString(key, bundle: bundle, comment: "")
Or with a bundle, you may also call localizedStringForKey:value:table: directly too.
#Radu I also made this working for XCUITests thanks to #Markus' original answer :
You can specify explicitly the path to your MainBundle, it will only work on your Mac with the Simulator, but it is often used in continuous integration platforms so this might be acceptable :
let language: String = "en"
let path = "/Users/{username}/{path_to_your_project}/\(language).lproj"
let bundle = Bundle(path: path)
let string = bundle?.localizedString(forKey: "key", value: nil, table: nil)
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 it as global 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
NSLocalizedString("StringToLocalize", tableName: nil, bundle: customLang.createBundlePath(), 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!

iOS: How to retrieve locale strings for a specific locale than what the user has set in OS?

Is it possible to retrieve strings for a specific locale programmatically regardless of what locale the phone is set to? For example, users may be running the phone in English, but I want to retrieve French strings instead without changing the OS locale setting.
Note: This is not a duplicate of the above question. I do not want to override the current setting within my app, I merely want to have the ability to retrieve language values of whatever locale I wish programatically. My app contents may be displaying English text, but I want a specific component of my app to display a different language instead.
I solved this by extending String with this method. You can get localized string for any locale you have in your app this way.
extension String {
func localized(forLanguageCode lanCode: String) -> String {
guard
let bundlePath = Bundle.main.path(forResource: lanCode, ofType: "lproj"),
let bundle = Bundle(path: bundlePath)
else { return "" }
return NSLocalizedString(
self,
bundle: bundle,
value: " ",
comment: ""
)
}
}
Example (get localized string for ukrainian language when system language is english):
"settings_choose_language".localized(forLanguageCode: "uk")

Resources