1. Vì sao quan trọng lấy locale từ nền tảng thay vì hỏi người dùng mỗi lần
Nếu tiếp cận bài toán bản địa hoá “kiểu cũ”, logic thường là: hiển thị modal “Chọn ngôn ngữ” và lưu kết quả vào localStorage. Trong ChatGPT Apps, cách làm khác: chúng ta đã có một nền tảng thông minh, và nó cung cấp hào phóng các tín hiệu về ngôn ngữ và vùng. Cần học cách sử dụng chúng để không làm phiền người dùng bằng các câu hỏi thừa.
ChatGPT trong mỗi yêu cầu gửi tới App của bạn sẽ thêm vào ngữ cảnh:
- locale ưa thích của người dùng (ngôn ngữ + vùng) — trong trường openai/locale / _meta["openai/locale"];
- vị trí/vùng của người dùng — trong trường _meta["openai/userLocation"].
Ở phía widget (frontend), bạn nhận locale qua window.openai hoặc hook từ SDK; ở phía MCP/backend — qua _meta trong yêu cầu MCP.
Kết quả là kịch bản bình thường sẽ như sau: người dùng viết “Hãy chọn quà cho mẹ trong khoảng 50 euro”. ChatGPT đã biết locale và userLocation của họ, nền tảng truyền các tín hiệu này vào App của bạn, và bạn:
- hiển thị UI bằng ngôn ngữ phù hợp,
- nạp đúng ngôn ngữ của danh mục,
- định dạng giá theo đúng tiền tệ và định dạng.
Không cần thêm đoạn hội thoại “Nhân tiện, bạn dùng ngôn ngữ nào?”.
2. Tín hiệu số 1: openai/locale — ngôn ngữ và vùng của người dùng
Trường này là gì và trông như thế nào
openai/locale — là chuỗi theo định dạng BCP‑47 mà có lẽ bạn đã gặp: "en", "en-US", "ru", "ru-RU", "uk-UA", v.v.
Quan trọng là nền tảng:
- có thể chỉ gửi ngôn ngữ ("en", "ru"),
- có thể gửi ngôn ngữ + vùng ("en-US", "en-GB", "fr-CA").
BCP‑47 — là tiêu chuẩn được hỗ trợ rất tốt bởi Intl-API trên trình duyệt và đa số thư viện i18n. Tức là có thể gần như truyền thẳng openai/locale vào Intl.NumberFormat, engine dịch, và vào bên trong các tools của bạn.
Locale khả dụng ở đâu trong widget
Trong UI tuỳ biến được render bên trong ChatGPT, Apps SDK cung cấp đối tượng toàn cục window.openai, trong đó có locale.
Thông thường trông như sau (TypeScript, Next.js 16, widget GiftGenius của chúng ta):
// 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";
}
Trong ứng dụng thực, tốt hơn là tạo một hook hoạt động được cả ở sandbox của ChatGPT và trong 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;
}
Bây giờ trong bất kỳ component nào:
import { useOpenAiLocale } from "../hooks/useOpenAiLocale";
export function GiftHeader() {
const locale = useOpenAiLocale();
return (
<h2>
{/* sau này sẽ có t('titles.gift_search') ở đây */}
{locale.startsWith("ru") ? "Tìm quà tặng" : "Gift search"}
</h2>
);
}
Trong bài giảng 4, chúng ta sẽ đưa toàn bộ chuỗi vào từ điển, nhưng ngay lúc này UI đã gắn với tín hiệu thực từ nền tảng, chứ không phải dựa vào navigator.language “ngẫu nhiên”. Hook này mang tính chuyên biệt; trong dự án thực tế, tiện hơn là xây nó trên cơ chế tổng quát để truy cập các globals của ChatGPT — chúng ta sẽ quay lại phần này bên dưới.
Locale khả dụng ở đâu trong MCP/backend
Khi ChatGPT gọi MCP‑tool, SDK sẽ truyền _meta["openai/locale"] trong yêu cầu JSON‑rpc. Trên máy chủ TypeScript (GiftGenius MCP), điều này thường khả dụng ở tham số thứ hai của handler công cụ.
Ví dụ:
// src/mcp/server.ts
import { McpServer } from "@openai/mcp-sdk";
const server = new McpServer();
server.registerTool(
"suggest_gifts",
{
title: "Gợi ý quà tặng",
description: "Đề xuất danh sách quà tặng theo sở thích",
inputSchema: {
type: "object",
properties: {
recipient: { type: "string" },
budget: { type: "number" }
},
required: ["recipient", "budget"]
}
},
async ({ input }, extra) => {
const locale = extra?._meta?.["openai/locale"] || "en";
// tiếp theo có thể nạp đúng danh mục
const gifts = await loadGiftCatalog(locale);
// ...
return {
content: [
{
type: "text",
text: `Found ${gifts.length} gifts for locale ${locale}`
}
],
structuredContent: { gifts }
};
}
);
Như vậy locale tồn tại xuyên suốt stack: ChatGPT → Apps SDK → máy chủ MCP của bạn.
Insight
Mỗi mcp-tool trên máy chủ có tham số extra, nơi mcp-server đặt mọi dữ liệu không nằm trong inputSchema. Đây là ví dụ về đối tượng như vậy:
{
sessionId: undefined, // luôn undefined, hãy dùng `openai/subject` bên dưới
_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 của máy người dùng, có thể không trùng ngôn ngữ trong chat
'openai/userLocation': { // vị trí người dùng tương đối chính xác
city: 'London',
region: 'London City',
country: 'GB',
timezone: 'Europe/London',
latitude: '5.45466',
longitude: '-0.52380'
},
timezone_offset_minutes: -240, // độ lệch múi giờ
'openai/subject': 'v1/sEtRuS92UEOPNdwzEUZORfeOKf7XSk2KZoIUGfAsb68BzZ8h5FAOgrH' // đây là 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', // miền gốc của ứng dụng
'mcp-protocol-version': '2025-11-25',
traceparent: '00-69399d3a000000004fb8cc13dc3a2203-8748a8698107eb34-00',
tracestate: 'dd=s:-1;p:01514e334c1ccef5;t.dm:-3',
'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'
}
},
}
Có thể một phần các header ở đây do ngrock điền, nhưng vẫn có rất nhiều dữ liệu thú vị.
3. Tín hiệu số 2: _meta["openai/userLocation"] — địa lý của người dùng
Cấu trúc và ý nghĩa
_meta["openai/userLocation"] — là đối tượng chứa thông tin địa lý: quốc gia, vùng, thành phố, múi giờ, thậm chí cả toạ độ. Ví dụ:
{
"city": "London",
"region": "England",
"country": "GB",
"timezone": "Europe/London",
"latitude": 51.5074,
"longitude": -0.1278
}
Các trường chính mà bạn sẽ dùng thực sự trong GiftGenius:
- country — mã quốc gia 2 ký tự theo ISO, quan trọng cho danh mục và tiền tệ;
- timezone — hữu ích cho định dạng ngày/giờ và nhắc nhở.
Insight
Đã kiểm chứng thực nghiệm — xác định userLocation hoạt động rất chất lượng. Dữ liệu có ở mỗi lần gọi MCP‑tool qua tham số extra._meta["openai/userLocation"]. Bạn có thể dựa vào chúng khi phát triển ứng dụng.
Cách dùng userLocation trong các MCP‑tool
Trên máy chủ MCP, userLocation nằm trong _meta["openai/userLocation"] bên cạnh _meta["openai/locale"].
Mở rộng ví dụ về công cụ của chúng ta:
server.registerTool(
"suggest_gifts",
{ /* schema như ở trên */ },
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 }
};
}
);
Hàm loadGiftCatalog(locale, country) có thể:
- chọn đúng tệp JSON: gift_catalog.en-US.json, gift_catalog.ru-RU.json,
- lọc các sản phẩm không thể giao đến quốc gia này,
- chọn tiền tệ cơ sở.
Sau này trong các module thương mại, bạn sẽ dựa trên country để chọn quy tắc thuế và map sang SKU phù hợp, nhưng về kiến trúc bạn vẫn dựa trên một tín hiệu — country.
userLocation bổ trợ cho locale như thế nào
Ví dụ kinh điển:
locale = "en", userLocation.country = "DE".
Logic có thể như sau:
- UI và gợi ý — bằng tiếng Anh (tôn trọng locale);
- định dạng tiền tệ và giá — euro, vì người dùng đang ở Đức;
- danh sách quà tặng — chỉ những gì giao được tới DE.
Trong GiftGenius có thể diễn đạt bằng một hàm trợ giúp nhỏ:
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";
}
Và dùng ở backend/frontend để định dạng giá:
const currency = deriveCurrency(locale, country);
const formatted = new Intl.NumberFormat(locale, {
style: "currency",
currency
}).format(price);
Ở backend chúng ta đã biết dùng locale và country để chọn danh mục và tiền tệ. Tiếp theo, quan trọng là truyền các tín hiệu này tới UI trong widget để người dùng thấy văn bản và giá theo định dạng mong đợi.
4. Cách lấy locale và userLocation trong widget GiftGenius
Chúng ta đã xem cách locale và userLocation tồn tại ở phía MCP và ảnh hưởng đến danh mục, tiền tệ. Giờ hãy xem cách lấy locale vào widget GiftGenius và dùng trực tiếp trong React‑UI.
Lưu ý: trong widget chúng ta chỉ truy cập trực tiếp được locale (qua window.openai và các hook của SDK). userLocation ở trong _meta và dùng ở phía MCP/backend — phần này ta đã làm phía trên.
Trong Apps SDK ngoài window.openai “thô” còn có tiện ích dưới dạng React‑hook. Tài liệu mô tả các hook như useOpenAiGlobal("locale"), kéo các giá trị ngữ cảnh toàn cục của ChatGPT vào React‑component.
Ta tự mô phỏng một hook như vậy để hiểu chuyện gì diễn ra bên trong.
Hook cơ bản useOpenAiGlobal
Trước đó chúng ta tạo useOpenAiLocale chuyên biệt. Trong thực tế, tiện hơn là có một hook dùng chung để truy cập các global của ChatGPT — từ đó dễ bọc thành useOpenAiLocale và các wrapper khác. Giả sử có hook như sau:
// src/app/hooks/useOpenAiGlobal.ts
import { useEffect, useState } from "react";
type OpenAiGlobals = {
locale?: string;
// về sau có thể thêm theme, userAgent, v.v.
};
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;
}
Giờ useOpenAiGlobal("locale", "en") cho ta giá trị locale hiện tại với mặc định "en".
Ứng dụng trong widget GiftGenius
Tạo một component nhỏ hiển thị lời chào đã bản địa hoá và locale hiện tại để debug:
// 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")
? "Xin chào! Tôi sẽ giúp bạn tìm một món quà phù hợp."
: "Hi! I’ll help you find a great gift.";
return (
<div>
<p>{greeting}</p>
<small style={{ opacity: 0.6 }}>Debug locale: {locale}</small>
<div>
);
}
Chưa cần từ điển hay thư viện i18n — phần đó để sau. Hiện tại điều quan trọng là chúng ta đã biết lấy trung thực ngôn ngữ từ ChatGPT, chứ không dựa vào phỏng đoán.
5. Khi nào cần hỏi rõ người dùng về ngôn ngữ
Nếu openai/locale và userLocation đã “xịn” như vậy, liệu có bao giờ cần hỏi người dùng muốn dùng ngôn ngữ nào? Đáng tiếc là đôi khi vẫn cần.
Khi tín hiệu là chưa đủ
Có vài tình huống tiêu biểu:
- Tài khoản ChatGPT là tiếng Anh (locale = "en"), nhưng người dùng viết bằng tiếng Nga. Mô hình trả lời bằng tiếng Nga, còn UI của bạn lại hiện tiếng Anh.
- Người dùng ở Đức (userLocation.country = "DE"), locale = "en", trong khi bạn sẵn sàng phục vụ cả UI tiếng Đức lẫn tiếng Anh.
- Ứng dụng yêu cầu ngôn ngữ giao tiếp chính xác: trị liệu tâm lý, tư vấn pháp lý, giáo dục. Ở đó độ chính xác quan trọng hơn sự tiện lợi của tự động nhận diện.
Trong các trường hợp này, nên hỏi ngắn gọn và lịch sự một lần ở đầu kịch bản, sau đó ghi nhớ lựa chọn.
Cách hỏi về ngôn ngữ một cách tinh tế
Thường câu hỏi được diễn đạt đơn giản và trực quan, ví dụ:
- “Bạn muốn dùng ngôn ngữ nào: English hay tiếng Nga?”
- “Chúng tôi xác định ngôn ngữ của bạn là English. Bạn có muốn chuyển sang ngôn ngữ khác không?”
Trong ChatGPT App, có hai cách làm:
- Qua UI của widget: vẽ một bộ chuyển đổi ngôn ngữ nhỏ ở trên cùng.
- Qua follow‑up message trong chat thay mặt App: gửi một follow‑up dạng văn bản với câu hỏi, rồi xử lý câu trả lời.
Mã: lựa chọn ngôn ngữ đơn giản trong GiftGenius
Tạo một component bộ chuyển đổi, nó sẽ:
- lấy ngôn ngữ khởi tạo từ locale,
- cho phép người dùng chọn ru hoặc en,
- lưu lựa chọn trong trạng thái của widget (tạm thời trong 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" ? "Ngôn ngữ:" : "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>
);
}
Và trong widget chính của GiftGenius, bạn có thể chọn văn bản/từ điển theo selectedLocale, thay vì theo dữ liệu “thô” từ ChatGPT.
Trong các bài giảng sau, bạn sẽ thay state cục bộ bằng cách lưu bền vững hơn (ví dụ, đẩy ngôn ngữ đã chọn sang MCP / Gateway qua _meta["openai/subject"]), nhưng pattern vẫn giữ nguyên.
6. Cách truyền locale và userLocation xuống backend và lưu trữ
Tín hiệu từ ChatGPT đến “từ trên xuống”, nhưng câu chuyện không dừng ở đó. Tiếp theo, cần đưa dữ liệu này vào các công cụ và dịch vụ của bạn, không để mất trên đường đi và không bắt mô hình đoán lại ngôn ngữ.
Trường locale rõ ràng trong tham số tools
Cách chắc chắn nhất — thêm locale (và nếu muốn cả country) như các trường riêng trong inputSchema của công cụ. Như vậy mô hình nhận được tín hiệu rõ ràng: “cần điền trường này”.
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) => {
// Nếu mô hình không điền locale/country, dự phòng từ _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";
// ...
}
);
Cách này giảm “ma thuật” trong máy chủ: nó thấy rõ các tham số mà mô hình định dùng.
Lưu locale ở cấp phiên/người dùng
Trong kiến trúc với MCP Gateway (các module sau), thông lệ là lưu “trạng thái client”: locale, currency, sở thích. Hiện tại, chỉ cần hiểu ý tưởng: đọc tín hiệu từ ChatGPT một lần — sau đó dùng như trạng thái phiên, thay vì tính lại mỗi lần.
Mã giả minh hoạ:
// gateway.ts
const sessionState = new Map<string, { locale: string; country?: string }>();
function onMcpRequest(request: any) {
const subject = request._meta?.["openai/subject"]; // id người dùng ẩn danh
const locale = request._meta?.["openai/locale"] || "en";
const country = request._meta?.["openai/userLocation"]?.country;
if (subject) {
sessionState.set(subject, { locale, country });
}
// sau đó truyền locale/country tới máy chủ MCP cụ thể
}
Trong phạm vi bài giảng này bạn chưa cần hiện thực Gateway, chỉ cần hiểu rằng locale và userLocation là các ứng viên tốt cho “trạng thái phiên”.
Insight
Dữ liệu thực nghiệm: request._meta?.["openai/locale"] cho thấy locale hiện đang được thiết lập của người dùng. Ngôn ngữ giao tiếp có thể nhận qua tham số tool trong inputSchema.
Tôi đặt locale EN trên máy, nhưng trò chuyện với ChatGPT bằng tiếng Đức (DE). Kết quả:
- request._meta?.["openai/locale"] bằng EN
- locale nhận như tham số tool qua inputSchema bằng DE
7. Locale so với tự động nhận diện ngôn ngữ theo văn bản
Đôi khi nhà phát triển bị hấp dẫn bởi ý tưởng: “Hãy cứ nhận diện ngôn ngữ theo văn bản người dùng, LLM làm được hết mà”. Trong thực tế, điều đó gần như luôn tệ hơn so với việc dựa vào openai/locale.
Lý do khá thực dụng:
- người dùng có thể viết trộn nhiều ngôn ngữ;
- các khác biệt tinh tế (uk-UA vs ru-RU) khó nhận ra chỉ qua một tin nhắn;
- ChatGPT đã làm giúp bạn và gửi locale.
Tự động nhận diện hữu ích như phương án dự phòng, nếu openai/locale tới dưới dạng kỳ lạ hoặc vắng mặt (hiện giờ khá hiếm), nhưng không nên xây toàn bộ logic chính dựa trên nó. Quy tắc “thô”:
- đầu tiên coi openai/locale là “sự thật”;
- sau đó xét đến userLocation (tiền tệ, danh mục);
- và chỉ khi rất khó quyết, mới nhìn thêm vào ngôn ngữ của tin nhắn gần nhất.
8. Các tổ hợp khác nhau của locale và userLocation: bảng kịch bản
Để củng cố, hãy xem GiftGenius nên hành xử thế nào trong các kịch bản khác nhau.
| Kịch bản | locale | userLocation.country | Ngôn ngữ UI | Tiền tệ | Danh mục |
|---|---|---|---|---|---|
| 1 | |
|
EN | |
Sản phẩm US |
| 2 | |
|
UKR/RU | |
Sản phẩm UA |
| 3 | |
|
EN | |
Sản phẩm DE |
| 4 | |
|
RU | |
Sản phẩm DE |
| 5 | |
(không có dữ liệu) | EN | |
Mặc định toàn cầu |
Góc nhìn này sẽ hữu ích khi ta bàn về thương mại sau này, nhưng ngay bây giờ có thể thấy hành vi thay đổi dễ dàng chỉ bằng cách cung cấp locale và country khác nhau.
9. Sơ đồ dòng tín hiệu locale
Để hệ thống lại, xem sơ đồ giản lược:
flowchart TD U[Người dùng<br/>viết tin nhắn] --> C[ChatGPT] C -->|xác định| L[openai/locale<br/>+ userLocation] L -->|truyền| W["Widget (Next.js)"] L -->|truyền qua _meta| S[MCP Server] W -->|locale| UI[GiftGenius UI<br/>văn bản + định dạng số] S -->|locale + country| DATA[Danh mục, giá, bộ lọc] style L fill:#e0f7ff,stroke:#00a style W fill:#f7fff0,stroke:#4b4 style S fill:#fdf0ff,stroke:#b4
Quan trọng: trong sơ đồ này không có modal “Chọn ngôn ngữ”. Nó chỉ cần như một lớp bổ sung khi tín hiệu mâu thuẫn với kỳ vọng của người dùng.
10. Thực hành: có thể làm gì ngay trong App của bạn
Để bài giảng không chỉ là lý thuyết, checklist ngắn cho GiftGenius:
- Trong widget: thêm hook useOpenAiGlobal("locale") hoặc tương đương và ít nhất ở một chỗ tạo nhánh RU/EN cho văn bản.
- Trong máy chủ MCP: ở một công cụ hiện có (suggest_gifts) lấy _meta["openai/locale"] và _meta["openai/userLocation"], ghi log và dùng để chọn danh mục.
- Viết hàm đơn giản deriveCurrency(locale, country) và dùng ở một nơi khi định dạng giá.
Chưa cần xây đầy đủ engine i18n và 15 ngôn ngữ — mục tiêu bây giờ là học cách dùng trung thực các tín hiệu của nền tảng.
11. Lỗi thường gặp khi làm việc với locale và userLocation
Lỗi số 1: hoàn toàn bỏ qua openai/locale và chỉ dựa vào navigator.language.
Cách làm này quen thuộc ở web app thông thường. Trong ChatGPT, người dùng có thể không mở gì trên trình duyệt, và navigator.language ở phía bạn — là ngôn ngữ của máy chủ đường hầm hoặc Vercel, không phải của người dùng. Kết quả là UI luôn “bí ẩn” hiện tiếng Anh, dù ChatGPT đều đặn gửi cho bạn ru-RU.
Lỗi số 2: lần nào cũng hỏi người dùng “bạn muốn dùng ngôn ngữ nào?”
Nếu trong mỗi chat, câu đầu tiên của widget là khảo sát ngôn ngữ, người dùng sẽ có cảm giác như ở sân bay, nơi họ bị hỏi về hành lý năm lần liền. Nền tảng đã biết ngôn ngữ và vùng — chỉ cần tôn trọng openai/locale và chỉ hỏi khi có xung đột rõ ràng (ví dụ, yêu cầu bằng tiếng Nga trong khi locale = "en").
Lỗi số 3: chỉ lưu ngôn ngữ đã chọn ở UI mà không truyền vào MCP‑tools.
Widget có thể là tiếng Nga, còn máy chủ vẫn trả danh mục tiếng Anh vì nó không biết việc đổi ngôn ngữ. Luôn nghĩ theo đường đi xuyên suốt: nếu UI có bộ chuyển đổi, kết quả của nó phải được chuyển xuống backend — hoặc trong tham số công cụ, hoặc qua session của Gateway.
Lỗi số 4: cố “đoán” ngôn ngữ chỉ theo văn bản, bỏ qua openai/locale.
Nhận diện theo văn bản có thể ổn… chừng nào người dùng dùng thuần một ngôn ngữ. Khi trộn ngôn ngữ hoặc có cụm từ tương đồng, kết quả sẽ dao động. openai/locale — là đánh giá đã đủ tin cậy, được nền tảng cung cấp. Hãy coi nó là nguồn chân lý chính, còn nhận diện văn bản chỉ là bổ sung.
Lỗi số 5: trộn logic nghiệp vụ và bản địa hoá kiểu if (locale === 'ru') { ... } khắp nơi.
Trong bài này ta còn làm một chút như vậy cho đơn giản, nhưng cần sớm lên kế hoạch tách chuỗi, định dạng và danh mục khỏi logic nghiệp vụ. Nếu không, vài tháng nữa bạn sẽ có code mà mỗi hàm bắt đầu bằng if (locale.startsWith("ru")), và thêm một ngôn ngữ mới sẽ rất đau. Ở bài giảng 44 chúng ta sẽ “chữa” vấn đề này, ghi nhớ rằng nguồn locale ta đã có và biết cách dùng.
GO TO FULL VERSION