When implementing App Shortcuts with App Intents, it can be a bit daunting to localize everything.

Below is a small inventory of each Strings file and how to translate them. But first…

🚀 The big picture 🔗

After wandering the net without finding anything that worked, I reached out to Andrea Gottardo, who kindly replied, and I quote:

"You do need to have localizations for each, so I would recommend making a separate loc file called AppShortcuts.strings"

I followed their advice, and it worked.

When I first read this, I thought that the key element was "for each" because when I wanted to test the localization, I only translated the first sentence to save time. It was a little weird because I thought I had also tested this at the very beginning, but I didn't pay more attention than that.

The thing is, translating all the phrases was not the only thing I did: I also created a file called "AppShortcuts.strings" and moved the translations there. I'm so used to breaking my Localizable.strings into multiple files that I didn't pay attention to this because it's part of my workflow. But it may be a key element.

Indeed, this is the only file that throws this type of error: "Invalid Utterance. Every App Shortcut utterance should have '${applicationName}' in it".

article-image

So it seems there are two files involved:

  • Localizable.strings: the default file for your app, for everything but the phrases
  • AppShortcuts.strings: for all the AppShortcut/AppShortcutsProvider phrases, and it has to be called exactly like this

☝️ Localizable.strings is not mandatory 🔗

While the AppShorcuts.strings must be used for phrases, you don't have to use Localizable.strings for the other strings.

Most strings rely on LocalizedStringResource for translation, and as you can see from the image below, you can use a custom file and provide its name as the "table" argument.

article-image

🚩 Translating App Shortcuts 🔗

Here's the inventory with code samples:

AppShortcutsProvider & AppShortcut 🔗

The AppShortcutsProvider prepares a set of preconfigured shortcuts that will appear in the Shortcuts.app. You can help your users find it using the new SwiftUI ShortcutsLink button.

Here's what my AppShortcutsProvider currently looks like:

struct NoMeatTodayAppShortcuts: AppShortcutsProvider {
    static var shortcutTileColor: ShortcutTileColor = .lime
    
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: AddMealIntent(quantity: 1),
            phrases: [
                "Add a meal \(\.$content) to \(.applicationName)",
            ],
            systemImageName: "leaf.fill"
        )
        AppShortcut(
            intent: AddMealIntent(1, content: .clear),
            phrases: [
                "Add a vegetarian meal in \(.applicationName)",
                "Add a vegetarian meal to \(.applicationName)",
                "Add a vegan meal in \(.applicationName)",
                "Add a vegan meal to \(.applicationName)",
                "Add a meatless meal in \(.applicationName)",
                "Add a meatless meal to \(.applicationName)",
                "Add a plant-based meal in \(.applicationName)",
                "Add a plant-based meal to \(.applicationName)",
            ],
            systemImageName: "leaf.fill"
        )
        AppShortcut(
            intent: OpenViewIntent(),
            phrases: [
                "Open \(\.$view) in \(.applicationName)",

            ],
            systemImageName: "leaf.fill"
        )
    }
}

As you may notice, there's some SwiftUI building magic involved since we're turning a list of AppShortcut into an array, all thanks to the AppShortcutsBuilder.

article-image

If you want to have an initializer like in the above (`AddMealIntent(quantity: 1)`), do like I did, listen to Emmanuel Crousivier.

article-image
Source: https://twitter.com/emcro/status/1555960780897415168

Anyway, phrases should go into your AppShortcuts.strings file, and the variables or the application name should be surrounded by ${}.

"Open ${view} in ${applicationName}" = "Ouvre la vue ${view} dans ${applicationName}";

"Add a meal ${content} to ${applicationName}" = "Ajoute un repas ${content} à ${applicationName}";

"Add a vegetarian meal in ${applicationName}" = "Ajoute un repas végétarien dans ${applicationName}";
"Add a vegetarian meal to ${applicationName}" = "Ajoute un repas végétarien à ${applicationName}";
"Add a vegan meal in ${applicationName}" = "Ajoute un repas végan dans ${applicationName}";
"Add a vegan meal to ${applicationName}" = "Ajoute un repas végan à ${applicationName}";
"Add a meatless meal in ${applicationName}" = "Ajoute un repas sans viande dans ${applicationName}";
"Add a meatless meal to ${applicationName}" = "Ajoute un repas sans viande à ${applicationName}";
"Add a plant-based meal in ${applicationName}" = "Ajoute un repas à base de plantes dans ${applicationName}";
"Add a plant-based meal to ${applicationName}" = "Ajoute un repas à base de plantes à ${applicationName}";

