Skip to main content

Overview

Faculty Bot uses rosetta-i18n for internationalization, supporting multiple languages including English, German, and Japanese.

How It Works

Build-Time Code Generation

Translation files are compiled into Rust code at build time using build.rs:
build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    rosetta_build::config()
        .source("en", "i18n/en.json")  // English translations
        .source("de", "i18n/de.json")  // German translations
        .source("ja", "i18n/ja.json")  // Japanese translations
        .fallback("en")                 // Default to English if key missing
        .generate()?;
    
    Ok(())
}

Translation Module

Translations are included in the main module:
src/main.rs
pub mod prelude {
    pub mod translations {
        rosetta_i18n::include_translations!();
    }
}
See main.rs:83-85

Translation Files

English (en.json)

i18n/en.json
{
    "invalid_email": "Invalid E-Mail Address",
    "email_not_found": "E-Mail Address not found",
    "err_already_verified": "You have already verified your E-Mail Address.",
    "verification_successful": "Your E-Mail Address has been verified successfully.",
    "err_invalid_code": "Invalid verification code.",
    
    "email_msg_header": "Hello {name}, \n\n use the following code to verify your E-Mail Address:",
    "email_msg_footer": "If you did not request this verification, you can safely ignore this E-Mail.",
    "email_send_err": "An error occured while sending the verification E-Mail. Please try again later.",
    "notice_slow_mailserver": "## The code has been sent. It can take a few minutes for the email to reach you, HS email servers are not the fastest :^)",
    
    "message_pinned": "Message pinned",
    "message_unpinned": "Message unpinned",
    "message_deleted": "Message deleted",
    "user_promoted": "{user} has been promoted to Semestermoderator",
    "user_demoted": "{user} has been demoted to Student",
    
    "leaderboard": "Leaderboard",
    "xp_msg": "You have {xp} XP, that equals to Level {level}.",
    "xp_msg_none": "You have no XP yet.",
    "lvl_up": "Congrats {user}, you have reached Level {level}!",
    
    "code_email_enqueued": "## Verification code has been sent to {email}."
}

German (de.json)

i18n/de.json
{
    "invalid_email": "Ungültige E-Mail-Adresse",
    "email_not_found": "E-Mail-Adresse nicht gefunden",
    "err_already_verified": "Dein Account ist bereits verifiziert.",
    "verification_successful": "Dein Account wurde erfolgreich verifiziert.",
    "err_invalid_code": "Ungültiger Code.",
    
    "email_msg_header": "Hallo {name},\n\n bitte benutze folgenden Code um deinen Account zu verifizieren:",
    "email_msg_footer": "Wenn du diese E-Mail nicht angefordert hast, kannst du sie ignorieren.",
    "email_send_err": "Beim Senden der E-Mail ist ein Fehler aufgetreten. Bitte versuche es später erneut.",
    "notice_slow_mailserver": "## Code wurde versendet (es kann z.T einige Minuten dauern bis die Mail ankommt. Der HS-Mailserver ist nicht der Schnellste ;))",
    
    "message_pinned": "Nachricht angepinnt",
    "message_unpinned": "Nachricht gelöst",
    "message_deleted": "Nachricht gelöscht",
    "user_promoted": "{user} wurde zum Semestermod befördert",
    "user_demoted": "{user} wurde vom Semestermod degradiert",
    
    "leaderboard": "Leaderboard",
    "xp_msg": "Du hast {xp} XP, das ist äquivalent zu Level {level}.",
    "xp_msg_none": "Du hast noch keine XP.",
    "lvl_up": "Glückwunsch {user}, du bist jetzt Level {level}!",
    
    "code_email_enqueued": "## Code wurde an {email} gesendet."
}

Japanese (ja.json)

Japanese translations follow the same structure (not fully implemented in the source).

Using Translations in Commands

Basic Usage

src/commands/user.rs
use crate::prelude::translations::Lang;

#[poise::command(slash_command)]
pub async fn verify_init(
    ctx: Context<'_>,
    email: String,
) -> Result<(), Error> {
    // Detect user's locale
    let lang = match ctx.locale() {
        Some("de") => Lang::De,
        Some("ja") => Lang::Ja,
        _ => Lang::En,  // Default to English
    };
    
    // Validate email
    if !email.ends_with("@stud.hs-kempten.de") {
        return Err(Error::WithMessage(lang.invalid_email().into()));
    }
    
    // Check if already verified
    if user_exists {
        return Err(Error::WithMessage(lang.err_already_verified().into()));
    }
    
    // Send success message
    ctx.send(|msg| {
        msg.embed(|embed| {
            embed.description(lang.notice_slow_mailserver())
        })
    })
    .await
    .map_err(Error::Serenity)?;
    
    Ok(())
}
See user.rs:48-108

With Parameters

Some translation strings support placeholders:
// Translation: "You have {xp} XP, that equals to Level {level}."
let message = lang.xp_msg(user.user_level, user.user_xp);

ctx.send(|f| {
    f.embed(|e| {
        e.description(message)
    })
})
.await?;
See user.rs:281

Command Localization

Poise supports localizing command names and descriptions:

Localized Command Names

