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:
- Add Startup Trigger
- 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"
}
}
- Add Set Variable for supported languages:
Variable: global.supported_languages
Value: ["en", "es", "fr", "de"]
- 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 keyparams(object, optional) — Variables for interpolation
Logic:
-
Add Transform to get user's language:
- Expression:
var.language ?? global.default_language - Save to
flow.user_lang
- Expression:
-
Add Transform to get translation:
global.translations[flow.user_lang][key] ?? global.translations[global.default_language][key] ?? key- Save to
flow.translated
- Save to
-
Add Condition:
params != nil
If has params:
- 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
- Add Command Trigger for
/start - Add Condition:
var.language != nil
If language not set:
-
Add Transform to detect from Telegram:
- Expression:
user.language_code ?? "en" - Save to
flow.detected_lang
- Expression:
-
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
-
Add Send Message:
🌍 Select Your Language / Seleccione su idioma / Choisissez votre langue -
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
-
Add Message Trigger for
lang_*callbacks -
Add Transform to extract language code:
- Expression:
replace(ctx.Input, "lang_", "") - Save to
flow.new_lang
- Expression:
-
Add Condition:
contains(global.supported_languages, flow.new_lang)
If valid:
-
Add Set Variable:
- Variable:
var.language - Value:
{{flow.new_lang}}
- Variable:
-
Add Edit Message with translated confirmation:
- Call
t("language_changed")
- Call
Result shows in the newly selected language.
- 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
-
After language is set, send welcome:
-
Add Transform for personalized welcome:
- Call subflow
t("welcome_name", {"name": user.first_name}) - Save to
flow.welcome_msg
- Call subflow
-
Add Send Message:
`{{flow.welcome_msg}}`
`{{t("help_intro")}}` -
Add Reply Markup with translated buttons:
{{t("main_menu")}}|menu{{t("settings")}}|settings
Step 3.2: Translated Main Menu
-
Add Message Trigger for
menucallback -
Add Send Message:
📋 `{{t("main_menu")}}` -
Dynamic buttons:
- 🛍️
{{t("shop")}}|shop - 👤
{{t("profile")}}|profile - ⚙️
{{t("settings")}}|settings
- 🛍️
Step 3.3: Settings with Language Option
-
Add Message Trigger for
settingscallback -
Add Send Message:
⚙️ `{{t("settings")}}`
`{{t("language")}}`: `{{global.translations[var.language].lang_flag}}` `{{global.translations[var.language].lang_name}}` -
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:
- 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
- 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:
- 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:
- 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
}
- Create subflow
getLocalizedField:
Inputs:
obj(object) — Object with language keysfield(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
- When building keyboards, translate each label:
// Instead of:
["Shop", "shop"]
// Use:
[t("shop"), "shop"]
- 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
-
Add Command Trigger for
/admin_languages -
Add Guard: Admin only
-
Add Transform to count by language:
reduce(
shared.all_users,
(acc, user) -> {
...acc,
[user.language]: (acc[user.language] ?? 0) + 1
},
{}
) -
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:
-
In the
t()subflow, if fallback was used:append(shared.missing_translations, {
"key": key,
"language": flow.user_lang,
"timestamp": now()
}) -
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:
- Add RTL flag to translations:
{
"ar": {
"lang_name": "العربية",
"lang_flag": "🇸🇦",
"rtl": true,
"welcome": "!مرحباً بك في البوت الخاص بنا",
...
}
}
-
Create subflow
isRTL:global.translations[var.language].rtl == true -
Adjust message formatting for RTL:
- Reverse list bullet direction
- Align text appropriately
Complete Architecture
Nodes Used
| Category | Nodes |
|---|---|
| Triggers | Command, Message/Callback, Startup |
| Actions | Send Message, Edit Message |
| Logic | Condition, Guard |
| Flow | Subflow, Ask |
| Data | Set Variable, Get, Filter, Map, Transform, Reduce |
Key Patterns
- Translation Subflow — Centralized
t()function - Fallback Chain — User lang → Default lang → Key
- Variable Interpolation —
{{name}}replacement - Language Persistence — Saved per user
- Analytics Tracking — Language usage stats
Tips
Get English + one other working first, then expand.
Don't put text directly in nodes — always use t("key").
Manually test every flow in every language.
Translations can be much longer in some languages — check UI doesn't break!
Next Steps
Congratulations! You've mastered advanced BotGami patterns:
- E-Commerce Bot → — Full shopping experience
- Booking System → — Appointment scheduling
- Best Practices → — More patterns