Skip to content

Responding with type-safe messages

Warning

This method works but the API is not stable yet, a new version could bring breaking changes.

The framework provides a module to define functions which retrieves translated messages, without having to implement anything, alongside a few other benefits:

  • Checks if the templates exists in your bundles, ensuring your content can always be displayed
  • Checks if function parameters exists in your template's arguments, meaning all parameters map to an argument
  • Checks if template arguments map to function parameters, so all arguments have values
  • Removes the need for magic strings (for the arguments), improving type safety and making regressions appear immediately

Tip

Even if you don't provide translations, using a "root" bundle is still useful to separate content from your code, and if you decide to add translations later, your code is already ready for it!

Installation

See the README.

Example

Creating a localization bundle

Assuming we have a MyBotMessages bundle:

{
  "bot.info": "I am in {guild_count, number} {guild_count, choice, 0#guilds|1#guild|1<guilds} and I am up since {uptime_timestamp}."
}
Bundle content details
  • The key is bot.info
  • The template is I am in {guild_count, number} {guild_count, choice, 0#guilds|1#guild|1<guilds} and I am up since {uptime_timestamp}.
    • guild_count and uptime_timestamp are variables
    • number and choice are format types
    • 0#guilds|1#guild|1<guilds is a subformat pattern for ChoiceFormat
    • See MessageFormat for more details!

Creating our message source

Create an interface which extends IMessageSource; it will contain functions annotated with @LocalizedContent, the annotation's value is the key present in your localization bundle, and the function needs to return a String.

// No need to implement this interface
interface CommandRepliesKt : IMessageSource {

    // The function can have any name you want
    @LocalizedContent("bot.info")
    fun botInfo(
        // Parameter names are converted to snake_case for use in the template, here it's 'guild_count'
        guildCount: Long,
        // You could also pass a Timestamp as it has a proper `toString()`
        uptimeTimestamp: String,
    ): String
}
// No need to implement this interface
public interface CommandReplies extends IMessageSource {

    // The function can have any name you want
    @LocalizedContent("bot.info")
    String botInfo(
            // Parameter names are converted to snake_case for use in the template, here it's 'guild_count'
            long guildCount,
            // You could also pass a Timestamp as it has a proper `toString()`
            String uptimeTimestamp
    );
}

Tip

You can inject instances of this interface in any interaction handler such as application commands, components and modals.

Tip

You can override the locale using @PreferLocale or by passing a DiscordLocale or a Locale in the first parameter.

Creating a factory for our source

We then need a way to get instances of our source; create an interface extending IMessageSourceFactory<CommandReplies> and annotate it with @MessageSourceFactory("MyBotMessages"), the MyBotMessages string is the name of the bundle we added in the first step.

// No need to implement this interface
@MessageSourceFactory(
    bundleName = "MyBotMessages",
    ignoreEmptyLocales = true, // We set it to true as we only have a root bundle
)
interface CommandRepliesKtFactory : IMessageSourceFactory<CommandRepliesKt>
// No need to implement this interface
@MessageSourceFactory(
        bundleName = "MyBotMessages",
        ignoreEmptyLocales = true // We set it to true as we only have a root bundle
)
public interface CommandRepliesFactory extends IMessageSourceFactory<CommandReplies> {

}

Instances of this interface can be injected like any other service, and will allow you to create CommandReplies instances from an Interaction.

Usage

@Command
class SlashInfo {

    @JDASlashCommand(
        name = "info",
        description = "Sends info about the bot",
    )
    fun onSlashInfo(event: GuildSlashEvent, replies: CommandRepliesKt) {
        val response = replies.botInfo(
            // Use named parameters to make the arguments clearer!
            guildCount = event.jda.guildCache.size(),
            uptimeTimestamp = TimeFormat.RELATIVE.format(ManagementFactory.getRuntimeMXBean().startTime),
        )

        event.reply(response)
            .setEphemeral(true)
            .queue()
    }
}
@Command
public class SlashInfo {

    @JDASlashCommand(
            name = "info",
            description = "Sends info about the bot"
    )
    public void onSlashInfo(GuildSlashEvent event, CommandReplies replies) {
        var response = replies.botInfo(
                event.getJDA().getGuildCache().size(),
                TimeFormat.RELATIVE.format(ManagementFactory.getRuntimeMXBean().getStartTime())
        );

        event.reply(response)
                .setEphemeral(true)
                .queue();
    }
}

Tip

Injecting the CommandReplies instance in the slash command function is the same as injecting CommandRepliesFactory in your class then using it in your command to create instances of CommandReplies.

Try out /info!

Improve safety across locales

It is highly recommended to put the locales your "message source" supports, this enables the framework to check that everything from your bundles and your methods matches. This way, errors come at startup instead of when it is used.

You can do so by setting MessageSourceFactory.discordLocales and/or MessageSourceFactory.locales.

If you don't have translations and only wish to use bundles to separate your responses from your code, you can set MessageSourceFactory.ignoreEmptyLocales to true.

Changing the locale

The default locales for interactions are taken from UserLocaleProvider (default if locale is unspecified) and GuildLocaleProvider.

Forcing a locale

You can force a specific DiscordLocale or Locale by passing it as a first argument:

interface CommandRepliesForcedLocaleKt : IMessageSource {

    @LocalizedContent("bot.info")
    fun botInfo(
        // Forces the locale to this one
        locale: DiscordLocale,
        guildCount: Long,
        uptimeTimestamp: String,
    ): String
}
public interface CommandRepliesForcedLocale extends IMessageSource {

    @LocalizedContent("bot.info")
    String botInfo(
            // Forces the locale to this one
            DiscordLocale locale,
            long guildCount,
            String uptimeTimestamp
    );
}

Setting the preferred locale

You can also make a method or an entire message source prefer a locale provider over another:

interface CommandRepliesPreferredLocaleKt : IMessageSource {

    // Prefers using GuildLocaleProvider
    @PreferLocale(LocalePreference.GUILD)
    @LocalizedContent("bot.info")
    fun botInfo(
        guildCount: Long,
        uptimeTimestamp: String,
    ): String
}
public interface CommandRepliesPreferredLocale extends IMessageSource {

    // Prefers using GuildLocaleProvider
    @PreferLocale(LocalePreference.GUILD)
    @LocalizedContent("bot.info")
    String botInfo(
            long guildCount,
            String uptimeTimestamp
    );
}