Skip to main content

Advanced: Multi-Language Bot

Build a fully internationalized bot that supports multiple languages with dynamic content switching, fallback handling, and language-specific formatting.


What We're Building

A professional multi-language bot that:

  • 🌍 Supports multiple languages (English, Spanish, French, etc.)
  • 💾 Saves user's language preference
  • 🔄 Switches languages on demand
  • 📝 Uses language-specific date/currency formats
  • 🔙 Falls back to default language for missing translations
  • 📊 Admin analytics for language usage

Part 1: Translation System Setup

Step 1.1: Define Translations

Create a Startup Trigger flow to initialize translations:

  1. Add Startup Trigger
  2. Add Set Variable for translations:

Variable: global.translations Value:

{
"en": {
"lang_name": "English",
"lang_flag": "🇬🇧",
"welcome": "Welcome to our bot!",
"welcome_name": "Welcome, `{{name}}`!",
"help_title": "Help & Commands",
"help_intro": "Here's what I can do:",
"settings": "Settings",
"language": "Language",
"change_language": "Change Language",
"language_changed": "Language changed to English!",
"select_language": "Please select your language:",
"main_menu": "Main Menu",
"back": "Back",
"cancel": "Cancel",
"confirm": "Confirm",
"yes": "Yes",
"no": "No",
"loading": "Loading...",
"error_generic": "Something went wrong. Please try again.",
"error_not_found": "Not found",
"profile": "My Profile",
"profile_name": "Name",
"profile_language": "Language",
"profile_joined": "Joined",
"shop": "Shop",
"cart": "Cart",
"checkout": "Checkout",
"order_placed": "Your order has been placed!",
"currency_symbol": "$",
"date_format": "January 2, 2006"
},
"es": {
"lang_name": "Español",
"lang_flag": "🇪🇸",
"welcome": "¡Bienvenido a nuestro bot!",
"welcome_name": "¡Bienvenido, `{{name}}`!",
"help_title": "Ayuda y Comandos",
"help_intro": "Esto es lo que puedo hacer:",
"settings": "Configuración",
"language": "Idioma",
"change_language": "Cambiar Idioma",
"language_changed": "¡Idioma cambiado a Español!",
"select_language": "Por favor selecciona tu idioma:",
"main_menu": "Menú Principal",
"back": "Atrás",
"cancel": "Cancelar",
"confirm": "Confirmar",
"yes": "Sí",
"no": "No",
"loading": "Cargando...",
"error_generic": "Algo salió mal. Por favor intenta de nuevo.",
"error_not_found": "No encontrado",
"profile": "Mi Perfil",
"profile_name": "Nombre",
"profile_language": "Idioma",
"profile_joined": "Registrado",
"shop": "Tienda",
"cart": "Carrito",
"checkout": "Pagar",
"order_placed": "¡Tu pedido ha sido realizado!",
"currency_symbol": "€",
"date_format": "2 de January de 2006"
},
"fr": {
"lang_name": "Français",
"lang_flag": "🇫🇷",
"welcome": "Bienvenue sur notre bot!",
"welcome_name": "Bienvenue, `{{name}}`!",
"help_title": "Aide et Commandes",
"help_intro": "Voici ce que je peux faire:",
"settings": "Paramètres",
"language": "Langue",
"change_language": "Changer de Langue",
"language_changed": "Langue changée en Français!",
"select_language": "Veuillez sélectionner votre langue:",
"main_menu": "Menu Principal",
"back": "Retour",
"cancel": "Annuler",
"confirm": "Confirmer",
"yes": "Oui",
"no": "Non",
"loading": "Chargement...",
"error_generic": "Quelque chose s'est mal passé. Veuillez réessayer.",
"error_not_found": "Non trouvé",
"profile": "Mon Profil",
"profile_name": "Nom",
"profile_language": "Langue",
"profile_joined": "Inscrit",
"shop": "Boutique",
"cart": "Panier",
"checkout": "Paiement",
"order_placed": "Votre commande a été passée!",
"currency_symbol": "€",
"date_format": "2 January 2006"
},
"de": {
"lang_name": "Deutsch",
"lang_flag": "🇩🇪",
"welcome": "Willkommen bei unserem Bot!",
"welcome_name": "Willkommen, `{{name}}`!",
"help_title": "Hilfe & Befehle",
"help_intro": "Folgendes kann ich tun:",
"settings": "Einstellungen",
"language": "Sprache",
"change_language": "Sprache ändern",
"language_changed": "Sprache auf Deutsch geändert!",
"select_language": "Bitte wählen Sie Ihre Sprache:",
"main_menu": "Hauptmenü",
"back": "Zurück",
"cancel": "Abbrechen",
"confirm": "Bestätigen",
"yes": "Ja",
"no": "Nein",
"loading": "Laden...",
"error_generic": "Etwas ist schief gelaufen. Bitte versuchen Sie es erneut.",
"error_not_found": "Nicht gefunden",
"profile": "Mein Profil",
"profile_name": "Name",
"profile_language": "Sprache",
"profile_joined": "Beigetreten",
"shop": "Shop",
"cart": "Warenkorb",
"checkout": "Kasse",
"order_placed": "Ihre Bestellung wurde aufgegeben!",
"currency_symbol": "€",
"date_format": "2. January 2006"
}
}
  1. Add Set Variable for supported languages:

