Last month I was on a call with a customer. Mid-size company, headquarters in Zurich, offices in Paris, Munich, and Toronto. Fourteen languages across the org. Their IT lead said something that stuck with me: “We built the onboarding buddy agent. It’s amazing. But half the company can’t use it because it only speaks English.”
That hit hard. You build this slick declarative agent that helps new hires navigate benefits, policies, and first-week logistics - and then you realize your German-speaking colleagues in Munich are getting instructions that feel like they were written by a robot doing a bad impression of a human. Because they were.
So together we fixed it. And I want to show you exactly how.
The Localization Architecture
Here’s the thing most people get wrong about localizing a declarative agent: they think it’s just about translating strings. It’s not. The M365 Copilot extensibility model gives you a proper localization architecture, and it has three layers:
- Tokenized manifest strings - Your declarative agent manifest uses placeholder tokens (
[[key]]) instead of hardcoded text - Language resource files - JSON files that map those tokens to actual strings in each language, using a specific schema with required fields
- localizationInfo in the app manifest - The Teams/M365 app manifest tells the platform which languages you support and where to find the resource files
This means Copilot can serve the right agent name, description, and conversation starters to each user based on their language preference. No runtime translation. No hacks. Just clean, static localization baked into the package.
Localization applies to the agent’s metadata - name, description, and conversation starters. The agent’s instructions are a separate concern that we’ll cover in the next section.
Tokenizing the Manifest
Let’s build this out with the Onboarding Buddy. First, the declarative agent manifest. Instead of writing strings directly, you use tokens wrapped in double square brackets:
{
"$schema": "https://developer.microsoft.com/json-schemas/copilot/declarative-agent/v1.4/schema.json",
"version": "v1.4",
"name": "[[AGENT_NAME]]",
"description": "[[AGENT_DESCRIPTION]]",
"instructions": "You are the Onboarding Buddy. Help new employees navigate their first days...",
"conversation_starters": [
{
"text": "[[STARTER_BENEFITS]]",
"title": "[[STARTER_BENEFITS_TITLE]]"
},
{
"text": "[[STARTER_FIRST_WEEK]]",
"title": "[[STARTER_FIRST_WEEK_TITLE]]"
},
{
"text": "[[STARTER_POLICIES]]",
"title": "[[STARTER_POLICIES_TITLE]]"
}
]
}
Now the language files. Each locale gets its own JSON file in your app package. The format is specific: it requires a $schema reference, the standard app manifest fields (name.short, name.full, description.short, description.full), and a localizationKeys object for all your agent-specific tokens.
Here’s the English one (en.json):
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.Localization.schema.json",
"name.short": "Onboarding Buddy",
"name.full": "Onboarding Buddy",
"description.short": "Your guide to getting started at Contoso.",
"description.full": "Your friendly guide to getting started at Contoso. Ask about benefits, policies, or your first week.",
"localizationKeys": {
"AGENT_NAME": "Onboarding Buddy",
"AGENT_DESCRIPTION": "Your friendly guide to getting started at Contoso. Ask about benefits, policies, or your first week.",
"STARTER_BENEFITS": "What health insurance options do I have?",
"STARTER_BENEFITS_TITLE": "Benefits overview",
"STARTER_FIRST_WEEK": "What should I do on my first day?",
"STARTER_FIRST_WEEK_TITLE": "First week guide",
"STARTER_POLICIES": "Where can I find the company leave policy?",
"STARTER_POLICIES_TITLE": "Company policies"
}
}
And the French version (fr.json):
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.Localization.schema.json",
"name.short": "Copain d'Intégration",
"name.full": "Copain d'Intégration",
"description.short": "Votre guide pour bien démarrer chez Contoso.",
"description.full": "Votre guide pour bien démarrer chez Contoso. Posez vos questions sur les avantages, les politiques ou votre première semaine.",
"localizationKeys": {
"AGENT_NAME": "Copain d'Intégration",
"AGENT_DESCRIPTION": "Votre guide pour bien démarrer chez Contoso. Posez vos questions sur les avantages, les politiques ou votre première semaine.",
"STARTER_BENEFITS": "Quelles sont mes options d'assurance santé ?",
"STARTER_BENEFITS_TITLE": "Aperçu des avantages",
"STARTER_FIRST_WEEK": "Que dois-je faire lors de mon premier jour ?",
"STARTER_FIRST_WEEK_TITLE": "Guide de la première semaine",
"STARTER_POLICIES": "Où puis-je trouver la politique de congés ?",
"STARTER_POLICIES_TITLE": "Politiques de l'entreprise"
}
}
Finally, wire it all up in your Teams app manifest with localizationInfo:
{
"localizationInfo": {
"defaultLanguageTag": "en",
"defaultLanguageFile": "en.json",
"additionalLanguages": [
{
"languageTag": "fr",
"file": "fr.json"
},
{
"languageTag": "de",
"file": "de.json"
}
]
}
}
When a French-speaking employee opens the agent, they see “Copain d’Intégration” with French conversation starters. No code. No runtime logic. The platform handles it.
Use the Microsoft 365 Agents Toolkit in VS Code to scaffold your localization files. It generates the token structure and language file templates for you, so you don’t have to wire everything by hand.
Instructions Need Cultural Adaptation, Not Just Translation
This is where most teams stumble. Instructions can’t be tokenized through the localization mechanism - only the agent’s name, description, and conversation starters are localizable fields. So your agent has a single instruction set. That’s actually fine, because the right approach isn’t separate instruction strings per language. It’s writing instructions that are language-aware from the start.
Here’s the thing: German business communication is fundamentally different from English or French. The customer I was working with learned this the hard way. Their English instructions said:
“Be friendly and casual. Use the employee’s first name. Keep it light.”
They translated that prompt into German. Literally. And the German employees were… uncomfortable. In German business culture, the formal “Sie” form is the default. Using “du” (informal you) with someone you’ve never met? That’s not friendly - it’s presumptuous.
The fix is to write a single, multilingual-aware instruction set that handles cultural nuances explicitly:
You are the Onboarding Buddy. Help new employees navigate their first days.
Always respond in the user's language.
Match your communication style to the user's locale:
- For English and French users: use a friendly, casual tone.
Use the employee's first name.
- For German users: use formal address (Sie) and prefer
structured, clear responses over conversational ones.
See the difference? You’re not shipping separate instruction files - you’re writing instructions that adapt. The model handles the rest.
Don’t just run your English instructions through a translation service. Agent instructions set the personality of your agent. A badly localized personality feels wrong in ways users can’t always articulate - they just stop using the agent.
Here’s my practical advice: for each target language, sit down with a native speaker who works in that business culture. Walk through the instructions together. Ask them: “Does this feel natural? Would you talk to a new colleague this way?” If the answer is no, rewrite it.
Grounding Documents in Multiple Languages
Now here’s where it gets really interesting. Your declarative agent is grounded in SharePoint content - onboarding guides, policy documents, benefits summaries. But those documents are in English.
The good news: SharePoint has built-in multilingual page support. You can create a page in English, then create translation copies in French, German, and any other language. SharePoint keeps them linked as variants of the same content.
When your agent is grounded on a SharePoint site that uses multilingual pages, Copilot can surface content in the user’s preferred language - if the translated pages exist. The agent doesn’t do the translation. SharePoint provides the content, and the grounding engine finds the right version.
Here’s the practical setup:
- Enable multilingual features on your SharePoint communication site
- Create your source pages in the default language (English)
- Create translation pages for each supported language
- Point your agent’s grounding at the site, not individual pages
{
"capabilities": [
{
"name": "OneDriveAndSharePoint",
"items_by_url": [
{
"url": "https://contoso.sharepoint.com/sites/hr-onboarding"
}
]
}
]
}
If a translated page doesn’t exist for a given language, Copilot will fall back to the default language version. This means your German users might get English content for pages that haven’t been translated yet. Audit your content coverage before launching the localized agent.
Gotchas and Tips
After rolling this out (across this customer and a few other orgs since), here are the things I wish someone had told me:
Test with actual users in each locale. Don’t just verify the strings render - watch someone use the agent in their language. You’ll catch tone issues, awkward phrasing, and missing conversation starters that automated testing never will.
Conversation starters are often overlooked. Teams translate the name and description but forget to localize the conversation starters. Users see “Copain d’Intégration” as the agent name, then English prompts below it. That’s jarring. Localize everything in the manifest.
A few more quick hits:
- Language fallback order matters. If you support
frbut a user’s locale isfr-ca(Canadian French), the platform may fall back to the default language. Consider adding broad locale support. - Keep your token keys in English. Even in the German language file, the keys should be
AGENT_NAME, notAGENT_BEZEICHNUNG. Consistency across files makes maintenance much easier. - Version your language files. When you update the English instructions, it’s easy to forget to update the French and German ones. Use a simple tracking spreadsheet or, better yet, a CI check that flags missing keys across locale files.
- Don’t localize the schema or structure. Only the values get translated - the JSON structure, schema references, and capability names stay in English.
Resources
Here are the official docs to get you started:
Have questions or want to share what you're building? Connect with me on LinkedIn or check out more on The Manifest.