Internationalization (i18n)

Overview

Forge’s i18n system loads locale strings from flat text files and provides forge_t(ctx, key) for translation in handlers and views. The locale is per-request, resolved from a cookie, query param, or Accept-Language header.

Locale file format

One key=value pair per line. Keys can use dot notation for namespacing. Lines starting with # are comments.

# locales/en.txt
greeting=Hello!
nav.home=Home
nav.posts=Posts
flash.saved=Changes saved.
flash.error=Something went wrong.
post.title_label=Post Title
post.body_label=Content
errors.blank=%s cannot be blank
# locales/fr.txt
greeting=Bonjour !
nav.home=Accueil
nav.posts=Articles
flash.saved=Modifications enregistrées.
flash.error=Une erreur est survenue.
post.title_label=Titre
post.body_label=Contenu

Loading locales

Call forge_i18n_load in main() before app_listen:

forge_i18n_load("en", "locales/en.txt")
forge_i18n_load("fr", "locales/fr.txt")
forge_i18n_load("es", "locales/es.txt")

Or group the calls in a dedicated function in config.jda:

fn load_i18n() {
    forge_i18n_load("en", "locales/en.txt")
    forge_i18n_load("fr", "locales/fr.txt")
}

Setting the locale per request

Use middleware to detect and set the locale before handlers run:

fn locale_middleware(ctx: i64) {
    // 1. Check cookie
    let locale = ctx_get_cookie(ctx, "locale")
    // 2. Fall back to Accept-Language header (just check first two chars)
    if locale.len == 0 {
        let al = ctx_header(ctx, "Accept-Language")
        if al.len >= 2 { locale = al[0..2] }
    }
    // 3. Default to "en"
    if locale.len == 0 { locale = "en" }
    forge_locale_set_ctx(ctx, locale)
}

// In main():
app_use(app, fn_addr(locale_middleware))

Translating strings

fn handle_posts_index(ctx: i64) {
    let heading = forge_t(ctx, "nav.posts")   // "Posts" or "Articles"
    ctx_html(ctx, 200, "<h1>" + heading + "</h1>")
}

forge_t(ctx, key) returns the translation for the locale set on ctx. If the key is not found in the current locale, it falls back to "en". If still not found, it returns the key itself.

Setting translation values programmatically

forge_i18n_set("en", "greeting", "Hello!")
forge_i18n_set("fr", "greeting", "Bonjour!")

Switching locale via URL parameter

fn set_locale_handler(ctx: i64) {
    let locale = ctx_query(ctx, "locale")
    // Whitelist allowed locales
    let allowed = false
    if forge_slice_eq(locale, "en") or forge_slice_eq(locale, "fr") { allowed = true }
    if allowed {
        ctx_set_cookie(ctx, "locale", locale, 31536000)   // 1 year
    }
    ctx_redirect(ctx, ctx_query(ctx, "return_to"))
}

Using translations in views

fn posts_new_page(ctx: i64) -> []i8 {
    let token = forge_csrf_token(ctx)
    ret layout(ctx, forge_t(ctx, "post.new_title"),
        forge_form_tag_open("/posts", "POST", token) +
        forge_label_tag("title", forge_t(ctx, "post.title_label")) +
        forge_input_tag("text", "title", "") +
        forge_submit_tag(forge_t(ctx, "post.submit")) +
        forge_form_tag_close())
}

API reference

FunctionDescription
forge_i18n_load(locale, path)Load a locale file
forge_i18n_set(locale, key, val)Set a single translation
forge_t(ctx, key)Get translation for ctx’s locale
forge_locale_set_ctx(ctx, locale)Set locale on a request context

Limits

LimitValue
Maximum entries per locale512 (FORGE_I18N_MAX_ENTRIES)
Maximum key length63 characters
Maximum value length255 characters