Variable: global.supported_languages Value: ["en", "es", "fr", "de"]

  1. Add Set Variable for default language:

Variable: global.default_language Value: "en"


Step 1.2: Create Translation Subflow

Create a reusable "Translate" subflow:

Subflow Name: t (short for translate)

Inputs:

  • key (string) — Translation key
  • params (object, optional) — Variables for interpolation

Logic:

  1. Add Transform to get user's language:

    • Expression: var.language ?? global.default_language
    • Save to flow.user_lang
  2. Add Transform to get translation:

    global.translations[flow.user_lang][key] ?? global.translations[global.default_language][key] ?? key
    • Save to flow.translated
  3. Add Condition: params != nil

If has params:

  1. Add Transform to interpolate:
    reduce(
    entries(params),
    (acc, entry) -> replace(acc, "`{{" + entry[0] + "}}`", string(entry[1])),
    flow.translated
    )

Output: The translated string


Part 2: Language Detection & Selection

Step 2.1: Detect Language on First Visit

  1. Add Command Trigger for /start
  2. Add Condition: var.language != nil

If language not set:

  1. Add Transform to detect from Telegram:

    • Expression: user.language_code ?? "en"
    • Save to flow.detected_lang
  2. Add Condition: contains(global.supported_languages, flow.detected_lang)

If supported:

  • Set var.language = flow.detected_lang
  • Show welcome in detected language

If not supported:

  • Show language picker

Step 2.2: Language Picker

  1. Add Send Message:

    🌍 Select Your Language / Seleccione su idioma / Choisissez votre langue
  2. Generate language buttons from global.supported_languages:

Reply Markup (generated dynamically):

  • 🇬🇧 English | lang_en
  • 🇪🇸 Español | lang_es
  • 🇫🇷 Français | lang_fr
  • 🇩🇪 Deutsch | lang_de

Step 2.3: Handle Language Selection

  1. Add Message Trigger for lang_* callbacks

  2. Add Transform to extract language code:

    • Expression: replace(ctx.Input, "lang_", "")
    • Save to flow.new_lang
  3. Add Condition: contains(global.supported_languages, flow.new_lang)

If valid:

  1. Add Set Variable:

    • Variable: var.language
    • Value: {{flow.new_lang}}
  2. Add Edit Message with translated confirmation:

    • Call t("language_changed")

Result shows in the newly selected language.

  1. Log language change for analytics:
    append(shared.language_changes, {
    "user_id": user.id,
    "from": var.language ?? "none",
    "to": flow.new_lang,
    "timestamp": now()
    })

Part 3: Using Translations Throughout

Step 3.1: Translated Welcome Message

  1. After language is set, send welcome:

  2. Add Transform for personalized welcome:

    • Call subflow t("welcome_name", {"name": user.first_name})
    • Save to flow.welcome_msg
  3. Add Send Message:

    `{{flow.welcome_msg}}`

    `{{t("help_intro")}}`
  4. Add Reply Markup with translated buttons:

    • {{t("main_menu")}} | menu
    • {{t("settings")}} | settings

Step 3.2: Translated Main Menu

  1. Add Message Trigger for menu callback

  2. Add Send Message:

    📋 `{{t("main_menu")}}`
  3. Dynamic buttons:

    • 🛍️ {{t("shop")}} | shop
    • 👤 {{t("profile")}} | profile
    • ⚙️ {{t("settings")}} | settings

Step 3.3: Settings with Language Option

  1. Add Message Trigger for settings callback

  2. Add Send Message:

    ⚙️ `{{t("settings")}}`

    `{{t("language")}}`: `{{global.translations[var.language].lang_flag}}` `{{global.translations[var.language].lang_name}}`
  3. Add Reply Markup:

    • 🌍 {{t("change_language")}} | change_language
    • 🔙 {{t("back")}} | menu

Part 4: Language-Specific Formatting

Step 4.1: Currency Formatting

Different languages may use different currencies or formats:

  1. Create subflow formatCurrency:

Inputs: amount (number)

Logic:

flow.symbol = global.translations[var.language].currency_symbol ?? "$"
flow.symbol + string(amount)

