Skip to main content

Getting Started

This plugin implements i18next, a powerful internationalization library. It provides a simple way to internationalize your bot's responses. It leverages a filesystem backend to load JSON files from a directory, and provides a simple way to get translated keys and have your languages also match your Discord data. By simply implementing fetchLanguage on the SapphireClient you can return a language key for a user, guild, interaction, or message and then the plugin will automatically get the proper internationalization data for you.

tip

All i18next methods and capabilities are available when using this plugin. This plugin is simply a tiny wrapper around the i18next API. Need to access the t function without using this plugin? You can just access it through i18next.t. Wondering how to handle formatting, interpolation, plurals and other i18next features? Yes, to everything.

In short: Everything that is possible with i18next is also possible with @sapphire/plugin-i18next

Installation

npm install @sapphire/plugin-i18next @sapphire/framework discord.js@14.x

Usage

First of all you will need to register the plugin. This will:

  • Register the TypeScript types for configuring the plugin
  • Load your language files when starting your bot
  • Add the i18n property to the container
// Main bot file
// Be sure to register the plugin before instantiating the client.
require('@sapphire/plugin-i18next/register');

Get the language set per server

Once you start to use i18next to offer your bot in different languages you are likely to also want each server to be able to configure their own language through some kind of configuration command while you store this data in a database. While this page will not cover those aspects, what we will cover here is how you can inform the i18next plugin about the language that should be used in the current context.

In order to achieve this, you will need to provide the options to the plugin. This is done through the i18n option on the ClientOptions, specifically the fetchLanguage property on that object. This method receives 1 parameter, which is an object that has guild, channel, user, interactionGuildLocale, and interactionLocale. Using these properties you can make a call to your database, and return the proper language key. Alternatively if you want some kind of fallback to a default language you can also specify that here.

const { SapphireClient } = require('@sapphire/framework');

const client = new SapphireClient({
intents: ['GUILDS', 'GUILD_MESSAGES'],
i18n: {
fetchLanguage: async (context) => {
if (context.interactionGuildLocale || context.interactionLocale) {
return context.interactionGuildLocale || context.interactionLocale;
}

if (!context.guild) {
return 'en-US';
}

// Example of querying your database. The exact syntax will depend on your ORM
const guildSettings = await db.find({ guild_id: context.guildId });
return guildSettings.language;
}
}
});

client.login('your-token-goes-here');
note

In this example we first check if fetchLanguage was called with an interaction. If so we return the language key that Discord gives us on the API about that interaction. This is either the server's configured primary language or a users own client language. If this is not an interaction we then check if the command was ran in a guild. If not we return the default language. If it was ran in a guild we then query our database for the guild's language and return that.

warning

Something important to keep in mind is that you have to make sure that the language exists in your language directory, otherwise an error will be thrown saying that the language obtained from the server is not valid.

Configuring languages

Every language has a name which is linked to the name of the folder that holds the language files. For example:

├── commands
├── listeners
└── languages
└── en-US
└── ping.json
└── misc.json
└── es-ES
└── ping.json
└── misc.json

In this case, we have the languages en-US and es-ES. Remember that the languages must be in the directory called languages (this can be customized but we do not advise doing so). Each of these folders should hold JSON files with key-value maps of language keys and their translated string values. Each folder can also have ONE (1) nested folder which is called a namespace in i18next. This namespace is used to separate the language keys into different categories.

danger

i18next does not support nesting namespaces in namespaces. You can have only 1 nested folder, not multiple.

Adding key-value pairs to a translation file

Now that we have created the folder structure and some basic language files lets populate one with key-value pairs. For example lets consider the file: languages/en-US/ping.json

We can fill this file with the key-value pairs:

{
"success": "Pong!",
"success_with_args": "Pong! Took me {{latency}}ms to reply"
}

And lets say we also have the language nl-NL. This means we should also have the file languages/nl-NL/ping.json, which will have the key-value pairs:

{
"success": "Pong!",
"success_with_args": "Pong! Het heeft {{latency}}ms geduurd om te reageren"
}

Using languages

resolveKey

The resolveKey function can be used anywhere to get translated text by its key. resolveKey takes 2-3 parameters.

  • The first parameter should always be a message or an interaction which provides the context to fetchLanguage to get the appropriate language key for i18next.
  • The second parameter is the key for the translated string you want to get, including the namespace if applicable.
  • The third parameter is any additional context to pass to i18next such as when leveraging interpolation, pluralization, etc.

Following is an example of a command that implements messageRun (for message based commands), chatInputRun (for chat input commands) and contextMenuRun (for context menu commands) to send the same translated message.

const { resolveKey } = require('@sapphire/plugin-i18next');
const { Command } = require('@sapphire/framework');

class PingCommand extends Command {
constructor(context, options) {
super(context, {
...options,
description: 'ping pong'
});
}

async messageRun(message) {
await message.channel.send(await resolveKey(message, 'ping:success'));
}

async chatInputRun(interaction) {
await interaction.reply(await resolveKey(interaction, 'ping:success'));
}

async contextMenuRun(interaction) {
await interaction.reply(await resolveKey(interaction, 'ping:success'));
}
}
module.exports = {
PingCommand
};
note

In this example we used ping:success as the key for the translation. This means that i18next will look for a file called ping.json in the languages folder, and if it finds that file look for a key called success. If you have followed this guide up to this point you should have this configured.

If you have instead opted for categorizing your JSON files in namespaces and you have for example followed a structure of languages/en-US/commands/ping.json then the key will be commands/ping:success.

fetchLanguage

fetchLanguage returns the language specific for the provided context.

const { fetchLanguage } = require('@sapphire/plugin-i18next');
const { Command } = require('@sapphire/framework');

class PingCommand extends Command {
constructor(context, options) {
super(context, {
...options,
description: 'ping pong'
});
}

async messageRun(message) {
const languageCodeForCurrentGuild = await fetchLanguage(message);
// ===> en-US
}

async chatInputRun(interaction) {
const languageCodeForCurrentGuild = await fetchLanguage(interaction);
// ===> en-US
}

async contextMenuRun(interaction) {
const languageCodeForCurrentGuild = await fetchLanguage(interaction);
// ===> en-US
}
}
module.exports = {
PingCommand
};

fetchT

fetchT returns an i18next TFunction with the language specific for the provided context.

const { Command } = require('@sapphire/framework');

class PingCommand extends Command {
constructor(context, options) {
super(context, {
...options,
description: 'ping pong'
});
}

async messageRun(message) {
const tFunction = await fetchT(message);
const translatedPingSuccess = tFunction('ping:success');
}

async chatInputRun(interaction) {
const tFunction = await fetchT(interaction);
const translatedPingSuccess = tFunction('ping:success');
}

async contextMenuRun(interaction) {
const tFunction = await fetchT(interaction);
const translatedPingSuccess = tFunction('ping:success');
}
}
module.exports = {
PingCommand
};