# Internationalization

> The i18n key formats for app settings pages, widgets, workflow actions, action inputs and triggers, with locale fallback rules.

Source: https://developer.kustomer.com/docs/apps/i18n

The `i18n` object holds every string your app shows in Kustomer's own UI: settings page labels, widget titles, and the names of your actions, their inputs and your triggers in the workflow builder. Keys are grouped by locale; Kustomer shows the agent's locale when you provide it and falls back to `en_us`.

```json title="i18n (shape)"
{
  "i18n": {
    "en_us": { "<key>": "<text>" },
    "fr_fr": { "<key>": "<texte>" }
  }
}
```

Always provide `en_us`. Missing keys fall back to `en_us`, and with no `en_us` entry Kustomer shows the raw key or system name.

Strings inside your KViews, widgets and custom settings pages are yours to translate; i18n covers only the UI Kustomer renders for you.

## Settings page strings

`<app>` is your app name as written in the definition.

| Key                                                 | Labels                                        |
| --------------------------------------------------- | --------------------------------------------- |
| `<app>.settings.page.title`                         | The settings page heading and menu entry      |
| `<app>.settings.page.description`                   | The text under the heading                    |
| `<app>.settings.path.<category>.<name>.displayName` | A setting's label                             |
| `<app>.settings.path.<category>.<name>.description` | A setting's help text. Markdown is supported. |

```json title="Settings strings"
{
  "en_us": {
    "acme_orders.settings.page.title": "Acme Orders",
    "acme_orders.settings.page.description": "Connect your Acme store.",
    "acme_orders.settings.path.default.apiKey.displayName": "API key",
    "acme_orders.settings.path.default.apiKey.description": "Create one in Acme under **Settings › API**."
  }
}
```

## Widget strings

| Key                          | Labels                                   |
| ---------------------------- | ---------------------------------------- |
| `<app>.widgets.<name>.title` | The title of the widget with that `name` |

## Workflow builder strings

Actions and triggers are labelled under an `app.<app>.workflow.builder` prefix followed by their full, prefixed name:

| Key                                                                               | Labels                             |
| --------------------------------------------------------------------------------- | ---------------------------------- |
| `app.<app>.workflow.builder.actions.<action name>.displayName`                    | The action in the step picker      |
| `app.<app>.workflow.builder.actions.<action name>.description`                    | Its description                    |
| `app.<app>.workflow.builder.actions.<action name>.properties.<input>.displayName` | An input field's label             |
| `app.<app>.workflow.builder.actions.<action name>.properties.<input>.exampleText` | An input's placeholder             |
| `app.<app>.workflow.builder.triggers.<event name>.displayName`                    | The trigger in the trigger picker  |
| `app.<app>.workflow.builder.triggers.<event name>.description`                    | Its description, shown when chosen |

`<action name>` and `<event name>` are the full names, including `kustomer.app.<app>.`, so the app name appears twice:

```json title="Workflow builder strings"
{
  "en_us": {
    "app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.displayName": "Refund order",
    "app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.description": "Issue a full or partial refund in Acme.",
    "app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.properties.orderId.displayName": "Order ID",
    "app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.properties.orderId.exampleText": "4417",
    "app.acme_orders.workflow.builder.actions.kustomer.app.acme_orders.refund.properties.amount.displayName": "Amount",
    "app.acme_orders.workflow.builder.triggers.kustomer.app.acme_orders.order-updated.displayName": "Acme order updated",
    "app.acme_orders.workflow.builder.triggers.kustomer.app.acme_orders.order-updated.description": "Runs whenever Acme sends an order update. Later steps can use the order ID, customer email, status and total."
  }
}
```

> **Private apps rewrite these keys:** When you register a private app, Kustomer appends your org ID to the app name inside action keys too, along with the action names themselves. Write keys with the plain app name; the registration response shows the rewritten ones.

## Keep keys in sync

Keys are long and easy to mistype, and a typo fails silently: the builder just shows the raw name. Generate them from the definition in your build step instead of writing them by hand:

```js title="build-i18n.mjs"
const app = 'acme_orders'
const prefix = `app.${app}.workflow.builder`

export function actionStrings(actions, labels) {
  const out = {}
  for (const action of actions) {
    const l = labels[action.name] ?? {}
    out[`${prefix}.actions.${action.name}.displayName`] = l.displayName ?? action.description
    if (l.description) out[`${prefix}.actions.${action.name}.description`] = l.description
    for (const input of Object.keys(action.inputSchema?.properties ?? {})) {
      const p = l.properties?.[input] ?? {}
      out[`${prefix}.actions.${action.name}.properties.${input}.displayName`] = p.displayName ?? input
      if (p.exampleText) out[`${prefix}.actions.${action.name}.properties.${input}.exampleText`] = p.exampleText
    }
  }
  return out
}
```

And check for gaps before you register. This prints the label key of every setting that doesn't have one:

```bash
jq -r '
  .app as $app | .i18n.en_us as $t
  | .settings | to_entries[] | .key as $category
  | .value | keys[]
  | "\($app).settings.path.\($category).\(.).displayName"
  | select($t[.] == null)
' dist/acme_orders.json
```
