Robust Multilingual Pattern in Copilot Studio: Preselected Supported Languages with Custom Regional Behaviors
Copilot Studio

Robust Multilingual Pattern in Copilot Studio: Preselected Supported Languages with Custom Regional Behaviors

By Elliot Margot•May 6, 2026•10 min
Copilot StudioMultilingualPower FxAdaptive CardsEnterprise
8
Languages
EN / FR / PT-BR / CS / ES / NL / DE / IT
1
Topic per intent
No duplication
0
Topics rewritten
Going from 4 to 8 langs
<1d
Time to add a 5th lang
Half-day in practice

For enterprise developers, the out-of-the-box translation capabilities of LLMs are often insufficient for production-grade agents. While generative AI can translate on the fly, enterprises require deterministic control over terminology, legal disclaimers, and brand voice across different locales.

The challenge is avoiding the maintenance nightmare: duplicating every topic for every language. If you have 10 topics and 4 languages, you do not want 40 topics to maintain.

This post outlines a scalable architecture for building a multilingual agent with runtime language switching. The pattern was hardened on a production deployment for a global industrial client (English, French, Portuguese-BR, Czech), and the demo agent that produced the screenshots in this post extends the same architecture to eight languages: English, French, Portuguese (BR), Czech, Spanish, Dutch, German, and Italian. Every screenshot below comes from a single agent, single topic per intent, single GPT prompt - no duplication.

Pro Tip

Scope of this pattern: this post covers one specific multilingual strategy - a preselected set of supported languages (English plus seven peers) with custom regional behaviors layered on top. It deliberately leaves space for two adjacent patterns: auto-detection of the user's language, and production-grade support for languages Copilot Studio does not officially list (an upcoming platform feature will make this easier).

1. Establishing the Single Source of Truth

The foundation of a multilingual bot is a single global variable that dictates the agent's behavior for the duration of the session: Global.UserLanguage.

Copilot Studio exposes System.User.Language as a read-only system variable populated from channel context. When you need explicit user choice (the recommended path), store it in a custom global named Global.UserLanguage - custom globals use a single identifier after the prefix, no dots.

Language Detection Strategies

You have three primary options for initializing this variable at the start of a session:

  1. User Profile (Office 365 Locale): Fast, but often inaccurate. Users may be logged into a US tenant but prefer French.
  2. URL Parameters: Excellent for embedded web chats where the parent page already knows the locale.
  3. Explicit Selection (Recommended): An Adaptive Card at the start of the conversation.

Why explicit selection? In enterprise environments, reliability beats magic. An explicit choice ensures the user is comfortable with the language and provides a clear trigger to set the variable. In Microsoft Teams specifically, the browser Accept-Language header is frequently overridden by the tenant locale setting - so a French user on a US-based tenant gets English. Do not rely on it.

Initialize all of this in the System - Conversation Start topic. Override the default to: (1) check if Global.UserLanguage is blank, (2) if blank, send the language selection Adaptive Card and capture the choice, (3) set Global.UserLanguage, (4) route to the actual greeting topic.

blog/multilingual-cs/img1-language-picker
The language picker Adaptive Card at conversation start. Buttons use messageBack so the selection is both displayed to the user and captured as a variable.

When the user selects a language from the Adaptive Card, add a Set Variable action node in your topic. Set the variable to Global.UserLanguage. Set the value field to the card's output. In Copilot Studio's Ask with Adaptive Card node, the captured response is exposed as Topic.AdaptiveCardOutput (or whatever output variable name you configured on the node). For a card with a selectedLanguage choice input, the expression is:

Topic.AdaptiveCardOutput.selectedLanguage

There is no Set() function in Copilot Studio topic authoring - that is Power Apps canvas syntax. Power Fx in CS appears only inside the value fields of action nodes.

Normalize Language Codes

Channels are inconsistent. Teams may send "fr-FR", web chat may send "fr", some legacy connectors send "fr_FR". Normalize at the top of Conversation Start before storing. Use System.User.Language as the source:

Switch(
    Lower(Substitute(System.User.Language, "_", "-")),
    "fr", "fr-FR",
    "fr-fr", "fr-FR",
    "pt", "pt-BR",
    "pt-br", "pt-BR",
    "cs", "cs-CZ",
    "cs-cz", "cs-CZ",
    "es", "es-ES",
    "es-es", "es-ES",
    "nl", "nl-NL",
    "nl-nl", "nl-NL",
    "de", "de-DE",
    "de-de", "de-DE",
    "it", "it-IT",
    "it-it", "it-IT",
    "en-US"
)

