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.
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
- yarn
- pnpm
npm install @sapphire/plugin-i18next @sapphire/framework discord.js@14.x
yarn add @sapphire/plugin-i18next @sapphire/framework discord.js@14.x
pnpm add @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 thecontainer
- CommonJS
- ESM
- TypeScript
// Main bot file
// Be sure to register the plugin before instantiating the client.
require('@sapphire/plugin-i18next/register');
// Main bot file
// Be sure to register the plugin before instantiating the client.
import '@sapphire/plugin-i18next/register';
// Main bot file
// Be sure to register the plugin before instantiating the client.
import '@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.
- CommonJS
- ESM
- TypeScript
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');
import { SapphireClient } from '@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');
import { SapphireClient } from '@sapphire/framework';
import type { InternationalizationContext } from '@sapphire/plugin-i18next';
const client = new SapphireClient({
intents: ['GUILDS', 'GUILD_MESSAGES'],
i18n: {
fetchLanguage: async (context: InternationalizationContext) => {
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');
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.
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.
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 aninteraction
which provides the context tofetchLanguage
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.
- CommonJS
- ESM
- TypeScript
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
};
import { resolveKey } from '@sapphire/plugin-i18next';
import { Command } from '@sapphire/framework';
export 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'));
}
}
import { resolveKey } from '@sapphire/plugin-i18next';
import { Command } from '@sapphire/framework';
export class PingCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
description: 'ping pong'
});
}
public override async messageRun(message: Message) {
await message.channel.send(await resolveKey(message, 'ping:success'));
}
public override async chatInputRun(interaction: Command.ChatInputInteraction) {
await interaction.reply(await resolveKey(interaction, 'ping:success'));
}
public override async contextMenuRun(interaction: Command.ContextMenuCommandInteraction) {
await interaction.reply(await resolveKey(interaction, 'ping:success'));
}
}
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.
- CommonJS
- ESM
- TypeScript
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
};
import { fetchLanguage } from '@sapphire/plugin-i18next';
import { Command } from '@sapphire/framework';
export 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
}
}
import { fetchLanguage } from '@sapphire/plugin-i18next';
import { Command } from '@sapphire/framework';
export class PingCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
description: 'ping pong'
});
}
public override async messageRun(message: Message) {
const languageCodeForCurrentGuild = await fetchLanguage(message);
// ===> en-US
}
public override async chatInputRun(interaction: Command.ChatInputInteraction) {
const languageCodeForCurrentGuild = await fetchLanguage(interaction);
// ===> en-US
}
public override async contextMenuRun(interaction: Command.ContextMenuCommandInteraction) {
const languageCodeForCurrentGuild = await fetchLanguage(interaction);
// ===> en-US
}
}
fetchT
fetchT
returns an i18next TFunction
with the language specific for the provided
context.
- CommonJS
- ESM
- TypeScript
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
};
import { Command } from '@sapphire/framework';
export 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');
}
}
import { fetchLanguage } from '@sapphire/plugin-i18next';
import { Command } from '@sapphire/framework';
export class PingCommand extends Command {
public constructor(context: Command.LoaderContext, options: Command.Options) {
super(context, {
...options,
description: 'ping pong'
});
}
public override async messageRun(message: Message) {
const tFunction = await fetchT(message);
const translatedPingSuccess = tFunction('ping:success');
}
public override async chatInputRun(interaction: Command.ChatInputInteraction) {
const tFunction = await fetchT(interaction);
const translatedPingSuccess = tFunction('ping:success');
}
public override async contextMenuRun(interaction: Command.ContextMenuCommandInteraction) {
const tFunction = await fetchT(interaction);
const translatedPingSuccess = tFunction('ping:success');
}
}