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

Discord Bot Çoklu Dil Desteği: Sunucuya Göre i18n

Botun büyüyüp farklı ülkelerden sunuculara katıldığında discord bot çoklu dil desteği bir lükse değil zorunluluğa dönüşür. Türk bir topluluk Türkçe yanıt beklerken Fransız bir sunucu her şeyi Fransızca görmek ister. Çözüm, her sunucunun (guild) kendi dilini seçebildiği ve botun tüm metinlerini tek bir merkezden geçiren bir i18n (internationalization) yapısı kurmaktır. Bu yazıda discord.js v14 ile çevirileri JSON dosyalarında tutan, sunucuya göre dili değiştiren ve hiçbir metni koda gömmeyen temiz bir mimari kuracağız.

Neden i18n yapısı? Metni koda gömmenin bedeli

Çoğu bot küçük başlar ve yanıtlar doğrudan koda yazılır: interaction.reply('Yetkin yok'). Bu yaklaşım ikinci dil eklediğin anda çöker. Tek bir mesajı değiştirmek için kodun her yerini taramak, aynı metni onlarca dosyada tekrar etmek ve çeviri tutarsızlıkları kaçınılmaz olur. Sağlam bir i18n yapısı şu sorunları kökten çözer:

  • Tek doğruluk kaynağı: Her metin, dil dosyalarında bir anahtarla yaşar; kodda sadece anahtarı çağırırsın.
  • Sunucu bazlı dil: Her guild kendi dilini seçer, seçim veritabanında saklanır.
  • Kolay genişleme: Yeni bir dil eklemek, yeni bir JSON dosyası oluşturmaktan ibarettir — koda dokunmazsın.
  • Çevirmen dostu: Kod bilmeyen biri bile JSON dosyasını düzenleyip çeviri ekleyebilir.

Çeviri dosyalarının yapısı

Her dil için ayrı bir JSON dosyası tutmak en sade ve okunabilir yöntemdir. Anahtarları konuya göre iç içe (nested) gruplamak, dosya büyüdükçe düzeni korur:

locales/
├─ tr.json
├─ en.json
├─ fr.json
└─ de.json
// locales/tr.json
{
  "common": {
    "no_permission": "Bu komutu kullanma yetkin yok.",
    "error": "Bir hata oluştu, lütfen tekrar dene."
  },
  "ping": {
    "reply": "Pong! Gecikme: {ms}ms"
  },
  "ban": {
    "success": "{user} sunucudan yasaklandı."
  }
}
// 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."
  }
}

Dikkat edersen metinlerde {ms} ve {user} gibi yer tutucular var. Bunlar çalışma anında gerçek değerlerle değiştirilecek — böylece dinamik veriyi de çeviriye gömmeden yönetebiliriz.

Dilleri yükleyen i18n çekirdeği

Bot açılırken locales/ klasöründeki tüm JSON dosyalarını okuyup bellekte bir nesnede toplarız. Asıl iş, bir anahtarı ve dili alıp doğru metni döndüren t() fonksiyonundadır. Bu fonksiyon noktalı anahtarı (ping.reply) çözer, yer tutucuları doldurur ve istenen dil yoksa varsayılana düşer:

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

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

// Tüm dil dosyalarını belleğe yükle
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'));
}

// "ping.reply" gibi noktalı anahtarı nesnede dolaş
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; // hiçbir yerde yoksa anahtarın kendisini göster

  // {ms} gibi yer tutucuları değerlerle değiştir
  return text.replace(/\{(\w+)\}/g, (_, name) =>
    name in vars ? vars[name] : `{${name}}`
  );
}

module.exports = { t, locales };

Buradaki üçlü yedekleme önemlidir: önce istenen dil, sonra varsayılan dil, en kötü ihtimalle anahtarın kendisi döner. Böylece bir çeviri eksik olsa bile bot çökmez, sadece o satır İngilizce ya da anahtar adıyla görünür.

Sunucunun dilini saklamak

Her guild kendi dilini seçebilmeli ve bu seçim kalıcı olmalı. Pratikte guild kimliğini dil koduna eşleyen küçük bir tablo yeterlidir. SQLite bu iş için fazlasıyla uygun; aşağıda fikri sade tutmak için bellek üstü bir Map ile gösteriyorum, ama üretimde bunu veritabanına yazarsın:

// guildSettings.js — üretimde SQLite/Mongo ile değiştir
const guildLocales = new Map();

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

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

module.exports = { getGuildLocale, setGuildLocale };

Discord'un kendi interaction.guildLocale alanı sunucunun Discord arayüz dilini verir; bunu bir varsayılan tahmin olarak kullanabilirsin, ama nihai kararı yöneticiye bırakmak çok daha esnektir.

Komutlarda kullanım: her yanıt t()'den geçer

Artık tüm parçalar yerinde. Bir komut çalıştığında önce o sunucunun dilini buluyor, sonra her metni t() üzerinden üretiyoruz. Kodda tek bir Türkçe ya da İngilizce cümle kalmıyor:

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

module.exports = {
  data: new SlashCommandBuilder()
    .setName('ping')
    .setDescription('Botun gecikmesini ölçer'),

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

Dili değiştiren /language komutu

Son adım, yöneticilerin dili seçebileceği bir komut. Seçenekleri addStringOption ile sunup yalnızca yetkili kişilerin değiştirebilmesi için setDefaultMemberPermissions kullanırız:

// 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('Sunucunun bot dilini ayarlar')
    .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
    .addStringOption(opt =>
      opt.setName('locale')
        .setDescription('Dil')
        .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,
    });
  },
};

Onay mesajını t(locale, ...) ile yeni seçilen dilde göndermek hoş bir dokunuştur: kullanıcı değişikliğin işe yaradığını anında o dilde görür.

Sık Sorulan Sorular

Çevirileri JSON yerine veritabanında tutsam daha mı iyi olur?

İkisinin de yeri var. JSON dosyaları sürüm kontrolüyle (Git) izlenebildiği, kolay düzenlendiği ve dağıtımda ek altyapı gerektirmediği için statik metinler için idealdir. Çevirileri kod bilmeyen kişilerin bir panelden düzenlemesini istiyorsan veritabanı daha uygundur. Yaygın yaklaşım, statik UI metinlerini JSON'da, kullanıcıların ürettiği içeriği veritabanında tutmaktır.

Yer tutucular (placeholder) neden önemli?

Diller cümle yapısını farklı kurar; kelime sırası değişebilir. "{user} yasaklandı" yerine metni parçalara bölüp birleştirirsen bir dilde doğru olan sıra başka dilde bozulur. Tam cümleyi yer tutucularla tek bir anahtarda tutmak, her dilin kelime sırasını kendi içinde doğru kurmasına izin verir.

Çoğul kuralları (1 üye / 5 üye) için ne yapmalıyım?

Basit projeler için ayrı _one ve _other anahtarları yeterlidir. Çoğul kuralları dile göre karmaşıklaşan ciddi projelerde ise i18next gibi olgun bir kütüphane bu mantığı hazır sunar ve kendi çözümünü yazmaktan kurtarır.

Botun küresel bir topluluğa mı hitap ediyor? Sunucuya göre dil değiştiren, çevirileri temiz tutan ve büyümeye hazır bir i18n mimarisini birlikte kurabiliriz. Benimle iletişime geç, botunu her dilde akıcı konuşan bir asistana dönüştürelim.

Bu kategorideki tüm yazılar →

Devamı için