i18n Plugin
Internationalization plugin modeled after @grammyjs/i18n. Supports YAML and FTL locale formats with CLDR plural rules for 30+ languages.
Install
go get github.com/mtgo-labs/plugins/i18nQuick Start
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
type Config struct {
DefaultLang language.Tag
Format LocaleFormat
EmbedFS embed.FS
LocaleDir string
SupportedLangs []language.Tag
GlobalContext GlobalContextFunc
}| Field | Type | Description |
|---|---|---|
DefaultLang | language.Tag | Fallback locale when no match is found |
Format | LocaleFormat | FormatYAML or FormatFTL |
EmbedFS | embed.FS | Embedded filesystem containing locale files |
LocaleDir | string | Subdirectory within EmbedFS to load locales from |
SupportedLangs | []language.Tag | Explicit list of languages to load (auto-discovered if empty) |
GlobalContext | GlobalContextFunc | Function returning global template variables for every translation |
Translation API
Context-aware (auto locale resolution)
tr.Translate(ctx, "welcome", "World")Uses the locale resolution chain to pick the right language.
Direct locale
tr.T(language.German, "welcome", "Welt")With pluralization / gender
tr.Tctx(language.English, "items_count", &i18n.Args{Count: 5})With named args
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:
client.OnMessage(func(c *tg.Context) {
c.Message.Reply(c.Message.T("start"))
}, tg.Command("start"))Full API reference
| Method | Signature | Description |
|---|---|---|
Translate | (ctx, key string, args ...any) string | Context-aware translation |
TranslateCtx | (ctx, key string, a *Args) string | Context-aware with pluralization/gender |
T | (locale, key string, args ...any) string | Translate for a specific locale |
Tctx | (locale, key string, ctx *Args) string | Translate with pluralization/gender for a specific locale |
Hears | (key string) func(*tg.Context) bool | Filter matching translated text |
ResolveLocale | (ctx *tg.Context) language.Tag | Get resolved locale for context |
SetLang | (userID int64, lang language.Tag) error | Persist locale via SessionStore |
GetLang | (userID int64) language.Tag | Get stored locale for a user |
HasLang | (lang language.Tag) bool | Check if a locale is loaded |
Locales | () []language.Tag | List all loaded locales |
Locale Resolution
The translator resolves the user's locale in this order:
- Custom negotiator — set via
tr.WithNegotiator(fn) - Session store — set via
tr.WithSession(store) - Telegram user language — from
user.Languagein the update - Default fallback —
Config.DefaultLang
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:
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:
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
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} itemsSet 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:
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:
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.goOr with directory-per-language:
locales/
├── en/
│ ├── common.yaml
│ └── menu.yaml
├── de/
│ ├── common.yaml
│ └── menu.yamlLicense
MIT
