SwiftUI automatic string localization question - swiftui

I've been working on a SwiftUI app's localization, and I faced a localization-related situation which I don't quite understand (given, I'm not too proficient in SwiftUI yet, to begin with)
As far as I understand, at least in iOS 14, SwiftUI pretty much automatically applies localization to all "normal" strings (as long as I have proper localization files set up - which I do). However, I have two instances of the same string literal - one gets automatic localization treatment. The other does not.
So here's the situation I'm trying to figure out.
I have the following code:
NavigationView {
NavigationLink(destination: CalendarSettingsView()) {
SettingsNavLinkView(label: "Calendar") // <- this doesn't get localized
}
}
And SettingsNavLinkView is set up as the following (just the skeleton related to question):
struct SettingsNavLinkView: View {
var label:String
var body: some View {
Text(label) // <- localized "Calendar" is expected to be passed here
}
}
In addition, CalendarSettingsView defines its title as in:
struct CalendarSettingsView: View {
var body: some View {
ScrollView {
//some code
}
.navigationBarTitle("Calendar", displayMode: .inline) // <- "Calendar" here does get localized
}
}
I do have key entry for "Calendar" in my localization files.
What is happening (and what I don't understand) is for the SettingsNavLinkView(label: "Calendar") component, the "Calendar" is NOT getting localized, HOWEVER, for CalendarSettingsView component (and related use case: .navigationBarTitle("Calendar", displayMode: .inline)) the "Calendar" string DOES get localized.
Both of these instances seem like the very same String to me, so I'm just trying to figure out what's going on here.
I did solve the issue by modifying the SettingsNavLinkView by specifically adding LocalizedStringKey initialization like below:
struct SettingsNavLinkView: View {
var label:String
let localizedLabel = LocalizedStringKey(label) // <-- NEW
var body: some View {
Text(localizedLabel) // <-- UPDATED to use localizedLabel instead of label
}
}
But why did I have to do that? Why wasn't the "Calendar" string automatically localized at the point when it was passed to the SettingsNavLinkView as per this code SettingsNavLinkView(label: "Calendar")?
A bug in SwiftUI localization? My incomplete understanding of how localization works?
I would prefer not having to resort to LocalizedStringKey for "simple strings"… But I'm not sure if what I'm asking for is even valid from the perspective of how "automatic" localization really works.
Any thoughts appreciated. Thanks!

Because different Text constructors are inferred for literal string and for variable string and that is documented in SwiftUI API
/// Creates a text view that displays a stored string without localization.
///
/// Use this intializer to create a text view that displays — without
/// localization — the text in a string variable.
///
/// Text(someString) // Displays the contents of `someString` without localization.
///
/// SwiftUI doesn't call the `init(_:)` method when you initialize a text
/// view with a string literal as the input. Instead, a string literal
/// triggers the ``Text/init(_:tableName:bundle:comment:)`` method — which
/// treats the input as a ``LocalizedStringKey`` instance — and attempts to
/// perform localization.
///
/// By default, SwiftUI assumes that you don't want to localize stored
/// strings, but if you do, you can first create a localized string key from
/// the value, and initialize the text view with that. Using a key as input
/// triggers the ``Text/init(_:tableName:bundle:comment:)`` method instead.
///
/// - Parameter content: The string value to display without localization.
public init<S>(_ content: S) where S : StringProtocol
as they said no localization.
but next with localization:
/// Creates a text view that displays localized content identified by a key.
///
/// Use this intializer to look for the `key` parameter in a localization
/// table and display the associated string value in the initialized text
/// view. If the initializer can't find the key in the table, or if no table
/// exists, the text view displays the string representation of the key
/// instead.
///
/// Text("pencil") // Localizes the key if possible, or displays "pencil" if not.
///
/// When you initialize a text view with a string literal, the view triggers
/// this initializer because it assumes you want the string localized, even
/// when you don't explicitly specify a table, as in the above example. If
/// you haven't provided localization for a particular string, you still get
/// reasonable behavior, because the initializer displays the key, which
/// typically contains the unlocalized string.
///
/// If you initialize a text view with a string variable rather than a
/// string literal, the view triggers the ``Text/init(_:)-9d1g4``
/// initializer instead, because it assumes that you don't want localization
/// in that case. If you do want to localize the value stored in a string
/// variable, you can choose to call the `init(_:tableName:bundle:comment:)`
/// initializer by first creating a ``LocalizedStringKey`` instance from the
/// string variable:
///
/// Text(LocalizedStringKey(someString)) // Localizes the contents of `someString`.
///
/// If you have a string literal that you don't want to localize, use the
/// ``Text/init(verbatim:)`` initializer instead.
///
/// - Parameters:
/// - key: The key for a string in the table identified by `tableName`.
/// - tableName: The name of the string table to search. If `nil`, use the
/// table in the `Localizable.strings` file.
/// - bundle: The bundle containing the strings file. If `nil`, use the
/// main bundle.
/// - comment: Contextual information about this key-value pair.
public init(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil)
I would recommend to use the following (tested with Xcode 12.1 / iOS 14.1)
struct SettingsNavLinkView: View {
var label: String
var body: some View {
Text(LocalizedStringKey(label)) // << inline !!
}
}

Related

SwiftUI Localization Issue

I have a project with two targets representing two different final products. Until now the localization was shared between the two targets, but now I have just one string to be localized differently according to the active target. In order to avoid duplicating the Localizable.string file ad tweak the file target membership, I decided to create two different Localizable-Ext.string files containing just the string to be translated differently for each target.
I'm using the SwiftUI Text initializer accepting a LocalizedStringKey parameter which automatically looks up the corresponding translation inside the file. This is what has to be done in most cases. I noticed that this initializer also accepts a tableName parameter corresponding to the .string filename where the match should be taken.
What I'm trying to achieve is to have a custom Text initializer which takes the string key, looks up for it inside the default Localizable.string file and, if no match is found, falls back to the right file extension (string table) and search for it there. Apparently this is tough to achieve since I cannot manage to get the key value from the LocalizedStringKey instance.
I think you need something like
extension Text {
public init<S>(looking text: S) where S : StringProtocol {
let text = String(text)
var translated = NSLocalizedString(text, comment: "")
// returns same if no translation, so ...
if translated == text {
// ... continue in other table
translated = NSLocalizedString(text, tableName: "Localizable-Ext",
bundle: .main, value: text, comment: "")
}
// already translated, so avoid search again
self.init(verbatim: translated)
}
}

SwiftUI and Combine

I'm following a video on the Firebase YouTube channel. Starting around 27:45, the instructor is trying to set a variable based on a Boolean and ends up with the following code in init(task: Task):
$task
.map { task in
task.isCompleted ? "checkmark.circle.fill" : "circle"
}
.assign(to: \.completionStateIconName, on: self)
.store(in: &cancellables)
This seems overly convoluted to me. First, I can't find documentation on using .map on a struct object, only on arrays, etc. Second, what is with this &cancellables thing? (It's defined as private var cancellables = Set<AnyCancellable>() before the init{}.) Third, why all this code, and not simply:
task.completionStateIconName = task.isCompleted ? "checkmark.circle.fill" : "circle"
This seems to give the same result, but will there be something down the line that the first code fragment works, but the second doesn't?
$task (with the $ prefix) is a projected value of the #Published property wrapper, and it returns a variable of the type Published.Publisher. In other words, its a Combine publisher, which publishes a value whenever the property - in this case Task - changes.
If you didn't learn about the Combine framework (or other reactive frameworks), this answer is definitely not going to be enough. At a high-level, a Combine publisher emits values, which you can transform through operators like .map, and eventually subscribe to, for example with .sink or .assign.
So, line-by-line:
// a publisher of Task values
$task
// next, transform Task into a String using its isCompleted property
.map { task in
task.isCompleted ? "circle.fill" : "circle"
}
// subscribe, by assigning the String value to the completionStateIconName prop
.assign(to: \.completionStateIconName, on: self)
Now, the above returns an instance of AnyCancellable, which you need to retain while you want to receive the values. So you either need to store it directly as a property, or use .store to add it to a Set<AnyCancellable> - a common approach.
So, why is it so convoluted? This is, presumably, built so that if task property ever changes, the Combine pipeline would update the completionStateIconName property.
If you just did:
completionStateIconName = task.isCompleted ? "circle.fill" : "circle"
that would assign the value just in the beginning.
That being said, in this particular case it might actually be unnecessarily too convoluted to use Combine, whereas just using didSet:
var task: Task {
didSet {
completionStateIconName ? task.isCompleted ? "circle.fill" : "circle"
}
}

