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

Bot Discord multilingue : un i18n par serveur

Dès que ton bot grandit et rejoint des serveurs de différents pays, le support bot Discord multilingue cesse d'être un bonus pour devenir une nécessité. Une communauté turque attend des réponses en turc, tandis qu'un serveur français veut tout voir en français. La solution est une architecture i18n (internationalisation) où chaque serveur (guild) choisit sa propre langue et où chaque texte du bot passe par une source unique. Dans ce guide, nous allons construire avec discord.js v14 une architecture propre qui conserve les traductions dans des fichiers JSON, change la langue par serveur et ne code jamais une seule chaîne en dur.

Pourquoi une architecture i18n ? Le coût du texte codé en dur

La plupart des bots commencent petits et les réponses sont écrites directement dans le code : interaction.reply('Tu n\'as pas la permission'). Cette approche s'effondre dès que tu ajoutes une deuxième langue. Modifier un seul message implique de parcourir tout le code, de répéter le même texte dans des dizaines de fichiers, et les incohérences de traduction deviennent inévitables. Une architecture i18n solide résout ces problèmes à la racine :

  • Source unique de vérité : chaque chaîne vit sous une clé dans les fichiers de langue ; le code n'appelle que la clé.
  • Langue par serveur : chaque guild choisit sa langue et le choix est stocké dans une base de données.
  • Facile à étendre : ajouter une langue revient à créer un nouveau fichier JSON — tu ne touches pas au code.
  • Adapté aux traducteurs : même une personne qui ne sait pas coder peut éditer le fichier JSON et ajouter une traduction.

La structure des fichiers de traduction

Conserver un fichier JSON distinct par langue est l'approche la plus simple et la plus lisible. Imbriquer les clés par thème préserve l'ordre à mesure que le fichier grandit :

locales/
├─ tr.json
├─ en.json
├─ fr.json
└─ de.json
// locales/fr.json
{
  "common": {
    "no_permission": "Tu n'as pas la permission d'utiliser cette commande.",
    "error": "Une erreur est survenue, réessaie."
  },
  "ping": {
    "reply": "Pong ! Latence : {ms}ms"
  },
  "ban": {
    "success": "{user} a été banni du serveur."
  }
}
// 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."
  }
}

Remarque les marqueurs comme {ms} et {user} dans le texte. Ils sont remplacés par de vraies valeurs à l'exécution — ainsi on gère les données dynamiques sans les figer dans la traduction.

Le cœur i18n qui charge les langues

Au démarrage du bot, on lit chaque fichier JSON de locales/ et on les rassemble dans un objet en mémoire. Le vrai travail réside dans une fonction t() qui prend une clé et une langue et renvoie le bon texte. Elle résout la clé pointée (ping.reply), remplit les marqueurs et bascule vers la langue par défaut si la langue demandée est absente :

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

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

// Charge chaque fichier de langue en mémoire
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'));
}

// Parcourt une clé pointée comme "ping.reply" dans l'objet
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; // introuvable partout : affiche la clé elle-même

  // remplace les marqueurs comme {ms} par les valeurs
  return text.replace(/\{(\w+)\}/g, (_, name) =>
    name in vars ? vars[name] : `{${name}}`
  );
}

module.exports = { t, locales };

Le triple repli ici est important : d'abord la langue demandée, puis la langue par défaut, et dans le pire des cas la clé elle-même. Ainsi, même si une traduction manque, le bot ne plante pas — cette ligne s'affiche simplement en anglais ou sous forme de clé.

Stocker la langue du serveur

Chaque guild doit pouvoir choisir sa langue, et ce choix doit persister. En pratique, une petite table associant l'ID du guild à un code de langue suffit. SQLite convient largement ; ci-dessous je garde l'idée simple avec un Map en mémoire, mais en production tu l'écrirais dans une base de données :

// guildSettings.js — à remplacer par SQLite/Mongo en production
const guildLocales = new Map();

function getGuildLocale(guildId) {
  return guildLocales.get(guildId) || 'en';
}

function setGuildLocale(guildId, locale) {
  guildLocales.set(guildId, locale);
}

module.exports = { getGuildLocale, setGuildLocale };

Le champ interaction.guildLocale de Discord donne la langue d'interface du serveur ; tu peux l'utiliser comme estimation par défaut, mais laisser la décision finale à un administrateur est bien plus flexible.

Usage dans les commandes : chaque réponse passe par t()

Toutes les pièces sont en place. Quand une commande s'exécute, on cherche d'abord la langue de ce serveur, puis on produit chaque texte via t(). Plus une seule phrase turque ou anglaise ne reste dans le code :

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

module.exports = {
  data: new SlashCommandBuilder()
    .setName('ping')
    .setDescription('Mesure la latence du bot'),

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

La commande /language qui change la langue

Dernière étape : une commande qui permet aux administrateurs de choisir la langue. On propose les options avec addStringOption et on utilise setDefaultMemberPermissions pour que seules les personnes autorisées puissent la modifier :

// 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('Définit la langue du bot pour le serveur')
    .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
    .addStringOption(opt =>
      opt.setName('locale')
        .setDescription('Langue')
        .setRequired(true)
        .addChoices(
          { name: 'Türkçe', value: 'tr' },
          { name: 'English', value: 'en' },
          { name: 'Français', value: 'fr' },
          { 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,
    });
  },
};

Envoyer la confirmation avec t(locale, ...) dans la langue tout juste choisie est une attention agréable : l'utilisateur voit immédiatement que le changement a fonctionné, dans cette langue.

Questions fréquentes

Vaut-il mieux conserver les traductions en base de données plutôt qu'en JSON ?

Les deux ont leur place. Les fichiers JSON sont idéaux pour le texte statique car ils sont suivis par le contrôle de version (Git), faciles à éditer et ne nécessitent aucune infrastructure supplémentaire au déploiement. Si tu veux que des non-développeurs éditent les traductions depuis un panneau, une base de données convient mieux. Une approche courante consiste à garder le texte d'interface statique en JSON et le contenu généré par les utilisateurs en base de données.

Pourquoi les marqueurs sont-ils importants ?

Les langues construisent les phrases différemment ; l'ordre des mots change. Si tu découpes le texte en fragments et les concatènes au lieu d'utiliser "{user} a été banni", un ordre correct dans une langue se casse dans une autre. Garder la phrase complète avec des marqueurs sous une seule clé permet à chaque langue de construire son propre ordre des mots correctement.

Que faire pour les règles de pluriel (1 membre / 5 membres) ?

Pour les projets simples, des clés distinctes _one et _other suffisent. Pour des projets sérieux où les règles de pluriel se complexifient selon la langue, une bibliothèque mature comme i18next gère cette logique nativement et t'évite d'écrire la tienne.

Ton bot s'adresse-t-il à une communauté mondiale ? On peut bâtir ensemble une architecture i18n qui change la langue par serveur, garde les traductions propres et est prête à grandir. Contacte-moi et transformons ton bot en un assistant qui parle couramment dans chaque langue.

Bu kategorideki tüm yazılar →

Devamı için