aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord-bots

Discord-bot meertalige ondersteuning: i18n per server

Zodra je bot groeit en servers uit verschillende landen toetreedt, wordt Discord-bot meertalige ondersteuning geen luxe meer maar een vereiste. Een Turkse community verwacht antwoorden in het Turks, terwijl een Franse server alles in het Frans wil zien. Het antwoord is een i18n (internationalisatie) opzet waarbij elke server (guild) zijn eigen taal kan kiezen en elke tekst van de bot via één bron loopt. In deze gids bouwen we met discord.js v14 een nette architectuur die vertalingen in JSON-bestanden bewaart, de taal per server wisselt en geen enkele string hardcodeert.

Waarom een i18n-opzet? De prijs van hardcoded tekst

De meeste bots beginnen klein en antwoorden worden direct in de code geschreven: interaction.reply('Je hebt geen rechten'). Die aanpak stort in zodra je een tweede taal toevoegt. Eén bericht aanpassen betekent de hele codebase doorzoeken, dezelfde tekst in tientallen bestanden herhalen, en vertaalverschillen worden onvermijdelijk. Een solide i18n-opzet lost deze problemen bij de wortel op:

  • Eén bron van waarheid: elke string leeft onder een sleutel in de taalbestanden; de code roept alleen de sleutel aan.
  • Taal per server: elke guild kiest zijn eigen taal en de keuze wordt in een database bewaard.
  • Eenvoudig uit te breiden: een nieuwe taal toevoegen is simpelweg een nieuw JSON-bestand maken — je raakt de code niet aan.
  • Vertalersvriendelijk: zelfs iemand die geen code kan lezen, kan het JSON-bestand bewerken en een vertaling toevoegen.

De structuur van de vertaalbestanden

Een apart JSON-bestand per taal bewaren is de eenvoudigste en best leesbare aanpak. Sleutels per onderwerp nesten houdt de orde bewaard naarmate het bestand groeit:

locales/
├─ tr.json
├─ en.json
├─ nl.json
└─ de.json
// locales/nl.json
{
  "common": {
    "no_permission": "Je hebt geen rechten om dit commando te gebruiken.",
    "error": "Er ging iets mis, probeer het opnieuw."
  },
  "ping": {
    "reply": "Pong! Latentie: {ms}ms"
  },
  "ban": {
    "success": "{user} is van de server verbannen."
  }
}
// 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."
  }
}

Merk de placeholders op zoals {ms} en {user} in de tekst. Die worden tijdens runtime vervangen door echte waarden — zo beheren we dynamische data zonder die in de vertaling te bakken.

De i18n-kern die de talen laadt

Bij het opstarten van de bot lezen we elk JSON-bestand in locales/ en verzamelen ze in een object in het geheugen. Het echte werk zit in een t()-functie die een sleutel en een taal neemt en de juiste tekst teruggeeft. Ze lost de sleutel met punten op (ping.reply), vult de placeholders in en valt terug op de standaard als de gevraagde taal ontbreekt:

// i18n.js
const fs = require('node:fs');
const path = require('node:path');

const DEFAULT_LOCALE = 'en';
const locales = {};

// Laad elk taalbestand in het geheugen
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'));
}

// Loop een sleutel met punten zoals "ping.reply" door het object
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; // nergens te vinden: toon de sleutel zelf

  // vervang placeholders zoals {ms} door waarden
  return text.replace(/\{(\w+)\}/g, (_, name) =>
    name in vars ? vars[name] : `{${name}}`
  );
}

module.exports = { t, locales };

De drievoudige terugval hier is belangrijk: eerst de gevraagde taal, dan de standaardtaal, en in het slechtste geval de sleutel zelf. Zo crasht de bot niet als een vertaling ontbreekt — die regel verschijnt simpelweg in het Engels of als sleutelnaam.

De taal van de server opslaan

