Kord Extensions Help

Internationalization (i18n)

While some smaller bots are only designed with one language in mind, many larger bots will need to support guilds and users using a variety of languages and locales.

To help with this, Kord Extensions was designed with internationalization (i18n) in mind, and it provides many relevant tools and systems.

Planning

Before you can begin to localize your bots, you'll need to do some planning:

  1. Decide on a default locale.

    This will be US English for most bots, but you're free to use something else if there's a more suitable locale.

  2. Consider the depth of your localization work.

    Making bots suitable for use in other locales requires extra consideration. Different languages and locales require very different approaches, and it's important to consider what may change between them. For example:

    • Number formats, including things such as currencies, and dates and times.

    • Units, such as those used for weights, volumes and temperatures.

    • Text direction for right-to-left languages.

    This isn't an exhaustive list — it's important to do research and chat with folks that speak your bot's target locales.

  3. Consider what external tooling you'll need.

    Automatic translation tools like DeepL and Google Translate can be useful in a small number of situations, but their output is usually inaccurate or otherwise unsuitable for use in your translations.

    Instead, consider crowdsourcing translations using tools like Weblate and Crowdin — and get your community involved! There are many tools that support Java Properties files and the ICU Message Format, and lots of them can integrate with your version control system. We recommend shopping around and finding something that suits your project and community.

Localizing a bot is often a pretty large undertaking, but it can be very worthwhile!

Basic Concepts

Kord Extensions approaches i18n in a rather specific way, so it's important to understand a few things.

Locales

Locales represent specific geographical, political, or cultural regions. In the context of programming, Locale objects are used to represent a set of language, script, region, variant, and extensions, which can be used together to format all kinds of data or to look up translations.

Discord has a predefined set of supported locales, which may be found here. This list of locales represents those supported by the Discord client and isn't an exhaustive list of all possible locales.

Kord Extensions uses locale resolvers to find the relevant Locale object for any given event. You can configure them in your bot's i18n builder.

You may want to consider writing a custom resolver and allowing users to configure their preferred locale using commands if you want to support locales other than those in the Discord client.

Translation Bundles

The most important unit in the i18n system is the translation bundle. By default, Kord Extensions uses properties-based Java Resource Bundles, where translations are stored in a set of .properties files with similarly prefixed names. Translation bundles are represented using Bundle objects in code.

The first thing you'll need to do is pick a name for your translation bundle. The name should contain two parts — the name of the containing folder, and the bundle itself — split using a dot or period (.). This name is used to locate the resource bundle in your bot's resources, within the translations/ base directory.

For example, mybot.strings refers to a resource bundle named strings stored within translations/mybot/ in your bot's resource files.

Your translation bundle must contain a base translation file, with a set of default translations. This file should be named without a locale, so in the above example, it'd be named strings.properties. The translations in this file will be used as a fallback when a translation is missing.

All other translations must be placed in translation files containing the relevant locale code. For example, the locale code for German is de, so a translation file for German in our example would be named strings_de.properties.

For more information on the properties file format, see this page on Wikipedia.

Translation Keys

A translation key represents a single translation. When you store translations in a properties file, they're referred to by name. For example:

command.about.name=about command.about.description=Learn about this bot.

In the above properties file, command.about.name is a translation key, referring to a specific translation.

Translation keys are represented using Key objects in code. These objects may also contain bundle and locale information with predefined replacement values, as explained in below.

Writing Translations

To quickly recap the previous section: Translations are (by default) written to UTF-8-encoded .properties files, grouped together into a translation bundle. These translations are referred to using a combination of the bundle name and translation key.

Translations are written using ICU Message Format, a standard message formatter provided by the Unicode Consortium. This format provides a few useful (but understated) tools that you can use to write better contextual translations.

Empty Translations

For translation keys that support an empty value for specific locales, you may provide a special value for the translation. This value consists of three empty set characters (∅∅∅) and should contain nothing else.