In the eight-language demo agent, the language picker uses readable identifiers (English, French, Portuguese_Brazilian, Czech, Spanish, Dutch, German, Italian) rather than locale codes - friendlier to bind into card buttons and to reference in Switch() expressions across topics. Pick the convention that fits your stack and stay consistent.

2. Language-Aware UI: Adaptive Cards and Dynamic Text

Once Global.UserLanguage is set, avoid hard-coding strings in message nodes.

The JSON Switch Pattern

For complex Adaptive Cards, do not translate individual fields with nested If statements inside the card designer. Store your card JSONs as variables and switch the entire payload.

Add a Set Variable node targeting Global.varCurrentCard. Use this expression in the value field:

Switch(
    Global.UserLanguage,
    "English", Global.varCard_EN,
    "French", Global.varCard_FR,
    "Portuguese_Brazilian", Global.varCard_PT,
    "Czech", Global.varCard_CS,
    "Spanish", Global.varCard_ES,
    "Dutch", Global.varCard_NL,
    "German", Global.varCard_DE,
    "Italian", Global.varCard_IT,
    Global.varCard_EN
)

Two production constraints: (1) card JSON variables must be Global-scoped to persist across topics; (2) Power Fx string variables have a length cap that complex cards will breach. For cards over the limit, fetch JSON from a Power Automate flow or Dataverse row instead of inlining as a variable.

Alternative: single card + OOB localization. The pattern above ships one payload per language. If your cards are mostly static text with a few dynamic values, you can author one card and let the localization file workflow handle the strings - using the SetTextVariable trick to mix static text with variables inside a translatable string. That approach is covered in The One Card: Build Once, Speak All Languages. Choose payload switching when each language needs a structurally different card (different fields, different layouts, different actions); choose single-card-plus-localization when the only thing that changes is the words.

For simple text blocks, inline Power Fx works fine - but with a critical caveat. In a SendActivity node, the activity: field does not evaluate Power Fx expressions. A =Switch(...) expression placed directly in activity: will render as raw text. The fix is a two-step pattern: compute the localized string in a SetVariable node (where Power Fx is evaluated), then reference the resulting variable from SendActivity:

- kind: SetVariable
  id: compute_greeting
  variable: Topic.Msg
  value: =Switch(Global.UserLanguage, "French", "Bonjour", "Portuguese_Brazilian", "Olá", "Czech", "Ahoj", "Spanish", "Hola", "Dutch", "Hallo", "German", "Hallo", "Italian", "Ciao", "Hello")
- kind: SendActivity
  id: greet
  activity: =Topic.Msg

This pattern is what unlocks clean per-language replies in every system topic (Greeting, Goodbye, ThankYou, Escalate, Fallback, StartOver, EndOfConversation, ResetConversation) of the eight-language demo agent without duplicating any of them.

blog/multilingual-cs/img2-french-it-card
After selecting French, the agent renders the per-language Adaptive Card with localized question buttons and greeting. The Switch() expression in a Set Variable node selected the correct card payload.

3. Topic-Level Routing vs. Topic Duplication

The anti-pattern: Creating Topic_Refund_EN, Topic_Refund_FR, Topic_Refund_PT, Topic_Refund_CS. This is an operational disaster. When a business rule changes, you update it in four places and miss at least one.

The recommended pattern: Conditional branching within a single topic.

For simple topics, use inline Switch as shown above. For complex topics with different business logic per region (different regulations, different backend calls), add a Condition node at the start:

Architecture
Topic StartSwitch onGlobal.UserLanguagefr-FRFR flowEU regulationsFR API endpointpt-BRPT flowBR regulationsBR API endpointcs-CZCZ flowEU regulationsCZ API endpointes-ESES flowEU regulationsES API endpointnl-NLNL flowEU regulationsNL API endpointde-DEDE flowEU regulationsDE API endpointit-ITIT flowEU regulationsIT API endpointdefaultEN flowGlobal API endpointCommon resolution pathDone
One topic, one Condition node on Global.UserLanguage, eight regional flows that converge on a shared resolution path.

This keeps logic centralized while allowing regional divergence where strictly necessary.

blog/multilingual-cs/img5-topic-canvas
The Conversation Start topic with a single Condition node checking whether Global.UserLanguage is blank - one topic handles all 8 languages.

Cross-Lingual Intent: Letting Generative Orchestration Bridge Locale Mismatches

If a user is in an English session but asks a question in Czech, the generative orchestration engine can identify the intent and trigger the correct topic regardless of what Global.UserLanguage is set to.