Elke guild moet zijn eigen taal kunnen kiezen, en die keuze moet blijven bestaan. In de praktijk volstaat een kleine tabel die het guild-ID koppelt aan een taalcode. SQLite is hiervoor ruim geschikt; hieronder houd ik het idee simpel met een Map in het geheugen, maar in productie zou je dit naar een database schrijven:

// guildSettings.js — vervang door SQLite/Mongo in productie
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 eigen veld interaction.guildLocale geeft de Discord-interfacetaal van de server; je kunt dat als standaardgok gebruiken, maar de uiteindelijke beslissing aan een beheerder laten is veel flexibeler.

Gebruik in commando's: elk antwoord gaat via t()

Alle stukken liggen nu op hun plaats. Wanneer een commando draait, zoeken we eerst de taal van die server op en produceren dan elke tekst via t(). Er blijft geen enkele Turkse of Engelse zin in de code achter:

// commands/ping.js
const { getGuildLocale } = require('../guildSettings');
const { t } = require('../i18n');

module.exports = {
  data: new SlashCommandBuilder()
    .setName('ping')
    .setDescription('Meet de latentie van de bot'),

  async execute(interaction) {
    const locale = getGuildLocale(interaction.guildId);
    const ms = interaction.client.ws.ping;
    await interaction.reply(t(locale, 'ping.reply', { ms }));
  },
};

Het /language-commando dat de taal wisselt

De laatste stap is een commando waarmee beheerders de taal kunnen kiezen. We bieden de opties aan met addStringOption en gebruiken setDefaultMemberPermissions zodat alleen bevoegde personen de taal kunnen wijzigen:

// 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('Stelt de bottaal van de server in')
    .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
    .addStringOption(opt =>
      opt.setName('locale')
        .setDescription('Taal')
        .setRequired(true)
        .addChoices(
          { name: 'Türkçe', value: 'tr' },
          { name: 'English', value: 'en' },
          { name: 'Nederlands', value: 'nl' },
          { name: 'Deutsch', value: 'de' },
        )),

  async execute(interaction) {
    const locale = interaction.options.getString('locale');
    setGuildLocale(interaction.guildId, locale);
    await interaction.reply({
      content: t(locale, 'language.changed'),
      ephemeral: true,
    });
  },
};

De bevestiging met t(locale, ...) in de zojuist gekozen taal sturen is een mooie toevoeging: de gebruiker ziet meteen dat de wijziging werkte, in die taal.

Veelgestelde vragen

Is het beter om vertalingen in een database te bewaren in plaats van JSON?

Beide hebben hun plek. JSON-bestanden zijn ideaal voor statische tekst omdat ze in versiebeheer (Git) worden gevolgd, makkelijk te bewerken zijn en geen extra infrastructuur vereisen bij het deployen. Wil je dat niet-programmeurs vertalingen vanuit een paneel bewerken, dan past een database beter. Een gangbare aanpak is statische UI-tekst in JSON te houden en door gebruikers gegenereerde content in een database.

Waarom zijn placeholders belangrijk?

Talen bouwen zinnen anders op; de woordvolgorde verandert. Als je tekst in fragmenten splitst en aan elkaar plakt in plaats van "{user} is verbannen", breekt een volgorde die in de ene taal klopt in een andere. De volledige zin met placeholders onder één sleutel houden laat elke taal zijn eigen woordvolgorde correct opbouwen.

Wat moet ik doen met meervoudsregels (1 lid / 5 leden)?

Voor eenvoudige projecten volstaan aparte sleutels _one en _other. Voor serieuze projecten waar meervoudsregels per taal complex worden, regelt een volwassen bibliotheek zoals i18next deze logica kant-en-klaar en bespaart je het schrijven van je eigen oplossing.

Bedient je bot een wereldwijde community? We kunnen samen een i18n-architectuur bouwen die de taal per server wisselt, vertalingen netjes houdt en klaar is om te groeien. Neem contact met me op en laten we je bot veranderen in een assistent die in elke taal vloeiend spreekt.

Bu kategorideki tüm yazılar →

Devamı için