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:
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.
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 basethis.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:
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:
{
"name": "Example",
"description": "Production ready example module.",
"example.customButton": "Deploy Item",
"example.modalTitle": "Create New Item"
}{
"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:
{
"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.
{
"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:
await this.medusa.logs.post("example", "itemCreated", {
guildId,
tokens: { owner: member.id, item: item.name },
fields: [{ name: "Owner", value: `<@${member.id}>` }]
});