Sobald dein Bot wächst und Servern aus verschiedenen Ländern beitritt, wird die Discord-Bot mehrsprachige Unterstützung vom netten Extra zur Notwendigkeit. Eine türkische Community erwartet Antworten auf Türkisch, während ein französischer Server alles auf Französisch sehen will. Die Lösung ist ein i18n-Aufbau (Internationalisierung), bei dem jeder Server (Guild) seine eigene Sprache wählen kann und jeder Text des Bots durch eine einzige Quelle läuft. In diesem Leitfaden bauen wir mit discord.js v14 eine saubere Architektur, die Übersetzungen in JSON-Dateien hält, die Sprache pro Server wechselt und keine einzige Zeichenkette fest verdrahtet.
Warum ein i18n-Aufbau? Die Kosten fest verdrahteter Texte
Die meisten Bots fangen klein an und Antworten werden direkt in den Code geschrieben: interaction.reply('Du hast keine Berechtigung'). Dieser Ansatz bricht zusammen, sobald du eine zweite Sprache hinzufügst. Eine einzige Nachricht zu ändern bedeutet, die gesamte Codebasis zu durchsuchen, denselben Text in Dutzenden Dateien zu wiederholen, und Übersetzungsabweichungen werden unvermeidbar. Ein solider i18n-Aufbau löst diese Probleme an der Wurzel:
- Eine einzige Wahrheitsquelle: jede Zeichenkette lebt unter einem Schlüssel in den Sprachdateien; der Code ruft nur den Schlüssel auf.
- Sprache pro Server: jede Guild wählt ihre eigene Sprache, und die Wahl wird in einer Datenbank gespeichert.
- Leicht erweiterbar: eine neue Sprache hinzuzufügen heißt nur, eine neue JSON-Datei anzulegen — du fasst den Code nicht an.
- Übersetzerfreundlich: selbst jemand, der keinen Code lesen kann, kann die JSON-Datei bearbeiten und eine Übersetzung ergänzen.
Der Aufbau der Übersetzungsdateien
Eine separate JSON-Datei pro Sprache zu führen ist der einfachste und lesbarste Ansatz. Schlüssel nach Thema zu verschachteln bewahrt die Ordnung, wenn die Datei wächst:
locales/
├─ tr.json
├─ en.json
├─ de.json
└─ fr.json
// locales/de.json
{
"common": {
"no_permission": "Du hast keine Berechtigung, diesen Befehl zu nutzen.",
"error": "Etwas ist schiefgelaufen, bitte versuche es erneut."
},
"ping": {
"reply": "Pong! Latenz: {ms}ms"
},
"ban": {
"success": "{user} wurde vom Server gebannt."
}
}
// locales/en.json
{
"common": {
"no_permission": "You don't have permission to use this command.",
"error": "Something went wrong, please try again."
},
"ping": {
"reply": "Pong! Latency: {ms}ms"
},
"ban": {
"success": "{user} has been banned from the server."
}
}
Beachte die Platzhalter wie {ms} und {user} im Text. Sie werden zur Laufzeit durch echte Werte ersetzt — so verwalten wir dynamische Daten, ohne sie in die Übersetzung einzubacken.
Der i18n-Kern, der die Sprachen lädt
Beim Start des Bots lesen wir jede JSON-Datei in locales/ und sammeln sie in einem Objekt im Speicher. Die eigentliche Arbeit steckt in einer t()-Funktion, die einen Schlüssel und eine Sprache nimmt und den richtigen Text zurückgibt. Sie löst den Punktschlüssel (ping.reply) auf, füllt die Platzhalter und greift auf die Standardsprache zurück, wenn die angeforderte fehlt:
// i18n.js
const fs = require('node:fs');
const path = require('node:path');
const DEFAULT_LOCALE = 'en';
const locales = {};
// Lade jede Sprachdatei in den Speicher
const dir = path.join(__dirname, 'locales');
for (const file of fs.readdirSync(dir)) {
if (!file.endsWith('.json')) continue;
const code = file.replace('.json', '');
locales[code] = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
}
// Laufe einen Punktschlüssel wie "ping.reply" durch das Objekt
function resolve(obj, key) {
return key.split('.').reduce((acc, part) => acc?.[part], obj);
}
function t(locale, key, vars = {}) {
const lang = locales[locale] ? locale : DEFAULT_LOCALE;
let text = resolve(locales[lang], key)
?? resolve(locales[DEFAULT_LOCALE], key)
?? key; // nirgends gefunden: zeige den Schlüssel selbst
// ersetze Platzhalter wie {ms} durch Werte
return text.replace(/\{(\w+)\}/g, (_, name) =>
name in vars ? vars[name] : `{${name}}`
);
}
module.exports = { t, locales };
Die dreifache Rückfallebene hier ist wichtig: zuerst die angeforderte Sprache, dann die Standardsprache und im schlimmsten Fall der Schlüssel selbst. So stürzt der Bot nicht ab, wenn eine Übersetzung fehlt — diese Zeile erscheint einfach auf Englisch oder als Schlüsselname.
Die Sprache des Servers speichern
Jede Guild sollte ihre eigene Sprache wählen können, und diese Wahl muss bestehen bleiben. In der Praxis genügt eine kleine Tabelle, die die Guild-ID einem Sprachcode zuordnet. SQLite ist dafür mehr als geeignet; unten halte ich die Idee mit einer Map im Speicher einfach, aber in der Produktion würdest du dies in eine Datenbank schreiben:
// guildSettings.js — in der Produktion durch SQLite/Mongo ersetzen
const guildLocales = new Map();
function getGuildLocale(guildId) {
return guildLocales.get(guildId) || 'en';
}
function setGuildLocale(guildId, locale) {
guildLocales.set(guildId, locale);
}
module.exports = { getGuildLocale, setGuildLocale };
Discords eigenes Feld interaction.guildLocale liefert die Discord-Oberflächensprache des Servers; du kannst sie als Standardvermutung verwenden, aber die endgültige Entscheidung einem Admin zu überlassen ist weitaus flexibler.
Einsatz in Befehlen: jede Antwort läuft durch t()
Alle Teile sind nun an ihrem Platz. Wenn ein Befehl läuft, ermitteln wir zuerst die Sprache dieses Servers und erzeugen dann jeden Text über t(). Im Code bleibt kein einziger türkischer oder englischer Satz übrig:
// commands/ping.js
const { getGuildLocale } = require('../guildSettings');
const { t } = require('../i18n');
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Misst die Latenz des Bots'),
async execute(interaction) {
const locale = getGuildLocale(interaction.guildId);
const ms = interaction.client.ws.ping;
await interaction.reply(t(locale, 'ping.reply', { ms }));
},
};
Der /language-Befehl, der die Sprache wechselt
Der letzte Schritt ist ein Befehl, mit dem Admins die Sprache wählen können. Wir bieten die Optionen mit addStringOption an und verwenden setDefaultMemberPermissions, damit nur Berechtigte sie ändern können:
// commands/language.js
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const { setGuildLocale } = require('../guildSettings');
const { t } = require('../i18n');
module.exports = {
data: new SlashCommandBuilder()
.setName('language')
.setDescription('Legt die Bot-Sprache des Servers fest')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addStringOption(opt =>
opt.setName('locale')
.setDescription('Sprache')
.setRequired(true)
.addChoices(
{ name: 'Türkçe', value: 'tr' },
{ name: 'English', value: 'en' },
{ name: 'Deutsch', value: 'de' },
{ name: 'Français', value: 'fr' },
)),
async execute(interaction) {
const locale = interaction.options.getString('locale');
setGuildLocale(interaction.guildId, locale);
await interaction.reply({
content: t(locale, 'language.changed'),
ephemeral: true,
});
},
};
Die Bestätigung mit t(locale, ...) in der gerade gewählten Sprache zu senden ist ein schöner Zug: der Nutzer sieht sofort, dass die Änderung funktioniert hat — in dieser Sprache.
Häufige Fragen
Ist es besser, Übersetzungen in einer Datenbank statt in JSON zu halten?
Beides hat seinen Platz. JSON-Dateien sind ideal für statische Texte, weil sie in der Versionskontrolle (Git) verfolgt werden, leicht zu bearbeiten sind und beim Deployen keine zusätzliche Infrastruktur benötigen. Wenn Nicht-Programmierer Übersetzungen über ein Panel bearbeiten sollen, passt eine Datenbank besser. Ein verbreiteter Ansatz ist, statische UI-Texte in JSON und von Nutzern erzeugte Inhalte in einer Datenbank zu halten.
Warum sind Platzhalter wichtig?
Sprachen bauen Sätze unterschiedlich auf; die Wortstellung ändert sich. Wenn du Text in Fragmente zerlegst und aneinanderhängst, statt "{user} wurde gebannt" zu nutzen, bricht eine in einer Sprache korrekte Reihenfolge in einer anderen. Den ganzen Satz mit Platzhaltern unter einem Schlüssel zu halten lässt jede Sprache ihre eigene Wortstellung korrekt aufbauen.
Was tue ich bei Pluralregeln (1 Mitglied / 5 Mitglieder)?
Für einfache Projekte genügen getrennte Schlüssel _one und _other. Für ernsthafte Projekte, in denen Pluralregeln je nach Sprache komplex werden, übernimmt eine ausgereifte Bibliothek wie i18next diese Logik fertig und erspart dir, eine eigene zu schreiben.
Bedient dein Bot eine globale Community? Wir können gemeinsam eine i18n-Architektur bauen, die die Sprache pro Server wechselt, Übersetzungen sauber hält und für Wachstum bereit ist. Kontaktiere mich und lass uns deinen Bot in einen Assistenten verwandeln, der in jeder Sprache fließend spricht.