I am aware of quite a few posts that say this should not be done, or is not possible. I tinkered with a few ideas and now I'm asking this question, because I want to be absolutely sure there are no other options.
Option 1:
The most popular solution is to change AppleLanguages as in this post. I do not mind the idea of requiring a restart, so this would be an acceptable solution for me, except that you cannot restart your app programmatically (can't find the method, or would be rejected by Apple). Asking the user to manually restart the application wouldn't be ideal.
Option 2:
The next solution is to get the appropriate bundle and perform a localizedStringForKey lookup on each and every UILabel, UIButton, etc. This can be a little tedious but is okay for me, since I already added localizationProperties (similar to this) to these views so that I can have a centralized strings file.
AppDelegate.swift:
static var userLanguage: String?
{
set
{
let defaults = NSUserDefaults.standardUserDefaults();
defaults.setObject(newValue, forKey: LanguageKey);
defaults.synchronize();
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle());
instance.window?.rootViewController = storyboard.instantiateInitialViewController();
}
get
{
let defaults = NSUserDefaults.standardUserDefaults();
return defaults.stringForKey(LanguageKey);
}
}
Localization.swift:
private var bundle: NSBundle
{
get
{
let bundle: NSBundle;
#if TARGET_INTERFACE_BUILDER
bundle = NSBundle(forClass: self.dynamicType);
#else
bundle = NSBundle.mainBundle();
#endif
let lang: String;
if(AppDelegate.userLanguage == nil || AppDelegate.userLanguage == "en")
{
lang = "Base";
}
else
{
lang = AppDelegate.userLanguage!;
}
let path = bundle.pathForResource(lang, ofType: "lproj");
if(path != nil)
{
let toreturn = NSBundle(path: path!);
if(toreturn != nil)
{
return toreturn!;
}
}
return bundle;
}
}
extension UILabel
{
#IBInspectable var localizedText: String?
{
get { return "" }
set
{
if(newValue != nil)
{
text = bundle.localizedStringForKey(newValue!, value:"", table: nil);
}
}
}
}
The problem with option 2 is that this only sets the language, for those fields. Layout direction will be unchanged, and files such as language specific layouts would not be used.
By extending UIApplication I am able to specify a custom userInterfaceLayoutDirection which successfully swaps all layouts between LTR and RTL.
DemoApplication.swift:
class DemoApplication: UIApplication
{
override internal var userInterfaceLayoutDirection: UIUserInterfaceLayoutDirection
{
get
{
if(AppDelegate.userLanguage == "ar")
{
return UIUserInterfaceLayoutDirection.RightToLeft;
}
return UIUserInterfaceLayoutDirection.LeftToRight;
}
}
}
Now when I set AppDelegate.userLanguage the application will reset to the initial view controller, displaying the new language, flipping the layout between LTR and RTL. This does not address the issue of language specific files, and I've also noticed that text remains left or right aligned within its own bounds.
Since I can't find the source code for native iOS classes, I can't see what language specific variables are set at startup so I assumed it is linked to the NSBundle.mainBundle. I tried to override it by using method swizzling.
extension NSBundle
{
override public class func initialize()
{
struct Static
{
static var token: dispatch_once_t = 0;
}
// make sure this isn't a subclass
if (self !== NSBundle.self)
{
return;
}
dispatch_once(&Static.token)
{
do
{
try jr_swizzleClassMethod("mainBundle", withClassMethod: "mainBundleExt");
}
catch
{
print("\(error)");
}
}
super.initialize();
}
public class func mainBundleExt() -> NSBundle
{
let bundle = self.mainBundleExt(); // Due to swizzling, this is the "super" method
let lang: String;
if(AppDelegate.userLanguage == nil || AppDelegate.userLanguage == "en")
{
lang = "Base";
}
else
{
lang = AppDelegate.userLanguage!;
}
let path = bundle.pathForResource(lang, ofType: "lproj");
if(path != nil)
{
let toreturn = NSBundle(path: path!);
if(toreturn != nil)
{
return toreturn!;
}
}
}
}
This does not work though, it seems as though the default mainBundle is still used.
So my question is this: How is mainBundle assigned a value? Which other language specific variables are set at startup, such as userInterfaceLayoutDirection.
I assume there are 2 or 3 of these variables. Finally, is it possible for this to work or am I just wasting my time?
Thanks.
I had this problem before and I have used a library. it helped me to change the language on the fly.
try to use this:
https://github.com/Decybel07/L10n-swift
L10n.shared.language = "en"
L10n.shared.language = "en-GB"
At runtime, you can switch the language at any time by setting the language property
Use this line of code it will change layout without closing application. From right to left
UIView.appearance().semanticContentAttribute = .forceRightToLeft
And for Left to Right Flip
UIView.appearance().semanticContentAttribute = .forceLeftToRight
and if you want to change textfield layout or text change then use this code because i faced this issue . textfield's texts was not changning layout. check this code to change layout of textfield text
extension UITextField {
open override func awakeFromNib() {
super.awakeFromNib()
if UserDefaults.languageCode == "ar" {
if textAlignment == .natural {
self.textAlignment = .right
}
}
}
}
Related
Localization is working fine in my application. I want localization the permission dialog text messages. It is working fine with the device language changes but I want to change message according to my application language.
I have tried the following code
import UIKit
class LocalizeHelper: NSObject {
private var myBundle: Bundle? = nil
static let shared: LocalizeHelper = {
let instance = LocalizeHelper()
return instance
}()
override init() {
super.init()
// use systems main bundle as default bundle
myBundle = Bundle.main
}
func localizedString(forKey key: String) -> String {
return myBundle!.localizedString(forKey: key, value: "", table: nil)
}
// Converted with Swiftify v1.0.6331 - https://objectivec2swift.com/
func setLanguage(_ lang: String) {
// path to this languages bundle
let path: String? = Bundle.main.path(forResource: lang, ofType: "lproj")
if path == nil {
// there is no bundle for that language
// use main bundle instead
myBundle = Bundle.main
}
else {
// use this bundle as my bundle from now on:
myBundle = Bundle(path: path!)
// to be absolutely shure (this is probably unnecessary):
if myBundle == nil {
myBundle = Bundle.main
}
}
}
func getLanguage() -> String {
print("\(String(describing: myBundle?.bundlePath.last))")
//return myBundle!.bundlePath.last >> Error
return myBundle!.bundlePath.lastCharacter!
}
}
extension String {
public var lastCharacter: String? {
guard let aLast = self.last else {
return nil
}
return String(aLast)
}
}
I have surfed in StackOverflow but didn't find any solution. Any help shell we appreciated.
Your best bet is to save the application's language in the user defaults as a string for example and then when you take it out of user defaults parse it as a custom app language enum. If you don't have any app language save you can always fall through to the device language but prefer or override it with the app language in user defaults not being nil. Also preferably you should wrap your user defaults interactions with a user defaults manager that you access in your view controller.
So the suggested steps are:
create userdefualts manager
define language enum
implement setLanguage func language parameter
implement getLanguage func language output with device language or
english as guard return
I've read lots of answers about this, but most are many years old and I don't know what's the latest info.
On first launch, my app will ask the user which language (s)he prefers, rather than just using the OS default language.
How can I set the localization rest of the app to the selected language?
How can I get which language the user has selected in other views?
I thought the following code would set the language, but it didn't do the job:
UserDefaults.standard.set("AR", forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
I have handled this with a LanguageManager singleton class, which handles all the localization. Here is some psuedo code, as its typically a fairly large class.
It has a list of all locales that are supported, for example:
let supportedLocales = ['en-US', 'en-CA', 'fr', 'es-ES', 'es-MX']
It also stores the selected language in UserDefaults. When the selectedLocale changes, it sends a Notification, in case you want to notify your views or anything else:
var selectedLocale: String? {
get {
return UserDefaults.standard.object(forKey: UserDefaultsKeys.selectedLocale) as? String
}
set (newLocale) {
let didChange = self.selectedLocale != newLocale
UserDefaults.standard.set(newLocale, forKey: UserDefaultsKeys.selectedLocale)
UserDefaults.standard.synchronize()
if didChange {
NotificationCenter.default.post(name: Notification.Name.localeDidChange, object: nil)
}
}
}
Now, in order to pull strings out of your localized.strings files, you can't use the standard Apple methods - you have to provide your own. Use LanguageManager.shared.getString(for key:String, alt:String) to reference keys in your .strings file to pull out localized strings.
var selectedLanguage:String? {
//returns just the language portion of the locale - eg: 'en' from 'en-US'
if let selectedLocale = selectedLocale {
return selectedLocale.components(separatedBy: "-")[0]
}
return nil
}
func getString(for key:String, alt:String) -> String
{
var val:String? = getString(for:key, language: selectedLocale)
if val == nil {
val = getString(for:key, language: selectedLanguage)
}
if val == nil {
val = getString(for:key, language: "en") //default to English
}
if let val = val {
return val
}
return alternate //use fallback
}
func getString(for key:String, language:String) -> String?
{
let path = Bundle.main.path(forResource:language, ofType:"lproj")
if let languageBundle = Bundle(path:path) {
return languageBundle.localizedString(for: key)
}
return nil
}
By default is the English Language // You can set Language according you
UserDefaults.standard.set("en", forKey: "Apple")
UserDefaults.standard.synchronize()
If you want to current system language than use code
let langStr = Locale.current.languageCode //and set in UserDeafaults
UserDefaults.standard.set(langStr, forKey: "Apple")
var currentlanguage: String?
self.currentlanguage = UserDefaults.standard.object(forKey: "Apple") as! String?
print("current language ---%#",self.currentlanguage)
As the changes of cocoapods 1.0.0.beta.1 say "Localized interface files (XIB, Storyboard) using Base Internationalization - Base.lproj/Main.xib and en.lproj/Main.strings are represented as a variant group named Main.xib" and as far as cocoapods 1.0.0.beta.1 "Special case interface files to use the XIB or Storyboard name for the variant group when using Base Internationalization."
I refer to the demo and use resource_bundles to organize my resources on my podspecs.
s.resource_bundles = {
'Resources' => ['LocalizationDemo/LocalizationDemo/Resources/**/*.{lproj,storyboard}']
}
and my cocoapods version is 1.0.1 but the directories result is
-Resources
--en.lproj
---LocalizationDemo.strings
--LocalizationDemo.storyboard
--de.lproj
---LocalizationDemo.strings
It's not the result directories which I expected and the interface internationalization don't work.
Cloud anyone show me a correct usage or demo?
ADD: I use import/export localization by xcode before be pod to other projects. and I want it's could work by imported xliff files directly instead of add the IBOutlet or a subclass for UI controls.
You should write classes for your controls used in XIB OR Storyboard Views & assign the classes to respective control types like this -
class LocalizedTextField: UITextField {
override func drawPlaceholderInRect(rect: CGRect) {
let localizedPlaceHolder = self.placeholder!.localized
self.placeholder = localizedPlaceHolder
super.drawPlaceholderInRect(rect)
}
}
class LocalizedLabel : UILabel {
override func awakeFromNib() {
if let text = text {
self.text = text.localized
self.bounds.size.width = CGFloat.max
self.sizeToFit()
}
}
}
class LocalizedButton : UIButton {
override func awakeFromNib() {
for state in [UIControlState.Normal, UIControlState.Highlighted, UIControlState.Selected, UIControlState.Disabled, UIControlState.Focused] {
if let title = titleForState(state) {
setTitle(title.localized, forState: state)
}
}
}
}
extension String {
var localized: String {
let localizedValue = NSLocalizedString(self, tableName: nil, bundle: NSBundle.mainBundle(), value: "", comment: "")
if localizedValue == "" {
return self
}
else
{
return localizedValue
}
return self
}
}
I have a
class Fancy:UIButton
and I want to find all the sibling views which are the same class.
I do this
for v:UIView in superview!.subviews
{
if v.isKindOfClass(Fancy)
{
// you may want... if (v==self) continue
print("found one")
(v as! Fancy).someProperty = 7
(v as! Fancy).someCall()
}
}
it seems to work reliably in testing (no siblings, many, etc)
But there's a lot of "!" in there.
Is this the right way in Swift?
BTW here's a cool way to do it with extensions based on the great answers below
Pass in a type to a generic Swift extension, or ideally infer it
What about using functional programming?
self.superview?
.subviews
.flatMap { $0 as? Fancy }
.filter { $0 != self }
.forEach { fancy in
fancy.someProperty = 4
fancy.someMethod()
}
What about:
for v in superview!.subviews
{
if let f = v as? Fancy{
print("found one")
f.someProperty = 7
f.someCall()
}
}
Or this:
if let views = superview?.subviews
{
for aView in views
{
if let fancyView = aView as? Fancy
{
fancyView.someProperty = 7
fancyView.someCall()
}
}
}
#RobMayoff has a good point about excluding self. The code really should be:
if let views = superview?.subviews
{
for aView in views
{
if let fancyView = aView as? Fancy where fancyView != self
{
fancyView.someProperty = 7
fancyView.someCall()
}
}
}
I am trying for a few days now to get this converted into Swift without really having much background with it.
This is what I've got so far ... and I have been looking on google not really knowing what to search for in order to be more specific. Can you please shed some light on what I'm doing wrong ? Thanks
Update:
I have aded the objective-c tag just so more people that are related to this thread may be able to see it and hopefully get an answer.
For those who are still looking, the WebKit team updated WKWebView (iOS 13+) so that you can subclass it to remove/update the input accessory view:
https://trac.webkit.org/changeset/246229/webkit#file1
In Swift, I subclassed it, and returned nil. Worked as expected. I hope it helps.
FYI: I checked the docs, and it doesn't mention not to subclass WKWebView, so subclassing is allowed.
import WebKit
class RichEditorWebView: WKWebView {
var accessoryView: UIView?
override var inputAccessoryView: UIView? {
// remove/replace the default accessory view
return accessoryView
}
}
You can find a working version of it here: https://github.com/cbess/RichEditorView/commits/master
Michael Dautermann answer has got everything right, but in order to hide the accessory bar you need to swizzle the method inputAccessoryView() of UIView Class with the inputAccessoryView() of the _NoInputAccessoryView class. I have just added the couple of extra lines to the code which does this job of method swizzling.
First you'll need a fake class to swap with
final class FauxBarHelper: NSObject {
var inputAccessoryView: AnyObject? { return nil }
}
Then create this method in your controller class
/// Removes the keyboard accessory view from the web view
/// Source: http://stackoverflow.com/a/32620344/308315 / http://stackoverflow.com/a/33939584/308315
func _removeInputAccessoryView(webView: UIWebView) {
var targetView: UIView? = nil
for view in webView.scrollView.subviews {
if String(describing: type(of: view)).hasPrefix("WKContent") {
targetView = view
}
}
guard let target = targetView else { return }
let noInputAccessoryViewClassName = "\(target.superclass!)_NoInputAccessoryView"
var newClass: AnyClass? = NSClassFromString(noInputAccessoryViewClassName)
if newClass == nil {
let targetClass: AnyClass = object_getClass(target)
newClass = objc_allocateClassPair(targetClass, noInputAccessoryViewClassName.cString(using: String.Encoding.ascii)!, 0)
}
let originalMethod = class_getInstanceMethod(FauxBarHelper.self, #selector(getter: FauxBarHelper.inputAccessoryView))
class_addMethod(newClass!.self, #selector(getter: FauxBarHelper.inputAccessoryView), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
object_setClass(target, newClass)
}
HTH ;)
Here's a slightly safer (no unsafe unwraps) version that works with Swift 4 and (at least) iOS 9 trough 12.
fileprivate final class InputAccessoryHackHelper: NSObject {
#objc var inputAccessoryView: AnyObject? { return nil }
}
extension WKWebView {
func hack_removeInputAccessory() {
guard let target = scrollView.subviews.first(where: {
String(describing: type(of: $0)).hasPrefix("WKContent")
}), let superclass = target.superclass else {
return
}
let noInputAccessoryViewClassName = "\(superclass)_NoInputAccessoryView"
var newClass: AnyClass? = NSClassFromString(noInputAccessoryViewClassName)
if newClass == nil, let targetClass = object_getClass(target), let classNameCString = noInputAccessoryViewClassName.cString(using: .ascii) {
newClass = objc_allocateClassPair(targetClass, classNameCString, 0)
if let newClass = newClass {
objc_registerClassPair(newClass)
}
}
guard let noInputAccessoryClass = newClass, let originalMethod = class_getInstanceMethod(InputAccessoryHackHelper.self, #selector(getter: InputAccessoryHackHelper.inputAccessoryView)) else {
return
}
class_addMethod(noInputAccessoryClass.self, #selector(getter: InputAccessoryHackHelper.inputAccessoryView), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
object_setClass(target, noInputAccessoryClass)
}
}
This code snippet should get you over your issue:
class _NoInputAccessoryView: NSObject {
func removeInputAccessoryViewFromWKWebView(webView: WKWebView) {
// make sure to make UIView an optional here...
var targetView: UIView? = nil
for view in webView.scrollView.subviews {
if String(view.dynamicType).hasPrefix("WKContent") {
targetView = view
}
}
// only optionals can be nil
if targetView == nil {
return
}
let noInputAccessoryViewClassName = "\(targetView!.superclass)_NoInputAccessoryView"
var newClass : AnyObject? = NSClassFromString(noInputAccessoryViewClassName)
if newClass == nil {
let uiViewClass : AnyClass = object_getClass(targetView!)
newClass = objc_allocateClassPair(uiViewClass, noInputAccessoryViewClassName.cStringUsingEncoding(NSASCIIStringEncoding)!, 0)
}
}
You can also use "String(view.dynamicType)" to get the class name of the object you're looking at, as I noticed via this answer as I was researching the way to solve your problem.
Using hasPrefix like that in both Objective-C and Swift is really hacky and perhaps a better way of hiding the keyboard could be found for production code?