Medusa/developer documentation/Language Files

Language Files

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:

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.

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:

reminderList: {
    type: "embed",
    default: {
        title: "%custom_emoji_clock% Your Reminders",
        description: "%reminders%",
        lists: { reminders: "**#%id%** <t:%time%:R> %text%\n" }
    },
    label: "Reminder List"
}
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:

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.

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:

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:

modules/example/resources/lang/en/common.json
{
    "name": "Example",
    "description": "Production ready example module.",
    "example.customButton": "Deploy Item",
    "example.modalTitle": "Create New Item"
}
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:

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 ElementDefault SourceOptional Localization Key
Module Display Namemeta.displayName in main.js"name" or "module.<id>.name"
Module Descriptionmeta.description in main.js"description" or "module.<id>.desc"
Page Navigation Labelmeta.label in page.js"page.<module>.<page>.label"
Page Descriptionmeta.description in page.js"page.<module>.<page>.desc"
Records Column Headerconfig.columns[].label in page.js"page.<module>.<page>.column.<key>"
Records Empty Stateconfig.empty.title / hint in page.js"page.<module>.<page>.empty.title" / ".empty.hint"
Records Cell Valuestring value returned by the route"page.<module>.<page>.value.<column>.<value>"
Config Group Labelmeta.label in resources/config/*.js"schema.<module>.config.<group>.label"
Config Field Labelfield.label in schema()"schema.<module>.config.<group>.<field>.label"
Config Group Descriptionmeta.description in resources/config/*.js"schema.<module>.config.<group>.desc"
Config Select Optionoptions[].label in schema()"schema.<module>.config.<group>.<field>.option.<value>"
List Item Subfield Labelitem[].label in a "list" field"schema.<module>.config.<group>.<field>.<subfield>.label"
List Item Select Optionitem[].options[].label"schema.<module>.config.<group>.<field>.<subfield>.option.<value>"
Logging Channel Labelmeta.label in resources/logs/*.js"schema.<module>.logs.channels.<category>.label"
Logging Event Labelevent.label in events()"schema.<module>.logs.events.<event>.label"
Command Descriptioncommand.getDescription()"commands.<command>.description"
Subcommand Descriptionoption.description in options"commands.<command><Subcommand>.description"
Option Descriptionoption.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.

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"
}
KeyReplaces
<event>.titleThe event's title in events()
<event>.summaryThe 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:

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