iOS user with language of "en", no region code, translation lookup fails - ios

We have a bug reported where a user has a device with an en language and nil region code, and thus all NSLocalizedString lookups in are failing, meaning our string key is what is rendered onscreen. Thus, if we had this in our en.lproj/Localizable.strings file:
"some_key" = "Some string.";
It would render some_key instead of Some string. in our UI.
First question: how do I replicate this scenario locally? This question on Stack seems to almost describe the issue, but does not describe how one enters this state.
Second question: why would iOS not fall back to English in the event the region code was nil?

Second question: why would iOS not fall back to English in the event
the region code was nil?
The cause can be "There is no base development language that is enabled". Or it is iOS logic.
Here is my solution for Localization. I just want to share with you an alternative solution if it can help you to solve the issue.
public enum AppResourceLang: String {
case en
case vi
}
public class AppResManager {
public static let shared = AppResManager()
public var lang = "en"
var textBundle: Bundle!
public var mainBundle: Bundle!
private init() {
let mainBundleId = Bundle.main.bundleIdentifier!
// we use mainBundle's bundleIdentifier because normally your running target will contains "lproj" in Copy Bundle Resource
// If your text/image is located on different project/target or framework, you need to enter that target's bundleId.
mainBundle = Bundle(identifier: mainBundle)!
let path = mainBundle.path(forResource: lang, ofType: "lproj")
textBundle = Bundle(path: path!)
}
public func changeLang(code: String) {
if let path = mainBundle.path(forResource: code, ofType: "lproj") {
lang = code; textBundle = Bundle(path: path)
} else {
// fallback to English
lang = "en"
let path = mainBundle.path(forResource: lang, ofType: "lproj")
textBundle = Bundle(path: path!)
}
}
}
Then we can use above textBundle like below:
public extension String {
var localText: String {
guard let bundle = AppResManager.shared.textBundle else { return self }
return NSLocalizedString(self, bundle: bundle, comment: "")
}
var lTextUpcased: String {
guard let bundle = AppResManager.shared.textBundle else { return self.uppercased() }
return NSLocalizedString(self, bundle: bundle, comment: "").uppercased()
}
}
Here is my AppResource (like a framework). We can see I have Localizable.strings and it is localized for EN, VI.
Here is the real file/folder structure, you can see if you check on English in Localization for *.string, *.storyboard,... file. It will be cloned and saved in this folder (ex: en.lproj). You can base on this to point to the corresponding resource file.
Then, how to use my above codes. It is just a way to allow you to completely control the Localization.
public enum AppLanguage: String {
case en
case vi
}
// MARK: - Device info
public static func getDeviceLang() -> AppLanguage {
guard let languageCode = Locale.current.languageCode else { return .en }
let appLang = AppLanguage(rawValue: languageCode.lowercased()) ?? .en
return appLang
}
// Then this will switch the language of your resource based on device language.
AppResManager.shared.changeLang(code: YourGlobalClass.getDeviceLang().rawValue)
// Then use the above string extension to load the corresponding text
// Anywhere in your project.
let text = "your_key".localText

Related

How to get top level domain name of URL in swift 4?

My requirement is to get domain name of URL by filtering out it's subdomain name.
i can get host name by using code as below
if let url = URL(string: "https://blog.abc.in/") {
if let hostName = url.host {
print("host name = \(hostName)") // output is: blog.mobilock.in
}
}
so here in URL blog is a subdomain and abc is a domain name, I wish to know/print only abc by excluding its subdomain parts.
In android, there is a class InternetDomainName which return domain name, the similar solution I am looking for iOS
I tried several answers and it's not duplicate of any or some of them is not working or that is a workaround.
Get the domain part of an URL string?
So finally i found better and standard approach for this issue -
Mozilla volunteers maintain Public Suffix List and there you can find list of library for respective language.
so in list Swift library is also present.
At the time of writing this answer Swift library don't have provison of adding it through CocoPods so you have to add downloaded project directly into your project. Code to get TLD name assuming Swift library is added into your project.
import DomainParser
static func getTLD(withSiteURL:String) -> String? {
do{
let domainParse = try DomainParser()
if let publicSuffixName = domainParse.parse(host: withSiteURL)?.publicSuffix {
if let domainName = domainParse.parse(host: withSiteURL)?.domain {
let tldName = domainName.replacingOccurrences(of: publicSuffixName, with: "").replacingOccurrences(of: ".", with: "")
print("top level name = \(tldName)")
return tldName
}
}
}catch{
}
return nil
}
Add Domain parser library as sub-project of your project, as pod of this library is not available yet
It is just a workaround but works perfectly:
if let url = URL(string: "https://x.y.z.a.b.blog.mobilock.in/") {
if let hostName = url.host {
print("host name = \(hostName)") // output is: x.y.z.a.b.blog.mobilock.in
let subStrings = hostName.components(separatedBy: ".")
var domainName = ""
let count = subStrings.count
if count > 2 {
domainName = subStrings[count - 2] + "." + subStrings[count - 1]
} else if count == 2 {
domainName = hostName
}
print(domainName)
}
}
Let me know if you face any issue.
There is no simple way, regardless of language. See How to extract top-level domain name (TLD) from URL for some good discussion of the difficulties involved.
To fetch the root domain of a URL, you can use the following URL extension:
extension URL {
var rootDomain: String? {
guard let hostName = self.host else { return nil }
let components = hostName.components(separatedBy: ".")
if components.count > 2 {
return components.suffix(2).joined(separator: ".")
} else {
return hostName
}
}
}