The strategy: Do not build keyword-based language detection topics. Let generative AI handle intent mapping. Once a topic is triggered, your Global.UserLanguage variable ensures the response is delivered in the user's preferred language.

Pro Tip

Generative orchestration resolves what the user wants. Deterministic variables control how it is presented. Keep those two concerns separate and you gain both flexibility and reliability.

blog/multilingual-cs/img3-french-gpt-response
The agent responds in French with structured troubleshooting steps. The GPT instructions bind to Global.UserLanguage as the single source of truth - no language detection in the response path.

Choosing Between Inline Switch() and the OOB Localization File

Both patterns are valid. Pick by audience and by what changes per language. The two compose well: a production agent can use the OOB localization file for in-topic message strings owned by translators, and Switch() for runtime branching, system topics, and payload selection.

Decision factorInline Switch() (this post)OOB localization file
What changes per languageLogic + textText only
Who owns the stringsThe builder (in the topic)A translation team (in a .resx-style file)
Source of truthTopic YAML (Git-diffable)Separate localization file
Runtime branching on other variables (role, channel, tenant)YesNo - substitution only
Languages outside the officially supported setWorks (any string identifier)Constrained to platform-supported locales
System topics, GPT instructions, conditional flowsSame Switch() shape everywhereMixed: file for messages, code for logic
Adaptive CardsSwitch entire payload per languageOne card + SetTextVariable (see The One Card)
The decision is not religious - it is about who owns which strings and which strings need to participate in conditional logic.

Lessons from Production: The 4-Language Industrial Bot

The architecture above was first deployed for a global industrial client requiring EN, FR, PT-BR, and CS support in a single agent. The demo agent that produced the screenshots in this post extends the same pattern to eight languages (EN, FR, PT-BR, CS, ES, NL, DE, IT) without adding a single duplicated topic. The cost of going from 4 to 8 languages was a few extra entries in each Switch() and a few extra card payloads - not a refactor.

  • Always define a fallback language. If a user provides an unsupported locale, default to English gracefully. An empty string in a card field is worse than the wrong language.
  • Test with real tenant configurations, not local browser settings. The most common bug in multilingual Teams bots is a US-tenant overriding all locale signals. Your test environment must mirror production.
  • Initialize all language card variables in Conversation Start. Storing each language's card JSON as a named global variable and initializing them centrally makes swapping content clean and testable. One author owns Conversation Start, translators own the card JSONs.
  • Czech is an edge case - and it has friends. Character encoding (diacritics) in adaptive card JSON strings requires careful escaping. Test ž, š, č explicitly. The same care applies to German umlauts (ä, ö, ü, ß), Spanish (ñ, á, é, í, ó, ú), and Portuguese (ã, ç, õ).
  • activity: does not evaluate Power Fx. If you see =Switch(...) rendered as plain text in your bot, you put the expression in SendActivity.activity instead of computing it in a preceding SetVariable.value. The fix is mechanical - lift the expression up.
  • Test all language paths via the test pane variable override. Set Global.UserLanguage to each target locale before triggering topics. The Copilot Studio Kit's automated test runner can do this systematically across all eight languages on every PR.

4. Session Management and Inactivity Reset

When the Reset Conversation system topic fires, all conversation-scoped variables are cleared. If a user returns after a break, Global.UserLanguage will be blank.

The pattern: Do not blindly clear everything. Instead:

  1. Clear transient variables (transaction IDs, search results, temp state).
  2. Check if Global.UserLanguage is blank.
  3. If blank, trigger the language selection Adaptive Card.
  4. If set, preserve it and greet the user in their language.
Do not store language in a persistent user-scoped variable unless you have a deliberate save preferences feature. Forcing a language from a session three months ago is jarring, especially for shared devices or travelling users.

Summary Checklist

AreaAnti-patternRecommended pattern
Topic structureDuplicate topics per languageSingle topic with Switch or Condition nodes
Language detectionBrowser / Teams localeExplicit Adaptive Card selection
UI contentHard-coded stringsPower Fx Switch or JSON payload switching
Intent handlingKeyword-based language routingGenerative orchestration + deterministic response
Session stateReset Conversation without language preservationSelective reset preserving Global.UserLanguage
PersistenceStore language in user-scoped varRe-ask at session restart
Six decisions that separate a maintainable multilingual agent from a duplication treadmill.
Share LinkedIn
Elliot Margot
Elliot Margot
Team Lead JumpStart - Copilot & Agents at Witivio. Microsoft AI Specialist & Power Platform Solutions Architect. Writing about Copilot Studio, multilingual agents, and enterprise AI delivery.
Connect on LinkedIn →