1. Why think about localization specifically in the ChatGPT App
If you’ve built typical web apps before, you probably associate localization with classic i18n: interface strings, a couple of date and number formats, dictionaries — and you carefully translate everything. In a ChatGPT App things get more interesting: there’s a third participant — the LLM itself. It reads your tool descriptions, prompts, and results, draws conclusions, and makes decisions.
In other words, language is not only about “how nicely to show text to the user,” but also “how the model will understand what your tool does, when to call it, and what arguments to pass.” You can translate a “Buy” button so‑so — the user will figure it out. But if you vaguely describe a tool that performs a payment in a mix of Russian and English, the model may either never call it or call it in a way you didn’t expect.
One more point: ChatGPT already passes locale and user location hints to your MCP server — _meta["openai/locale"] and _meta["openai/userLocation"]. This happens at the level of MCP requests to tools so you can adapt text and data to the user’s language and region. The platform already “feeds” you this context; the developer’s job is to make good use of it.
Therefore, in this module we look at localization as a first‑class architectural aspect of a ChatGPT App, not as “we translated the UI and forgot about it.”
2. Layers that need localization
Let’s view the App as a layer cake. Each layer can (and often should) be localized. To stay on top of things, let’s start with a map.
Overview of layers
First — a high‑level table, then we’ll break it down.
| Layer | What it is | Examples | Impact |
|---|---|---|---|
| Widget UI | All visible frontend in the widget | Headings, buttons, errors, hints | User UX |
| Model texts and prompts | System prompt and canned phrases | Instructions, answer templates | ChatGPT behavior |
| Data and content | Texts the App shows and processes | Product catalogs, descriptions, dates, prices | Both UX and answer accuracy |
| Tool/schema descriptions | Tool metadata and JSON Schema field descriptions | description, type hints | How the model calls your tools |
| Commerce and legal | Everything related to purchases and policies | SKU names, Terms, Privacy, emails | Legal correctness, trust |
This is effectively the first layer of our localization map; later we’ll add depth (cosmetics/semantics), languages, and specific elements.
Now let’s go through the layers one by one.
Widget UI
The most obvious layer is the widget interface. In GiftGenius, this includes:
- section headings;
- field labels (“Recipient,” “Budget,” “Interests”);
- buttons (“Find a gift,” “Reset filters”);
- input placeholders (“e.g., colleague, mom…”);
- error messages and empty states (“No gifts found”).
In a typical React app these are the first candidates to extract into dictionaries. Same here — with the caveat that the UI is not the whole App, just one of its faces.
A bit later in the module we’ll build a proper i18n architecture for the widget, but even now it’s important to lock in: there should be no hardcoded strings in JSX. Even if you currently support one language, it’s convenient to structure UI text from the start.
A mini example with our GiftGenius (without a real i18n library yet):
type Locale = "en" | "ru";
const uiText = {
en: {
title: "GiftGenius: find a perfect present",
recipientLabel: "Recipient",
},
ru: {
title: "GiftGenius: pick the perfect present",
recipientLabel: "Recipient",
},
};
function GiftForm({ locale }: { locale: Locale }) {
const t = uiText[locale];
return (
<div>
<h2>{t.title}</h2>
<label>{t.recipientLabel}</label>
{/* other fields */}
</div>
);
}
We’re not doing “real” localization yet, but we’re already carving out the UI‑text layer explicitly.
GPT texts and prompts
The next layer is system and helper texts that aren’t directly visible to the user but strongly influence the model’s behavior:
- your App’s system prompt (“You are a gift‑selection assistant…”);
- explanation templates you give the model (“Produce a brief summary of the choice”);
- prewritten follow‑ups and hints for the model (“ask the user to clarify the budget if…”).
These texts can (and often should) be localized too. A simple example: if the user writes in Russian while your system prompt is entirely in English, the model will cope, but you lose precise control over tone and wording for that language.
Later, in the lectures on localizing prompts and descriptions, we’ll look at how to carefully play with multilingual system prompts. For now it’s important to state: prompts are just as localizable a layer as the UI.
Data and content
Next come your data. For GiftGenius that’s the gift catalog: names, descriptions, categories, sometimes tips on how to use the gift. For a commercial App it’s also prices, currencies, units, date/number formats, etc. In product feed specs for ChatGPT (the format in which you describe your products and services for the platform), these text fields (title, description) and prices are called out explicitly so they can be shown correctly to users inside ChatGPT.
If you want a global App, your gift catalog raises at least these questions:
- do we store names/descriptions in multiple languages;
- how do we choose which language to return to the user;
- what do we do if a translation is missing (fallback);
- how do we show currencies and date/price formats for different regions.
A small typed example for the catalog:
type Locale = "en" | "ru";
interface LocalizedString {
en: string;
ru: string;
}
interface Gift {
id: string;
title: LocalizedString;
description: LocalizedString;
priceCents: number;
currency: "USD" | "EUR" | "RUB";
}
function getLocalizedTitle(gift: Gift, locale: Locale) {
return gift.title[locale] ?? gift.title.en;
}
So localization isn’t just frontend — it’s also the data structure in your DB and MCP resources. We’ll return to this when we talk about the Gateway (the bridge between ChatGPT and your services) and the MCP server.
Tool descriptions and JSON Schema
The fourth layer is descriptions of tools and their arguments. That’s exactly how the model understands when to call your tool and what arguments to pass. In MCP these are the tool’s title, description, and the fields’ description in JSON Schema.
The Apps SDK docs emphasize that the model uses names, descriptions, and parameter docs to choose tools and build arguments.
A hypothetical GiftGenius tool in a TypeScript MCP server:
server.registerTool(
"suggest_gifts",
{
title: "Suggest gifts",
description: "Suggest 3–5 gift ideas based on recipient profile.",
inputSchema: {
type: "object",
properties: {
recipient: {
type: "string",
description: "Who is the gift for (e.g. mother, colleague)?",
},
},
required: ["recipient"],
},
},
async ({ input }) => { /* ... */ }
);
Right now everything is in English — the model understands it just fine. But what if the user writes in Russian? It can still map “mom” to recipient, but with complex fields and domain terms the chance of a miss goes up. In the lecture on localization strategies for descriptions we’ll discuss: a single English description vs localized descriptions.
At this stage in the localization map it’s important to note: tool and JSON Schema descriptions can also be localized — and that affects the model’s behavior.
Commerce and legal
Finally, the layer people often remember last — everything related to money and legal texts:
- SKU and subscription plan names;
- title/description fields in commerce feeds (products, services, subscriptions);
- Terms of Service, Privacy Policy, Refund Policy;
- emails and notifications (email, push) if the App sends anything outside ChatGPT;
- order statuses and payment errors you show the user (“Payment declined,” “Not available in your region”).
There are two aspects here: UX and law. The user must understand what they agree to and what they pay for — in their language. At the same time translations must be legally correct: sometimes legal requires that only one language (e.g., English) is legally binding, and other translations are “for reference.”
In our localization map we explicitly mark commerce and legal content as a separate layer because it often requires a different process (legal, compliance, marketing approvals).
3. Depth of localization: “cosmetics” vs “semantics”
When we say “localize the App,” it’s useful to distinguish two levels of depth: cosmetic and semantic.
Cosmetic localization
Cosmetics are everything that changes how things look and read but barely changes system behavior. Examples:
- translated headings and button labels;
- translated placeholders in inputs;
- “human” UI error messages;
- a localized marketing banner in the widget.
For classic web apps, this is often where teams stop. In a ChatGPT App it’s important — but just the tip of the iceberg.
Semantic localization
Semantics are things that change the model’s behavior and the App’s logic. Here, language affects:
- which tool the model chooses;
- how it fills tool arguments;
- which data it considers “correct” for this user.
Examples of semantic localization:
- a system prompt in the user’s language that sets tone and interaction rules;
- tool and field descriptions in the language the user is speaking;
- different hints/instructions depending on cultural context;
- date/currency format settings that affect parsing and generation (31.12.2025 vs 12/31/2025).
If you localized only cosmetics but not semantics, your App may look localized but behave like an “English‑first” app under the hood. In the localization map it’s useful to explicitly mark elements that are critical to model behavior.
For our GiftGenius, for example:
- the budget field description in JSON Schema (“Budget in the user’s currency”) — semantics;
- the “Find a gift” button label — cosmetics (important for UX, but the model doesn’t see it).
Now that we distinguish cosmetics and semantics, it’s logical to answer: how many languages do you want your App to actually work in?
4. Single‑language vs multilingual ChatGPT App
Before drawing the map, clarify your ambition: are you building a strictly single‑language App, or are you targeting a multilingual audience?
Single‑language App
A single‑language App is where you deliberately support just one language. For example, English only.
The UI widget, prompts, tool descriptions, and data — all in one language. This greatly simplifies things:
- a single codebase without branching on language;
- a single catalog schema (no title_en, title_ru, etc.);
- easier to maintain and test.
But, clearly, the audience is limited. In the case of a ChatGPT App, it also means: if a user comes with another locale, ChatGPT can still show your App, but it will constantly have to “map” the user’s language to the App’s internal language. In some niches that’s fine, but for a mass consumer gift service — unlikely.
Multilingual App
A multilingual App is already an architectural decision. Here:
- the UI and text are shown correctly based on the user’s locale;
- data (catalogs, product descriptions) are also tied to language/region;
- tool descriptions and system prompts may vary by language;
- commerce scenarios account for local currencies, taxes, and restrictions.
In this case, sprinkling if (locale === "ru") throughout the codebase is clearly not enough. You need an architecture: dictionaries, localizable resources, a single place where locale and userLocation are stored and processed, and agreements between the widget and the MCP server.
The Apps SDK docs explicitly emphasize that ChatGPT passes you locale and userLocation in _meta when invoking your tools — specifically so you can choose the right language and data format on the server. This is the “fuel” for multilingual Apps.
A small comparison
For clarity — a mini comparison:
| Characteristic | Single‑language App | Multilingual App |
|---|---|---|
| Code size | Smaller | Larger (dictionaries, selection logic) |
| Audience reach | Limited | Global |
| Testing complexity | Lower | Higher |
| Commerce/legal handling | Simpler | Requires processes and legal |
| GPT behavior | Single‑language prompt | Multilingual prompts/descriptions |
At the course level, we’ll assume GiftGenius becomes multilingual (at least EN/RU) to show a “grown‑up” scheme. But many techniques will also help a clean single‑language App if you want to be ready to expand.
5. Where language really affects the model
Now let’s highlight the points where language directly affects ChatGPT’s behavior.
User input language vs tool description language
Imagine:
- the user writes: “Pick a gift for a colleague for 50 euros”;
- your suggest_gifts tool is described only in English;
- schema fields: recipient, budget, currency, interests.
The model must:
- decide that it should call suggest_gifts at all;
- extract recipient = "colleague", budget = 50, currency = "EUR";
- serialize this correctly into JSON arguments.
If descriptions are brief and in another language, the model can handle it, but the probability of filling fields incorrectly is higher. For example, it might confuse budget and price_limit or pass text into the interests field because the field description said something vague like “Any extra info about the gift.”
For Russian user text and English descriptions, the model is also constantly “jumping” between languages.
A variant with a localized schema:
const locale = _meta?.["openai/locale"] ?? "en"; // comes from ChatGPT
const isRu = locale.startsWith("ru");
server.registerTool(
"suggest_gifts",
{
title: isRu ? "Gift selection" : "Suggest gifts",
description: isRu
? "Pick 3–5 gift ideas based on the recipient’s profile."
: "Suggest 3–5 gift ideas based on recipient profile.",
inputSchema: { /* ... */ },
},
async ({ input }) => { /* ... */ }
);
This is simplified: in reality you should generate descriptions once at server startup rather than on every call, but the idea is clear. We can return different descriptions to ChatGPT depending on the locale to make it easier for the model to understand the user.
Data language vs request language
If your gift catalog is only in English while the user interacts in Russian, the model will surface English names and descriptions. Sometimes that’s okay, sometimes not. More importantly — how you format the output:
- do you show users the original titles/descriptions from the server,
- or does the model paraphrase them in the user’s language in its own text,
- or does your tool itself return already localized text based on locale.
In the Apps SDK, structured content (structured data returned by tools) and the textual answer can live separately. You can return structured data (e.g., JSON with product fields) and a separate user‑facing text, and the model will decide how to render or paraphrase it.
Localization can occur at the server level (data) or at the model level (rephrasing in the needed language). When composing the map, decide where you want the “source of truth” to live.
System prompt and follow‑ups
If your system prompt is only in English while the user speaks Russian, the model will constantly balance two languages. That can be acceptable, but sometimes you want to set tone strictly: for instance, a more informal style in the Russian version and a more formal one in English.
Therefore, in the localization map, mark:
- system prompt EN;
- system prompt RU;
- follow‑up templates (EN/RU);
- any “hard” hints in tool prompts.
6. Localization map for GiftGenius
Now let’s gather everything we’ve discussed about layers, depth, and languages into an explicit localization map for GiftGenius. Let’s do what you’ll later do for your own App: build a localization map. The idea is simple: a table with layers and entity types as columns and specific elements as rows.
Example map
Here’s a simplified map for GiftGenius (EN/RU):
| Category | Element | Example value (EN) | Example (RU) | Cosmetics or semantics |
|---|---|---|---|---|
| UI | Widget title | GiftGenius: find a perfect present | GiftGenius: pick the perfect present | Cosmetics |
| UI | Recipient label | Recipient | Recipient | Cosmetics |
| UI | Empty list error message | No gifts found | No gifts found | Cosmetics |
| Prompts | System prompt | You are GiftGenius, a gift assistant… | You are GiftGenius, a gift assistant… | Semantics |
| Prompts | Selection summary template | Here’s why these gifts fit… | Here’s why these gifts fit… | Semantics |
| Data | Gift name | Smart mug | Smart mug | Both UX and semantics |
| Data | Gift description | Self‑heating mug with app control… | Self‑heating mug with app control… | Both UX and semantics |
| Data | Currency | 59.99 USD | 5,499 ₽ / 59.99 € | Semantics (format/currency) |
| Tools/schema | |
Suggest gift ideas based on profile… | Selects gift ideas based on the profile… | Semantics |
| Tools/schema | |
Budget in user’s currency | Budget in the user’s currency | Semantics |
| Commerce | SKU name in the feed | “Premium subscription – 1 year” | “Premium subscription – 1 year” | Both UX and legal |
| Commerce | Terms page | Terms of Service (EN only) | Notice: only the EN text is legally binding | Semantics/legal |
| Errors (backend) | Payment error message | Payment failed, please try again later | Payment failed, please try again later | Cosmetics + UX |
On the left we group by layers, then list specific elements. The last column helps you understand what shouldn’t be changed without alignment with the prompt designer/modeling owner: anything marked as semantics affects GPT behavior.
A small code sketch
To connect the map with code, you can define a simple type for localizable entities:
type LocalizedTextKey =
| "ui.title"
| "ui.recipient_label"
| "error.no_gifts"
| "prompt.summary_intro";
type Locale = "en" | "ru";
type Messages = Record<Locale, Record<LocalizedTextKey, string>>;
const messages: Messages = {
en: {
"ui.title": "GiftGenius: find a perfect present",
"ui.recipient_label": "Recipient",
"error.no_gifts": "No gifts found",
"prompt.summary_intro": "Here’s why these gifts fit:",
},
ru: {
"ui.title": "GiftGenius: pick the perfect present",
"ui.recipient_label": "Recipient",
"error.no_gifts": "No gifts found",
"prompt.summary_intro": "Here’s why these gifts fit:",
},
};
You can then use the same keys both in the widget and when forming prompts on the server (provided you pass locale through the stack — that’s in the next lecture). This way your “localization map” gradually turns into a typed dictionary rather than a scattered set of strings.
7. Practice: build a localization map for your App
Before diving into concrete i18n techniques and the Gateway/MCP architecture, do one boring but very useful exercise: honestly describe what exactly you plan to localize.
A good approach is to open any editor (Google Sheets or Notion will do) and create a table with columns:
- category/layer (UI, prompts, data, tools, commerce and legal, errors);
- element (a specific button, a specific field description, a specific endpoint with text);
- example EN value;
- example value in the second language (if you already have one, or at least a draft);
- marking “cosmetics/semantics/legally important”;
- owner (who is responsible for changes: frontend, MCP server, product, legal).
Then go through your App and honestly list everything that has text or where language affects data formats.
For GiftGenius you’d get a roughly expanded version of the table above. Along the way you will almost certainly discover a couple of “hidden” spots:
- text constants in MCP server code (e.g., error messages);
- default values in structuredContent (e.g., categories you didn’t render in the UI);
- outdated tool labels that no longer match actual behavior.
This exercise is especially useful to do before you connect localization to a real business process (payments, subscription activations, legal documents). Renaming a charge_user tool in a multilingual system with ACP and legal texts later is much more painful.
Overall, if you draw a localization map in advance and honestly mark what exactly and in which languages you plan to support, you’ll save yourself a lot of nerves in the next modules — when MCP, Gateway, commerce, and the Store come into play.
8. Common mistakes when planning localization
Mistake No. 1: assuming localization = “translate the buttons.”
A very common scenario: the team carefully extracts all UI strings into dictionaries, translates them, and celebrates. Meanwhile, the system prompt remains English only, tool descriptions too, and the product catalog contains English names exclusively. As a result, the App looks localized, but the model inside still lives in its own world and behavior remains Anglocentric. In practice, this shows up as odd recommendations and mistakes in tool arguments.
Mistake No. 2: not distinguishing cosmetics and semantics.
Sometimes a PM asks to “slightly tweak the wording” in a tool description or in the system prompt, and a developer changes the text as if it were a simple UI label. But a JSON Schema field description or a phrase in the system prompt is part of the contract with the model. Such changes can radically alter how GPT calls your tool. If you don’t mark semantic elements in the localization map ahead of time, it’s easy to accidentally break App behavior.
Mistake No. 3: starting with multilingual chaos and no architecture.
It’s tempting at the start to just scatter if (locale === "ru") through the code and plug in Russian strings wherever needed. A couple of weeks later the app turns into “localization hell”: one component reads from a dictionary, another hardcodes strings in JSX, the server uses a third naming scheme for keys. Later, plugging in a proper i18n system and unifying everything becomes much harder.
Mistake No. 4: forgetting about data and money.
Even experienced teams often start by translating the UI and prompts but miss that product catalogs, prices, currencies, and legal texts must also account for locale and userLocation. In the ChatGPT product feed specs, which text fields and prices are needed to display products correctly for the user are strictly defined. If you don’t plan for multilingualism at the data level, you’ll either duplicate the feed later or endure painful migrations.
Mistake No. 5: ignoring the platform’s locale and location signals.
ChatGPT already passes _meta["openai/locale"] and _meta["openai/userLocation"] in MCP calls so you can understand the language and region you’re interacting with. Some developers still ask the user “Choose interface language” on first run and in no way use these signals to pick resources and prices. The result is worse UX and an architecture that’s more complex than needed.
Mistake No. 6: not assigning “owners” to localizable elements.
Once things go live, translations get scattered among different people: frontend engineers tweak UI texts, backend engineers handle tool descriptions, the ML specialist owns the system prompt, and legal sends edited Terms. If the localization map doesn’t indicate who owns which layer, changes start to conflict, and some texts get updated in one place but not in another.
GO TO FULL VERSION