RelativeDateTimeFormatter Context for Localised String - ios

I have a RelativeDateTimeFormatter that I'm using to sho strings like this:
"Next lottery draw in 2 days"
"Next lottery draw tomorrow"
This comes from the strings file with string format like this "Next lottery draw %#"
My problem is how do I localise this properly when the %# is in a different place in the string. For example, in Japanese the %# is somewhere in the middle of the string.
Here's my code for setting up the date formatter.
let exampleDate = Date().addingTimeInterval(-15000)
let formatter = RelativeDateTimeFormatter()
formatter.context = .dynamic
let relativeDate = formatter.localizedString(for: exampleDate, relativeTo: Date())
let myString = String(format: NSLocalizedString("string_key", comment: ""), relativeDate)
Formatters have a context you can pass to tell the formatter where the string's going to be used.
https://developer.apple.com/documentation/foundation/formatter/context
And one of the cases is called dynamic which says that it's going to determine it at runtime. However, I can't see how that works or how I'm supposed to use the RelativeDateTimeFormatter with that.

Related

How to format dates and times in the form of "2 hours before"?

I want to format dates and times in the form of "5 hours before", just like the Calendar app does. And I also want the fomatted string be localized for many languages.
There is a new API called RelativeDateTimeFormatter introduced in iOS 13. However, this API can only produce localized string like "2 hours ago" and "in 2 hours". This is not suitable for my purpose.
I wonder if there is an API that provides the functionality I'm looking for.
If you are interested how Apple does it, the localization is available in EventKitUI framework.
import EventKitUI
...
let bundle = EventKitUIBundle()!
// access the string that adds "before" information to an interval
let beforeKey = bundle.localizedString(forKey: "%# before", value: nil, table: nil)
// access the string that localizes some interval (days, in this case)
let intervalKey = bundle.localizedString(forKey: "interval_days_long", value: nil, table: nil)
// use the strings with a value
print(String(format: beforeKey, String(format: intervalKey, 1))) // 1 day before
print(String(format: beforeKey, String(format: intervalKey, 3))) // 3 days before
The localization dictionary contains proper pluralization for number values 1-9.
You can do exactly the same. I don't recommend using Apple's localization strings since these are not documented.
If it is possible for you to localized the before part, maybe consider using DateComponentsFormatter?
(Maybe this is more suitable as a comment, but seems to be too long)
For example:
let componentsList = [
DateComponents(minute:5),
DateComponents(minute:10),
DateComponents(minute:15),
DateComponents(minute:30),
DateComponents(hour:1),
DateComponents(hour:2),
DateComponents(day:1),
DateComponents(day:2),
DateComponents(weekOfMonth:1)
]
for components in componentsList {
if let text = DateComponentsFormatter.localizedString(from: components, unitsStyle: .full) {
print("\(text) before")
}
}

Xcode 11 broke DateFormatter?

One day, the app worked. The next day I updated to Xcode 11 and now the app crashes with "unexpectedly found nil" on line 27 (when executing line 15) in the picture.
I asked my co-worker who doesn't yet have Xcode 11, and his doesn't crash. we are on the same branch/commit...everything.
Any advice? any way around this?
My code:
// ticket.timeOrdered == "2019-10-03 22:54:57 +0000"
let ticketDate = ticket.timeOrdered.asCrazyDate.timeIntervalSince1970
extension String {
var asCrazyDate: Date {
let dateFormatterGet = DateFormatter()
dateFormatterGet.dateFormat = "yyyy-MM-dd HH:mm:ss +zzzz"
dateFormatterGet.timeZone = .current
return dateFormatterGet.date(from: self)!
}
}
The date format string is incorrect. The +zzzz is not an acceptable format. See the timezone related sections of the table in Date Format Patterns. The + shouldn’t be there. And zzzz is for long descriptions of the time zone (e.g. “Pacific Daylight Time”). You can verify this by using the same formatter to build a string from Date() and you’ll see that it’s not resulting in the +0000 like you might have expected.
The latest SDK’s date formatter is no longer as forgiving regarding these sorts of format string errors as the previous versions were. But rather than reverting your Xcode version, you really should just fix that date format string. For example, you could use Z instead of +zzzz, which will correctly interpret the +0000 (or whatever) as the time zone portion of the string.
A few other suggestions, if you don’t mind:
You don’t need asCrazyDate in this example. There’s no point in getting a date, using string interpolation to build the string representation, and then using a formatter to convert the string back to a date (which you originally started with). You can just use the Date directly:
func getDate() -> TimeInterval {
return Date().timeIntervalSince1970
}
Date formatters are notoriously computationally intensive to create, and if you’re using this computed property a lot, that can really affect performance. It’s better to instantiate date formatters once, if possible.
If you’re trying to build some invariant date string for some reason, it’s better to use something like ISO8601DateFormatter. So don’t build your date strings using string interpolation, and don’t build your own formatter.
let formatter = ISO8601DateFormatter()
let now = Date()
let string = formatter.string(from: now) // not "\(now)"
let date = formatter.date(from: string)
print(now, string, date)
If you’re stuck with this date format (perhaps you’ve already stored dates using this string format), you can use the custom dateFormat string, if you must. But as Technical Q&A 1480 suggests, you might want to set the locale (and I’d suggest setting the timeZone, too, so your date strings are comparable).
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)