How to store array of strings into Realm instance using a Dictionary?

I am new to Realm and having this issue.
I am having a Dictionary like this
{
firstName : "Mohshin"
lastName : "Shah"
nickNames : ["John","2","3","4"]
}
and a class like this
class User: Object {
var firstName: String?
var lastName: String?
var nickNames: [String]?
}
While I am trying to insert values it is throwing an exception as below
Property 'nickNames' is declared as 'NSArray', which is not a supported RLMObject property type. All properties must be primitives, NSString, NSDate, NSData, NSNumber, RLMArray, RLMLinkingObjects, or subclasses of RLMObject.
See https://realm.io/docs/objc/latest/api/Classes/RLMObject.html for more information.
I have also tried
var nickNames = NSArray()
var nickNames = NSMutableArray()
But not working.Do I need to make the Nickname model class and create a property as follow or there's a way to do this ?
var nickNames = List<Nickname>()
UPDATE:
You can now store primitive types or their nullable counterparts (more specifically: booleans, integer and floating-point number types, strings, dates, and data) directly within RLMArrays or Lists. If you want to define a list of such primitive values you no longer need to define cumbersome single-field wrapper objects. Instead, you can just store the primitive values themselves.
Lists of primitive values work much the same way as lists containing objects, as the example below demonstrates for Swift:
class Student : Object {
#objc dynamic var name: String = ""
let testScores = List<Int>()
}
// Retrieve a student.
let realm = try! Realm()
let bob = realm.objects(Student.self).filter("name = 'Bob'").first!
// Give him a few test scores, and then print his average score.
try! realm.write {
bob.testScores.removeAll()
bob.testScores.append(94)
bob.testScores.append(89)
bob.testScores.append(96)
}
print("\(bob.testScores.average()!)") // 93.0
All other languages supported by Realm also supports lists of primitive types.
you can find more details here. https://academy.realm.io/posts/realm-list-new-superpowers-array-primitives/
class BlogPost: Object {
#objc var title = ""
let tags = List<String>()
convenience init(title: String, tag: String) {
self.init()
self.title = title
self.tags.append(tag)
}
}
more details here
Realm doesn't support model properties that are NSArrays, and currently doesn't support properties that are Lists of primitive types (such as Lists of strings). For now, you should create a Nickname model that wraps the nickname string, and then store a List<Nickname>, as in your sample code above.
This ticket on our GitHub repository tracks support for lists of primitives, although none of the comments from 2014 are particularly relevant anymore. You can follow that ticket if you want to be informed as to when that feature will become available.
(Also note that you should declare your list property as let, not var.)
Using List is pretty much the only way to do it. When you initialize the Nickname object (the realm object you created for using in List), you should provide an array for the value param, even if the value is actually just one string. For example:
let aNickName = Nickname(value:["John"])
That is why it was throwing an error saying "Invalid value 'John' to initialize object of type 'Nickname'".