Get all URLs for resources in sub-directory in Swift

I am attempting to create an array of URLs for all of the resources in a sub-directory in my iOS app. I can not seem to get to the correct path, I want to be able to retrieve the URLs even if I do not know the names (i.e. I don't want to hard code the file names into the code).
Below is a screen shot of the hierarchy, I am attempting to get all the files in the 'test' folder:
Any help is greatly appreciated, I have attempted to use file manager and bundle main path but to no joy so far.
This is the only code I have currently:
let fileManager = FileManager.default
let path = Bundle.main.urls(forResourcesWithExtension: "pdf", subdirectory: "Files/test")
print(path)
I have also tried this code but this prints all resources, I can't seem to specify a sub-directory:
let fm = FileManager.default
let path = Bundle.main.resourcePath!
do {
let items = try fm.contentsOfDirectory(atPath: path)
for item in items {
print("Found \(item)")
}
} catch {
// failed to read directory – bad permissions, perhaps?
}
Based on an answer from #vadian , The folders were changed from virtual groups to real folders. Using the following code I was able to get a list of resources:
let fileManager = FileManager.default
let path = Bundle.main.resourcePath
let enumerator:FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: "\(path!)/Files/test")!
while let element = enumerator.nextObject() as? String {
if element.hasSuffix("pdf") || element.hasSuffix("jpg") { // checks the extension
print(element)
}
}
Consider that the yellow folders are virtual groups, not real folders (although Xcode creates real folders in the project directory). All files in the yellow folders are moved into the Resources directory in the bundle when the app is built.
Real folders in the bundle are in the project navigator.
You can follow the following steps to get them:
Create a new folder inside your project folder with the extension is .bundle (for example: Images.bundle).
Copy resource files into that new folder.
Drag that new folder into the project that opening in Xcode.
Retrieve the URLs by using the following code snippet:
let urls = Bundle.main.urls(forResourcesWithExtension: nil, subdirectory: "Images.bundle")
You can also view the guide video here: https://youtu.be/SpMaZp0ReEo
I came across a similar issue today. I needed to retrieve the URL of a resource file in a bundle ignoring its path.
I wrote the following:
public extension Bundle {
/// Returns the file URL for the resource identified by the specified name, searching all bundle resources.
/// - Parameter resource: The name of the resource file, including the extension.
/// - Returns: The file URL for the resource file or nil if the file could not be located.
func recurseUrl(forResource resource: String) -> URL? {
let contents = FileManager.default.allContentsOfDirectory(atPath: self.bundlePath)
for content in contents {
let fileNameWithPath = NSString(string: content)
if let fileName = fileNameWithPath.pathComponents.last {
if resource == fileName {
return URL(fileURLWithPath: content)
}
}
}
return nil
}
Based on this:
public extension FileManager {
/// Performs a deep search of the specified directory and returns the paths of any contained items.
/// - Parameter path: The path to the directory whose contents you want to enumerate.
/// - Returns: An array of String objects, each of which identifies a file or symbolic link contained in path. Returns an empty array if the directory does not exists or has no contents.
func allContentsOfDirectory(atPath path: String) -> [String] {
var paths = [String]()
do {
let url = URL(fileURLWithPath: path)
let contents = try FileManager.default.contentsOfDirectory(atPath: path)
for content in contents {
let contentUrl = url.appendingPathComponent(content)
if contentUrl.hasDirectoryPath {
paths.append(contentsOf: allContentsOfDirectory(atPath: contentUrl.path))
}
else {
paths.append(contentUrl.path)
}
}
}
catch {}
return paths
}
}
Which achieves the goal of retrieving the URL for the first match of a given resource filename in a bundle's resources, all directories wide.
I tend to think that Swift's func url(forResource name: String?, withExtension ext: String?) -> URL? should behave this way in the first place.

Changing language at runtime using SwiftGen

My app is supposed to support language change at runtime. I'm using SwiftGen 5.0. ViewControllers subscribe to language change notification and I've checked that the localisation function fires correctly. My overriden tr function looks like this:
fileprivate static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
guard let bundle = LanguageManager.shared.bundle else {
fatalError("Cannot find bundle!")
}
let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "")
let locale = Locale(identifier: LanguageManager.shared.currentLanguageKey!)
return String(format: format, locale: locale, arguments: args)
}
The bundle is set like so:
if let path = Bundle.main.path(forResource: currentLanguageKey, ofType: "lproj") {
bundle = Bundle(path: path)
}
However, the tr function returns mostly previous language strings. Only one out of all labels currently in memory refreshes. Setting a breakpoint inside the function and printing bundle returns
NSBundle </var/containers/Bundle/Application/ED5A6C7D-1807-4319-8817-45E693BC45E2/MyApp.app/en_US.lproj> (not yet loaded)
which is the correct new language. After app restarts the language is set correctly. Am I doing something wrong?
Okay, I found the problem. The stencil was generating static variables:
static let label = L10n.tr("Localizable", "registration_verify.pin_code.label")
Changing stencil to generate computed properties fixed the behaviour:
static var label: String {
return L10n.tr("Localizable", "registration_verify.pin_code.label")
}
Now you can config lookupFunction params in swiftgen.yml file
strings:
inputs:
- en.lproj
outputs:
- templateName: structured-swift5
params:
lookupFunction: Localize_Swift_bridge(forKey:table:fallbackValue:)
output: L10n-Constants.swift
in your project you just need implement lookupFunction,
your can use use Localize_Swift library
import Localize_Swift;
func Localize_Swift_bridge(forKey:String,table:String,fallbackValue:String)->String {
return forKey.localized(using: table);
}
generated code may like this:
internal enum Localizable {
internal static var baseConfig: String { return
L10n.tr("Localizable", "base config", fallback: #"Base Config"#) }}
extension L10n {
private static func tr(_ table: String, _ key: String, _ args: CVarArg..., fallback value: String) -> String {
let format = Localize_Swift_bridge(forKey:table:fallbackValue:)(key, table, value)
return String(format: format, locale: Locale.current, arguments: args)
}
}
https://github.com/SwiftGen/SwiftGen/blob/stable/Documentation/templates/strings/structured-swift5.md
https://github.com/marmelroy/Localize-Swift

Should I set Build Configuration in Xcode

When distribution app on app store should I also set Build Configuration in Xcode from edit scheme like this
I would recommend to set up two schemas.
First one: Development -> setup with Debug build configuration.
You can use this while you are developing your app. This will give you logging, easy debugging, etc..
Second one: Distribution -> setup with Release build configuration.
Logging will not happen on this schema, also debugging will be unavailable, because the build is not optimizaed for that.
When you are preparing your submittal to the App Store, archive the Distribution schema using the Release build configuration.
You can find some more detailed description here about the difference between Debug and Release build configurations.
This will cover your question in depth. I have used build configuration environment so that if you make a build in release Configuration. your automatic values will be se according to release version. you can also download the sample code from the link below to see actually what happens when you change the scheme.
First step.
Add the variable "Configuration" in your info.plist and add value "$(CONFIGURATION)" there.
Make a Config.swift file and copy paste the below code there.
`
import Foundation
private let configManagerSharedInstance = ConfigManager()
class Config {
class var sharedInstance: ConfigManager {
return configManagerSharedInstance
}
}
// You can put as much Environment as you need but you make sure you also put these environment in the config.plist file.
enum Environment: String {
case Debug = "Debug"
case Production = "Release"
}
class ConfigManager: NSObject {
private var environment: Environment?
var config: NSDictionary?
override init() {
super.init()
// Retrieve the current evironment from the main bundle
if let currentEnvironment = Bundle.main.infoDictionary?["Configuration"] as? String {
// Store the current environment for later use
environment = Environment(rawValue: currentEnvironment)
if let projectConfigPath = Bundle.main.path(forResource: "Config", ofType: "plist") {
if let projectConfigContents = NSDictionary(contentsOfFile: projectConfigPath) as? Dictionary<String, AnyObject> {
config = projectConfigContents[currentEnvironment] as? Dictionary<String, AnyObject> as NSDictionary?
}
} else {
print("config file not found")
}
}
}
func getCurrentEnvironment() -> Environment? {
return self.environment
}
func configForKey(key: String) -> String {
return config?[key] as! String
}
//It will use to get sub dictionary and their values.
func configForCategory(category: String, andKey: String) -> String {
let configuration = config?.value(forKeyPath: category) as! NSDictionary
return configuration.value(forKeyPath: andKey) as! String
}
}
`
I have also made a file Constants.swift in which i have set the varibles using the above code.
`
//
// Constants.swift
// BuildConfiguration
//
// Created by Ourangzaib khan on 4/6/17.
// Copyright © 2017 Ourangzaib khan. All rights reserved.
//
let kBASE_URL : String = {
print(Config.sharedInstance.configForKey(key: "kBASE_URL"));
return Config.sharedInstance.configForKey(key: "kBASE_URL")
}()
let STRIPEKEY : String = {
return Config.sharedInstance.configForCategory(category: "Stripe", andKey: "Publishable Key")
}()
let PUBNUBKEYSUBSCRIBE : String = {
return Config.sharedInstance.configForCategory(category: "PubNub", andKey: "Publish Key")
}()
let PUBNUBKEYPUBLISH : String = {
return Config.sharedInstance.configForCategory(category: "PubNub", andKey: "Subscribe Key")
}()
let WOWZAKEY : String = {
return Config.sharedInstance.configForKey(key: "Wowza");
}()
`
Now you just have to select the environment using edit sceme go into the edit scheme and chose Build Configuration Now when you will run the project you will see this output WRT build configuration in below images.
https://github.com/ourangzeb/Build-Configuration-for-IOS

Check language in iOS app

Task is : I have got two UIImageViews, and I want present ImageView1 if system language is Ukrainian, and if it is not Ukrainian(English/Polish etc) I want present ImageView2.
I tried :
println(NSUserDefaults.standardUserDefaults().objectForKey("AppleLanguages"))
but this code gives only list of available languages. I also tried
var language: AnyObject? = NSLocale.preferredLanguages().first
but how can I compare this variable with English or Ukrainian language?
Swift 3
You can take the language code like this
let preferredLanguage = NSLocale.preferredLanguages[0]
And then you need to compare it with code string
if preferredLanguage == "en" {
print("this is English")
} else if preferredLanguage == "uk" {
print("this is Ukrainian")
}
You can find codes here
An example to check if French ...
/// Is Device use french language
/// Consider, "fr-CA", "fr-FR", "fr-CH" et cetera
///
/// - Returns: Bool
static func isFrench() -> Bool {
return NSLocale.preferredLanguages[0].range(of:"fr") != nil
}
Swift 5
Locale.current.regionCode // Optional("US")
Locale.current.languageCode // Optional("en")
Locale.current.identifier // en_US
With extension
extension Locale {
var isKorean: Bool {
return languageCode == "ko"
}
}
Locale.current.isKorean => false
Swift 4 If you have more languages in a queue (preferredLanguage will returns: "uk-US" for example) but you want first in it. You can do it like this:
let preferredLanguage = NSLocale.preferredLanguages[0]
if preferredLanguage.starts(with: "uk"){
print("this is Ukrainian")
} else{
print("this is not Ukrainian")
}
you may use the below code
it works fine with swift 3
if Bundle.main.preferredLocalizations.first == "en" {
print("this is english")
}else{
print("this not english")
}
In addition to what mentioned before, you can add an entry in each Localizable file to tell you which dictionary is being used.
// Localizable.strings (en)
"language" = "en";
// Localizable.strings (ar)
"language" = "ar";
// Usage
NSLocalizedString("language", comment: "")
// check the resulted string

Resources