Formatting localized numbers with unit symbols

Situation
I want to format a Double 23.54435678 into a String like 23.54 fps respecting the user's locale.
let formatter = NumberFormatter()
formatter.maximumFractionDigits = 2
let formatted = formatter.string(from: fps as NSNumber)! + " fps"
For the localized number formatting I use DateFormatter.
Question
How should I handle the unit part? Is it valid to just append the unit to the formatted number? Is the placement of the symbol not locale dependent? How do I handle that?
Cocoa has no built-in support for the unit "frames per second", so you will have to provide the suffix yourself, e.g. using Xcode's localization system.
You still need to format the numeric value with NumberFormatter for the current locale and then insert the resulting number string into the localized format string:
let formatter = NumberFormatter()
formatter.maximumFractionDigits = 2
let numberString = formatter.string(from: fps)
let formatString = NSLocalizedString("%# fps", comment: "") // provide localizations via .strings files
let fpsString = String(format: formatString, arguments: numberString)
If unit placement is locale-dependent (you will have to this find out yourself for the target locales of your app), you have to deal with this manually as well. You can leverage the localization system here by providing localizations with an adequately positioned placeholder for the numeric value, e.g. %# fps for English and x %# yz for... well, Fantasy Language.

From dateFromString to stringFromDate

I have a date in String that arrives in the following format:
"2015-11-15 14:16:15 +0000"
and I want to display it in a localised format and even calculate the age.
To get to NSDate I use this code:
dateFormatter.dateFormat = "yyyy-MM-dd hh:mm:ss +SSSS"
let birthDate = self.dateFormatter.dateFromString(userProfile["birthdate"] as! String)
Then to display the date (in a short format) I change the dateFormatter
dateFormatter.dateFormat = "yyyy-MM-dd"
let birthDateShort = self.dateFormatter.stringFromDate(birthDate!)
I could of course just pull out the text from the initial string if I didn't need the NSDate format for other reasons. But it still seems like a long roundtrip , is this the "correct" method?
This is exactly the right thing to do. And it's not a "long roundtrip". A string is just a string: just a representation, a bunch of letters / glyphs for a human being to read. A date is serious calendar-related piece of information. You need that date as a central pivot point for doing anything calendrically meaningful. And that's exactly what you're doing. NSDateFormatter is provided exactly so you can do the very kind of thing you are doing.

iOS localized string constants for time symbols ("MINUTE(S)", "HOUR(S)" etc.)

I am writing an app which need to show dates with corresponding time symbols like "hours", "minutes" etc.
There are cool localized constants for months and weekdays names in NSDateFormatter like monthSymbols (January, February etc). However, I can't find anything like this for such symbols as "hour", "minute" itself. Does such constants exists or I should create and localize these symbols by myself?
UPD:
My goal is a text for label under "30" - only localized "MIN" string without any numbers I should get rid of before placing text from date formatter in the label at the bottom.
Depending on the details of what you need, NSDateComponentsFormatter might be the solution. It includes NSDateComponentsFormatterUnitsStyleSpellOut, which renders strings in formats like “One hour, ten minutes”.
Update: After reading comments I'm still not sure what kind of formatting you really need. But hopefully this will be a useful example:
let formatter = NSDateComponentsFormatter()
formatter.unitsStyle = NSDateComponentsFormatterUnitsStyle.Full
formatter.allowedUnits = [ NSCalendarUnit.Minute, NSCalendarUnit.Hour ]
let date = NSDate()
let dateComponents = NSCalendar.currentCalendar().components([.Minute, .Hour], fromDate: date)
let dateString = formatter.stringFromDateComponents(dateComponents)
At this point, dateString will be something like "15 hours, 11 minutes".
If you need just the min string localized you can use a MeasurementFormatter()
let measurementFormatter: MeasurementFormatter = {
let measurementFormatter = MeasurementFormatter()
measurementFormatter.locale = Locale.current
measurementFormatter.unitOptions = .providedUnit
measurementFormatter.unitStyle = .short
return measurementFormatter
}()
label.text = measurementFormatter.string(from: UnitDuration.minutes)

Resources