Pass variable from initializer to handlebars .hbs (Discourse Plugin, maybe Ember in general)

In a .hbs file I am creating a navigation menu with various items. One of the items will only display if a user is a pro (boolean).
var pro is a variable set in my initializer and I need to pass it my .hbs file for the purposes of a conditionally showing one of the menu items.
In Ember, how is this accomplished?
In a such case, we use service to store variables during the application lifecycle. Initializer puts the variables and menu items to service. Menu components retrieve variables from service.
You may also define a helper to retrieve variable from service.
I had quite the same problem : I had to use a language variable that is set in my html and I needed to use this language variable in my template. I used a helper to do so : in the helper you can use the variable pro that is set in your initializer (provided you declared it).
example :
function myInit(pro) {
var template = ...;
var data = ...;
Handlebars.registerHelper('ifProUser', function(item) {
if (pro) {
return "pro menu here";
} else {
return "";
}
}
var html = template(data);
...
}
Then in the template just use :
{{ifProInit}}{{/ifProInit}}

Sitecore - Image field in parameter template

If I have an image field in a parameter template, what are the steps involved in getting the URL of the image in c#?
#mdresser makes a valid point about what should and should not be a rendering parameter. However, I don't think that Sitecore intentionally made it difficult to use image fields in parameter templates. They simply built the parameter template functionality over the existing key-value pair rendering parameter functionality.
If the name of your image field on the rendering parameters template was BackgroundImage, you could use the following code to get the URL of the selected image:
var imageId = XmlUtil.GetAttribute("mediaid", XmlUtil.LoadXml(this.Parameters["BackgroundImage"]));
MediaItem imageItem = Sitecore.Context.Database.GetItem(imageId);
backgroundImageUrl = MediaManager.GetMediaUrl(imageItem);
If you don't already have a base class for your sublayouts that provides the Parameters property, you will need to also add this:
private NameValueCollection parameters;
public virtual NameValueCollection Parameters
{
get
{
if (this.parameters == null)
{
var parameters = this.Attributes["sc_parameters"];
this.parameters = string.IsNullOrEmpty(parameters)
? new NameValueCollection()
: WebUtil.ParseUrlParameters(parameters);
}
return this.parameters;
}
}
To achieve this you would have to look at how the sitecore image field renders the raw text value into an img tag. However, there is a reason that this doesn't work out of the box with sitecore; parameters templates are designed to define info about how a rendering or sublayout should render. E.g. You could use it to tell a list control to show a certain number of items etc. I'd advise against using rendering parameters for content as this will make content editing very cumbersome. If your aim is to have the content of a particular sublayout defined somewhere other than the page itself, put it into a sub item instead.
You can have a 2 separate functions to retrieve the value of image field in the parameter template that you gave to the sub layout .
First Step : Get the value associated with the parameter of image . Please use below function to retrieve the value .
/// <summary>
/// Returns a specific parameter value
/// Use this for Single-line, multiline text fields, linkfield, Image field etc.
/// </summary>
/// <param name="parameterName"></param>
/// <returns></returns>
public MediaItem GetValueFromRenderingParameter(string parameterName)
{
var item = !string.IsNullOrEmpty(_params[parameterName]) ? _params[parameterName] : string.Empty;
if(item == null)
{
return null ;
}
return Sitecore.Context.Database.GetItem(item);
}
Second step : Create another function where you can use the above mentioned function to retrieve the value of image field and use it appropriately. Here is the code snippet of the same :
Public string RenderImage()
{
Sitecore.Data.Fields.ImageField imageField =GetValueFromRenderingParameter("Image parameter name");
return MediaManager.GetMediaUrl(imageField.MediaItem) ;
}
Hope This Helps .