Note: This value is not supported for all translations. You can use it in the following places:

Placeholder Substitution

ICU Message Format allowed for two types of placeholders:

  • Ordinal placeholders: Hello, {0}!

    Ordinal placeholders are filled using an array of arbitrary values. Array indices are specified by placing them within curly braces.

    For example, to specify the first element of the array, you would use {0}.

  • Named placeholders: Hello, {name}!

    Named placeholders are filled using a string-keyed map of arbitrary values. The map's keys are specified by placing them within curly braces.

    For example, to specify the name key of the map, you would use {name}.

Mixed placeholder types are unsupported, so you'll need to pick one or the other. We recommend using named placeholders, as they result in more readable translations.

Placeholders are automatically formatted using the default ICU formatter for the corresponding type. However, you can further format placeholders by supplying a style modifier.

Numeric Formatting

ICU Message Format provides several useful modifiers you can use to format numeric types. The following examples use 0 in place of any ordinal or named placeholder you may use.

Numbers

  • Automatic: {0, number} (12.5)

  • Integer: {0, number, integer} (13)

  • Currency: {0, number, currency} ($12.50)

  • Percent: {0, number, percent} (1,250%)

  • Custom: {0, number, ###,###.###} (uses Java's DecimalFormat)

Dates

  • Automatic: {0, date} (Sep 22, 2024)

  • Short: {0, date, short} (9/22/24)

  • Medium: {0, date, medium} (Sep 22, 2024)

  • Long: {0, date, long} (September 22, 2024)

  • Full: {0, date, full} (Sunday, September 22, 2024)

  • Custom: {0, date, dd/MM/yyyy} (uses Java's SimpleDateFormat)

Times

  • Automatic: {0, time} (4:30:00 PM)

  • Short: {0, time, short} (4:30 PM)

  • Medium: {0, time, medium} (4:30:00 PM)

  • Long: {0, time, long} (4:30:00 PM GMT+1)

  • Full: {0, time, full} (4:30:00 PM GMT+1)

  • Custom: {0, time, kk:mm:ss} (uses Java's SimpleDateFormat)

Others

Durations: {0, duration} - for formatting numbers (representing seconds) as simple durations. For example, 123 becomes 02:03 in English.

Ordinals: {0, ordinal} - for formatting numbers with ordinal suffixes. For example, 3 becomes 3rd in English.

Spellouts: {0, spellout} - for formatting numbers as text. For example, 100 becomes one hundred in English.

For more advanced number formatting, see RuleBasedNumberFormat.

Selections

Selections are a more advanced formatting construct that allows you to provide different translations based on a set of rules. The following examples use 0 in place of any ordinal or named placeholder you may use.

Keyword Selections

Keyword selections allow you to provide a different translation based on the contents of a placeholder. This is done by configuring the select modifier with matching patterns.

A matching pattern uses the form of keyword {translation}, and you may provide multiple patterns separated by spaces. The other pattern is used when no other keywords match the placeholder, and it must always be included in your keyword selection.

Translations may themselves contain placeholders, selectors, and other patterns.

For example, to pick a pronoun based on the given gender, you could do something like this:

{0, select, male {he} female {she} doll {it} other {they}}

Plural Selections

Plural selections allow you to provide a separate translation based on the value of a numeric placeholder. This is done by configuring the plural modifier with various options and some matching patterns.

Plural selections support a number of options, provided using the form name:value before the matching patterns:

  • offset - By providing a number, it will be subtracted from the placeholder's value before being used to match the given cases.

A matching pattern uses the form of case {translation}, and you may provide multiple patterns separated by spaces. The other case is used if no other keywords match the placeholder, and it must always be included in your keyword selection. The following default cases are supported:

  • zero - Matched when the value is 0.

  • one - Matched when the value is 1.

  • two - Matched when the value is 2.

  • few - Matched when you'd normally say "a few" to represent the value.

  • many - Matched when you'd normally say "many" to represent the value.

You can also match a specific value by prefixing it with =. For example, to match a value of 7, you could use =7.

Translations may themselves contain placeholders, selectors, and other patterns. Additionally, providing a # in the translation will insert the value of the placeholder.

As an example, if you were hosting a party, you might do something like this:

{guests, plural, offset:1 zero {{host} does not throw a party.} one {{host} invites {guest} to the party.} two {{host} invites {guest} and one other person to the party.} other {{host} invites {guest} and # other people to the party.} }

Using Translations

At its most basic, the translations system only requires you to provide a Key object to a translatable property. The Key objects are instances of an immutable data class, which can contain the following information:

  • A String representing a translation key in a bundle.

  • A Bundle representing the corresponding translation bundle

  • A Locale representing the locale to translate this Key to.

  • A pre-defined array of ordinal placeholders or map of named placeholders.

  • A setting (presetPlaceholderPosition) which defines whether to place pre-defined ordinal placeholders before or after the placeholders provided to a translate call.

  • A setting (translateNestedKeys) which defines whether nested Key objects in translation placeholders should also be translated before use.

    Note: This only supports a single level of nesting.

Creating Keys

If you're using the KordEx Gradle plugin, you can configure it to generate translation classes from your translation bundle.

This is the recommended approach and will result in a tree of translation classes containing generated Key objects that you can reference in your code. If you can't use the Gradle plugin, you can find a CLI tool and an API in this GitHub repository, to integrate with your own tooling.

Alternatively, Key objects can be manually constructed as required, or created from String keys using the String.toKey(bundle?, locale?, presetPlaceholderPosition?, translateNestedKeys?) extension function.

Configuring Keys

Key objects are immutable, but they provide several functions that allow you to configure cloned objects.

To create a copy of the Key with a bundle or locale, you can use the withBundle, withLocale or withBoth functions, or with the withContext function to copy the locale from a command context. Creating a copy without either a bundle or locale can be done by calling the corresponding without functions.

To create a copy with more pre-defined placeholders, use the withOrdinalPlaceholders or withNamedPlaceholders functions. You can create a copy without pre-defined placeholders by calling the corresponding without functions, or create a copy with a set of filtered placeholders using the corresponding filter functions.

To create a copy with an additional post-processor (which you can use to manipulate the result of a translation), use the wthPostProcessor DSL function. To add multiple, you can use withPostProcessors. It's also possible to remove post-processors using the filterPostProcessors function.

To create a copy with updated settings, use the withPresetPlaceholderPosition or withTranslateNestedKeys functions.

Key Utilities

Kord Extensions provides a number of additional utility functions.

  • capitalize - Creates a clone of the current Key, with an additional post-processor which capitalizes the first character of the translated string. It uses the Key's locale, or your bot's configured default locale if one isn't present.

  • capitalizeWords - Creates a clone of the current Key, with an additional post-processor which capitalizes each word in the translated string. It uses the Key's locale, or your bot's configured default locale if one isn't present.

  • lowercase - Creates a clone of the current Key, with an additional post-processor which lower-cases each character in the translated string. It uses the Key's locale, or your bot's configured default locale if one isn't present.

  • uppercase - Creates a clone of the current Key, with an additional post-processor which upper-cases each character in the translated string. It uses the Key's locale, or your bot's configured default locale if one isn't present.

  • withContext - Creates a clone of the current Key, updating its stored locale to match the one corresponding with the given CommandContext.

Translating Keys

When you find a property or function expecting a Key object, it's enough to configure the key as explained above and use it directly. For situations where you need to translate a Key yourself, you'll need to use one of the provided translation functions.

The translate and translateArray functions can be used to translate the current Key with a set of ordinal placeholders. If any pre-defined ordinal placeholders are present, they'll be used based on the presetPlaceholderPosition setting.

The translateNamed function can be used to translate the current Key with a set of named placeholders. If any pre-defined named placeholders are present, they'll be used based on the presetPlaceholderPosition setting.

All translation functions include a version suffixed with Locale, which will use the given locale for translation if the Key doesn't already have a locale set.

Putting It All Together

To better illustrate how you might use translation keys, consider this example:

// Assume we're in an Extension's `setup` function publicSlashCommand { // Name and description properties take Keys directly. name = Translations.Commands.About.name description = Translations.Commands.About.description action { respond { embed { // Translate keys for String properties. title = Translations.Embeds.About.title .withContext(this@action) .translateNamed("botName" to "My Bot") description = Translations.Embeds.About.description .withContext(this@action) .translateNamed("triggeringUser" to user.mention) } } } }

Override Bundles

In addition to the normal translation bundles explained above, it's possible to override translations provided by other bundles. You can do this by creating your own translation bundle files, suffixing _override to the bundle name.

For example, to override the default translations in the kordex.strings bundle, you could create translations/kordex/strings_override.properties, and add the keys you wish to override.

Formatters

A number of locale-aware formatters are available for formatting rich types into human-readable strings.

  • DateTimePeriod.format(Locale) - uses ICU4J Measures to create a duration-like string.

Parsers

A number of locale-aware parsers are available for parsing strings into rich types. These parsers are cached when appropriate and allow you to convert various values without worrying about writing translations yourself.

  • BooleanParser - a string parser which can parse truthy and falsey values from strings in a locale-aware manner. This parser is used by the boolean converters, and it's also used by String.parseBoolean(Locale).

    The relevant translation keys are utils.string.false and utils.string.true, and multiple comma-separated values are supported for each key.

    If a value can't be parsed, this parser returns null.

  • ColorParser - a string parser which can parse Discord branding colours from strings in a locale-aware manner. This parser is used by the color converters and it supports black, blurple, fuchsia, green, red, white, and yellow.

    The relevant translation keys are utils.colors.black, utils.colors.blurple, utils.colors.fuchsia, utils.colors.green, utils.colors.red, utils.colors.white, and utils.string.yellow, and multiple comma-separated values are supported for each key.

    Translations for blurple include "purple," and translations for fuchsia include "pink."

    If a value can't be parsed, this parser returns null.

  • DurationParser - a string parser which can parse DateTimePeriod objects from strings in a locale-aware manner. This parser is used by the duration converters.

    This (surprisingly complex) parser supports positive and negative durations specified in a number of forms. Bare numbers are not supported.

    • Long: 1 day, 2 hours, 3 minutes, 4 seconds

    • Short: 1d2h3m4s

    All supported forms may be written right-to-left or left-to-right, as long as they're written consistently.

    The relevant translation keys are utils.units.day, utils.units.week, utils.units.month, utils.units.year, utils.units.hour, utils.colors.minute, and utils.string.second, and multiple comma-separated values are supported for each key.

Kord Translations

Kord Extensions provides translations for multiple Kord types, which may be useful for display to users on Discord. This is exposed via toTranslationKey and translate extension functions against the following types:

  • NsfwLevel - translation keys starting with nsfwLevel.

  • Permission - translation keys starting with permission.

Utilities

A number of miscellaneous utilities for working with the translations system are available.

  • EMPTY_KEY - a Key object referencing the empty string, useful as a default value in some situations (such as paginators with a single group).

  • EMPTY_VALUE_STRING - a special string value representing an "empty" or "missing" value. This is a standard "nothing" value, and you should check for it when you need one. The string contains three empty set characters (∅∅∅).

  • SupportedLocales - a collection of Locale objects representing locales with partial or full translations bundled with Kord Extensions.

Custom Providers

While we recommend sticking with the bundled ResourceBundleTranslations provider, we recognize that there may be cases where the default provider is unsuitable.

In situations like that, you can extend the ResourceBundleTranslations or TranslationsProvider types, and configure your bot's translation provider.

Last modified: 20 November 2024