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:
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.
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.
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:
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:
Ignored words in duration parsers
Reset words for the PluralKit module
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:
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:
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 bundleA
Locale
representing the locale to translate thisKey
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 atranslate
call.A setting (
translateNestedKeys
) which defines whether nestedKey
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'slocale
, 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'slocale
, 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'slocale
, 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'slocale
, or your bot's configured default locale if one isn't present.withContext
- Creates a clone of the current Key, updating its storedlocale
to match the one corresponding with the givenCommandContext
.
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:
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 byString.parseBoolean(Locale)
.The relevant translation keys are
utils.string.false
andutils.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
, andutils.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 parseDateTimePeriod
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
, andutils.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 withnsfwLevel.
Permission
- translation keys starting withpermission.
Utilities
A number of miscellaneous utilities for working with the translations system are available.
EMPTY_KEY
- aKey
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.