Usage:

`{{formatCurrency(product.price)}}`  
// English: $99.99
// Spanish: €99.99

Step 4.2: Date Formatting

  1. Create subflow formatDate:

Inputs: date (datetime)

Logic:

flow.format = global.translations[var.language].date_format ?? "January 2, 2006"
format(date, flow.format)

Usage:

`{{formatDate(order.created_at)}}`
// English: December 22, 2024
// German: 22. December 2024

Step 4.3: Pluralization

Handle plurals correctly per language:

  1. Create subflow pluralize:

Inputs:

  • count (number)
  • singular (string)
  • plural (string)

Logic:

count == 1 ? singular : plural

For more complex languages (like Russian with 3 plural forms), extend with language-specific rules.

Usage:

`{{pluralize(cart_count, t("item"), t("items"))}}`
// 1 item / 2 items
// 1 artículo / 2 artículos

Part 5: Dynamic Content Translation

Step 5.1: Product Catalog with Translations

For products that have translations:

  1. Extend product structure:
{
"id": "prod_001",
"name": {
"en": "Wireless Headphones",
"es": "Auriculares Inalámbricos",
"fr": "Écouteurs Sans Fil",
"de": "Kabellose Kopfhörer"
},
"description": {
"en": "Premium sound quality",
"es": "Calidad de sonido premium",
"fr": "Qualité sonore premium",
"de": "Premium-Klangqualität"
},
"price": 79.99
}
  1. Create subflow getLocalizedField:

Inputs:

  • obj (object) — Object with language keys
  • field (string) — Field name

Logic:

obj[field][var.language] ?? obj[field][global.default_language] ?? obj[field]

Usage:

`{{getLocalizedField(product, "name")}}`
`{{getLocalizedField(product, "description")}}`

Step 5.2: Dynamic Keyboard Labels

  1. When building keyboards, translate each label:
// Instead of:
["Shop", "shop"]

// Use:
[t("shop"), "shop"]
  1. Create subflow buildLocalizedKeyboard:

Inputs: buttons (array of {key: string, callback: string})

Logic:

map(buttons, b -> [t(b.key), b.callback])

Part 6: Admin Language Analytics

Step 6.1: Language Distribution

  1. Add Command Trigger for /admin_languages

  2. Add Guard: Admin only

  3. Add Transform to count by language:

    reduce(
    shared.all_users,
    (acc, user) -> {
    ...acc,
    [user.language]: (acc[user.language] ?? 0) + 1
    },
    {}
    )
  4. Add Send Message:

📊 Language Distribution

`{{range entries(flow.language_counts)}}`
`{{global.translations[.key].lang_flag}}` `{{global.translations[.key].lang_name}}`: `{{.value}}` users (`{{percent(.value, total)}}`%)
`{{end}}`

Total Users: `{{total}}`

Step 6.2: Missing Translations Report

Track when fallback is used:

  1. In the t() subflow, if fallback was used:

    append(shared.missing_translations, {
    "key": key,
    "language": flow.user_lang,
    "timestamp": now()
    })
  2. Admin command /missing_translations:

    📝 Missing Translations

    `{{range unique(shared.missing_translations, "key")}}`
    • "`{{.key}}`" missing in: `{{join(languages_missing(.key), ", ")}}`
    `{{end}}`

Part 7: Right-to-Left (RTL) Support

Step 7.1: Add RTL Languages

For languages like Arabic or Hebrew:

  1. Add RTL flag to translations:
{
"ar": {
"lang_name": "العربية",
"lang_flag": "🇸🇦",
"rtl": true,
"welcome": "!مرحباً بك في البوت الخاص بنا",
...
}
}
  1. Create subflow isRTL:

    global.translations[var.language].rtl == true
  2. Adjust message formatting for RTL:

    • Reverse list bullet direction
    • Align text appropriately

Complete Architecture


Nodes Used

CategoryNodes
TriggersCommand, Message/Callback, Startup
ActionsSend Message, Edit Message
LogicCondition, Guard
FlowSubflow, Ask
DataSet Variable, Get, Filter, Map, Transform, Reduce

Key Patterns

  1. Translation Subflow — Centralized t() function
  2. Fallback Chain — User lang → Default lang → Key
  3. Variable Interpolation{{name}} replacement
  4. Language Persistence — Saved per user
  5. Analytics Tracking — Language usage stats

Tips

Start with 2 Languages

Get English + one other working first, then expand.

Use Translation Keys

Don't put text directly in nodes — always use t("key").

Test Each Language

Manually test every flow in every language.

Watch String Length

Translations can be much longer in some languages — check UI doesn't break!


Next Steps

Congratulations! You've mastered advanced BotGami patterns: