I am trying to localise the Widget.
Setting .environment(\.locale, Locale(identifier: "ja"))
does not work for most of the code:
It only works for:
Text(entry.date, style: .date)
but does NOT work for:
Text(Date.now.advanced(by: 60*60*12), style: .relative)
andif {} else {}
statement
It should display something like:
こんにちは世界!
2022年12月31日
11時間, 57分
Instead it is displaying this:
struct SuperWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
WidgetView()
.environment(\.locale, Locale(identifier: "ja"))
}
var languageCode: String? { Locale.autoupdatingCurrent.language.languageCode?.identifier }
var localeCode: String? { Locale.autoupdatingCurrent.identifier }
var isJapaneseLanguage: Bool { languageCode == "ja" }
var isJapaneseLocale: Bool { localeCode == "ja_JP" }
@ViewBuilder
func WidgetView() -> some View {
VStack {
if isJapaneseLocale || isJapaneseLanguage {
Text("こんにちは世界!")
} else {
Text("Hello, world!")
}
VStack {
Text(entry.date, style: .date)
Text(Date.now.advanced(by: 60*60*12), style: .relative)
}
}
}
}
CodePudding user response:
I've written a workaround for the issue in my comment. It's kinda elaborate, but it seems to work.
It also is apparent that the Text
view does not use a RelativeDateTimeFormatter
internally.
Note, that the original Text
facilitates "life updates", which requires a Model to implement this behaviour in the custom view. It uses a timer and a RelativeDateTimeFormatter to accomplish this.
Update
In order to make it clear, this code only demonstrates the issue with the broken localisation of the SwiftUI Text
view when using a Date
as parameter and relative
as style
. It can not be just copy&pasted into your Widget.
Below you can experiment with the code:
import SwiftUI
import Combine
struct RelativeDateText: View {
private let date: Date
private let unitsStyle: RelativeDateTimeFormatter.UnitsStyle
init(
from date: Date,
unitsStyle: RelativeDateTimeFormatter.UnitsStyle
) {
self.date = date
self.unitsStyle = unitsStyle
}
var body: some View {
EnvironmentReader(\.locale) { locale in
_RelativeDateText(
from: date,
unitsStyle: unitsStyle,
locale: locale
)
}
}
struct EnvironmentReader<Content: View, Env>: View {
@Environment private var env: Env
let content: (Env) -> Content
init(
_ keyPath: KeyPath<EnvironmentValues, Env>,
content: @escaping (Env) -> Content
) {
self._env = .init(keyPath)
self.content = content
}
var body: some View {
content(env)
}
}
struct _RelativeDateText: View {
@StateObject private var model: Model
init(
from date: Date,
unitsStyle: RelativeDateTimeFormatter.UnitsStyle = .full,
locale: Locale
) {
self._model = .init(
wrappedValue: .init(
date: date,
unitsStyle: unitsStyle,
locale: locale
)
)
}
var body: some View {
Text(model.relativeDateString)
}
final class Model: ObservableObject {
@Published var relativeDateString: String = ""
private let relativeDateTimeFormatter: RelativeDateTimeFormatter
private var cancellable: AnyCancellable!
init(
date: Date,
unitsStyle: RelativeDateTimeFormatter.UnitsStyle = .full,
locale: Locale
) {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .abbreviated
formatter.locale = locale
self.relativeDateTimeFormatter = formatter
cancellable = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.prepend(Date.now)
.map { _ in
formatter.localizedString(for: date, relativeTo: Date.now)
}
.assign(to: \.relativeDateString, on: self)
}
}
}
}
struct RelativeDateText_Previews: PreviewProvider {
static var previews: some View {
// Compare the original `Text` with custom `RelativeDateText`:
VStack {
Text(Date.now, style: .relative)
RelativeDateText(
from: .now,
unitsStyle: .short
)
}
.environment(\.locale, Locale(identifier: "ja_JP")) // <== "ja_JP" !!
}
}
CodePudding user response:
Short Answer
I ran your code and assuming that Locale.autoupdatingCurrent
gets a new locale from .environment(\.locale, entry.locale)
is the issue.
@Environment(\.locale) var locale
is the only way you can get SwiftUI's locale.
struct WidgetSample_WEntryView : View {
var entry: Provider.Entry
//This reads SwiftUI's locale not autoupdating
@Environment(\.locale) var locale
var languageCode: String? { locale.language.languageCode?.identifier
}
var localeCode: String? { locale.identifier }
var isJapaneseLanguage: Bool { languageCode == "ja" }
var isJapaneseLocale: Bool { localeCode == "ja_JP" }
var body: some View {
VStack{
Text(Date.now.advanced(by: 60*60*12), style: .relative) //This does not localize, it is an Apple bug in a Main App as well.
if isJapaneseLocale || isJapaneseLanguage {
Text("こんにちは世界!")
} else {
Text("Hello, world!")
}
}
//This will tell SwiftUI to use `locale`
.environment(\.locale, .getCustomLocale(entry.languageCode))
}
}
extension Locale{
///Gets locale of provided `languageCode` if `languageCode` == nil the `.current` locale will be returned
static func getCustomLocale(_ languageCode: String?) -> Locale{
guard let languageCode = languageCode else {
return .current //Use device locale
}
let locale = Locale(identifier: languageCode)
return locale //Use language code locale
}
}
Long Answer
Since style: .relative
doesn't work you have to manually implement the updates by providing the necessary entries in the getTimeline
method and use a custom formatter than uses the custom locale
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline
let currentDate = Date()
for offset in 0..<60 {
let entryDate = Calendar.current.date(byAdding: .second, value: offset, to: currentDate)!
//All the important data must come from here.
let entry = SimpleEntry(date: entryDate.advanced(by: 60*60*12), configuration: configuration, languageCode: "ja")
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
Then your SimpleEntry
and View
can look something like this.
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
/// nil uses device's localization
let languageCode: String?
// Workaround for Apple bug
// Needs entries for every minute/second as desired and as allowed by the "Widget Budget"
var localizedDate: String{
let f = RelativeDateTimeFormatter()
f.locale = .getCustomLocale(languageCode)
f.unitsStyle = .abbreviated
return f.string(for: date) ?? "unable to localize"
}
}
struct WidgetSample_WEntryView : View {
var entry: Provider.Entry
@Environment(\.locale) var locale
var body: some View {
VStack{
Text(entry.date, style: .relative) //This does not localize, it is an Apple bug in a Main App as well.
Text(entry.localizedDate) //Needs entries for every minute/second as desired and as allowed by the "Widget Budget"
if locale == Locale(identifier: "ja_JP"){
Text("こんにちは世界!")
}else{
Text("Hello, world!")
}
}
//This will tell SwiftUI to use `locale`
.environment(\.locale, .getCustomLocale(entry.languageCode))
}
}
Long Answer Part 2
String
s can also be localized by SwiftUI if you implement "Localization" in your app/extension. This would remove the if else
logic where you check the locale
for Text("Hello, world!")
.
Here are some basic steps to implement Localization
- Go to PROJECT > Localizations > > Select languages
- Create a "Localizable.strings" file
- Click on the "Localizable.strings" file and in the "File Inspector", "Localize" the file.
- Click on the "Localizable.strings" file and in the "File Inspector" look at "Localization" and click on the relevant languages.
- Click on the "Localizable.strings" file and in the "File Inspector" look at "Target Membership" and make sure the widget extension is selected.
The English file will look something like
/*
Localizable.strings (English)
*/
"Hello, world!" = "Hello, world!";
and the Japanese file will look something like
/*
Localizable.strings (Japanese)
*/
"Hello, world!" = "こんにちは世界!";
Then your View can be simplified to look something like this and you would get the English and Japanese versions of the key.
struct WidgetSample_WEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack{
Text(entry.localizedDate) //Needs entries for every minute/second as desired and as allowed by the "Widget Budget"
Text("Hello, world!") //Text knows to look for the "Localizable.strings" file
}
//This will tell SwiftUI to use `locale`
.environment(\.locale, .getCustomLocale(entry.languageCode))
}
}