#[poise::command(
    slash_command,
    rename = "verify",
    name_localized("de", "verifizieren"),  // German name
    name_localized("ja", "確認"),           // Japanese name
)]
pub async fn verify(ctx: Context<'_>) -> Result<(), Error> {
    // ...
}
Result:
  • English users see: /verify
  • German users see: /verifizieren
  • Japanese users see: /確認
See user.rs:11-19

Localized Descriptions

#[poise::command(
    slash_command,
    description = "Verify yourself with your student email",
    description_localized("de", "Verifiziere dich mit deiner Studierenden E-Mail Adresse"),
)]
pub async fn verify_init(ctx: Context<'_>) -> Result<(), Error> {
    // ...
}
See user.rs:28-35

Localized Parameters

#[poise::command(slash_command)]
pub async fn verify_init(
    ctx: Context<'_>,
    #[description = "Your student email address"]
    #[description_localized("de", "Deine Studierenden E-Mail Adresse")]
    #[name_localized("de", "email-adresse")]
    #[rename = "email"]
    email: String,
) -> Result<(), Error> {
    // ...
}
See user.rs:38-45

Adding New Translations

1

Add Translation Key

Add the new key to all translation files:i18n/en.json:
{
    "welcome_message": "Welcome to the server, {username}!"
}
i18n/de.json:
{
    "welcome_message": "Willkommen auf dem Server, {username}!"
}
i18n/ja.json:
{
    "welcome_message": "サーバーへようこそ、{username}!"
}
2

Rebuild the Project

Translations are compiled at build time:
cargo build
3

Use the Translation

let lang = match ctx.locale() {
    Some("de") => Lang::De,
    Some("ja") => Lang::Ja,
    _ => Lang::En,
};

let message = lang.welcome_message(username);
ctx.say(message).await?;

Translation String Formats

Simple Strings

{
    "key": "Simple message without parameters"
}
Usage:
lang.key()

With Parameters

{
    "greeting": "Hello {name}, welcome!"
}
Usage:
lang.greeting("Alice")

With Multiple Parameters

{
    "xp_msg": "You have {xp} XP, that equals to Level {level}."
}
Usage:
lang.xp_msg(level, xp)  // Parameters in order of appearance

Locale Detection

Poise automatically provides the user’s Discord locale:
let locale = ctx.locale();  // Returns Option<&str>

match locale {
    Some("de") => Lang::De,      // German
    Some("ja") => Lang::Ja,      // Japanese
    Some("en-US") => Lang::En,   // English (US)
    Some("en-GB") => Lang::En,   // English (UK)
    _ => Lang::En,               // Default fallback
}

Common Discord Locales

  • "en-US": English (United States)
  • "en-GB": English (United Kingdom)
  • "de": German
  • "ja": Japanese
  • "es-ES": Spanish (Spain)
  • "fr": French
  • "zh-CN": Chinese (Simplified)

Best Practices

  1. Always provide fallback: Default to English if locale is unknown
  2. Keep keys semantic: Use descriptive key names like err_invalid_email
  3. Maintain consistency: Keep the same keys across all language files
  4. Use placeholders wisely: Make parameters generic and reusable
  5. Test all languages: Verify translations work in different locales
  6. Handle missing translations: Rosetta-i18n falls back to the default language if a key is missing
  7. Keep translations short: Discord has character limits for embeds and messages

Translation Guidelines

For Error Messages

{
    "err_invalid_email": "Invalid email address",
    "err_already_verified": "Already verified",
    "err_database": "Database error occurred"
}
Prefix error messages with err_ for clarity.

For User Actions

{
    "message_pinned": "Message pinned",
    "user_promoted": "{user} has been promoted",
    "verification_successful": "Verification successful"
}
Use past tense for completed actions.

For Instructions

{
    "notice_slow_mailserver": "Email sent. It may take a few minutes to arrive.",
    "email_msg_header": "Use the following code to verify your email:"
}
Be clear and concise.

Adding a New Language

1

Create Translation File

Create a new JSON file in i18n/ directory:
cp i18n/en.json i18n/fr.json
2

Translate All Keys

Translate all strings in the new file:
i18n/fr.json
{
    "invalid_email": "Adresse e-mail invalide",
    "verification_successful": "Votre e-mail a été vérifié avec succès."
}
3

Register in build.rs

Add the language to the build configuration:
build.rs
rosetta_build::config()
    .source("en", "i18n/en.json")
    .source("de", "i18n/de.json")
    .source("ja", "i18n/ja.json")
    .source("fr", "i18n/fr.json")  // Add new language
    .fallback("en")
    .generate()?;
4

Update Language Detection

Add the new locale to your detection logic:
let lang = match ctx.locale() {
    Some("de") => Lang::De,
    Some("ja") => Lang::Ja,
    Some("fr") => Lang::Fr,  // Add new language
    _ => Lang::En,
};
5

Rebuild and Test

cargo build
# Test commands with French locale

Troubleshooting

Translation Key Not Found

Error: method not found in Lang Solution: Ensure the key exists in all translation files and rebuild:
cargo clean
cargo build

Wrong Language Displayed

Issue: User sees wrong language Solution: Check locale detection logic and ensure the locale string matches Discord’s format.

Build Errors

Error: failed to run custom build command for rosetta-build Solution: Verify all translation files are valid JSON:
jq . i18n/en.json
jq . i18n/de.json
jq . i18n/ja.json

Resources

Next Steps

Build docs developers (and LLMs) love