SwiftUI - Localization of a dynamic Text - swift

I am struggling with the locilization of some of my TextFields.
Usually a "normal" localization of a Text() or TextField() works without any problem in my App if the text I want to translate is hardcoded like this:
Text("English Text")
I translate it in my Localizable.strings like this:
"English Text" = "German Text";
Now I want to translate TextFields which are more dynamic, but where I know each possible entry:
TextField("New note" + (refresh ? "" : " "),text: $newToDo, onCommit: {
self.addToDo()
self.refresh.toggle()
})
(The refresh is necessary because of a SwiftUI bug sometimes not showing the placeholder-text again.)
Another example would be:
func dayWord() -> String {
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone.current
dateFormatter.locale = Locale(identifier: "de_DE")
dateFormatter.dateFormat = "EEEE"
return dateFormatter.string(from: self)
}
var day: String {
return data.date.dateFromMilliseconds().dayWord()
}
Text(day.prefix(2))
The Text(day.prefix(2)) has only seven possible states, but I don't know what to write as a key in my Localizable.strings.

Use NSLocalizedString, like
TextField(NSLocalizedString("New note", comment: "") + (refresh ? "" : " "), ...

According to SwiftUI convention, the Text() label is automatically localized only when it has a string "literal". When the text is a variable, you need to use LocalizedStringKey(). Other localizable texts inside Button() or TextField() also require LocalizedStringKey(). So you need to change this to:
Text(LocalizedStringKey(String(day.prefix(2))))
This converts day.prefix(2) into a String, because it is actually a substring, and then calls LocalizedStringKey() to make it localizable. In your Localizable.strings file you could add all 7 possibilities.
"Mo" = "Mo";
"Di" = "Tu";
//etc.
but why would you? Instead, use:
dateFormatter.locale = Locale(identifier: Locale.preferredLanguages.first)
...
Text(day.prefix(2))
to determine the user's current language and display that. Apple returns the text in the proper language, so this text doesn't need to be localized any further.
TextField() does need localization using LocalizedStringKey():
TextField(LocalizedStringKey("New note") + (refresh ? "" : " "),text: $newToDo, onCommit: {
self.addToDo()
self.refresh.toggle()
})
As Asperi points out, for "New note" the same can be accomplished using NSLocalizedString(), which might be better depending on how you like to work. The benefits are: easily adding a comment for the translator, and automatic export into the xliff when you choose Editor -> Export for localization…
By contrast, SwiftUI's LocalizedStringKey() requires you to manually add strings to the Localizable.strings file. For your day.prefix(2) example, I think it would make more sense to get the user's preferred language and display the localized date directly.

SwiftUI Text will only localize literal strings, strings defined in double quotes. You have to either create localized key or retrieve localized string.
Examples
Simple localization with string literal key.
Text("Label")
If you construct the label, you can use LocalizedStringKey function to localize your computed label.
let key = "Label"
let localizedKey = LocalizedStringKey(key)
Text(localizedKey)
You can also get the localized string using NSLocalizedString.
let localizedString = NSLocalizedString("Label", comment: "")
Text(localizedString)
It's also possible to have localized strings accept arguments (#, lld, and lf) through String Interpolation. For example you could have the following localizations in your project Localizable.strings file:
"Name %#" = "Name %#";
"Number %lld" = "%lld is the number";
And you can use it like this:
Text("Label \(object)")
Text("Number \(number)")
Xcode Search
If you want to search for calls of Text that aren't done with literal strings in your projects. You can use Xcode Find Regular Expression feature. View > Navigators > Find or Cmd-4 keyboard shortcut. Change the search type just above the search field to Regular Expression.
Use the following regular expression: Text\([^\"]

Related

SwiftUI Localization of Strings inside an Array

I'm trying to localize my App in English and German and everything worked for now, expect the strings inside an array apparently get not localized.
I have this array, which holds the options for my Picker:
let sorting = ["Newest First", "Oldest First"]
This is my Picker, which works correctly function wise:
Picker("Sort by", selection: $sortingSelection) {
ForEach(sorting, id: \.self) {
Text($0)
.foregroundColor(.accentColor)
.font(.footnote)
}
}
.pickerStyle(MenuPickerStyle())
And this is the correleting Localizable.strings (German):
"Newest First" = "Neueres Zuerst";
"Oldest First" = "Älteres Zuerst";
So I tried just writing it as strings, which is the easiest way to use localization now.
I also tried using the it as a variable, but this doesn't work:
let localizedString1 = "Newest First"
let localizedString2 = "Oldest First"
let sorting = [localizedString1, localizedString2]
I also saw this post:
Swift: Localize an Array of strings
but like the comment on the answer says, is there a way to get some example code? Or is there a better method now?
As stated in Text documentation :
If you intialize a text view with a variable value, the view uses the init(:) initializer, which doesn’t localize the string. However, you can request localization by creating a LocalizedStringKey instance first, which triggers the init(:tableName:bundle:comment:) initializer instead:
// Don't localize a string variable...
Text(writingImplement)
// ...unless you explicitly convert it to a localized string key.
Text(LocalizedStringKey(writingImplement))
The array defaults to type String, you can specify the type to LocalizedStringKey
let sorting: [LocalizedStringKey] = ["Newest First", "Oldest First"]
You might have to change the selection type too to make them match.
This works for me:
Picker("Select Category", selection: $transactionTypeString) {
ForEach(transactionTypes, id:\.self) {
Text(LocalizedStringKey($0))
}
}

Build Recursive Text View in SwiftUI

My goal is to create a SwiftUI view that takes a String and automatically formats that text into Text views. The portion of the string that needs formatting is found using regex and then returned as a Range<String.Index>. This can be used to reconstruct the String once the formatting has been applied to the appropriate Text views. Since there could be multiple instances of text that needs to be formatted, running the formatting function should be done recursively.
struct AttributedText: View {
#State var text: String
var body: some View {
AttributedTextView(text: text)
}
#ViewBuilder
private func AttributedTextView(text: String) -> some View {
if let range = text.range(of: "[0-9]+d[0-9]+", options: .regularExpression) {
//The unattributed text
Text(text[text.startIndex..<range.lowerBound]) +
//Append the attributed text
Text(text[range]).bold() +
//Search for additional instances of text that needs attribution
AttributedTextView(text: String(text[range.upperBound..<text.endIndex]))
} else {
//If the searched text is not found, add the rest of the string to the end
Text(text)
}
}
I get an error Cannot convert value of type 'some View' to expected argument type 'Text', with the recommended fix being to update the recursive line to AttributedTextView(text: String(text[range.upperBound..<text.endIndex])) as! Text. I apply this fix, but still see the same compiler error with the same suggested fix.
A few workarounds that I've tried:
Changing the return type from some View to Text. This creates a different error Cannot convert value of type '_ConditionalContent<Text, Text>' to specified type 'Text'. I didn't really explore this further, as it does make sense that the return value is reliant on that conditional.
Returning a Group rather than a Text, which causes additional errors throughout the SwiftUI file
Neither of these solutions feel very "Swifty". What is another way to go about this? Am I misunderstanding something in SwiftUI?
There are a few things to clarify here:
The + overload of Text only works between Texts which is why it's saying it cannot convert some View (your return type) to Text. Text + Text == Text, Text + some View == ☠️
Changing the return type to Text doesn't work for you because you're using #ViewBuilder, remove #ViewBuilder and it'll work fine.
Why? #ViewBuilder allows SwiftUI to defer evaluation of the closure until later but ensures it'll result in a specific view type (not AnyView). In the case where your closure returns either a Text or an Image this is handy but in your case where it always results in Text there's no need, #ViewBuilder forces the return type to be ConditionalContent<Text, Text> so that it could have different types.
Here's what should work:
private static func attributedTextView(text: String) -> Text {
if let range = text.range(of: "[0-9]+d[0-9]+", options: .regularExpression) {
//The unattributed text
return Text(text[text.startIndex..<range.lowerBound]) +
//Append the attributed text
Text(text[range]).bold() +
//Search for additional instances of text that needs attribution
AttributedTextView(text: String(text[range.upperBound..<text.endIndex]))
} else {
//If the searched text is not found, add the rest of the string to the end
return Text(text)
}
}
I made it static too because there's no state here it's a pure function and lowercased it so it was clear it was a function not a type (the function name looks like a View type).
You'd just call it Self.attributedTextView(text: ...)

Can I create a Date object from a predefined string (typescript)?

I have a value returned in a string (numbers separated by commas) and I'd like to make a Date object out of it. It looks like this is not possible, can someone confirm and/or suggest me a solution.
This does not work :
let dateString='2017,3,22,0';
let dateFromString = new Date(dateString);
This works though (when I pass a list of numbers) :
let dateFromString = new Date(2017,3,22,0);
And this works also :
let dateString = '2008/05/10 12:08:20';
let dateFromString = new Date(dateString);
The goal would be to create a Date object from a uniform string. Is that possible ?
Can I create a Date object from a predefined string, which has only one type of separator (comma, colon, slash or whatever) ?
If your environment is compatible with ES6 (eg. Babel, TypeScript, modern Chrome/Firefox etc), you can use the string's .split(',') and decompose the array into arguments like the following:
const dateString = '2017,3,22,0';
const date = new Date(...dateString.split(',')); // date object for 2017/03/22
ES5 compatible version:
var dateString = '2017,1,2,0';
var date = new (Function.prototype.bind.apply(Date, [null].concat(dateString.split(','))));
As for how the .bind.apply method works with new, you can take a look at Use of .apply() with 'new' operator. Is this possible?
Note: Thanks to the two comments below for spotting my errors 👍

Why does NSLocale.current.identifier include currency on macOS?

If I request the current locales identifier on iOS, it returns just the identifier string:
let identifier = NSLocale.current.identifier // en_GB
However, on macOS 10.12.2 it also returns the currency:
let identifier = NSLocale.current.identifier // en_GB#currency=GBP
Is this a bug or expected behaviour?
I ran into this recently. I'm not sure why, but apparently as of 10.12, localeIdentifier can include a bunch of stuff besides country and language.
Unfortunately, the documentation doesn't elaborate on the cases in which other metadata is included.
However, also as of 10.12, there's another property languageCode, which, in conjunction with countryCode, you can use to generate en_US.
This is what iTerm2 does.
I think the best option for me here is to generate the code myself. To help with this I have created an extension on Locale:
extension Locale {
var iso3166code: String {
guard
let language = languageCode,
let region = regionCode
else { return "en-US" }
return "\(language)-\(region)"
}
}
While this is accurate enough for my purposes, you should probably ensure it returns expected values for your project.

Add UIButtons to data points in a line graph, plotting date and time in X-axis

I am trying to plot some data on a simple line graph. Below is my class for storing the data.
How do I plot my yValues with time or date which are strings?
Also, I would like to add title as a UIButton to each data point in the graph. Tapping on the UIButton should display the corresponding detailedNote is a small popup.
I have searched and digged into a lot of Chart libraries for iOS but didn't find a way to add this simple functionality.
graph-with-buttons-at-data-points.png
Also, what is the way to filter and plot data for "Today", "Last Week", "Last Month" or for any interval of time?
Can I do all of this with iOS libraries like Core Plot and UIBeizerPath without help from any 3rd party frameworks/libraries?
I prefer to use UIBeizerPath as I can smoothen the graph..
https://medium.com/#ramshandilya/draw-smooth-curves-through-a-set-of-points-in-ios-34f6d73c8f9#.28frf03to
Can someone point me to the right resources..
Posting swift code is highly appreciated!
class NoteEntry {
var title: String?
var detailedNote: String?
var yValue: Float? = 0
let timeString : String = {
let formatter = NSDateFormatter()
formatter.dateFormat = "h:mm a"
formatter.AMSymbol = "AM"
formatter.PMSymbol = "PM"
return formatter.stringFromDate(NSDate())
}()
let dateString: String = {
let formatter = NSDateFormatter()
formatter.dateFormat = "d"
return formatter.stringFromDate(NSDate())
}()
let dayOfTheWeekString: String = {
let formatter = NSDateFormatter()
formatter.dateFormat = "EEE"
return formatter.stringFromDate(NSDate())
}()
let monthAndYearString: String = {
let formatter = NSDateFormatter()
formatter.dateFormat = ""
return formatter.stringFromDate(NSDate())
}()
}
Here is an answer of how to do this with SwiftCharts:
How do I plot my yValues with time or date which are strings?
You have to convert the strings back to dates. Otherwise the chart will not know how to order them. I also advice to store in your model class dates instead of strings. Do the formatting where you need strings, or have this at least in separate (computed) variables or extensions. About how to make charts with dates in SwiftCharts there are some examples in the library, just orient with them.
title as a UIButton
You can use any kind of UIViews as chart points / overlays / etc. For the line with titles, you would have a line layer and a layer that adds the tappable titles, see e.g. NotificationsExample - which is very near to what you want to do. It uses HandlingView instead of UIButton to be able to do tap handling in closure instead of using selectors. With selector you would lose context of which chart point you're handling the tap for.
Filtering data for "today", "last week" etc.
I recommend opening a new question for this. You shouldn't bundle together so many questions. This is also not related with the charts library, no charts library will do this for you. You have to use NSDateComponents. Maybe there's a library for time operations - as said this is a separate question.
Core plot is a third party framework :) You of course can develop a chart from scratch but I'd recommend using a library. For you particular case SwiftCharts seems very suitable, you should be able to get your chart done with a modified version of NotificationsExample to use dates instead of numbers (copy the dates from other example), and adjusting the tappable overlays.
For more questions feel free to comment this or the issue you opened in SwiftCharts.
(Disclaimer: I'm the author of SwiftCharts).