Discover how the latest updates to Foundation can help you improve your app's localization and internationalization support. Find out about the new AttributedString, designed specifically for Swift, and learn how you can use Markdown to apply style to your localized strings. Explore the grammar agreement engine, which automatically fixes up localized strings so they match grammatical gender and pluralization. And we'll take you through improvements to date and number formatting that simplify complex requirements while also improving performance.
Hello, my name is Tony, and I'm an engineer on the Foundation team. Welcome to “What’s new in Foundation.” The Foundation framework provides base functionality for all apps and frameworks. It has plenty of features, including everything from file handling to networking and notifications. Today I’d like to focus on something all apps need: internationalization and localization. In this year’s releases, we have some of the biggest advancements ever in this API. We started at a low level, rethinking what an attributed string is in Swift. We rebuilt our formatters for Swift, making them faster, easier to use, and adding new features. And finally, we have a brand new feature called automatic grammar agreement. It dramatically reduces the number of localized strings you need to provide while, at the same time, making your code simpler. Let’s jump right in to attributed strings. An attributed string is a combination of characters, a set of ranges, and a dictionary. Attributed strings allow you to associate attributes, which are key-value pairs, to a specific range of a string. The most common attributes are defined by the SDK, but you can also create your own. You’ll often find attributed strings in API that supports rich text. Let’s look at an example. Here is an app I’m working on called Caffé. It’s a simple menu. I pick the food that I want, the size, the quantity. And at the end, it shows me a receipt with a list of everything I’ve ordered. At the bottom here, I decided to add a short thank-you note. This is an attributed string. Part of the string is in bold and part is in italics. The last word has a link as well. As you can tell here, attributes can overlap. Since the beginning of Foundation, we’ve had a reference type called NSAttributedString. This year, we introduce a new struct AttributedString, which takes full advantage of all of the features that Swift provides us. First and foremost, it’s a value type. It also has the same character-counting behavior as Swift String. As part of our commitment to making it easy to write inclusive software, AttributedString is now fully localizable. Finally, it is built with safety and security in mind. This includes both compile time safety by using strong typing and also safety during unarchiving using Codable. Let’s take a brief tour of what you can do with the new AttributedString. We’re going to build our thank-you message. First, we create an attributed string using a simple initializer. I want to set an attribute on this entire string. That’s as simple as setting the font property. In struct AttributedString, all attributes are available directly, and they use the correct types. This property, for example, is a SwiftUI Font. Next, we create another attributed string. This one is a reference to our website, so we set the link property to a URL. Here I’m setting the font and link on the whole string. We’ll look at how to change just part of a string later. Another useful tool is an attribute container. It’s a place you can hold attributes and values on their own without the string. Here, I create a container and set some attributes on it, depending on the importance of my message. And finally, I merge those attributes into both attributed strings. As I mentioned, attributed string is a combination of characters, ranges, and a dictionary. The attributed string itself is not a collection of any single one of those things. Instead, in order to gain access to these properties, it has what we call views. The two most important views are characters, which provides access to the string, and runs, which provides access to the attributes. These views are Swift collections, which means the functions you are familiar with from types like Array are available here too. Let’s look at another example. Let’s say our designers told us to add a little bit of pizzazz to our thank-you message by making all punctuation a fun orange color. To do this, first I need to find where the punctuation is in the attributed string. Like other Swift collections, attributed string views use indices, which are simply a position for some element in the collection. To iterate by index, I use the standard library indices function. Next, I use the function isPunctuation to check if this character is one that we need to change. Finally, I use another feature of attributed string, slicing, to apply an attribute to only a subrange of the whole string. The range starts at this index and continues until the next index after that, so one character. And now the punctuation is orange. Let’s look at another view, runs. A run is the starting location, length, and value of a particular attribute. We can start by counting all the runs in our message. This will iterate each contiguous range of attribute values in the string. There are four runs in this string. Each run has a value or nil for each attribute. The ranges from characters and runs are interchangeable, so you can find the string for an attribute or vice-versa. Here I use the range of the attribute in a subscript for the characters view, converting the result into a standalone string. It’s often most useful to look at runs by focusing on a specific attribute. Here, we use the key path link to coalesce for just the link attribute. Each element of the resulting collection will give us the value of the link attribute, not taking into account other attributes which may be set in the string. If we only look at links, we have three runs: the first, where it is not set; the second, where it is set to a value; and the third, the last period in the sentence, where it is not set again. Iterating the runs gives us a tuple of the value and the range. Since the values are type safe, we can use an API, like scheme, which exists on URL, without casting or worrying about having the wrong type. Here I’m checking that every link in the attributed string is https. Another useful technique is to look for a substring and use that range to edit the characters or the attributes. Let’s say I want to replace the word "visit' with something that has a more throwback vibe. First, I look for the range of a substring. Next, I use that range to set the attributes and characters on only that subrange. The result is an attributed string with six runs, like this. Next, let’s talk about localization. AttributedString is fully localizable. We also added localization support to NSAttributedString in Objective-C. Attributed strings are located in your app’s strings files, just like a regular string. In Swift, we now support localized formatting of String and AttributedString using string interpolation just like SwiftUI’s text view. Here’s a simple example. This function returns a localized string which is customized with a user’s document name. Instead of using format specifiers like %@ or %d and calling a format function, you can now just pass the value in directly. The same approach works for AttributedString. Xcode can generate your strings files from these new initializers using the compiler. To turn it on, go to your build settings, look for the localization settings, and turn on Use Compiler to Extract Swift Strings. You may be wondering how the localized attributed string gets its attributes. I’m excited to tell you that we’ve added support for Markdown to AttributedString. Here’s an example of using a localized attributed string, right in a SwiftUI Text. I start off with just a plain string. By adding two asterisks around the text, I make it strongly emphasized, which SwiftUI renders as bold. I can use underscores to make the text emphasized, which SwiftUI renders as italic. We also support links. This is a great opportunity for localizers to provide customized URLs for different languages. We also support other inline styles like strikethrough and code voice. Finally, let’s talk about archiving attributed strings. First, we need the ability to convert to and from the NSAttributedString reference type. Attributed strings can be part of your data model, which means we need to be able to encode and decode them. Finally, we want a way to specify custom attributes in Markdown. These operations are all related to each other. Let’s start by looking at conversion. We’ve all written a lot of code that uses NSAttributedString, so we’ve made it easy to convert from the struct to the class type. Here’s a view that has an NSAttributedString property. To convert, just pass our struct AttributedString to the NSAttributedString initializer. For attributes that are part of the SDK, this is all I need to do. Now let’s look at encoding and decoding. Here is a struct that holds a receipt from our Caffé app. Again, I’m using the attributes provided by SwiftUI, UIKit, AppKit, and Foundation. That means that AttributedString’s default Codable implementation is all I need. I just add the Codable conformance to Receipt, and I’m done. Let’s go a bit further and add support for encoding a custom attribute. We’ll start by talking about the attribute itself. An attribute is two parts: a key and a value. The key is a type which conforms with the new AttributedStringKey protocol. That defines what type of value it requires and a name for archiving. This key can also conform with other protocols to customize how the value is encoded or decoded. Let’s say we want to define a range of an attributed string to have some extra color. This rainbow effect comes in three levels: plain, fun, or extreme. We’ll use an enum to represent that value and set the name to rainbow. Defining the type and the name are the only requirements of this protocol. Now, let’s say we want to make this attribute Codable so that it will be part of the encoded attributed string. All I have to do is add Codable conformance, like this. Finally, let’s say we want the rainbow level to be part of our localized string. That means it can be applied to the right part of the string, no matter where that is, in any language. All we need is one more protocol conformance to opt in. When we say that an attribute is Markdown decodable, then we can decode it straight from Markdown and insert it into an attributed string. All that’s required is that the value is Codable. Next, let’s take a look at the custom attribute syntax for Markdown. In this first example, we have a reference to a link. It uses square brackets for the link text and parentheses for the link destination, a URL. In this second example, we have a reference to an image. It starts with an exclamation point, then uses square brackets for the image description and parentheses for the image source. These first two examples are common in Markdown. The third example shows our syntax for custom attributes. It starts with a caret, then uses square brackets for the text, and parentheses for its attributes. The attributes are represented with JSON 5. JSON 5 is compatible with JSON and allows for unquoted keys, comments, and a few other features. It’s a good match for a human-friendly string like this one. We’ve also added JSON 5 support to our other JSON API in Foundation. Because custom attributes use JSON, anything that can be decoded with JSONDecoder is automatically compatible with the new custom Markdown syntax. Here we have a single attribute, two attributes, one string and one number, and a single attribute with multiple properties. There is just one additional piece here, which is how we connect these names in Markdown to a Swift type. That piece is called an attribute scope. Scopes are a group of attribute keys. Scopes are useful when decoding from JSON or Markdown, because they tell us which attributes we expect to find, their names, and how to decode them. We define one scope each for Foundation, UIKit, AppKit, and SwiftUI. You can define a scope of your own attributes as well. Let’s define a scope for our Caffé App. We’ll nest the scope inside AttributeScopes and conform it to the AttributeScope protocol. Then all we need to do is list the attributes inside our scope with “let.” So far, we have just our rainbow attribute. Next, we’ll include the SwiftUI scope inside of ours. That allows all of those attributes in addition to our own. Scopes nest recursively, so this also includes Foundation attributes. It’s useful to define a property for our new scope. That allows us to use a key path syntax in functions which take the scope as an argument. Finally, we can now load our localized and colorful attributed string from custom Markdown. You’ll also find scope-taking functions for archiving and conversion to and from NSAttributedString. This allows customization of behavior at every step. Here’s the first screen of the Caffé app again, where you can see our custom rainbow attribute in the title. After the localized string is converted from Markdown into an attributed string, the app finds the attribute and applies a fun effect to just that range of the string. Because the attribute comes from our localized strings file, this works correctly for all languages that Caffé supports, like Spanish. We’re just getting started, though. We also have a totally new Formatter API. Formatters are another long-time Foundation feature. They are responsible for taking data, like numbers, dates, times, and more and converting it into a localized and user-presentable string. Formatters are backed by quite a bit of configuration data, so it’s a common pattern to cache and reuse them. However, apps are composed of many distinct pieces of code. It may not always make sense to share a formatter between all of them. Also, because of the large variety of ways that people read dates and times, plus our desire as app authors to present this data in a way that fits our design, there are a lot of opportunities for edge cases. This year, we improved both performance and usability by rethinking our Formatter API from the ground up. In short, our new APIs focus on the format. Let’s take a look at this code from the Earthquakes sample app, where we can see the caching pattern in action. It has a two-step process. First, create and configure a formatter. Next, give the formatter our date and get a string. How could it be simpler? Well, let’s start by removing the requirement to create our own date formatter. It was really easy to forget that this needed to be cached, which would lead to re-creating the same formatter for every cell in the table. Next, the formatting step. Instead of passing our date through the formatter, let’s just use the date itself. Now it’s just one line of code. You specify the format you want, and that’s it. Let’s talk more about this number format. It’s not a lot of code, but it’s hiding some complexity and has a few pitfalls to be aware of. If the argument is not a floating point number, you’ll get entirely the wrong output here. Readers have to be aware of a special case syntax for formatting floating point numbers, along with a set of modifiers that are just string constants. We think this code is easier to understand, maintain, and read. It uses regular Swift functions to specify exactly how we want our number to be formatted. You also get autocompletion and type safety. We’ve applied this new approach to all ten formatters in Foundation. We’ve cleaned up and simplified the interfaces, made changes to help avoid common pitfalls, and added a bunch of new features along the way. Let’s take a look in detail at two of the most popular formatted types: dates and numbers. Date formatting is about using a calendar and time zone to convert an absolute point in time into something a human understands as a date. And even more than that, it takes into account all of the preferences that humans have about how they like their own dates to look. We call those preferences locales. Let’s take a look at the tiny amount of code you need in order to format a date. First, I’ll get the current point in time by using Date.now. Next, I call the formatted function. That's it. Of course, as we just saw in our example, date formatting is something that has a lot of options. So let’s expand this out a bit. The formatted function can be configured to show only the date or only the time. Both of those arguments have several options to pick from. An important goal for this new formatting API is to provide as much compile time help as possible in creating correct formats. Formatting using magic string values is notorious for creating pitfalls, where the format looks correct under normal circumstances but produces entirely the wrong value in edge cases, like at the end of the year. Here again is our default format. This is a short version of asking for the date and time style, like this. For both the no-argument and simple style versions, we pick a default format for you. However, if you want to really customize the date, you just go from here and add the fields you care about. In this example, I build up a format by appending fields to the style. I want only the year, day, and month. Other possibilities include hours, minutes, seconds, and so on. The output format will automatically adjust according to the user’s locale. These fields are also configurable. In this case, I change the month to the wide format, which means that the full month name is printed. Formatting just part of the date is easy with this API as well. Here I want to get just the weekday. Dates can be formatted into different styles, too. Here I choose to use the ISO 8601 format and iso8601, but only with the year, month, and day, and separated with a dash. With these examples, the formatting pattern becomes clear. We start with the value we want to format. We call the formatted function and the argument is the style. There may be more than one kind of style for each type. Date, for example, has both dateTime and iso8601. The style can be used in a default configuration or customized. This formatting API works by specifying a list of fields, some of which have additional options. The order of the fields you provide does not matter. Each field just tells the formatter what values should be included somewhere in the final output. We choose a sensible default for the shortest versions of the API, those with no arguments or just a style name. Once you begin adding fields to that, the output becomes customized and reflects only what you choose to display, somewhat like placeholder text in your UI. There is also a new API for formatting two dates that are relative to each other. Here are some examples. First, formatting two dates in a range. You can just use the regular Swift range syntax with two dates. Formatting ranges allows configuration of showing the date and time, just as we did with a single date. You can format this range as a duration or as components or a single date relative to right now. Another new feature in formatting is attributed output. This allows you to find the location a formatter put a particular part of a formatted value after it is done rearranging it to fit the user’s preferences. This, of course, uses our new struct AttributedString. Applying styling to formatted output shows up in all kinds of places. On watchOS, many of the complications are formatted strings. Since Apple Watch is such a personal device, it’s important to take into account the user’s preferences, but it’s also a place where we want to apply a certain kind of design language, like giving part of a date a color of the user’s choosing. Setting this up is actually quite a lot of fun in SwiftUI. Let’s look at it together in a demo. Here I have the starting point for my Caffé companion app, which shows when your next free coffee is. I’ve got a SwiftUI view which just shows a formatted date. I set the locale on the format so I can control it with my SwiftUI preview here. It’s a pretty good start, but I'd like to customize it a bit more. Let’s begin by making this a little more specific to my app. I only care about the minute, the hour, and the weekday. OK, looking pretty good. Now let’s add a splash of color. First, we’ll change the return type to AttributedString and ask for an attributed output.
Next, we’ll use an attribute container. These can hold attributes without being attached to any particular characters in a string. We’re going to create one for the weekday attribute that date formats put on their output. It’s set on the range of the string that contains the weekday.
Next, we’ll make a container for the color attribute we want to set.
And finally, we’ll use an attributed string function to replace the attributes matching those in the first container with the values in the second one on our attributed string.
Because AttributedString is a value type and replacing is a mutating function, we need to change our “let” to a “var.” Looks great. Even better, it works for all locales. Let’s add a few more to our preview to double check.
You can see here that the weekday is orange no matter where it is in the formatted date for these locales. Let’s keep learning about even more new formatter APIs. Now that we’ve seen how to turn dates into strings, let’s talk about how to strings into dates. Date now has an initializer that takes a strategy argument. The strategy is used to tell the parser what fields to expect in the input. For dates, the format is also a kind of strategy. That’s useful for round-tripping dates, like in a text field that both displays output and allows the user to type in a new date. Here is an example of a round trip. You’ll notice that the parsing can throw. This is because, depending on the input, parsing can fail. Some strategies have more advanced options for parsing. Here, we parse a fixed format, which is useful when the date format is something received from a server. To use it, initialize a strategy with a format string. Instead of using magic string values, though, we use string interpolation. In this case, we expect a string of the format year-month-day. Each interpolation is clearly identified by field, and each specifies exactly what format to expect. One really nice thing about this is the autocomplete experience. If I want to use a different day format, autocomplete shows me the valid options along with documentation about what each means. No, more guessing how many Y characters you should use to parse a year. Let’s move on to numbers. Number formatting is about converting an integer or floating point value into something a human can read. Like all formatting, it takes into account preferences on how numbers should be displayed. This includes everything from the kind of digit used to what character is used to group the digits. As with date formatting, getting great output is easy with no additional parameters required. There are many supported kinds of options and outputs. Here we show percentage, scientific notation, and currency. Finally, let’s put a few formats together. List formatting is now just formatting an array. This member style argument specifies the format style of each element in the array. These are numbers, so I’d like to use a percentage. The output is correct for every user’s locale. So far we’ve focused on formatting the values directly. SwiftUI also supports attaching a format style to a TextField. Since format styles have type information about the kinds of values they format, we can use a readable but safe syntax for the tip percentage on my receipt here. Let’s look again at our Caffé app and see just how many places formatting shows up. We use a list format for the ingredients. We use a currency format for the prices. We use a number format for the quantity and also to localize the count in the order button. We can’t forget about the date format you always see in the corner up here. You’ll find formatted output all over the place, and we think this new API will make it easy, even fun, in your app as well. There are a lot more resources available to help with localized strings and formatters. We have two more sessions on this topic, “Localize your SwiftUI App” and “Streamline your localized strings.” Next, let’s talk about a new feature called automatic grammar agreement. Localizers in languages like Spanish have been limited in their ability to express natural translations, sometimes leading to awkward dialog. These languages require transformations to achieve gender and pluralization agreement between different parts of speech and sometimes even require knowledge of the user’s preferred term of address. English has this feature too, with nouns that have different forms for singular and plural. I threw out a lot of language jargon there, so let’s go through an example. In my Caffé app, I can pick a food item, a size, and a quantity. I choose 1 small salad. Now my friend says she will join me, so I increase the count to 2. In English, the word “salad” has to change to match the number 2. This is called agreement. All that means is that the words in this sentence have to match each other. In English, fixing up words due to pluralization is a common kind of agreement. Now let’s switch our app to Spanish and order 1 ensalada pequeña, or 1 small salad. When I order for my friend, this order button needs the same pluralization as in English but with a twist. In Spanish, both the adjective, pequeña, and the noun, ensalada, have to agree with the count, dos. So instead of ensalada pequeña, the button says ensaladas pequeñas. Next, I move on to drinks. For this sentence, the button not only needs correct pluralization but also agreement in the grammatical gender of these words. Juice, jugo, is masculine. The adjective pequeño has to match as well. In order to localize text like this correctly, we end up with a combinatorial explosion. A different localized string is needed for each combination of food, size, and count. In code, it often ends up looking like this, where we need to switch over each item, then switch over each size, and so on. There is also a stringsdict file, which can pluralize each of these strings correctly for counts. Now, by leveraging the same technology that powers suggestions in keyboards, we’ve created a new API that can easily handle all of these cases and more. We call this feature automatic grammar agreement, because the system automatically fixes up localized strings so that they have correct grammar. Now the code becomes far simpler. You can combine the quantity, size, and food in one string. Automatic grammar agreement will fix up the string for you using a process called inflection. Let’s break it down. In order to inflect, we need to know which part of the string needs to be fixed up. Luckily, we have a type in Swift that can do that, AttributedString, and a custom attribute in Markdown. In this string, I use that syntax to wrap the food, size, and count with our inflect attribute. The value of the attribute is true. When we export this project’s localizations, we'll get a strings file that contains our annotated string as well as other localized strings in our source code, like the names of the foods and sizes. Here are the strings for Latin American Spanish. The localizer has used the argument reordering syntax %1, %3, %2, because an adjective like "small" or “large” goes before the noun in Spanish. They keep the custom attribute syntax for inflecting this region of the string and supply translations for the foods and sizes. The automatic grammar engine takes care of fixing up the rest. Some languages feature agreement not only between words in the localized text itself but between that text and the person reading it. Automatic grammar agreement can help with that too. For example, let’s take a look at this welcome screen for Notes. In English, we say “Welcome to Notes.” In Spanish, we say “Te damos la bienvenida a Notas,” or “We give you our welcome to Notes.” We would like to have the same experience for Spanish as we do in English. However, in Spanish, the word “bienvenido” must match with the user’s preferred term of address. This term could be one of several choices, and that choice changes the text. Using the correct term of address results in a more personal and inclusive experience. In this year’s releases, we’ve made it possible for people who use Spanish to specify their term of address. In the Language & Region settings, there is a new Term of Address option. When you choose it, you can pick your preference and also choose to share it with all apps. Here, you see the new welcome screen in Notes for a feminine term of address. And here it is for a masculine term of address. If we do not know or the user prefers not to specify, we keep our original string as an alternative. The same inflection attribute we saw earlier is also used in localized strings which refer to the user. In our “welcome to” case, we apply the inflection attribute to the word bienvenido. The English string does not need to change. I can also add an inflection alternative, which is a replacement string the engine will use if it has no information on what the user’s preferences are. This year, we support automatic agreement for Spanish and English. We’ve adopted it in several places across the OS, like the welcome screen in Notes. You can adopt it in your apps too. The code changes required are mostly just deleting a lot of logic to pick different strings. The instructions on what to inflect are part of the localized string itself, allowing localizers more control over how the string appears in their language. Let’s take a look at automatic grammar agreement in action for our Caffé app. Let’s go through the Caffé app running in English. I’ll start by adding some pizza to the order. I think I’ll have a large one. And just 1. Notice how the text on the button has changed from 0 large pizzas to 1 large pizza. That was done automatically. If I pick 2, it fixes it up again. I’ll just have one. At the bottom of this screen, the order button has changed to say 1 item. How about something to drink? Small is fine, and just 1 of these. Notice how the button has changed to say “items” instead of “item”? That string was changed automatically. Let’s check out. Here is our receipt. It lists our pizza and juice, along with formatted prices. At the bottom is our attributed string, with its custom fonts and link to the website. Let’s go back to Xcode and take a look at the source. I’ll start in the food details view.
This is the view that shows the size selection screen. Let’s add a new size so that we can see how that only requires us to add one localized string for Spanish instead of one for each kind of food, each size, and each count. This line shows the list. The list comes from our model object. Let’s go there.
The size enum already has small and large. I’m going to add a new size for our hungriest customers, called “huge.” To do that, I’ll add a new case, along with a localized string.
Now I just need to add prices for the huge size. For this demo, I’ve just put them in the initializers. Now I just need to add prices for the huge size. For this demo, I’ve just put them in our initializers.
Let’s check out our view again.
Here in the preview is our new size. Our source already contains an English string. I just need a Spanish one now. To generate the new strings, I’ll use the compiler to find the new localized string for “huge.” To do that, I choose Product > Export Localizations, and save the Spanish strings.
Now let’s add a translation for Spanish.
I can filter for our new string and type in our Spanish word.
Next, I import these localizations to put them into my app.
Now I’ll run our app again but in Spanish. To do that, I choose Product > Scheme > Edit Scheme.
In the Options, I can pick the language I want to test in.
And run. You can tell from our header that we're now running in Spanish. Let’s make another order, starting with salad. Notice as I change the quantity, the order button is fixed up. Our new huge size is correctly pluralized for 2 salads. Plus, it matches the grammatical gender for "ensalada," all with just one string. There are a lot of great new features in Foundation this year, and they are ready for you to try in your apps today. AttributedString provides a fast, easy-to-use, and Swift-first interface for adding key-value pairs to ranges of a string. You can use it with SwiftUI in text and start using Markdown in your localized strings. Our new Formatter API puts the focus on the format, simplifying your code and improving performance. Use formats in all of the places you present data in your app. Finally, automatic grammar agreement will intelligently fix up localized strings so that they match grammatical gender, counts, and the user’s own term of address. I hope you’ll love these new features. We’re looking forward to finding them in your apps. Thank you. [music]
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.