REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY
5
Localization
ScuroNeko edited this page 2026-05-20 13:19:27 +03:00

Localization

Russian version: Localization-RU

Localization in Laniakea is centered around L10n, a small key-based translation store with fallback behavior. It is designed to keep handler code simple: handlers ask for keys, and the bot resolves the best available translation for the current user.

Overview

The localization flow has three main parts:

  • create an L10n store with a fallback language;
  • add translation entries keyed by stable identifiers;
  • attach the store to the bot with SetL10n(...).

Inside handlers, the most convenient lookup is ctx.Translate(key).

Creating a localization store

Create the store with NewL10n(fallbackLanguage).

l10n := laniakea.NewL10n("en")

The fallback language is used when:

  • the user's language has no translation for the key;
  • the user language is unknown but a fallback translation exists.

Adding translations

Translations are stored per key with AddDictEntry(...).

l10n.AddDictEntry("greeting", laniakea.DictEntry{
	"en": "Hello",
	"ru": "Privet",
})

Each key should represent meaning, not a specific language string. Good keys are stable names such as:

  • greeting
  • errors.not_allowed
  • menu.settings

Avoid using the source-language sentence itself as the key. Stable keys are easier to refactor and reuse.

Fallback behavior

L10n.Translate(lang, key) uses this order:

  1. exact match for lang;
  2. fallback language configured in NewL10n(...);
  3. the key itself.

That means a missing translation degrades predictably instead of returning an empty string.

Example:

l10n := laniakea.NewL10n("en").
	AddDictEntry("greeting", laniakea.DictEntry{
		"en": "Hello",
		"ru": "Privet",
	})

l10n.Translate("ru", "greeting") // "Privet"
l10n.Translate("es", "greeting") // "Hello"
l10n.Translate("es", "unknown")  // "unknown"

Attaching localization to the bot

After building the store, attach it with Bot.SetL10n(...).

bot.SetL10n(l10n)

From then on:

  • bot.L10n(lang, key) is available for manual lookups;
  • MessageContext.Translate(key) becomes the ergonomic handler-level helper.

If SetL10n(nil) is called, the bot logs a warning and keeps the existing localization provider unchanged.

MessageContext.Translate

ctx.Translate(key) is the usual choice inside handlers.

It:

  • reads the current user's language code from ctx.From;
  • falls back to the bot's configured fallback language when needed;
  • returns the key itself when no translation is available.

Example:

func start(ctx *laniakea.MessageContext, db *App) error {
	ctx.Answer(ctx.Translate("greeting"))
	return nil
}

One subtle point:

  • if ctx.From is nil, ctx.Translate(key) returns the key directly.

That can happen for some update shapes where there is no user object in context.

Direct lookups with bot.L10n(...)

Use bot.L10n(lang, key) when you need a translation outside handler flow or when the language choice is explicit.

Examples:

  • preparing background-runner messages;
  • rendering text for stored user preferences;
  • translating outside MessageContext.

Concurrency and mutation safety

L10n is safe for concurrent use.

The implementation protects access with a mutex, so concurrent translation reads and dictionary updates do not require external synchronization.

Also, AddDictEntry(...) copies the provided DictEntry map before storing it. That means later external mutation of your original map does not silently rewrite the localization store.

Organizing dictionaries

For small bots, a single l10n.go or translations.go file is often enough.

For larger bots, a good pattern is:

  • keep one section or file per domain, such as auth, menu, errors, admin;
  • keep keys stable and descriptive;
  • group related translations together;
  • use the same key names across all languages.

Example structure:

l10n.
	AddDictEntry("menu.start", laniakea.DictEntry{"en": "Start", "ru": "Старт"}).
	AddDictEntry("menu.help", laniakea.DictEntry{"en": "Help", "ru": "Помощь"}).
	AddDictEntry("errors.denied", laniakea.DictEntry{"en": "Access denied", "ru": "Доступ запрещен"})

Recommendations

  • Pick one fallback language and keep it complete.
  • Prefer stable semantic keys over sentence-as-key schemes.
  • Use ctx.Translate(...) in handlers unless you have a strong reason to do manual lookup.
  • Treat missing translations as a documentation problem and keep the fallback language authoritative.