How Internationalization (i18n) is Implemented in NextChat: A Technical Deep Dive
NextChat implements internationalization through a lightweight, file-based TypeScript system that uses a central locale registry with automatic fallback merging, browser language detection, and localStorage persistence.
The ChatGPTNextWeb/NextChat repository handles multilingual support without heavy external libraries, instead relying on a custom-built i18n layer that prioritizes type safety and runtime flexibility. This article examines the exact implementation details found in the source code, from locale definitions to UI integration.
Locale Architecture and Type Safety
NextChat stores each language in a dedicated file under app/locales/, such as en.ts for English or cn.ts for Chinese. Every file exports a constant that conforms to either the full LocaleType shape or a partial variant (PartialLocaleType).
The Chinese locale file (app/locales/cn.ts) serves as the master type definition. TypeScript infers LocaleType from this file, ensuring all other translations implement the same structure:
// app/locales/en.ts
const en: LocaleType = { /* full translation object */ };
export default en;
This approach guarantees compile-time verification while allowing partial translations during development.
The Central Locale Registry
The app/locales/index.ts file functions as the single source of truth for all language operations. It imports every language module, constructs an ALL_LANGS mapping, and exposes helper functions for the rest of the application:
import cn from "./cn";
import en from "./en";
// ... additional imports
const ALL_LANGS = { cn, en, tw, pt /* ... */ };
export type Lang = keyof typeof ALL_LANGS;
export const AllLangs = Object.keys(ALL_LANGS) as Lang[];
The registry also defines ALL_LANG_OPTIONS, a mapping of language codes to human-readable names used in the settings UI dropdown.
Language Detection and Persistence
NextChat detects the user's preferred language through browser capabilities and persists the choice across sessions using localStorage.
The getLanguage() function utilizes Intl.Locale to parse navigator.language, extracting both region and language codes to match against supported locales:
function getLanguage() {
try {
const locale = new Intl.Locale(navigator.language).maximize();
const region = locale?.region?.toLowerCase();
if (AllLangs.includes(region as Lang)) return region as Lang;
if (AllLangs.includes(locale.language as Lang)) return locale.language as Lang;
return DEFAULT_LANG;
} catch {
return DEFAULT_LANG;
}
}
The system stores the selection under the "lang" key (LANG_KEY) in localStorage. The getLang() function checks this stored value first, falling back to getLanguage() if none exists. To switch languages programmatically, components call changeLang(), which updates storage and reloads the page to apply the new locale.
Fallback Merging Strategy
To prevent missing translation keys from breaking the UI, NextChat implements a recursive merge strategy. The English locale (en) serves as the universal fallback.
The utility in app/utils/merge.ts provides a merge(fallbackLang, targetLang) function that recursively copies missing fields from the fallback into the target language object:
import { merge } from "../utils/merge";
// Merges fallback (en) into target, ensuring all keys exist
const mergedLocale = merge(fallbackLang, targetLang);
This guarantees that every UI string has a value, even when a translation is incomplete.
Using Locales in React Components
Components consume the locale system by importing the default export from app/locales. This export resolves to the fully merged locale object for the currently active language:
import Locale from "@/app/locales";
export function SendButton() {
return (
<button type="submit">
{Locale.Chat.Send}
</button>
);
}
Because the imported Locale object already contains merged fallback values, components require no additional null checks or conditional logic. Complex strings with parameters, such as Locale.Chat.SubTitle(3), work immediately.
For language selection interfaces, import the control functions directly:
import { changeLang, getLang, ALL_LANG_OPTIONS } from "@/app/locales";
export function LanguageSelector() {
const current = getLang();
return (
<select
value={current}
onChange={e => changeLang(e.target.value as any)}
>
{Object.entries(ALL_LANG_OPTIONS).map(([code, name]) => (
<option key={code} value={code}>{name}</option>
))}
</select>
);
}
Adding New Languages
The repository includes a contributor guide at docs/translation.md that outlines the standardized process for adding support for new languages. The workflow involves creating a new file in app/locales/, exporting a PartialLocaleType object, and registering the language in the central index:
// app/locales/ja.ts
import { PartialLocaleType } from "./index";
const ja: PartialLocaleType = {
WIP: "開発中です...",
Chat: {
SubTitle: (count) => `${count} 件のメッセージ`,
// Additional translations
},
};
export default ja;
After creating the file, edit app/locales/index.ts to import the module and add entries to both ALL_LANGS and ALL_LANG_OPTIONS. The merge system automatically fills any untranslated keys from the English fallback.
Summary
- NextChat uses a file-based TypeScript architecture where each language resides in
app/locales/and implements type-safe interfaces derived from the Chinese base file. - The central registry in
app/locales/index.tsmanages language imports, type exports, and UI option mappings. - Runtime detection leverages
Intl.Localeto parse browser preferences, with persistence handled throughlocalStorageunder the"lang"key. - Automatic fallback merging via
app/utils/merge.tsensures incomplete translations default to English strings without runtime errors. - Components import a singleton
Localeobject that reflects the currently selected and merged language state.
Frequently Asked Questions
How does NextChat detect the user's language on first visit?
NextChat calls getLanguage() from app/locales/index.ts, which instantiates Intl.Locale(navigator.language).maximize() to extract region and language codes. It checks these against the AllLangs array, prioritizing exact region matches (e.g., zh-CN) before falling back to language-only codes (e.g., zh) and finally defaulting to English if no match exists.
What happens if a translation file is missing some keys?
The system imports the merge utility from app/utils/merge.ts to recursively overlay the English locale onto the target language. Any undefined keys in the partial translation receive values from the fallback, ensuring the UI never displays undefined strings.
Can users switch languages without restarting the application?
While the selection updates immediately in localStorage via changeLang(), the function triggers a full page reload to ensure all components re-render with the new locale imports. This design choice avoids complex state management while guaranteeing consistency across the application.
Where is the source of truth for type definitions in the i18n system?
The app/locales/cn.ts file defines the complete LocaleType interface implicitly through its export. All other locales either implement this full type or the PartialLocaleType subset, creating a single reference point for TypeScript checking across all supported languages.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →