CodeGym /课程 /ChatGPT Apps /从哪里获取 locale:openai/locale,_meta["openai/userLocation"]

从哪里获取 locale:openai/locale,_meta["openai/userLocation"]

ChatGPT Apps
第 9 级 , 课程 1
可用

1. 为什么要从平台获取 locale,而不是每次都问用户

如果用“老办法”做本地化,通常的逻辑是:弹出“选择语言”的模态框,并把结果存到 localStorage。 在 ChatGPT Apps 中做法不同:平台已经很“聪明”,它会慷慨地提供语言与地区的信号。 我们需要学会使用这些信号,而不是用多余的问题打扰用户。

ChatGPT 在发给你的 App 的每个请求里,都会在上下文中附带:

  • 用户的偏好本地化(语言 + 地区)——在字段 openai/locale / _meta["openai/locale"]
  • 用户的地理位置/地区——在字段 _meta["openai/userLocation"]

在小部件(前端)侧,你可以通过 window.openai 或 SDK 的 hook 获取 locale;在 MCP/后端侧,则在 MCP 请求中的 _meta 里获取。

因此一个正常的场景是:用户输入“给妈妈挑一份 50 欧元以内的礼物”。ChatGPT 已经知道他的 localeuserLocation,平台把这些信号传给你的 App,然后你:

  • 用用户能看懂的语言展示 UI,
  • 加载正确语言的商品目录,
  • 用合适的货币与格式显示价格。

无需再来一段“顺便问一下,您用什么语言?”的对话。

2. 信号一:openai/locale —— 用户的语言与地区

这是个什么字段,长什么样

openai/locale 是一条 BCP‑47 格式的字符串,这个你多半见过: "en""en-US""ru""ru-RU""uk-UA" 等等。

需要注意的是,平台:

  • 可能只发语言("en""ru"),
  • 也可能发语言 + 地区("en-US""en-GB""fr-CA")。

BCP‑47 是浏览器的 Intl API 与多数 i18n 库都支持得很好的标准。 也就是说,你基本可以把 openai/locale 直接传入 Intl.NumberFormat、你的翻译引擎,以及你的 tools 中。

小部件里哪里能拿到 locale

在 ChatGPT 内部渲染的自定义 UI 中,Apps SDK 提供了全局对象 window.openai,其中包含 locale

典型写法如下(TypeScript,Next.js 16,我们的 GiftGenius 小部件):

// src/app/widgets/gift-widget.tsx
declare global {
  interface Window {
    openai?: { locale?: string };
  }
}

function getOpenAiLocale(): string {
  if (typeof window === "undefined") return "en";
  return window.openai?.locale || "en";
}

在真实应用中,写一个 hook 会更方便,它能同时在 ChatGPT 的沙箱与 Storybook 中工作:

// src/app/hooks/useOpenAiLocale.ts
import { useEffect, useState } from "react";

export function useOpenAiLocale(defaultLocale: string = "en") {
  const [locale, setLocale] = useState(defaultLocale);

  useEffect(() => {
    if (typeof window === "undefined") return;
    const next = window.openai?.locale || defaultLocale;
    setLocale(next);
  }, [defaultLocale]);

  return locale;
}

现在在任何组件里:

import { useOpenAiLocale } from "../hooks/useOpenAiLocale";

export function GiftHeader() {
  const locale = useOpenAiLocale();

  return (
    <h2>
      {/* 稍后这里会是 t('titles.gift_search') */}
      {locale.startsWith("ru") ? "礼物搜索" : "Gift search"}
    </h2>
  );
}

第 4 讲我们会把所有文案整理到字典里,但现在我们已经把 UI 绑定到了平台提供的真实信号, 而不是随机的 navigator.language。 这个 hook 很聚焦;在真实项目中,通常会基于更通用的 ChatGPT 全局访问机制来构建它——我们会在下文单独回到这个话题。

MCP/后端里哪里能拿到 locale

当 ChatGPT 调用 MCP 工具时,SDK 会在 JSON‑rpc 请求里传递 _meta["openai/locale"]。 在 TypeScript 服务器(我们的 GiftGenius MCP)上,它通常出现在工具处理器的第二个参数里。

示例:

// src/mcp/server.ts
import { McpServer } from "@openai/mcp-sdk";

const server = new McpServer();

server.registerTool(
  "suggest_gifts",
  {
    title: "礼物推荐",
    description: "根据偏好给出礼物清单",
    inputSchema: {
      type: "object",
      properties: {
        recipient: { type: "string" },
        budget: { type: "number" }
      },
      required: ["recipient", "budget"]
    }
  },
  async ({ input }, extra) => {
    const locale = extra?._meta?.["openai/locale"] || "en";
    // 接下来可以加载正确的目录
    const gifts = await loadGiftCatalog(locale);
    // ...
    return {
      content: [
        {
          type: "text",
          text: `Found ${gifts.length} gifts for locale ${locale}`
        }
      ],
      structuredContent: { gifts }
    };
  }
);

由此可见,locale 贯穿了整个栈:ChatGPT → Apps SDK → 你的 MCP 服务器。

Insight

每个服务器端的 mcp-tool 都有一个 extra 参数,mcp 服务器会把不适合放进 inputSchema 的数据都放在这里。 下面是这样一个对象的示例:

{
  sessionId: undefined,			// 始终为 undefined,请改用 `openai/subject`(见下方)
  _meta: {
    'openai/userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
    'openai/locale': 'en-US',		// 用户计算机的 locale,可能与聊天语言不一致
    'openai/userLocation': {		// 用户的较为精确的位置
      city: 'London',
      region: 'London City',
      country: 'GB',
      timezone: 'Europe/London',
      latitude: '5.45466',
      longitude: '-0.52380'
    },
    timezone_offset_minutes: -240,	// 时区偏移(分钟)
    'openai/subject': 'v1/sEtRuS92UEOPNdwzEUZORfeOKf7XSk2KZoIUGfAsb68BzZ8h5FAOgrH'	// 这就是 sessionId
  },
  authInfo: undefined,
  requestId: 1,
  requestInfo: {
    headers: {
      accept: 'application/json, text/event-stream',
      'accept-encoding': 'gzip, deflate, br, zstd',
      'access-control-allow-headers': '*',
      'access-control-allow-methods': 'GET,POST,PUT,DELETE,OPTIONS',
      'access-control-allow-origin': '*',
      'content-length': '542',
      'content-type': 'application/json',
      host: 'test.ngrok.app',					// 应用的原始域名
      'mcp-protocol-version': '2025-11-25',	
      traceparent: '00-69399d3a000000004fb8cc13dc3a2203-8748a8698107eb34-00',
      tracestate: 'dd=s:-1;p:01514e334c1ccef5;t.dm:-3',
      'user-agent': 'openai-mcp/1.0.0',
      'x-datadog-parent-id': '6089244476286233754',
      'x-datadog-sampling-priority': '-1',
      'x-datadog-tags': '_dd.p.tid=69399c3a00000000,_dd.p.dm=-3',
      'x-datadog-trace-id': '5744565710382309891',
      'x-forwarded-for': '199.210.139.232',
      'x-forwarded-host': 'test.ngrok.app',
      'x-forwarded-port': '3001',
      'x-forwarded-proto': 'https'
    }
  },
}

部分头部可能是由 ngrok 填充的,但仍然包含许多有价值的信息。

3. 信号二:_meta["openai/userLocation"] —— 用户地理信息

结构与作用

_meta["openai/userLocation"] 是一个地理信息对象:国家、地区、城市、时区,甚至经纬度。大致如下:

{
  "city": "London",
  "region": "England",
  "country": "GB",
  "timezone": "Europe/London",
  "latitude": 51.5074,
  "longitude": -0.1278
}

在 GiftGenius 中你最常用的字段有:

  • country —— 两位 ISO 国家码,对商品选择与货币尤为关键;
  • timezone —— 对日期/时间格式与提醒很有用。

Insight

经实验验证——userLocation 的判定质量很高。 数据会在每次调用 MCP 工具时通过参数 extra._meta["openai/userLocation"] 传入。 在开发你的应用时可以放心依赖。

如何在 MCP 工具中使用 userLocation

在 MCP 服务器上,userLocation 位于 _meta["openai/userLocation"], 紧邻 _meta["openai/locale"]

扩展我们工具的示例:

server.registerTool(
  "suggest_gifts",
  { /* schema 同上 */ },
  async ({ input }, extra) => {
    const meta = extra?._meta ?? {};
    const locale = (meta["openai/locale"] as string) || "en";
    const userLocation = meta["openai/userLocation"] as
      | { country?: string; city?: string }
      | undefined;

    const country = userLocation?.country || "US";

    const gifts = await loadGiftCatalog(locale, country);

    return {
      content: [
        {
          type: "text",
          text: `Found ${gifts.length} gifts for locale=${locale}, country=${country}`
        }
      ],
      structuredContent: { gifts }
    };
  }
);

函数 loadGiftCatalog(locale, country) 可以:

  • 选择合适的 JSON 文件:gift_catalog.en-US.jsongift_catalog.ru-RU.json
  • 过滤无法配送到该国家的商品,
  • 选择基础货币。

在后续的电商模块里,你还会基于 country 选择税务规则并映射到正确的 SKU,但从架构角度看,你依然是基于同一个信号——country 来做决策。

userLocation 如何补充 locale

一个经典示例:

locale = "en"userLocation.country = "DE"

可能的逻辑:

  • UI 与提示用英语(尊重 locale);
  • 货币与价格格式用欧元,因为用户身处德国;
  • 商品列表只展示可配送到 DE 的商品。

在 GiftGenius 中,你可以写一个小工具函数来表达这一点:

export function deriveCurrency(locale: string, country?: string): string {
  if (country === "DE") return "EUR";
  if (country === "JP") return "JPY";   
  if (locale === "zh_CN") return "CNY"; 
  return "USD";
}

并在后端 / 前端用它来格式化价格:

const currency = deriveCurrency(locale, country);
const formatted = new Intl.NumberFormat(locale, {
  style: "currency",
  currency
}).format(price);

在后端我们已经学会用 localecountry 选择目录与货币。 接下来要把同样的信号传递到小部件的 UI 中,让用户看到符合预期语言与格式的文本与价格。

4. 在 GiftGenius 小部件中获取 locale 与 userLocation

我们已经看到 localeuserLocation 如何在 MCP 侧影响目录与货币。 现在来看,如何把 locale 拿到 GiftGenius 小部件里,并直接用于 React UI。

要点:在小部件中我们只能直接访问 locale(通过 window.openai 与 SDK 的 hooks)。 userLocation 存在于 _meta,用于 MCP/后端——上文已介绍过。

Apps SDK 除了“原始”的 window.openai 外,还提供 React hooks 形式的实用函数。 文档里会描述类似 useOpenAiGlobal("locale") 的 hook,它能把 ChatGPT 的全局上下文值带入 React 组件。

我们自己实现一个,帮助理解它的底层原理。

基础 hook:useOpenAiGlobal

之前我们写了一个更专用的 useOpenAiLocale。 在实践中,做一个统一访问 ChatGPT 全局的 hook 更方便——基于它可以很容易封装出 useOpenAiLocale 等其他工具。 可以这么写:

// src/app/hooks/useOpenAiGlobal.ts
import { useEffect, useState } from "react";

type OpenAiGlobals = {
  locale?: string;
  // 之后可以加入 theme、userAgent 等
};

export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(
  key: K,
  fallback?: NonNullable<OpenAiGlobals[K]>
): NonNullable<OpenAiGlobals[K]> {
  const [value, setValue] = useState<NonNullable<OpenAiGlobals[K]>>(
    (fallback ?? "") as NonNullable<OpenAiGlobals[K]>
  );

  useEffect(() => {
    if (typeof window === "undefined") return;
    const globals = (window.openai || {}) as OpenAiGlobals;
    const next = globals[key] ?? fallback;
    if (next !== undefined) {
      setValue(next as NonNullable<OpenAiGlobals[K]>);
    }
  }, [key, fallback]);

  return value;
}

现在 useOpenAiGlobal("locale", "en") 会返回带有默认值 "en" 的最新 locale

在 GiftGenius 小部件中的应用

我们来写一个小组件,显示本地化的问候语以及当前 locale(便于调试):

// src/app/widgets/GiftWelcome.tsx
"use client";

import React from "react";
import { useOpenAiGlobal } from "../hooks/useOpenAiGlobal";

export function GiftWelcome() {
  const locale = useOpenAiGlobal("locale", "en");

  const greeting =
    locale.startsWith("ru") || locale.startsWith("uk")
      ? "你好!我会帮你挑选一份合适的礼物。"
      : "Hi! I’ll help you find a great gift.";

  return (
    <div>
      <p>{greeting}</p>
      <small style={{ opacity: 0.6 }}>Debug locale: {locale}</small>
    </div>
  );
}

暂时先不引入字典与 i18n 库——那是后面的事情。 现在重要的是:我们已经能“老老实实”从 ChatGPT 获取语言,而不是基于各种猜测。

5. 何时需要显式询问用户语言

既然 openai/localeuserLocation 如此给力,是否就再也不用询问用户偏好语言了? 很遗憾,有时还是需要的。

当信号不够用时

几种常见情况:

  • ChatGPT 账号的语言是英文(locale = "en"),但用户用俄语输入。模型用俄语回答,但你的 UI 用英文显示。
  • 用户在德国(userLocation.country = "DE"),locale = "en",而你的产品既能提供德语也能提供英语界面。
  • 应用对交流语言非常敏感:心理咨询、法律咨询、教学等。此时准确理解比自动检测的“省事”更重要。

在这些情况下,建议在流程开头礼貌地问一次,之后记住用户的选择即可。

如何不打扰地询问语言

通常用尽量简洁、直观的表述,例如:

  • “您更习惯使用哪种语言:English 还是 俄语?”
  • “我们检测到您的语言是 English。需要切换到其他语言吗?”

在 ChatGPT App 中可以用两种方式实现:

  • 通过小部件的 UI:在顶部渲染一个简易语言切换器。
  • 通过 App 在聊天里发出一个后续消息(follow‑up):发送文本问题,然后处理用户的回答。

代码:GiftGenius 中的简单语言选择

我们做一个语言切换组件,它会:

  • locale 读取初始语言,
  • 允许用户选择 ruen
  • 把选择结果存到小部件状态中(暂时用 React state)。
// src/app/widgets/LanguageSwitcher.tsx
"use client";

import React, { useState, useEffect } from "react";
import { useOpenAiGlobal } from "../hooks/useOpenAiGlobal";

type SupportedLocale = "en" | "ru";

export function LanguageSwitcher(props: {
  onChange?: (locale: SupportedLocale) => void;
}) {
  const initialLocale = useOpenAiGlobal("locale", "en");
  const [locale, setLocale] = useState<SupportedLocale>("en");

  useEffect(() => {
    const normalized: SupportedLocale = initialLocale.startsWith("ru")
      ? "ru"
      : "en";
    setLocale(normalized);
    props.onChange?.(normalized);
  }, [initialLocale, props]);

  const handleChange = (next: SupportedLocale) => {
    setLocale(next);
    props.onChange?.(next);
  };

  return (
    <div style={{ marginBottom: 8 }}>
      <span style={{ marginRight: 8 }}>
        {locale === "ru" ? "语言:" : "Language:"}
      </span>
      <button
        type="button"
        onClick={() => handleChange("en")}
        style={{ fontWeight: locale === "en" ? "bold" : "normal" }}
      >
        EN
      </button>
      <button
        type="button"
        onClick={() => handleChange("ru")}
        style={{ fontWeight: locale === "ru" ? "bold" : "normal", marginLeft: 4 }}
      >
        RU
      </button>
    </div>
  );
}

在 GiftGenius 的主小部件中,就可以基于 selectedLocale 来选择文本/字典,而不是直接使用 ChatGPT 传入的“原始”数据。

在后续课程中,你会把本地 state 换成更稳妥的存储方式(例如通过 _meta["openai/subject"] 把选择的语言传到 MCP / Gateway),但模式不变。

6. 如何把 locale 与 userLocation 传到后端并进行存储

ChatGPT 发来的信号只是“上游开始”,接下来你需要把这些数据传递给工具与服务,沿途不丢失,也不要让模型反复去“猜语言”。

在 tools 的参数中显式加入 locale 字段

最稳妥的做法——把 locale(以及需要的话 country)作为独立字段加入工具的 inputSchema。 这样模型会收到清晰的指令:“需要填写这个字段”。

server.registerTool(
  "suggest_gifts",
  {
    title: "Gift suggestions",
    description: "Suggest gifts based on recipient and budget",
    inputSchema: {
      type: "object",
      properties: {
        recipient: { type: "string" },
        budget: { type: "number" },
        locale: {
          type: "string",
          description: "Current user UI locale, BCP-47 (e.g. en-US, fr-FR)"
        },
        country: {
          type: "string",
          description: "ISO country code (e.g. US, DE)"
        }
      },
      required: ["recipient", "budget"]
    }
  },
  async ({ input }, extra) => {
    // 如果模型没有填写 locale/country,就从 _meta 兜底:
    const meta = extra?._meta ?? {};
    const locale = input.locale || (meta["openai/locale"] as string) || "en";
    const country =
      input.country ||
      (meta["openai/userLocation"] as any)?.country ||
      "US";

    // ...
  }
);

这会减少服务器内部的“魔法”:它能清楚看到模型打算使用的参数。

在会话/用户层面存储 locale

在包含 MCP Gateway 的架构(后续章节)中,通常会存储“客户端状态”:localecurrency、偏好等。 此处只需要理解这个思路:一旦从 ChatGPT 读取了信号,就把它当作会话状态的一部分使用,而不是每次都重新计算。

伪代码示意:

// gateway.ts
const sessionState = new Map<string, { locale: string; country?: string }>();

function onMcpRequest(request: any) {
  const subject = request._meta?.["openai/subject"]; // 匿名用户 ID
  const locale = request._meta?.["openai/locale"] || "en";
  const country = request._meta?.["openai/userLocation"]?.country;

  if (subject) {
    sessionState.set(subject, { locale, country });
  }

  // 然后把 locale/country 传给具体的 MCP 服务器
}

在本讲中你不需要实现 Gateway,只需理解:localeuserLocation 非常适合作为“会话状态”的候选。

Insight

实验结论:request._meta?.["openai/locale"] 反映的是用户当前设置的 locale。 交流语言可以通过工具参数(inputSchema)传入。

我把电脑的 locale 设为 EN,但与 ChatGPT 用德语(DE)交流。结果是:

  • request._meta?.["openai/locale"] 等于 EN
  • 通过 inputSchema 作为工具参数传入的 locale 等于 DE

7. Locale vs 基于文本的语言自动识别

有时开发者会想:“我们直接按用户文本做语言检测吧,LLM 不是啥都能做么。” 在实践中,这几乎总是不如依赖 openai/locale 来得可靠。

原因很实际:

  • 用户可能混用多种语言;
  • 细微差异(如 uk-UA vs ru-RU)很难靠一条消息准确识别;
  • ChatGPT 已经替你做了这件事,并把 locale 发给你了。

自动识别作为兜底很有用——当 openai/locale 缺失或异常(现今较少见)时, 但不建议把它作为主要逻辑。一个朴素的规则:

  • 首先把 openai/locale 当作“真相”;
  • 其后考虑 userLocation(货币、商品);
  • 只有在非常纠结的情况下,再额外看看最近一条消息的语言。

8. 不同 locale 与 userLocation 组合:场景表

为了加深印象,来看 GiftGenius 在不同场景下应如何表现。

场景 locale userLocation.country UI 语言 货币 目录
1
en-US
US
EN
USD
US 商品
2
uk-UA
UA
UKR/RU
UAH
UA 商品
3
en
DE
EN
EUR
DE 商品
4
ru-RU
DE
RU
EUR
DE 商品
5
en
(无数据) EN
USD
Global default

这种视角在后面讨论电商时会很有用,但现在已经可以看出:只要提供不同的 localecountry,行为就能轻松切换。

9. 本地化信号的简化流程图

为了更好地理解,看看这张简化示意:

flowchart TD
  U[用户<br/>发送消息] --> C[ChatGPT]
  C -->|确定| L[openai/locale<br/>+ userLocation]
  L -->|传递给| W["Widget (Next.js)"]
  L -->|通过 _meta 传递| S[MCP Server]

  W -->|locale| UI[GiftGenius UI<br/>文案 + 数字格式]
  S -->|locale + country| DATA[目录、价格、过滤器]

  style L fill:#e0f7ff,stroke:#00a
  style W fill:#f7fff0,stroke:#4b4
  style S fill:#fdf0ff,stroke:#b4

重要的是:这张图里没有“选择语言”的模态框。只有当信号与用户预期明显冲突时,它才作为额外层出现。

10. 实践:现在就能在你的 App 里做什么

为了避免停留在理论上,给 GiftGenius 一个简短的实践清单:

  • 在小部件中:添加 useOpenAiGlobal("locale") 或等价的 hook,并至少在一个地方做 RU/EN 的文案分支。
  • 在 MCP 服务器中:在某个已存在的工具(suggest_gifts)里读取 _meta["openai/locale"]_meta["openai/userLocation"],把它们写到日志,并用来选择目录。
  • 写一个简单函数 deriveCurrency(locale, country),并在至少一个地方用于价格格式化。

无需一开始就搭完整的 i18n 引擎与 15 种语言——当前目标是学会如实使用平台提供的信号。

11. 处理 locale 与 userLocation 的常见错误

错误一:完全忽略 openai/locale,只依赖 navigator.language。
这通常是习惯于传统 Web 应用的人会犯的错。在 ChatGPT 中,用户可能根本没打开你的网页, 而你这边的 navigator.language 代表的是隧道服务器或 Vercel 的语言,而不是用户的。 最终 UI 总是“神秘地”显示为英文,尽管 ChatGPT 一直稳定地给你传 ru-RU

错误二:每次都问用户“你更喜欢用哪种语言?”
如果你的小部件每个会话的第一条消息都是语言调查,用户会有在机场被反复询问行李的感觉。 平台已经知道语言与地区了——尊重 openai/locale,只在明显冲突(例如 locale 是 "en",但请求为俄语)时再询问。

错误三:只在 UI 中保存所选语言,却不把它传到 MCP 工具。
小部件可能用俄语显示,但服务器还在返回英文目录,因为它不知道语言已切换。 始终考虑“端到端”的路径:如果 UI 有切换器,它的结果需要传到后端——要么作为工具参数,要么通过 Gateway 会话。

错误四:只根据文本去“猜语言”,却忽略 openai/locale。
基于文本的自动检测在“纯英文”时可能还不错;一旦出现混合语言或相似表达,结果就会飘。 openai/locale 是平台提供的、足够可靠的评估,应把它作为主要事实来源,而文本检测只是补充信号。

错误五:把业务逻辑与本地化混在一起,到处写 if (locale === 'ru') { ... }。
在本讲的示例里我们为了简单还会这样做一点,但必须尽早规划:把字符串、格式与目录跟业务逻辑分离。 否则几个月后你会面对满地的 if(locale.startsWith("ru")) 开头的函数,再加一种语言就会很痛苦。到第 44 讲我们会系统地解决这个问题——好消息是:我们已经有了可靠的 locale 来源并会用它了。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION