So I'm localizing a project using an Extension to NSString that I found on SO. That extension looks like this:
extension String {
var localized: String {
return NSLocalizedString(self, tableName: nil, bundle: NSBundle.mainBundle(), value: "", comment: "")
}
}
I however have come across strings in my Localizable.strings list that contain parameters. For example:
"explore_item_desc1" = "Welcome to rent my %1$s!";
Before I was able to do this:
uiLabel.text = "localizedString".localized
How do I do something similar for those strings holding parameters?
Your localized keys should look like this:
"localized_key_name1" = "foo";
"localized_key_name2" = "%# foo %#";
Make your localized variable into a function rather
extension String {
var localized: String {
return NSLocalizedString(self, tableName: nil, bundle: NSBundle.mainBundle(), value: "", comment: "")
}
func localized(args : CVarArgType...) -> String {
return withVaList(args, { (f:CVaListPointer) -> String in
(NSString.init(format:NSLocalizedString(self, comment:""), arguments: f) as String)
})
}
}
usage without Parameters
uiLabel.text = "localized_key_name1".localized // "foo"
usage with Parameters
uiLabel.text = "localized_key_name2".localized("param1", "param2") // "param1 foo param2"
credit
Related
I want to use enumeration with localized string, so I do like this, it works, but
the problem of this solution is : I can't get easily enum value from localized string, I must have the key to do it :
let option = DietWithoutResidueOption(rawValue: "NoDiet")
If not I must to call dietWithoutResidueOptionWith method to get enum value... :/
There are a better solution to store directly localizedString and not keys in enum ?
Thanks
Enumeration
enum DietWithoutResidueOption: String {
case NoDiet = "NoDiet"
case ThreeDays = "ThreeDays"
case FiveDays = "FiveDays"
private func localizedString() -> String {
return NSLocalizedString(self.rawValue, comment: "")
}
static func dietWithoutResidueOptionWith(#localizedString: String) -> DietWithoutResidueOption {
switch localizedString {
case DietWithoutResidueOption.ThreeDays.localizedString():
return DietWithoutResidueOption.ThreeDays
case DietWithoutResidueOption.FiveDays.localizedString():
return DietWithoutResidueOption.FiveDays
default:
return DietWithoutResidueOption.NoDiet
}
}
}
Localizable.strings
"NoDiet" = "NON, JE N'AI PAS DE RÉGIME";
"ThreeDays" = "OUI, SUR 3 JOURS";
"FiveDays" = "OUI, SUR 5 JOURS";
call
println(DietWithoutResidueOption.FiveDays.localizedString())
Try this, it's pretty simple and straight forward:
enum ChoicesTitle: String {
case choice1 = "Choice 1"
case choice2 = "Choice 2"
case choice3 = "Choice 3"
case choice4 = "Choice 4"
case choice5 = "Choice 5"
case choice6 = "Choice 6"
func localizedString() -> String {
return NSLocalizedString(self.rawValue, comment: "")
}
static func getTitleFor(title: ChoicesTitle) -> String {
return title.localizedString()
}
}
And you could use it like this:
let stringOfChoice1: String = ChoicesTitle.getTitleFor(title: .choice1)
Hope this works for you
You can use any StringLiteralConvertible, Equatable type for RawValue type of enum.
So, how about:
import Foundation
struct LocalizedString: StringLiteralConvertible, Equatable {
let v: String
init(key: String) {
self.v = NSLocalizedString(key, comment: "")
}
init(localized: String) {
self.v = localized
}
init(stringLiteral value:String) {
self.init(key: value)
}
init(extendedGraphemeClusterLiteral value: String) {
self.init(key: value)
}
init(unicodeScalarLiteral value: String) {
self.init(key: value)
}
}
func ==(lhs:LocalizedString, rhs:LocalizedString) -> Bool {
return lhs.v == rhs.v
}
enum DietWithoutResidueOption: LocalizedString {
case NoDiet = "NoDiet"
case ThreeDays = "ThreeDays"
case FiveDays = "FiveDays"
var localizedString: String {
return self.rawValue.v
}
init?(localizedString: String) {
self.init(rawValue: LocalizedString(localized: localizedString))
}
}
Using this, you can construct DietWithoutResidueOption by 3 ways:
let option1 = DietWithoutResidueOption.ThreeDays
let option2 = DietWithoutResidueOption(rawValue: "ThreeDays") // as Optional
let option3 = DietWithoutResidueOption(localizedString: "OUI, SUR 3 JOURS") // as Optional
and extract the localized string with:
let localized = option1.localizedString
this is a late answer, but I just had a chat with Apple Engineers about that topic they recommend to do it that way:
enum LocalizedStrings {
case title
var localized: String {
switch self {
case .title:
return NSLocalizedString("My Title", comment: "My Comment")
}
}
}
In your case the solution would be not much different from the original code:
enum DietWithoutResidueOption {
case NoDiet
case ThreeDays
case FiveDays
var localizedString: String {
switch self {
case .NoDiet:
return NSLocalizedString("NoDiet", comment: "Some comment")
case .ThreeDays:
return NSLocalizedString("ThreeDays", comment: "Some comment")
case .FiveDays:
return NSLocalizedString("FiveDays", comment: "Some comment")
}
}
static func dietWithoutResidueOptionWith(localizedString: String) -> DietWithoutResidueOption {
switch localizedString {
case DietWithoutResidueOption.ThreeDays.localizedString:
return DietWithoutResidueOption.ThreeDays
case DietWithoutResidueOption.FiveDays.localizedString:
return DietWithoutResidueOption.FiveDays
default:
return DietWithoutResidueOption.NoDiet
}
}
}
The reason is that they don't want you to pass variables into NSLocalizedString(). This has something to do with optimization and parsing the strings. Imagine Xcode generating the localizable.strings file on it's own at some point, but it could not find the strings, because they are passed as variables.
A nice approach is to create a struct of Localizable Strings with static variables like so:
LocalizableStrings.swift
struct LocalizableStrings {
static let noDiet = NSLocalizedString("NoDiet", comment: "")
static let threeDays = NSLocalizedString("ThreeDays", comment: "")
static let fiveDays = NSLocalizedString("FiveDays", comment: "")
}
Localizable.strings
"NoDiet" = "NON, JE N'AI PAS DE RÉGIME";
"ThreeDays" = "OUI, SUR 3 JOURS";
"FiveDays" = "OUI, SUR 5 JOURS";
And your enum would look like that:
Enum
enum DietWithoutResidueOption {
case NoDiet,
ThreeDays,
FiveDays
var description : String {
get {
switch(self) {
case .NoDiet:
return LocalizableStrings.noDiet
case .ThreeDays:
return LocalizableStrings.threeDays
case .FiveDays:
return LocalizableStrings.fiveDays
}
}
}
}
So, for instance, to get your description you can do like below:
DietWithoutResidueOption.NoDiet.description
The good thing about this approach is that you put the keys of your localizable strings on a single file. So, for instance, if you change the NoDiet key on your Localizable.strings file you only need to update the LocalizableStrings.swift file, instead of all the places where we have the NoDiet key as a string. Furthermore,
you take the risk of spelling wrong the NoDiet key in some file where it is being used and your code will compile with no errors, meanwhile using a static variable from LocalizableStrings.swift you can avoid that, as your code will not compile and you will see an error message saying where the error is.
Ohter alternative :
Enum
enum Title : String {
case CEO = "CEOKey"
case CTO = "CTOKey"
case CFO = "CFOKey"
private static let allTitles = [CEO, CTO, CFO]
var localizedString: String {
return NSLocalizedString(self.rawValue, comment: "")
}
init!(rawValue: String) {
var keys = Title.allTitles
var filtered = keys.filter { $0.rawValue == rawValue }
self = filtered.first!
}
init!(localizedString: String) {
var keys = Title.allTitles
var filtered = keys.filter { $0.localizedString == localizedString }
self = filtered.first!
}
}
Localizable.strings
"CEOKey" = "Chief Executive Officer";
"CTOKey" = "Chief Technical Officer";
"CFOKey" = "Chief Financial Officer";
Constract enum :
let option1 = Title.CFO
let option2 = Title(rawValue: "CTOKey") // init from key
let option3 = Title(localizedString: NSLocalizedString("CEOKey", comment: "")) // init from value
Extract the localized strings :
println("option1 localized string : \(option1.localizedString)")
println("option2 localized string : \(option2.localizedString)")
println("option3 localized string : \(option3.localizedString)")
Input
option1 localized string : Chief Financial Officer
option2 localized string : Chief Technical Officer
option3 localized string : Chief Executive Officer
This code will generate exception, if localized strings or keys not found
This is my example
enum Localization: String {
case appName = "app_name"
case appOk = "app_ok"
case appError = "app_error"
case placeholderNoContent = "placeholder_no_content"
case homeTitle = "home_title"
public func localized(args: CVarArg...) -> String {
let localizedString = NSLocalizedString(self.rawValue, comment: "")
return withVaList(args, { (args) -> String in
return NSString(format: localizedString, locale: Locale.current, arguments: args) as String
})
}
}
Usage
self.homeTitleLabel = Localization.homeTitle.localized()
This Localization enum can easily be used with string formats.
Try this protocol which I created, and you can import, use it like below.
https://github.com/Wei18/ZWExt/blob/master/ZWExt/Classes/Protocol/Localizable.swift
enum SomeKey: String, Localizable {
case MenuGreeting = "lb_menu_greeting"
case HaveBook = "I have %# books"
}
// Sample
let menuGreeting: String = SomeKey.MenuGreeting.localized()
let iHaveBoxes: String = SomeKey.HaveBook.localized([3])
/*
// You also can make it with html.
SomeKey.CustomCase.localizedHTML()
SomeKey.CustomCase.localizedHTML([])
*/
Currently, in search, I am looking for a string that contains some characters:
func search(for keyword: String?) {
self.keyword = keyword
guard let keyword = keyword?
.folding(options: .diacriticInsensitive, locale: nil)
.lowercased(),
!keyword.isEmpty else {
filteredNames = names
return
}
filteredNames = names
.filter { $0.title.lowercased()
.folding(options: .diacriticInsensitive, locale: nil)
.contains(keyword)
}
}
The problem is that when the list is bigger, it shows more than expected.
I would use not .contain(keyword), but .starts(with: keywoard), but then it would miss some results.
let's say I have an array of:
let myArray = ["name", "my name", "some name", "else string", "string", "stringname"]
so when looking for 'ame' not it shows all that contains the string, and what I want is to shown only if the word in an array starts with that. So when typing 'name' it should return me "name", "some name" but NOT the "stringname"
An alternative is searching with Regular Expression and range(of:options:)
The benefit is no extra objects are created, it's O(n) and you can specify the case insensitive and diacritics insensitive options at the same time. The \b in the regex pattern represents word boundaries
let keyword = "name"
let myArray = ["name", "my name", "some name", "else string", "string", "stringname"]
let filtered = myArray.filter {
$0.range(of: "\\b\(keyword)", options: [.regularExpression, .caseInsensitive, .diacriticInsensitive]) != nil
}
print(filtered)
You can do it like this -
extension String {
// Assumption: Both `keyword` & `self` are already lowercased
func hasMatch(for keyword: String) -> Bool {
let comps = self
.folding(options: .diacriticInsensitive, locale: nil)
.components(separatedBy: " ")
return (comps.first(where: { $0.starts(with: keyword) }) != nil)
}
}
Now your filter logic can use it like this -
filteredNames = names
.filter { $0.title.lowercased()
.hasMatch(for: keyword)
}
You could split each String by their whitespace and match each component.
let searchTerm = "name"
let myArray = ["name", "my name", "some name", "else string", "string", "stringname"]
let matches = myArray.filter {
$0.components(separatedBy: " ").contains {
$0.hasPrefix(searchTerm)
}
}
Improvements left to the reader:
Split string by any kind of whitespace
Case-insensitive, diacritic-insensitive search
I have this code that I need to make reusable by other apps.
This code shows messages where the localized app's name is present.
So, I have localized strings like this:
"DoYou" = "Do you really want to close $$$?";
"Quit" = "Quit $$$";
"Keep Running" = "Keep $$$ Running";
Where $$$ must be replaced with the localized app's name at run time.
Because I want to learn about property wrappers, I have tried to create this one:
extension String {
func findReplace(_ target: String, withString: String) -> String
{
return self.replacingOccurrences(of: target,
with: withString,
options: NSString.CompareOptions.literal,
range: nil)
}
}
#propertyWrapper
struct AdjustTextWithAppName<String> {
private var value: String?
init(wrappedValue: String?) {
self.value = wrappedValue
}
var wrappedValue: String? {
get { value }
set {
if let localizedAppName = Bundle.main.localizedInfoDictionary?["CFBundleName"] as? String {
let replaced = value.findReplace("$$$", withString: localizedAppName)
}
value = nil
}
}
}
I get this error on the replaced line:
Value of type String? has no name findReplace
I have also tried to use this line
let replaced = value!.findReplace("$$$", withString: localizedAppName)
Same error...
The string may contain the app's name more than once. This is why I have that extension to String.
To solve this your property wrapper can't have a genereic type named String because that shadows the built-in type String that your extension belongs to. So change the struct declaration to not be generic
#propertyWrapper
struct AdjustTextWithAppName {
or name the type to something else
#propertyWrapper
struct AdjustTextWithAppName<T> {
and fix the set method
set {
guard let str = newValue, let localizedAppName = Bundle.main.localizedInfoDictionary?["CFBundleName"] as? String else {
value = nil
}
value = str.findReplace(target: "$$$", withString: localizedAppName)
}
I want to add an array of localized strings as datasource for a collection view in a swift application.
I have written this :
let text = [NSLocalizedString("s0",comment: ""),NSLocalizedString("s1",comment: ""),NSLocalizedString("s2",comment: ""),NSLocalizedString("s3",comment: ""),NSLocalizedString("s4",comment: ""),NSLocalizedString("s5",comment: ""),NSLocalizedString("s6",comment: ""),NSLocalizedString("s7",comment: ""),NSLocalizedString("s8",comment: "")]
Is there a more simple way to put all the strings in the array, because I have 1000 localized strings and this does not look right.
EDIT: Realized you could write this in two steps and not three. Simplified my answer.
Write a helper function to localize the array
public func Localized(_ key: String) -> String {
return NSLocalizedString(key, comment: "")
}
Call it using map
// for example
let localized = ["s0", "s1", "s2"].map { Localized($0) }
You can always set your strings with a format (s%d.4 , for instance) and build your array around that format.
var text:NSMutableArray = NSMutableArray()
for index in 1...10 {
let formattedString = String(format: "s%.4d", index)
let localizedString = NSLocalizedString(formattedString, comment: "comment")
text.addObject(localizedString)
}
Then you declare your strings like: s0001, s0002, s0003, etc.
Alternatively you can do it like that:
HelperClass:
static func getLocalized(withKey key: String, targetSpecific:Bool) -> String{
if targetSpecific{
return NSLocalizedString(key, tableName:"TargetSpecific", comment: "")
}
else{
return NSLocalizedString(key, comment: "")
}
}
static func getLocalizedArray(withKey key: String, targetSpecific:Bool) -> [String]{
return getLocalized(withKey: key, targetSpecific: targetSpecific).components(separatedBy: ",")
}
Localizable.string
"optionsArray" = "Show devices,Add device";
So you can call it like that:
let optionsArray:[String] = AppHelper.getLocalizedArray(withKey: "optionsArray", targetSpecific: false)
I want to use enumeration with localized string, so I do like this, it works, but
the problem of this solution is : I can't get easily enum value from localized string, I must have the key to do it :
let option = DietWithoutResidueOption(rawValue: "NoDiet")
If not I must to call dietWithoutResidueOptionWith method to get enum value... :/
There are a better solution to store directly localizedString and not keys in enum ?
Thanks
Enumeration
enum DietWithoutResidueOption: String {
case NoDiet = "NoDiet"
case ThreeDays = "ThreeDays"
case FiveDays = "FiveDays"
private func localizedString() -> String {
return NSLocalizedString(self.rawValue, comment: "")
}
static func dietWithoutResidueOptionWith(#localizedString: String) -> DietWithoutResidueOption {
switch localizedString {
case DietWithoutResidueOption.ThreeDays.localizedString():
return DietWithoutResidueOption.ThreeDays
case DietWithoutResidueOption.FiveDays.localizedString():
return DietWithoutResidueOption.FiveDays
default:
return DietWithoutResidueOption.NoDiet
}
}
}
Localizable.strings
"NoDiet" = "NON, JE N'AI PAS DE RÉGIME";
"ThreeDays" = "OUI, SUR 3 JOURS";
"FiveDays" = "OUI, SUR 5 JOURS";
call
println(DietWithoutResidueOption.FiveDays.localizedString())
Try this, it's pretty simple and straight forward:
enum ChoicesTitle: String {
case choice1 = "Choice 1"
case choice2 = "Choice 2"
case choice3 = "Choice 3"
case choice4 = "Choice 4"
case choice5 = "Choice 5"
case choice6 = "Choice 6"
func localizedString() -> String {
return NSLocalizedString(self.rawValue, comment: "")
}
static func getTitleFor(title: ChoicesTitle) -> String {
return title.localizedString()
}
}
And you could use it like this:
let stringOfChoice1: String = ChoicesTitle.getTitleFor(title: .choice1)
Hope this works for you
You can use any StringLiteralConvertible, Equatable type for RawValue type of enum.
So, how about:
import Foundation
struct LocalizedString: StringLiteralConvertible, Equatable {
let v: String
init(key: String) {
self.v = NSLocalizedString(key, comment: "")
}
init(localized: String) {
self.v = localized
}
init(stringLiteral value:String) {
self.init(key: value)
}
init(extendedGraphemeClusterLiteral value: String) {
self.init(key: value)
}
init(unicodeScalarLiteral value: String) {
self.init(key: value)
}
}
func ==(lhs:LocalizedString, rhs:LocalizedString) -> Bool {
return lhs.v == rhs.v
}
enum DietWithoutResidueOption: LocalizedString {
case NoDiet = "NoDiet"
case ThreeDays = "ThreeDays"
case FiveDays = "FiveDays"
var localizedString: String {
return self.rawValue.v
}
init?(localizedString: String) {
self.init(rawValue: LocalizedString(localized: localizedString))
}
}
Using this, you can construct DietWithoutResidueOption by 3 ways:
let option1 = DietWithoutResidueOption.ThreeDays
let option2 = DietWithoutResidueOption(rawValue: "ThreeDays") // as Optional
let option3 = DietWithoutResidueOption(localizedString: "OUI, SUR 3 JOURS") // as Optional
and extract the localized string with:
let localized = option1.localizedString
this is a late answer, but I just had a chat with Apple Engineers about that topic they recommend to do it that way:
enum LocalizedStrings {
case title
var localized: String {
switch self {
case .title:
return NSLocalizedString("My Title", comment: "My Comment")
}
}
}
In your case the solution would be not much different from the original code:
enum DietWithoutResidueOption {
case NoDiet
case ThreeDays
case FiveDays
var localizedString: String {
switch self {
case .NoDiet:
return NSLocalizedString("NoDiet", comment: "Some comment")
case .ThreeDays:
return NSLocalizedString("ThreeDays", comment: "Some comment")
case .FiveDays:
return NSLocalizedString("FiveDays", comment: "Some comment")
}
}
static func dietWithoutResidueOptionWith(localizedString: String) -> DietWithoutResidueOption {
switch localizedString {
case DietWithoutResidueOption.ThreeDays.localizedString:
return DietWithoutResidueOption.ThreeDays
case DietWithoutResidueOption.FiveDays.localizedString:
return DietWithoutResidueOption.FiveDays
default:
return DietWithoutResidueOption.NoDiet
}
}
}
The reason is that they don't want you to pass variables into NSLocalizedString(). This has something to do with optimization and parsing the strings. Imagine Xcode generating the localizable.strings file on it's own at some point, but it could not find the strings, because they are passed as variables.
A nice approach is to create a struct of Localizable Strings with static variables like so:
LocalizableStrings.swift
struct LocalizableStrings {
static let noDiet = NSLocalizedString("NoDiet", comment: "")
static let threeDays = NSLocalizedString("ThreeDays", comment: "")
static let fiveDays = NSLocalizedString("FiveDays", comment: "")
}
Localizable.strings
"NoDiet" = "NON, JE N'AI PAS DE RÉGIME";
"ThreeDays" = "OUI, SUR 3 JOURS";
"FiveDays" = "OUI, SUR 5 JOURS";
And your enum would look like that:
Enum
enum DietWithoutResidueOption {
case NoDiet,
ThreeDays,
FiveDays
var description : String {
get {
switch(self) {
case .NoDiet:
return LocalizableStrings.noDiet
case .ThreeDays:
return LocalizableStrings.threeDays
case .FiveDays:
return LocalizableStrings.fiveDays
}
}
}
}
So, for instance, to get your description you can do like below:
DietWithoutResidueOption.NoDiet.description
The good thing about this approach is that you put the keys of your localizable strings on a single file. So, for instance, if you change the NoDiet key on your Localizable.strings file you only need to update the LocalizableStrings.swift file, instead of all the places where we have the NoDiet key as a string. Furthermore,
you take the risk of spelling wrong the NoDiet key in some file where it is being used and your code will compile with no errors, meanwhile using a static variable from LocalizableStrings.swift you can avoid that, as your code will not compile and you will see an error message saying where the error is.
Ohter alternative :
Enum
enum Title : String {
case CEO = "CEOKey"
case CTO = "CTOKey"
case CFO = "CFOKey"
private static let allTitles = [CEO, CTO, CFO]
var localizedString: String {
return NSLocalizedString(self.rawValue, comment: "")
}
init!(rawValue: String) {
var keys = Title.allTitles
var filtered = keys.filter { $0.rawValue == rawValue }
self = filtered.first!
}
init!(localizedString: String) {
var keys = Title.allTitles
var filtered = keys.filter { $0.localizedString == localizedString }
self = filtered.first!
}
}
Localizable.strings
"CEOKey" = "Chief Executive Officer";
"CTOKey" = "Chief Technical Officer";
"CFOKey" = "Chief Financial Officer";
Constract enum :
let option1 = Title.CFO
let option2 = Title(rawValue: "CTOKey") // init from key
let option3 = Title(localizedString: NSLocalizedString("CEOKey", comment: "")) // init from value
Extract the localized strings :
println("option1 localized string : \(option1.localizedString)")
println("option2 localized string : \(option2.localizedString)")
println("option3 localized string : \(option3.localizedString)")
Input
option1 localized string : Chief Financial Officer
option2 localized string : Chief Technical Officer
option3 localized string : Chief Executive Officer
This code will generate exception, if localized strings or keys not found
This is my example
enum Localization: String {
case appName = "app_name"
case appOk = "app_ok"
case appError = "app_error"
case placeholderNoContent = "placeholder_no_content"
case homeTitle = "home_title"
public func localized(args: CVarArg...) -> String {
let localizedString = NSLocalizedString(self.rawValue, comment: "")
return withVaList(args, { (args) -> String in
return NSString(format: localizedString, locale: Locale.current, arguments: args) as String
})
}
}
Usage
self.homeTitleLabel = Localization.homeTitle.localized()
This Localization enum can easily be used with string formats.
Try this protocol which I created, and you can import, use it like below.
https://github.com/Wei18/ZWExt/blob/master/ZWExt/Classes/Protocol/Localizable.swift
enum SomeKey: String, Localizable {
case MenuGreeting = "lb_menu_greeting"
case HaveBook = "I have %# books"
}
// Sample
let menuGreeting: String = SomeKey.MenuGreeting.localized()
let iHaveBoxes: String = SomeKey.HaveBook.localized([3])
/*
// You also can make it with html.
SomeKey.CustomCase.localizedHTML()
SomeKey.CustomCase.localizedHTML([])
*/