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 的每個請求中都會加到上下文:

  • 使用者偏好的 locale(語言 + 區域)— 位於 openai/locale / _meta["openai/locale"] 欄位;
  • 使用者的地理位置/區域 — 位於 _meta["openai/userLocation"]

在小工具(前端)端,你可以透過 window.openai 或 SDK 的 hook 取得 locale;在 MCP/後端端,則能透過請求中的 _meta 取得。

因此一個正常的情境會是:使用者輸入「幫我在 50 歐元內為媽媽挑禮物」。ChatGPT 已知道他的 localeuserLocation,平台把這些訊號傳給你的 App,而你會:

  • 用使用者懂的語言顯示 UI,
  • 載入正確語言的目錄,
  • 用正確的貨幣與格式呈現價格。

而不需要額外的對話「順便問一下,您的語言是?」。

2. 訊號 1: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、翻譯引擎,以及你的工具內部。

在小工具中哪裡能取得 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'
    }
  },
}

或許部分標頭是由 ngrock 填入,但仍有許多有用的資訊。

3. 訊號 2:_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",
  { /* 結構同上 */ },
  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
  • 過濾無法配送至該國家的商品,
  • 選擇基礎貨幣。

在 commerce 模組中,你之後會根據 country 選擇稅則、對應正確的 SKU,但在架構角度你仍舊依賴同一個訊號 — country

userLocation 如何補足 locale

典型的例子:

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

邏輯可以是:

  • UI 與提示文字使用英文(尊重 locale);
  • 貨幣與價格格式用歐元,因為使用者實際在德國;
  • 禮物清單只顯示可配送至 DE 的商品。

在 GiftGenius 中可以用一個小小的 helper 函式描述:

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,理解其原理。

基礎 hook:useOpenAiGlobal

先前我們做了專用的 useOpenAiLocale。 實務上有一個通用的全域存取 hook 更方便 — 它可以很容易地包出 useOpenAiLocale 與其他封裝。 假設這個 hook 長這樣:

// 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") 就能提供 locale 的最新值,且有預設值 "en"

在 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 或 Russian?」
  • 「我們偵測到您的語言為 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 的訊號只是起點。接下來要把這些資料傳到你的工具與服務,不在途中遺失,也不要讓模型一次次重新猜測語言。

在工具參數中顯式加入 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,並用德文(DE)和 ChatGPT 對話。結果:

  • request._meta?.["openai/locale"] 等於 EN
  • 透過 inputSchema 傳入工具的 locale 等於 DE

7. Locale 與依據文字自動判斷語言的比較

有時開發者會想:「乾脆直接用文字偵測語言吧,LLM 不是什麼都會嗎」。 實務上,這幾乎總是比不上依賴 openai/locale

原因很務實:

  • 使用者可能混用多種語言;
  • 細微差異(如 uk-UA vs ru-RU)很難從單一訊息偵測;
  • ChatGPT 已經幫你做了這件事,並提供了 locale

自動偵測可以作為 fallback(例如 openai/locale 缺失或格式怪異 — 但現在很少見), 但不應該成為主要邏輯。簡單的原則:

  • 先把 openai/locale 視為「事實來源」;
  • 接著考慮 userLocation(貨幣、供給);
  • 只有在爭議很大的情況下,才另外參考最後一則訊息的語言。

8. 不同的 locale 與 userLocation 組合:情境表

為了鞏固概念,我們看看 GiftGenius 在不同情境下該怎麼做。

情境 locale userLocation.country UI 語言 貨幣 目錄
1
en-US
US
EN
USD
美國商品
2
uk-UA
UA
UKR/RU
UAH
烏克蘭商品
3
en
DE
EN
EUR
德國商品
4
ru-RU
DE
RU
EUR
德國商品
5
en
(沒有資料) EN
USD
全域預設

這個視角在我們之後討論 commerce 時會很有用;但現在已經能看出,只要提供不同的 localecountry, 行為就能輕鬆調整。

9. 小型流程圖:locale 訊號的流向

為了統整思路,看看簡化的架構圖:

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"],把它們寫到 log,並用來選擇目錄。
  • 撰寫簡單的 deriveCurrency(locale, country) 函式,並在一處用它來格式化價格。

不需要一開始就搭建完整的 i18n 引擎與 15 種語言 — 現階段的目標是誠實地使用平台提供的訊號。

11. 使用 locale 與 userLocation 時的常見錯誤

錯誤 1:完全忽視 openai/locale,只依賴 navigator.language。
這是習慣一般網頁應用的人常見的做法。在 ChatGPT 中,使用者甚至不一定是用瀏覽器打開, 而你這端的 navigator.language 可能是通道伺服器或 Vercel 的語言,而不是使用者的。 結果就是 UI「莫名其妙」一直是英文,儘管 ChatGPT 穩定地傳來 ru-RU

錯誤 2:每次都問使用者「你想用哪種語言?」
如果每次聊天小工具的第一句都是語言問卷,使用者會覺得像在機場被不停盤問是否遺失行李。平台已經知道語言與區域 — 尊重 openai/locale 即可,只有在明顯衝突時(例如 locale = "en",但輸入是俄文)再詢問。

錯誤 3:只在 UI 儲存選擇的語言,卻不傳到 MCP 工具。
小工具可能是俄文,但伺服器仍回傳英文目錄,因為它不知道語言已經切換。 務必思考端到端的路徑:如果 UI 有切換器,它的結果要傳達到後端 — 可以放在工具參數, 或透過 Gateway 會話。

錯誤 4:只靠訊息文字來「猜」語言,忽略 openai/locale。
只靠文字做自動偵測可能在純英文時表現不錯……但一旦混用語言或遇到相似片語,結果會不穩。 openai/locale 是平台提供、相當可靠的評估;應視為主要的真實來源,而文字偵測只是補充訊號。

錯誤 5:把商業邏輯與在地化混在一起,到處寫 if (locale === 'ru') { ... }。
在本講為求簡化我們還會稍微這樣做,但務必要預先規劃:字串、格式與目錄應與商業邏輯分離。 否則幾個月後你會發現每個函式都從 if (locale.startsWith("ru")) 開始,而新增一種語言會很痛苦。在第 44 講我們會處理這個問題,同時記得我們已經有且會使用 locale 的來源。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION