Skip to content

Supporting different languages

Seyfert has a i18n built-in feature that allows you to create language files and use them in your bot.

Updating seyfert config

Before starting this chapter we could update seyfert.config.mjs to tell seyfert where our languages file will be.

seyfert.config.mjs
// @ts-check
import {
const config: {
bot(data: RuntimeConfig): InternalRuntimeConfig;
http(data: RuntimeConfigHTTP): InternalRuntimeConfigHTTP;
}
config
} from "seyfert";
export default
const config: {
bot(data: RuntimeConfig): InternalRuntimeConfig;
http(data: RuntimeConfigHTTP): InternalRuntimeConfigHTTP;
}
config
.
function bot(data: RuntimeConfig): InternalRuntimeConfig

Configurations for the bot.

@paramdata - The runtime configuration data for gateway connections.

@returnsThe internal runtime configuration.

bot
({
token: string
token
:
var process: NodeJS.Process
process
.
NodeJS.Process.env: NodeJS.ProcessEnv

The process.env property returns an object containing the user environment. See environ(7).

An example of this object looks like:

{
TERM: 'xterm-256color',
SHELL: '/usr/local/bin/bash',
USER: 'maciej',
PATH: '~/.bin/:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin',
PWD: '/Users/maciej',
EDITOR: 'vim',
SHLVL: '1',
HOME: '/Users/maciej',
LOGNAME: 'maciej',
_: '/usr/local/bin/node'
}

It is possible to modify this object, but such modifications will not be reflected outside the Node.js process, or (unless explicitly requested) to other Worker threads. In other words, the following example would not work:

Terminal window
node -e 'process.env.foo = "bar"' && echo $foo

While the following will:

import { env } from 'node:process';
env.foo = 'bar';
console.log(env.foo);

Assigning a property on process.env will implicitly convert the value to a string. This behavior is deprecated. Future versions of Node.js may throw an error when the value is not a string, number, or boolean.

import { env } from 'node:process';
env.test = null;
console.log(env.test);
// => 'null'
env.test = undefined;
console.log(env.test);
// => 'undefined'

Use delete to delete a property from process.env.

import { env } from 'node:process';
env.TEST = 1;
delete env.TEST;
console.log(env.TEST);
// => undefined

On Windows operating systems, environment variables are case-insensitive.

import { env } from 'node:process';
env.TEST = 1;
console.log(env.test);
// => 1

Unless explicitly specified when creating a Worker instance, each Worker thread has its own copy of process.env, based on its parent thread's process.env, or whatever was specified as the env option to the Worker constructor. Changes to process.env will not be visible across Worker threads, and only the main thread can make changes that are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner unlike the main thread.

@sincev0.1.27

env
.
string | undefined
BOT_TOKEN
?? "",
intents?: number | IntentStrings | number[]
intents
: ["Guilds"],
locations: RCLocations
locations
: {
RCLocations.base: string
base
: "dist",
RCLocations.commands?: string
commands
: "commands",
RCLocations.events?: string
events
: "events",
RCLocations.langs?: string
langs
: "languages" // - src/languages will be our languages directory
}
});

Creating a language file

Each language file shall export by default an object containing the translations for the language.

languages/en.ts
export default {
hello: string
hello
: "Each key value pair will be the translation for the key",
foo: {
bar: string;
baz: () => string;
ping: ({ ping }: {
ping: number;
}) => string;
}
foo
: {
bar: string
bar
: "You may nest objects to create a more complex language file",
baz: () => string
baz
: () => `You may also use functions to pass variables to the translation and add some logic`,
ping: ({ ping }: {
ping: number;
}) => string
ping
: ({
ping: number
ping
}: {
ping: number
ping
: number }) => `The ping is ${
ping: number
ping
}`
},
qux: string
qux
: [
"You may also use arrays to create a list of translations",
"This is the second item in the list"
].
Array<string>.join(separator?: string): string

Adds all the elements of an array into a string, separated by the specified separator string.

@paramseparator A string used to separate one element of the array from the next in the resulting string. If omitted, the array elements are separated with a comma.

join
("\n")
}

You can create as many language files as you want, seyfert will load them and they will be available to use in your bot.

languages/es.ts
import type English from "./en";
export default {
hello: "Hola, mundo!",
foo: {
bar: "Puedes anidar objetos para crear un archivo de idioma más complejo",
baz: () => `Puedes usar funciones para pasar variables a la traducción y agregar lógica`,
ping: ({ ping }) => `El ping es ${ping}`
},
qux: [
"También puedes usar arrays para crear una lista de traducciones",
"Este es el segundo elemento de la lista"
].join("\n")
} satisfies typeof English; // This is a utility type to ensure that object is the same across languages

Next we must do some updates to the declare module on index file:

src/index.ts
import type English from './languages/en';
import { Client, type ParseClient, type ParseLocales } from "seyfert";
const client = new Client();
client.start();
declare module 'seyfert' {
interface UsingClient extends ParseClient<Client<true>> { }
// interface UsingClient extends ParseClient<HttpClient> { } // If you are using the rest api
interface DefaultLocale extends ParseLocales<typeof English> { }
}

After doing this, you can use the language in your commands, events, components, etc.

Using translations in your commands

Let’s see an example with our ping command by adding an option to respond in a specific language

src/commands/ping.ts
import {
class Command
Command
,
function Declare(declare: CommandDeclareOptions): <T extends {
new (...args: any[]): object;
}>(target: T) => {
new (...args: any[]): {
name: string;
nsfw: boolean | undefined;
props: ExtraProps | undefined;
contexts: InteractionContextType[];
integrationTypes: ApplicationIntegrationType[];
defaultMemberPermissions: bigint | undefined;
botPermissions: bigint | undefined;
description: string;
type: ApplicationCommandType;
guildId?: string[];
ignore?: IgnoreCommand;
aliases?: string[];
handler?: EntryPointCommandHandlerType;
};
} & T
Declare
,
function Options(options: (new () => SubCommand)[] | OptionsRecord): <T extends {
new (...args: any[]): object;
}>(target: T) => {
new (...args: any[]): {
options: SubCommand[] | CommandOption[];
};
} & T
Options
,
function createBooleanOption<R extends boolean, T extends SeyfertBooleanOption<R> = SeyfertBooleanOption<R>>(data: T): T & {
readonly type: ApplicationCommandOptionType.Boolean;
}
createBooleanOption
,
function createStringOption<R extends boolean, C extends SeyfertChoice<string>[] = SeyfertChoice<string>[], VC = never>(data: SeyfertStringOption<C, R, VC>): {
readonly type: ApplicationCommandOptionType.String;
readonly required?: R | undefined;
readonly choices?: C | undefined;
readonly value?: ValueCallback<ApplicationCommandOptionType.String, C, VC> | undefined;
readonly description: string;
readonly description_localizations?: APIApplicationCommandBasicOption["description_localizations"];
readonly name_localizations?: APIApplicationCommandBasicOption["name_localizations"];
readonly locales?: {
name?: FlatObjectKeys<DefaultLocale>;
description?: FlatObjectKeys<DefaultLocale>;
};
readonly autocomplete?: AutocompleteCallback;
readonly onAutocompleteError?: OnAutocompleteErrorCallback;
readonly min_length?: number;
readonly max_length?: number;
}
createStringOption
,
type
class CommandContext<T extends OptionsRecord = {}, M extends keyof RegisteredMiddlewares = never>
interface CommandContext<T extends OptionsRecord = {}, M extends keyof RegisteredMiddlewares = never>
CommandContext
} from 'seyfert';
import { MessageFlags } from 'seyfert/lib/types';
const
const options: {
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}
options
= {
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
}
hide
:
createBooleanOption<boolean, {
description: string;
}>(data: {
description: string;
}): {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
}
createBooleanOption
({
description: string
description
: "Hide command output",
}),
language: {
readonly type: ApplicationCommandOptionType.String;
readonly required?: boolean | undefined;
readonly choices?: {
...;
}[] | undefined;
... 8 more ...;
readonly max_length?: number;
}
language
:
createStringOption<boolean, {
name: string;
value: string;
}[], never>(data: SeyfertStringOption<{
name: string;
value: string;
}[], boolean, never>): {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
}
createStringOption
({
SeyfertBaseChoiceableOption<T extends keyof ReturnOptionsTypes, C = T extends ChoiceableTypes ? SeyfertChoice<ChoiceableValues[T]>[] : never, R = boolean, VC = never>.description: string
description
: "Language to respond in",
SeyfertBaseChoiceableOption<ApplicationCommandOptionType.String, { name: string; value: string; }[], boolean, never>.choices?: {
name: string;
value: string;
}[] | undefined
choices
: [
{
name: string
name
: "English",
value: string
value
: "en" },
{
name: string
name
: "Spanish",
value: string
value
: "es" }
]
})
}
@
function Declare(declare: CommandDeclareOptions): <T extends {
new (...args: any[]): object;
}>(target: T) => {
new (...args: any[]): {
name: string;
nsfw: boolean | undefined;
props: ExtraProps | undefined;
contexts: InteractionContextType[];
integrationTypes: ApplicationIntegrationType[];
defaultMemberPermissions: bigint | undefined;
botPermissions: bigint | undefined;
description: string;
type: ApplicationCommandType;
guildId?: string[];
ignore?: IgnoreCommand;
aliases?: string[];
handler?: EntryPointCommandHandlerType;
};
} & T
Declare
({
name: string
name
: 'ping',
description: string
description
: 'Show the ping with discord'
})
@
function Options(options: (new () => SubCommand)[] | OptionsRecord): <T extends {
new (...args: any[]): object;
}>(target: T) => {
new (...args: any[]): {
options: SubCommand[] | CommandOption[];
};
} & T
Options
(
const options: {
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}
options
)
export default class
class PingCommand
PingCommand
extends
class Command
Command
{
async
PingCommand.run(ctx: CommandContext<typeof options>): Promise<void>
run
(
ctx: CommandContext<{
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}, never>
ctx
:
class CommandContext<T extends OptionsRecord = {}, M extends keyof RegisteredMiddlewares = never>
interface CommandContext<T extends OptionsRecord = {}, M extends keyof RegisteredMiddlewares = never>
CommandContext
<typeof
const options: {
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}
options
>) {
const
const flags: MessageFlags.Ephemeral | undefined
flags
=
ctx: CommandContext<{
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}, never>
ctx
.
CommandContext<{ hide: { description: string; } & { readonly type: ApplicationCommandOptionType.Boolean; }; language: { readonly type: ApplicationCommandOptionType.String; ... 10 more ...; readonly max_length?: number; }; }, never>.options: ContextOptions<{
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}>
options
.
hide?: boolean | undefined
hide
? MessageFlags.
function (enum member) MessageFlags.Ephemeral = 64

This message is only visible to the user who invoked the Interaction

Ephemeral
:
var undefined
undefined
;
const
const lang: string | undefined
lang
=
ctx: CommandContext<{
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}, never>
ctx
.
CommandContext<{ hide: { description: string; } & { readonly type: ApplicationCommandOptionType.Boolean; }; language: { readonly type: ApplicationCommandOptionType.String; ... 10 more ...; readonly max_length?: number; }; }, never>.options: ContextOptions<{
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}>
options
.
language?: string | undefined
language
;
// Get the translations for the language
const
const t: DefaultLocale
t
=
ctx: CommandContext<{
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}, never>
ctx
.
CommandContext<{ hide: { description: string; } & { readonly type: ApplicationCommandOptionType.Boolean; }; language: { readonly type: ApplicationCommandOptionType.String; ... 10 more ...; readonly max_length?: number; }; }, never>.t: __InternalParseLocale<DefaultLocale> & {
get(locale?: string): DefaultLocale;
}
t
.
function get(locale?: string): DefaultLocale
get
(
const lang: string | undefined
lang
);
// average latency between shards
const
const ping: number
ping
=
ctx: CommandContext<{
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}, never>
ctx
.
CommandContext<{ hide: { description: string; } & { readonly type: ApplicationCommandOptionType.Boolean; }; language: { readonly type: ApplicationCommandOptionType.String; ... 10 more ...; readonly max_length?: number; }; }, never>.client: UsingClient
client
.
Client<true>.gateway: ShardManager
gateway
.
ShardManager.latency: number
latency
;
await
ctx: CommandContext<{
hide: {
description: string;
} & {
readonly type: ApplicationCommandOptionType.Boolean;
};
language: {
readonly type: ApplicationCommandOptionType.String;
... 10 more ...;
readonly max_length?: number;
};
}, never>
ctx
.
CommandContext<{ hide: { description: string; } & { readonly type: ApplicationCommandOptionType.Boolean; }; language: { readonly type: ApplicationCommandOptionType.String; ... 10 more ...; readonly max_length?: number; }; }, never>.write<false>(body: InteractionCreateBodyRequest, withResponse?: false | undefined): Promise<void | WebhookMessage>
write
({
content?: string | undefined

The message contents (up to 2000 characters)

content
:
const t: DefaultLocale
t
.
foo: {
ping(data: {
ping: number;
}): `Pong: ${number}`;
}
foo
.
function ping(data: {
ping: number;
}): `Pong: ${number}`
ping
({
ping: number
ping
}),
flags?: MessageFlags | undefined

Message flags combined as a bitfield

flags
,
});
}
}

Below is the current file tree of the project if you did follow the previous steps.

  • Directorysrc
    • Directorycommands
      • ping.ts
    • Directoryevents
      • botReady.ts
      • guildDelete.ts
    • Directorylanguages
      • en.ts
      • es.ts
    • index.ts
  • package.json
  • seyfert.config.mjs
  • .env
  • tsconfig.json