The strange part is that when ${applicationName} is the only variable; only one AppShortcut will be available in the Library (Siri is supposed to understand them all, though), whereas adding two phrases with an AppEnum having three values will result in six Shortcuts.

For instance, with Open ${view} in ${applicationName} where the view can have 3 values, this creates in 3 preconfigured Shortcuts. But if I were to add Show ${view} in ${applicationName} to the list, I'd get 6.

That being said, if you want to create one preconfigured Shortcut for each of your phrases having only ${applicationName} in it, add multiple AppShortcuts with one phrase instead of adding one AppShortcut with multiple phrases.

AppIntent 🔗

When it comes to AppIntent, everything goes into your Localizable.strings (or the file of your choice if you use tables, see above).

struct OpenViewIntent: AppIntent {
    static var openAppWhenRun: Bool = true

    static var title: LocalizedStringResource = "Open View"
    static var description: IntentDescription = .init("Opens the selected view", categoryName: "Open in App", searchKeywords: ["open", "view"])
    
    @Parameter(title: "View")
    var view: ShortcutableView
    
    @MainActor
    func perform() async throws -> some IntentResult {}
    
    static var parameterSummary: some ParameterSummary {
        Summary("Open \(\.$view)")
    }
}

Where do they appear? 🔗

title and description appear when you click on the (i) next to an action.

article-image

The @Parameter(title:) "Vue" and ParameterSummary "Ouvrir …" appear in the details of your action, while the values "Aujourd'hui/Population/Historique" are provided by the AppEnum (see below)

article-image

title and @Parameter(title:) 🔗

Just give them a string that you'll translate in Localizable.strings, or use one of LocalizedStringResource's init if you want to use a custom table.

description 🔗

As you can see, I didn't use a simple string for the description. This is because I want to be able to group Shortcuts by type of intent in the action picker. I only have 2 Shortcuts for now so it's not very useful, but I intend (…) to add more.

article-image

ParameterSummary 🔗

This one is tricky. You'd think that because it has a variable, it should be with the phrases translations in AppShortcuts.strings, but no, it belongs in Localizable.strings.

But, you still need to use the same syntax with the ${}

"Open ${view}" = "Ouvrir ${view}";

AppEnum 🔗

Here's the enum I used in the example above, with Today/Population/History being translated in French ("Aujourd'hui/Population/Historique").

public enum ShortcutableView: String, AppEnum {
    case today
    case population
    case history
    
    static var typeDisplayName: LocalizedStringResource = "View"

    public static var typeDisplayRepresentation: TypeDisplayRepresentation = "View"
    
    public static var caseDisplayRepresentations: [ShortcutableView: DisplayRepresentation] = [
        .today: "Today",
        .population: "Population",
        .history: "History",
    ]
}

typeDisplayName, typeDisplayRepresentation (which I'm not sure ever appears anywhere anyway) and caseDisplayRepresentations all go in your Localizable.strings file or the file of your choice with the proper initializer.

For instance, for caseDisplayRepresentations you will have to use something like.init(title: LocalizedStringResource("Today", table: "CustomFileName")) instead of the simple "Today" I used.

Other strings 🔗

I think it's safe to assume that anything that's not covered here goes into Localizable.strings, but we'll see :)

✔️ Parting notes 🔗

  • You need two files: Localizable.strings and AppShortcuts.strings
  • For phrases without variables / having only \(.applicationName) in them, only the first one will appear in the Shortcuts library
  • Translate all phrases (although I'm not sure that's a real requirement)
  • When testing your actions (creating a new shortcut using the shortcuts you provide), make sure you create a new shortcut each time. There is some caching involved, and the translations might not show up otherwise.
  • I haven't covered Application Name synonyms and SiriTipView yet, so come back sometime later or follow me on Twitter for updates.

And if you're looking for a tool to manage the localization, I'd suggest you use my tool of choice, Localazy. I wrote some articles to get you set up and have a referral link that should earn you some free stuff.

Before you leave 😇

Consider doing one or more of these:

  • Follow me on Twitter @sowenjub
  • Download No Meat Today, a companion app for people who want to eat less meat, whatever you put behind "less" or "meat." Most of us are eating too much meat for our own sake these days (health, environment, climate, money…). This app will help you get on top of your consumption.