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_countanduptime_timestampare variablesnumberandchoiceare format types0#guilds|1#guild|1<guildsis 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
);
}