# Language Files

URL: https://zerodev.ca/docs/medusa/developer-documentation/languages

> Manage multi-language translations and runtime phrases across all 16 supported languages.



Medusa supports 16 languages. Base English schemas live in `resources/lang/en/*.js` extending `Langs`. Translations for all other languages are placed in `resources/lang/translations.json` or `resources/lang/<locale>/common.json`.

## Base English Schemas (`resources/lang/en/`)

Define message and embed phrases by extending `Langs` from `#medusa/modules`:

```js title="modules/example/resources/lang/en/messages.js"
import { Langs } from "#medusa/modules";

export class MessagesLang extends Langs {
    constructor(medusa) {
        super(medusa, {
            name: "messages",
            label: "User Messages",
            description: "Discord responses and embed templates."
        });
    }

    schema() {
        return {
            created: {
                type: "phrase",
                default: "Successfully created item **%name%**."
            },
            notFound: {
                type: "phrase",
                default: "Item **%identifier%** does not exist."
            },
            successEmbed: {
                type: "embed",
                default: {
                    title: "Action Complete",
                    description: "%custom_emoji_check% • Operation completed for **%user%**.",
                    color: "#22c55e"
                }
            }
        };
    }
}
```

## Sections (`type: "section"`)

Group related phrases under a collapsible heading with a `section` entry. Put every phrase a command, feature or panel uses in its own section, and name command sections after the slash command (`/remind`) so it is clear what each phrase is attached to. Keys stay flat, so a section can be added or renamed without touching stored overrides, `translations.json` or the code that resolves the phrase. Keys must be unique across all sections of one lang group.

```js title="modules/example/resources/lang/en/commands.js"
schema() {
    return {
        remind: {
            type: "section",
            label: "/remind",
            fields: {
                remind: { type: "phrase", default: "Set, list and cancel personal reminders", label: "Command" },
                remindMe: { type: "phrase", default: "Set a reminder", label: "me Subcommand" }
            }
        }
    };
}
```

The dashboard renders each section as a collapsed block with its phrase count. Section labels translate through `schema.<module>.lang.<group>.section.<section>.label` and `.desc` in `common.json`.

## Embeds Belong in Lang

Every embed a module sends is declared as a `type: "embed"` entry and resolved with `lang.embed`. Module code never assembles an embed from separate title and description phrases. Repeating lines use the embed `lists` map, and resolve options pass through as the fifth argument:

```js
reminderList: {
    type: "embed",
    default: {
        title: "%custom_emoji_clock% Your Reminders",
        description: "%reminders%",
        lists: { reminders: "**#%id%** <t:%time%:R> %text%\n" }
    },
    label: "Reminder List"
}
```

```js
this.medusa.modules.lang.embed(this.module, "messages", "reminderList", { reminders: rows.map(row => ({ id: row.id, time: row.time, text: row.text })) }, { thumbnail: false });
```

## Resolving Phrases and Embeds

Resolve localized phrases and embed templates in your handlers and commands:

```js
const text = this.medusa.modules.lang.phrase(this.module, "messages", "created", { name: "Iron Sword" });

const embed = this.medusa.modules.lang.embed(this.module, "messages", "successEmbed", { user: interaction.user.username });

const quiet = this.medusa.modules.lang.embed(this.module, "messages", "successEmbed", { user: interaction.user.username }, { thumbnail: false, footer: false });
```

## Language Fallback Chain

Two server-wide settings pick the language the bot speaks:

* `LANGUAGE` - the primary language.
* `SECONDARY_LANGUAGE` - the fallback language used when a module has not translated the primary language (default `"en"`).

Resolution is per key, from lowest to highest priority: English base schema, then the secondary language, then the primary language, with dashboard overrides layered on top of each locale. When a module does not provide a key in the primary language it falls back to the secondary language, and when the secondary language is also missing that key it falls back to English. Both settings live in the dashboard **Settings → Language** card.

```txt
key value = primary translation → secondary translation → English base
```

`this.medusa.locales.chain()` returns the ordered, de-duplicated locale list (`[primary, secondary, "en"]`) that drives this resolution. Setting `SECONDARY_LANGUAGE` equal to `LANGUAGE` or to `"en"` collapses the chain back to the single-language behaviour.

## Dashboard Localization

Medusa's dashboard loads its own base UI strings. Modules provide display names, page titles, and custom phrases through their language files. When Medusa starts, the core API aggregates each module's language dictionary and serves it to the web dashboard.

### Default Display Name (`displayName`)

Set a readable module title in `main.js`:

```js title="modules/example/main.js"
export class ExampleModule extends Modules {
    constructor(medusa) {
        super(medusa, {
            name: "example",
            displayName: "Example",
            author: "Zero Development",
            version: "1.0.0.0",
            description: "Production ready example module."
        });
    }
}
```

When no translation files are present, the dashboard uses `displayName`.

### Multi-Language Dictionaries (`common.json`)

To localize your module across Medusa's 16 supported languages, create `resources/lang/<locale>/common.json`:

```json title="modules/example/resources/lang/en/common.json"
{
    "name": "Example",
    "description": "Production ready example module.",
    "example.customButton": "Deploy Item",
    "example.modalTitle": "Create New Item"
}
```

```json title="modules/example/resources/lang/fr/common.json"
{
    "name": "Exemple",
    "description": "Module d'exemple pret pour la production.",
    "example.customButton": "Deployer l'article",
    "example.modalTitle": "Creer un nouvel article"
}
```

