Skip to content

i18n Plugin

Internationalization plugin modeled after @grammyjs/i18n. Supports YAML and FTL locale formats with CLDR plural rules for 30+ languages.

Install

bash
go get github.com/mtgo-labs/plugins/i18n

Quick Start

go
import (
    "embed"
    tg "github.com/mtgo-labs/mtgo/telegram"
    "github.com/mtgo-labs/plugins/i18n"
    "golang.org/x/text/language"
)

//go:embed locales/*.yaml
var locales embed.FS

func main() {
    client, _ := tg.NewClient(apiID, apiHash, &tg.Config{
        BotToken:    botToken,
        SessionName: "i18n_bot",
    })

    tr := i18n.NewTranslator(&i18n.Config{
        DefaultLang: language.English,
        Format:      i18n.FormatYAML,
        EmbedFS:     locales,
        LocaleDir:   "locales",
    })

    client.Use(tr)

    client.OnMessage(func(c *tg.Context) {
        c.Message.Reply(c.Message.T("start"))
    }, tg.Command("start"))

    client.Connect(0)
}

Configuration

Config struct

go
type Config struct {
    DefaultLang    language.Tag
    Format         LocaleFormat
    EmbedFS        embed.FS
    LocaleDir      string
    SupportedLangs []language.Tag
    GlobalContext  GlobalContextFunc
}
FieldTypeDescription
DefaultLanglanguage.TagFallback locale when no match is found
FormatLocaleFormatFormatYAML or FormatFTL
EmbedFSembed.FSEmbedded filesystem containing locale files
LocaleDirstringSubdirectory within EmbedFS to load locales from
SupportedLangs[]language.TagExplicit list of languages to load (auto-discovered if empty)
GlobalContextGlobalContextFuncFunction returning global template variables for every translation

Translation API

Context-aware (auto locale resolution)

go
tr.Translate(ctx, "welcome", "World")

Uses the locale resolution chain to pick the right language.

Direct locale

go
tr.T(language.German, "welcome", "Welt")

With pluralization / gender

go
tr.Tctx(language.English, "items_count", &i18n.Args{Count: 5})

With named args

go
tr.Tctx(language.English, "greeting", &i18n.Args{
    Args: map[string]any{"name": "Alice"},
})

From handler via Message.T()

The plugin injects a T() function on *Message via middleware:

go
client.OnMessage(func(c *tg.Context) {
    c.Message.Reply(c.Message.T("start"))
}, tg.Command("start"))

Full API reference

MethodSignatureDescription
Translate(ctx, key string, args ...any) stringContext-aware translation
TranslateCtx(ctx, key string, a *Args) stringContext-aware with pluralization/gender
T(locale, key string, args ...any) stringTranslate for a specific locale
Tctx(locale, key string, ctx *Args) stringTranslate with pluralization/gender for a specific locale
Hears(key string) func(*tg.Context) boolFilter matching translated text
ResolveLocale(ctx *tg.Context) language.TagGet resolved locale for context
SetLang(userID int64, lang language.Tag) errorPersist locale via SessionStore
GetLang(userID int64) language.TagGet stored locale for a user
HasLang(lang language.Tag) boolCheck if a locale is loaded
Locales() []language.TagList all loaded locales

Locale Resolution

The translator resolves the user's locale in this order:

  1. Custom negotiator — set via tr.WithNegotiator(fn)
  2. Session store — set via tr.WithSession(store)
  3. Telegram user language — from user.Language in the update
  4. Default fallbackConfig.DefaultLang
go
tr.WithNegotiator(func(ctx *tg.Context) language.Tag {
    if ctx.Message != nil && strings.HasPrefix(ctx.Message.Text, "/ru") {
        return language.Russian
    }
    return language.English
})

Session Store

Implement SessionStore to persist user locale preferences:

go
type SessionStore interface {
    GetLocale(userID int64) string
    SetLocale(userID int64, locale string) error
}

tr.WithSession(myStore)

Global Context

Supply template variables available in every translation:

go
tr.WithGlobalContext(func(ctx *tg.Context) map[string]any {
    name := ""
    if sender := ctx.Sender(); sender != nil {
        name = sender.FirstName
    }
    return map[string]any{"name": name}
})

Template {name} is automatically filled without passing it explicitly.

YAML Format

yaml
welcome: "Hello, {0}!"
greeting: "Welcome, {name}!"
items_count:
  one: "You have {count} item."
  other: "You have {count} items."
menu:
  home: "Home"
  settings: "Settings"

Nested keys are accessed with dot notation: menu.home.

Plural variants use CLDR keys: zero, one, two, few, many, other.

FTL Format

welcome = Hello, {0}!
greeting = Welcome, {name}!
items_count = {count} items

Set Format: i18n.FormatFTL in config to use Fluent files.

Plural Rules

Built-in CLDR rules for 30+ languages including:

  • English, German, Spanish, Italian, Portuguese (English-style)
  • French, Hindi (0/1 = singular)
  • Russian, Ukrainian, Serbian, Croatian (Slavic)
  • Polish, Czech, Slovak, Arabic, Hebrew, Romanian
  • Japanese, Chinese, Korean, Vietnamese, Thai (no plural)

Add custom rules:

go
tr.pluralizer.AddRule(language.Tag{}, func(n int) string {
    if n == 0 { return "zero" }
    if n == 1 { return "one" }
    return "other"
})

Hears Filter

Match localized button/keyboard text across languages:

go
client.OnMessage(func(c *tg.Context) {
    c.Message.Reply("Menu pressed!")
}, tg.Create(tr.Hears("menu_btn")))

Hears("menu_btn") resolves the key to the user's locale and matches against message text.

Directory Structure

bot/
├── locales/
│   ├── en.yaml
│   ├── de.yaml
│   └── ru.yaml
├── main.go

Or with directory-per-language:

locales/
├── en/
│   ├── common.yaml
│   └── menu.yaml
├── de/
│   ├── common.yaml
│   └── menu.yaml

License

MIT

Released under the Apache-2.0 License.