Smart Strings
Smart Strings are a modified version of the SmartFormat library.
Smart Strings are a powerful alternative to using String.Format when generating dynamic strings; they enable data-driven templates to have proper pluralization, gender conjugation, lists, and conditional language logic. Named placeholders give a more intuitive and less error-prone way to introduce variables which can help to provide additional context to translators.
To mark a localized string as smart, click the button at the bottom-right of the entry (⋮) in the Localization Tables window and select Smart Format from the menu. A Smart String can be identified by the {S} icon at the bottom-right of the entry.
You can also mark a string as smart in the Smart field in the Localized String editor.
To mark a field as smart using an Editor script, add the IsSmart property to the field and set it to true.
// Get the collection.
var collection = LocalizationEditorSettings.GetStringTableCollection("My Strings");
// Get the English table.
var englishTable = collection.GetTable("en") as StringTable;
// Get the Entry we want to mark as Smart.
var entry = englishTable.GetEntry("My Entry");
entry.IsSmart = true;
#if UNITY_EDITOR
// If we are in the Editor then we need to mark the table dirty so the changes are saved.
EditorUtility.SetDirty(englishTable);
#endif
Introduction
A Smart String consists of literal text and format items, also known as placeholders. Placeholders are encapsulated inside of a {placeholder} block, similar to String.Format placeholders.
A placeholder can consist of multiple components, including:
- Selectors
- Formatter name
- Formatter options
- Format
The image below displays the structure of an example Smart String:
The following diagram illustrates the steps taken to format a placeholder. First sources are evaluated until the source data has been selected, then the formatter is applied.
Selector
Selectors extract the object from the placeholder. This often means extracting a value from an argument or from an external source, for example the Persistent Variables source. The sources evaluate the selectors; then the formatter formats the object.
For example, if the argument passed is a reference to the script attached to the GameObject, the Reflection source could extract a GameObject’s name with the following smart string:
The name of the GameObject is {gameObject.name}.
Multiple sources can be used on a single format item.
For example: The name of the GameObject is {1.gameObject.name}
The Smart String would first use the Default Source to extract the argument at index 1, the Reflection Source would then process the extracted value.
Note
When no index value is included, the argument at index 0 is used by default.
Sources are evaluated starting with the first item and working through them until a source can handle the selector. The order of the sources can result in different strings.
For example if the following dictionary instance was used as an argument to the Smart String The value is {Count}
.
var localizedString = new LocalizedString("My Table", "My Table Entry");
var dict = new Dictionary<string, string> { { "Count", "Hello World" } };
localizedString.Arguments = new object[] { dict };
If the Reflection Source was before the Dictionary Source, then the output would be
The value is 1
, however if the Dictionary was first, the output would be The value is Hello World
.
It is important to consider the order of the sources and avoid using named placeholders that might conflict with other sources.
Selector syntax is similar to C# interpolated strings; you can use selectors to filter each subset of data to get to the desired object using dot notation. In the example below, slider and value are both selectors. The slider selector evaluates the argument passed into the Smart String at index 0. The value Selector then evaluates the results of the previous Selector.
For example the string could be used with a UI Slider reference to extract the slider’s current value and use that as the source value.
{slider.value}: In this example, the slider Local Variable references a UI Slider. (1). The Persistent Variables Source would extract this value.
{slider.value}: When the slider has been retrieved, the value placeholder returns the slider’s value (2). The Reflection Source would extract this value.
Multiple arguments
Like with String.Format, you can use multiple data sources as parameters. You can access any value that is not at index 0 using an indexed placeholder.
var localizedString = new LocalizedString("My Table", "My Table Entry");
var dict1 = new Dictionary<string, string>() { { "Name", "John" } };
var dict2 = new Dictionary<string, string>() { { "City", "Washington" } };
localizedString.Arguments = new object[] { dict1, dict2 };
Debug.Log("The value is: " + localizedString.GetLocalizedString());
For example the localizedString
could take the following forms to achieve the same resulting string "First dictionary: John, second dictionary: Washington".
Dot notation | First dictionary: {0.Name}, second dictionary: {1.City} |
Nested scope | First dictionary: {0:{Name}}, second dictionary: {1:{City}} |
When no index is specified, Item 0 is used by default. | First dictionary: {Name}, second dictionary: {1.City} |
To define multiple arguments without scripting, see the Persistent Variables source.
Nesting and scope
You can nest Smart Strings to avoid repetition, for example: {User.Address:{Street}, {City}, {State} {Zip}}
Nesting is often used with conditionals, plurals, and lists. To illustrate this, the following are equivalent when using an instance of the Person class as an argument.
public class Address
{
public string StreetAddress { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleName { get; set; }
public string FullName { get; set; }
public string Name => FirstName + " " + LastName;
public DateTime Birthday { get; set; }
public int Age
{
get
{
if (Birthday.Month < DateTime.Now.Month || (Birthday.Month == DateTime.Now.Month && Birthday.Day <= DateTime.Now.Day))
{
return DateTime.Now.Year - Birthday.Year;
}
else
{
return DateTime.Now.Year - 1 - Birthday.Year;
}
}
}
public Address Address { get; set; }
public List<Person> Friends { get; set; }
public int NumberOfFriends => this.Friends.Count;
public Person Spouse { get; set; }
}
Without nesting: {User.Name} ({User.Age}) {User.Address.City} {User.Address.State}
With Nesting: {User:{Name} ({Age})} {User.Address:{City} {State}}
Here, Name and Age are within the User scope and City and State are within the User.Address scope.
Within any nested scope, you can access the outer scopes. For example, in the string {User.Address:{User.Name} {City} {State}}
, {User.Name}
, which is in the root scope, is accessible from within the nested User.Address scope.
To use the current value in scope, use an empty placeholder {}.
Formatters
Formatters convert the object returned by the selector into a string. You can use formatters to format date, time, lists, plurals or even apply conditional logic.
To use a specific formatter, specify the formatter name. If you don’t specify a name, the formatter is chosen based on the context of the provided arguments and data. For example the Plural formatter contains three names: "plural", "p" and "". This means the plural formatter can be explicitly used with the names "plural" or "p"; when no name is specified it is tried implicitly.
Explicit use | I have {0:plural:1 Apple|{} Apples} |
Explicit use | I have {0:p:1 Apple|{} Apples} |
Implicit use | I have {0:1 Apple|{} Apples} |
Implicit formatters are evaluated starting with the first item at the top of the list and working downwards. The order of the formatters can result in different strings.
In most cases using an explicit formatter is preferred because it avoids conflicts with other formatters. However, if the same string is driven by different data, different sources and formatters are used. This requires an implicit formatter.
You can also create custom formatters by inheriting from the FormatterBase class.
Some formatters require additional options to be provided, inside of brackets. For example the options provided in {0:choose(1|2|3):one|two|three}
would be 1,2,3. The choose formatter would then use them to determine which literal text to use from one, two or three.
Localized control formatting
Smart Strings use the Locale for the table they belong to when applying any localized formatting such as currency symbols, number formatting, date and time.
Information on locale formatting is available in the Locale inspector.
Example | Result |
---|---|
English: The value is {0:N} | The value is 123,456,789.12 |
Spanish: The value is {0:N} | The value is 123.456.789.12 |