The shorthand `"name"` and `"description"` keys map to `module.<id>.name` and `module.<id>.desc`. Additional keys can be read directly in custom React pages using `useT()`.

### Translations Bundle (`translations.json`)

Translations for non-English languages can also be grouped in `resources/lang/translations.json`:

```json title="modules/example/resources/lang/translations.json"
{
    "fr": {
        "common": {
            "name": "Exemple",
            "description": "Module d'exemple pret pour la production."
        },
        "messages": {
            "created": "L'article **%name%** a ete cree avec succes.",
            "notFound": "L'article **%identifier%** n'existe pas."
        }
    },
    "es-ES": {
        "common": {
            "name": "Ejemplo",
            "description": "Modulo de ejemplo listo para produccion."
        },
        "messages": {
            "created": "Articulo **%name%** creado con exito.",
            "notFound": "El articulo **%identifier%** no existe."
        }
    }
}
```

### Automatic Schema Fallbacks

All configuration schemas (`resources/config/`), logging categories (`resources/logs/`), and custom pages (`resources/dashboard/pages/`) define labels and descriptions inline in code. Medusa automatically renders these labels without requiring translation entries. Adding explicit keys to `common.json` allows per-locale translations:

| Target Element           | Default Source                                | Optional Localization Key                                            |
| ------------------------ | --------------------------------------------- | -------------------------------------------------------------------- |
| Module Display Name      | `meta.displayName` in `main.js`               | `"name"` or `"module.<id>.name"`                                     |
| Module Description       | `meta.description` in `main.js`               | `"description"` or `"module.<id>.desc"`                              |
| Page Navigation Label    | `meta.label` in `page.js`                     | `"page.<module>.<page>.label"`                                       |
| Page Description         | `meta.description` in `page.js`               | `"page.<module>.<page>.desc"`                                        |
| Records Column Header    | `config.columns[].label` in `page.js`         | `"page.<module>.<page>.column.<key>"`                                |
| Records Empty State      | `config.empty.title` / `hint` in `page.js`    | `"page.<module>.<page>.empty.title"` / `".empty.hint"`               |
| Records Cell Value       | string value returned by the route            | `"page.<module>.<page>.value.<column>.<value>"`                      |
| Config Group Label       | `meta.label` in `resources/config/*.js`       | `"schema.<module>.config.<group>.label"`                             |
| Config Field Label       | `field.label` in `schema()`                   | `"schema.<module>.config.<group>.<field>.label"`                     |
| Config Group Description | `meta.description` in `resources/config/*.js` | `"schema.<module>.config.<group>.desc"`                              |
| Config Select Option     | `options[].label` in `schema()`               | `"schema.<module>.config.<group>.<field>.option.<value>"`            |
| List Item Subfield Label | `item[].label` in a `"list"` field            | `"schema.<module>.config.<group>.<field>.<subfield>.label"`          |
| List Item Select Option  | `item[].options[].label`                      | `"schema.<module>.config.<group>.<field>.<subfield>.option.<value>"` |
| Logging Channel Label    | `meta.label` in `resources/logs/*.js`         | `"schema.<module>.logs.channels.<category>.label"`                   |
| Logging Event Label      | `event.label` in `events()`                   | `"schema.<module>.logs.events.<event>.label"`                        |
| Command Description      | `command.getDescription()`                    | `"commands.<command>.description"`                                   |
| Subcommand Description   | `option.description` in `options`             | `"commands.<command><Subcommand>.description"`                       |
| Option Description       | `option.description` in `options`             | `"commands.<command><Option>.description"`                           |

The shared "Log channels" and "Log events" group headers are translated once by the dashboard itself (`logging.group.channels.*` and `logging.group.events.*`), so modules never need to repeat them.

### Log Embed Translations (`logs.json`)

The labels above only cover the dashboard. The log embeds the bot posts into Discord are translated from `resources/lang/<locale>/logs.json`. Every `.json` file in a locale folder loads as a translation group named after the file, so `logs.json` becomes the `logs` group.

```json title="modules/example/resources/lang/de/logs.json"
{
    "itemCreated.title": "%custom_emoji_plus% • <server> » Eintrag erstellt",
    "itemCreated.summary": "%subject% hat einen Eintrag erstellt.",
    "field.Owner": "Eigentümer"
}
```

| Key                     | Replaces                                                                                      |
| ----------------------- | --------------------------------------------------------------------------------------------- |
| `<event>.title`         | The event's `title` in `events()`                                                             |
| `<event>.summary`       | The event's `summary` in `events()`                                                           |
| `field.<English label>` | Any field name, actor label, stamp label, or `changes[].label` passed to `medusa.logs.post()` |

English needs no file: the definitions in `resources/logs/*.js` are the English source. When a key is missing, `field.*` lookups fall back to the core module's shared glossary (common labels such as `Channel`, `Reason`, `User`, `Case ID` and every `<Verb> at` stamp), then to the English text. Keep `summary` free of hardcoded English by passing values through `tokens` instead of a prebuilt `summary` string:

```js
await this.medusa.logs.post("example", "itemCreated", {
    guildId,
    tokens: { owner: member.id, item: item.name },
    fields: [{ name: "Owner", value: `<@${member.id}>` }]
});
```
