# Localization

Litexa features support for localizing your skill for multiple locales (opens new window). You can localize your skill in 2 ways:

  • project structure-based overrides
  • string replacement map

# Skill Manifest changes

To add your skill in multiple locales, you first need to declare that locale in your skill.js/ts/coffee/json file. The default generated project sets up your skill manifest object for 'en-US' like so:

{
  manifest: {
    publishingInformation: {
      isAvailableWorldwide: false,
      distributionCountries: ['US'],
      distributionMode: 'PUBLIC',
      category: 'GAMES',
      testingInstructions: 'replace with testing instructions',
      locales: {
        'en-US': {
          name: 'Cat Chase in Space',
          invocation: 'cat chase in space',
          // ... more fields
        }
      }
  ...

All other locales will contain the same fields as in 'en-US', so you can copy the existing contents over to the other locale and then modify its values.

Distribution Countries

There is a field in the manifest called 'distributionCountries,' which is not related to individual locales supported in your skill. This field instead affects your skill's availability in select regions. For more information, read the distribution documentation (opens new window).

Now, you can proceed with one of the localization methods.

# Option 1: Project Structure-based Overrides

This localization method is best suited for:

  • few changes to dialogue (e.g default skill is 'en-US' and localized skill is 'en-GB')
  • assets
  • functionality changes per locale (e.g monetization not available in some locales yet)

# Project Structure

Let's review your existing Litexa project's structure. In the litexa directory are .litexa files, an assets directory, and some code files. These contents are known as your default skill code. This means that unless your skill contains localization, it will use the content in these files.

To localize your skill using this method, create a languages directory under the litexa directory. Then, for all locales you will be supporting, create a directory named after the locale inside the languages directory. From there, for whichever content changes that differ from the default skill (e.g contents of .litexa files in litexa directory), mirror the project structure of the default skill inside the target locale directory.

For example, here is what a project structure may look like if the skill contains content overrides for 'en-GB' and 'de' (assuming that the default skill is written for 'en-US'):

.
├── README.md
├── artifacts.json
├── litexa
│   ├── assets
|   |   ├── intro.mp3
│   │   ├── icon-108.png
│   │   └── icon-512.png
│   ├── main.js
│   ├── main.litexa
|   ├── tutorial.litexa
│   ├── main.test.litexa
|   ├── slotbuilder.build.js
|   └── languages
|       ├── de # create a locale directory here
│       |   ├── assets
│       |   |   └── intro.mp3
|       |   ├── slotbuilder.build.js
│       |   ├── main.litexa
│       |   └── tutorial.litexa
|       └── en-GB # create a locale directory here
│           ├── assets
│           |   └── intro.mp3
|           ├── slotbuilder.build.js
│           └── main.litexa
├── litexa.config.js
└── skill.js

Here, you can see that running the skill in 'en-GB' will:

  • override the intro.mp3 asset (any reference will play the localized file)
  • override contents of main.litexa and slotbuilder.build.js with the localized files
  • use the default tutorial.litexa (because there is no localized counterpart)

Running the skill in the 'de' locale will behave similarly, with the exception that it will override tutorial.litexa with the localized file (which is meaningful because it is a different language).

Specifying language instead of locale code

Although your skill manifest needs to conform to the list of locales (opens new window), in the Litexa project structure you can choose to use the first 2 letters of the locale code if you don't need to differentiate between regions.

For example, if you want to have a uniform French language skill experience, you will still need to have separate 'fr-CA' and 'fr-FR' fields in skill.js/json/ts/coffee, but you can have a single 'fr' locale directory that will apply to both locales.

Now that we've seen where to place localized files, let's take a look at writing localization content. Litexa localization works by overrides - meaning that unless override content exists for a locale, it will use the default skill code's content. In Litexa, you can override states and assets.

# State Overrides

Litexa allows you to override skills using states as the unit of change. This means that any logic and handlers of that state will be overridden by the localized one. To override a state, simply write the new state logic with the same name. For example, let's say your default skill code's main.litexa looks like this:

launch
  say "Hello! How many people are playing today?"
  -> waitForPlayers

waitForPlayers
  when "$count players"
    or "we have $count players"
    with $count = AMAZON.PLAYERS
    if $count < 5 and $count > 1
      say "Great. You have $count players in this game."
      -> playGame
    else
      say "I'm sorry, but we will need between 2 and 4 players to play. How many will be playing today?"

Then, suppose in 'en-GB' you want to localize the interjection and limit the number of players the game can play. Your override main.litexa may look like this:

waitForPlayers
  when "$count players"
    or "we have $count players"
    with $count = AMAZON.PLAYERS
    if $count < 4 and $count > 1
      say "Brilliant. You have $count players in this game."
      -> playGame
    else
      say "I'm sorry, but we will need 2 or 3 players to play. How many will be playing today?"

Note that the localized file does not have a launch state. Therefore, the 'en-GB' version of the skill will use the default skill's launch state code.

Match .litexa file names for clarity

Since Litexa overrides .litexa content by state names instead of file names, you could theoretically name your localized .litexa files differently from their default counterparts. However, we strongly recommend matching the file names for organizational clarity.

# Skill Model Overrides

You can use the state override functionality to change your skill model as well.

Let's modify the override from the example above.

Utterances

Assuming that the COUNT_PLAYERS intent is solely used in the waitForPlayers state, the following replacement will change the utterances in the 'en-GB' skill model:

waitForPlayers
  when "$count players"
    or "playing with $count people"
    with $count = AMAZON.PLAYERS
  ...

The default skill model contains this:

...
    "intents": [
      {
        "name": "COUNT_PLAYERS",
        "samples": [
          "{count} players",
          "we have {count} players"
        ],
        "slots": [
          {
            "name": "count",
            "type": "AMAZON.NUMBER"
          }
        ]
      },
...

The 'en-GB' skill model will change that intent to:

...
    "intents": [
      {
        "name": "COUNT_PLAYERS",
        "samples": [
          "{count} players",
          "playing with {count} people"
        ],
        "slots": [
          {
            "name": "count",
            "type": "AMAZON.NUMBER"
          }
        ]
      },
...

Utterance aggregation

Utterance aggregation for the same intents across multiple states also follows the override logic. If the COUNT_PLAYERS intent was also handled in a different state and contained more sample utterances, and there was no change to the handler in that state for localization, the 'en-GB' skill model would then aggregate the 'en-GB' waitForPlayers state's COUNT_PLAYERS utterances with the ones in the other state of the default skill for the skill model.

Please take a look at the Intent Handlers section of the State Management Chapter for more information about intents and utterances.

Slots

Suppose you want to change the values of the the custom slot type defined in the function in slotbuilder.build.js. The only thing you'd need to do is define your localized slots in the locale folder, and Litexa will automatically use that one for that skill locale. In the example above, you can see that there is a slotbuilder.build.js file inside the 'en-GB' directory to fulfill this purpose. See Slots for how to define custom slot types.

Intents

Because overrides use states as the single unit of change, you can remove and add intents from/to your model. For example, if you have a quiz skill that asks different categories of questions, but you want to change one of the categories in a different locale, you can override the state(s) that contain the relevant category-specific intents. If your default skill code contains:

waitForAnswer
  when AnimalsIntent
    or "the animal is $animal"
    with $animal = animals.build.js:slotBuilder

  otherwise
    say "Here is your question again."
    ...

You can overwrite that state's handler with:

waitForAnswer
  when PlantsIntent
    or "the plant is $plant"
    with $plant = plants.build.js:slotBuilder

  otherwise
    say "Here is your question again."
    ...

The overridden locale's skill model will then have a PlantsIntent instead of an AnimalsIntent, with the custom slot type built from the function in plants.build.js.

Let's say you want to remove this category entirely from the localized locale. To do so, remove any -> waitForAnswer transition statement and its relevant skill flow logic. Then, make this state empty:

waitForAnswer
  # do nothing

Then, your target locale's skill model would no longer include AnimalsIntent.

# Asset Overrides

Much like slots, asset overrides are a one-to-one replacement based on whether they exist in the locale-specific directory's assets directory or not. This means that any asset references in the skill will use a localized asset over the default counterpart. To see the directory structure of assets in the deployed skill, see the S3 Configuration section in the Deployment Chapter.

# Option 2: String Replacement Map

This localization method is best suited for:

  • minimal functional differences
  • major dialogue differences

between supported locales.

This localization method is similar to traditional localization methods - meaning that all translated strings are aggregated and stored by reference in a single file, instead of integrating with the skill code.

In this method, we reuse the definition of default skill code defined in the previous method under the Project Structure section.

Note

This method only localizes utterances and say/reprompt strings. Strings in code are not captured; if you use slot builders, or inline code speech injection, you will need to do the localization in the same location.

See Localization in code for details on implementing this.

WARNING

Inline slot definitions (e.g. with $color = "red", "yellow", "blue") do not get localized with this method.

To localize your skill with this method to a new language, there are 2 steps.

First, you will need to generate a localization.json in your project root via the Litexa command line:

litexa localize

The second step is to create a Litexa file for your target language. This file can be blank; it just has to exist for Litexa to recognize that you are supporting that language. So, for example, if you are localizing for French, you could create litexa/languages/fr/blank.litexa and this would complete your setup. Make sure this conforms to the project structure above.

# The localization file: localization.json

You are now ready to add localization content. Let's take a look at the localization.json file. This will serve as the source file for your translations. This is what it looks like from running it on the vanilla litexa generate skill:

{
  "intents": {
    "AMAZON.StopIntent": {
      "default": []
    },
    "AMAZON.CancelIntent": {
      "default": []
    },
    "AMAZON.StartOverIntent": {
      "default": []
    },
    "MY_NAME_IS_NAME": {
      "default": [
        "my name is {name}",
        "call me {name}",
        "{name}"
      ]
    },
    "AMAZON.HelpIntent": {
      "default": []
    }
  },
  "speech": {
    "Hello again, @name. Wait a minute... you could be someone else.": {},
    "Hi there, human.": {},
    "What's your name?": {},
    "Please tell me your name?": {},
    "Sorry, I didn't understand that.": {},
    "Nice to meet you, $name. It's a fine {todayName()}, isn't it?": {},
    "Just tell me your name please. I'd like to know it.": {},
    "Please? I'm really curious to know what your name is.": {},
    "Bye now!": {}
  }
}

localization.json follows this structure:

{
  "intents": {
    "SomeIntentName": { // map of all skill intents to their utterances by Litexa language
      "default": [ // utterances parsed from the default Litexa files
        "default utterance {slot_name}",
        "another default utterance {slot_name}"
      ],
      "fr": [ // translated utterances added to the localization.json by a translator for fr
        "french utterance {slot_name}",
        "another french utterance {slot_name}"
      ]
    }
  },
  "speech": { // map of all in-skill speech to map of Litexa language with any available translations
    "default speech string": { // say/reprompt string found in default Litexa files
      "fr": "override string for FR"
    },
    "say statement|alternate one": { // say/reprompt with their `or` alternates are delineated by `|` characters
      "fr": "french alternate one|french alternate two|french alternate three"
    }
  }
}

To add your localized strings, map them from the target language to the string, for each default string you wish to localize. Litexa's SSML shorthand and interpolation rules continue to apply.

Escaped SSML tags require an extra `\`

Due to how say strings are parsed, if you have escaped SSML tags:

  say "\<amazon:emotion name='excited'>Cats are cute!\</amazon:emotion>"

They render in localization.json as:

"<amazon:emotion name='excited'>Cats are cute!</amazon:emotion>"

And you need to add the escapes back into the mapped translations with another backslash:

"speech": {
  "<amazon:emotion name='excited'>Cats are cute!</amazon:emotion>": {
    "fr": "\\<amazon:emotion name='excited'>Cats are cute!\\</amazon:emotion>"
  },
  ...
}

There's a few things to note. First, utterances for a language are not mapped one to one, meaning that an intent in the default language might have 3 utterances, and the one for fr-FR might have 5. You can have any number of utterances for an intent for any language.

Second, say/reprompt strings and their alternatives are mapped together. They appear as one entry in the file as pipe (|) separated strings. As such, your localized language can have asymmetrical alternatives. Both default and localized strings follow the same format.

As an example, the say statement in a Litexa file:

launch
  say "one thing"
    or "another thing"

would look like this in localization.json:

"speech": {
  // (...)
  "one thing|another thing": {
    "fr-CA": "une chose", // translation can have fewer alternatives
    "fr-FR": "une chose|autre chose|encore une autre chose" // translation can have more alternatives
  }
  // (...)
}

Finally, the speech strings function as overrides in the skill. If there is no translated string for a given language, it will fall back to the default string.

# Modifying strings in between localization iterations

In the process of skill development, you might revise your default skill code strings at the same time you are adding localizations to your skill. To help you keep track of changes, the localize command will output a summary of the localization.json strings that have changed.

It's important to remember that the localize command reads strings from only the default skill code. To keep things straight, you can keep in mind these two truths as you localize:

  • the default skill code is the source of truth for what utterances and say/reprompt strings to localize
  • localization.json is the source of truth for what's covered for localized languages

Here's an example on what would happen if you had modified a string after it was localized. Your Litexa project initially looks like this, after running litexa localize and adding translations for Spanish:

# main.litexa
launch
  say "Hello."
    or "Hi."
  say "What's your name?"
// localization.json
"speech": {
  "Hello.|Hi.": {
    "es": "Hola."
  },
  "What's your name?": {
    "es": "¿Cómo se llama?"
  }
}

If we then changed main.litexa to this:

# main.litexa
launch
  say "Howdy."
  say "What's your name?"

Running litexa localize would then output this in the console:

[localization] +441ms parsing default skill intents, utterances, and output speech ...
[localization] +320ms the following speech lines are new since the last localization:
[localization] +1ms + Howdy.
[localization] +0ms the following speech lines in localization.json are missing in skill:
[localization] +0ms - Hello.|Hi.

And localization.json would now have:

"speech": {
  "Howdy.": {}, // new line
  "What's your name?": {
    "es": "¿Cómo se llama?"
  },
  "Hello.|Hi.": { // orphaned line
    "es": "Hola."
  }
}

So, if the translations from the original string still apply, they should be copied to the new string mapping manually.

Intents and utterances changed in the same way follow the same behavior.

# litexa localize arguments

litexa localize can receive some arguments to modify its behavior. To see the complete list, run litexa localize --help.

# Verbose logs

The --verbose flags turns on logging to give more detail on the contents that have changed from the existing localization.json.

# Auto-removing orphaned strings

Orphaned speech strings and orphaned utterances can be automatically removed with the --remove-orphaned-speech and --remove-orphaned-utterances flags, respectively, but note that doing so will also remove their translations from localization.json.

# Cloning translations

You can clone existing strings of a language to another language:

litexa localize --clone-from fr-FR --clone-to fr-CA

Cloning existing translations for one language (e.g. fr-FR) to another language (e.g. fr-CA) can be useful as a starting point for regionalizing a translation with the same base language.

Cloning to an existing language

If you clone to a language that already exists, you will overwrite its existing localization in the localization file.

# Combining both localization strategies

You can combine both localization methods in accordance with your use case (e.g, project structure-based overrides for assets, and string replacement map for dialogue). However, they function completely independently of each other. Also, remember: the string replacement map method only extracts information from the default language.

# Localization in code

Both localization methods do not cover all cases of localizing strings. this means that slotbuilder functions, context.say.push calls, function calls that may use strings, and inline code speech injection are not localized.

In your Litexa files, you can pass context.language to code to retrieve the Litexa language.

As an example, let's assume your Litexa code had a custom color slot type and an inline function that injects speech directly into context.say:

# main.litexa
launch
  when "$color"
    with $color = mySlotBuilders.js:myColorSlotBuilder
    injectSpeech(context)

The inline code could use context.language to do any necessary localization.

// inlineCode.js
function injectSpeech(context) {
  switch (context.language) {
    case 'fr':
      context.say.push('Bien!');
      break;
    default:
      context.say.push('Great!');
      break;
  }
}

Likewise, slot builders will be provided the same value as a language argument which can be used to do any necessary localization:

// mySlotBuilders.js
function myColorSlotBuilder(skill, language){
  const slot = { name: 'myColorSlotName' }
  switch (language) {
    case 'fr':
      slot.values = [ 'bleu', 'rouge' ];
      break;
    default:
      slot.values = [ 'blue', 'red' ];
      break;
  }
  return slot;
}

# Testing

To test your skill in a different locale, add the -r or --region flag to the litexa test command. By default, a vanilla litexa test will run your skill in the en-US locale.

Alternatively, you can use the setRegion statement in your Litexa test cases. This will override the containing test's locale to the one in the statement's from that point in the test. However, the simulation's locale does not change, and as such, the model generated in the .test directory is of the simulation's locale. This statement is useful for iterative testing of localization, or for regression tests meant specifically for one locale.

WARNING

Only tests written in the default litexa directory will be run. Do not write test cases within the languages directory or any of its subdirectories.

# Inspecting the skill model

To inspect your skill model for a specific locale, execute litexa model -r <locale>. This will print out the skill model for that locale. You can also look at the .deploy directory contents for each skill model produced for your skill on deployment.

If you've used DEPLOY variables to change your model for specific deployment targets, you may specify which target to run the command for by adding the -d <deployment> flag.