Pi Web's interface uses a small internal i18n layer. It is intentionally kept inside the application instead of introducing another runtime dependency, so the UI can support more languages without coupling the server, API responses, or Pi's own output to a particular language.
The built-in language packages are:
en- Englishzh-CN- Simplified Chinese
The initial locale is inferred from the browser. Users can change the locale
from the language control in the top bar. The selected locale is persisted in
localStorage under pi-locale.
Client components should get the translation function from useI18n:
import { useI18n } from "@/hooks/useI18n";
export function ExampleButton() {
const { t } = useI18n();
return <button aria-label={t("example.open")}>{t("example.open")}</button>;
}The component must be rendered below I18nProvider. The application root
already provides it in app/page.tsx.
Use parameters for values that change at runtime instead of concatenating translated fragments:
<span>{t("session.messageCount", { count: messageCount })}</span>The corresponding message can use {count}:
"session.messageCount": "{count} messages"This keeps the sentence structure under the control of each language package.
Add a file under lib/i18n/messages/, using a BCP 47-style identifier. For
example, a Japanese package would be lib/i18n/messages/ja.ts:
import type { LocalePlugin } from "../types";
/** Pi Web Japanese language package. */
export const jaLocale: LocalePlugin = {
id: "ja",
label: "日本語",
messages: {
"common.language": "言語",
// Add the rest of the shared keys here.
},
};The message keys must remain stable and language-neutral. Copy the complete
key set from lib/i18n/messages/en.ts when starting a new package. Missing
keys fall back to English, but a complete package is preferred for a user
facing language.
Import and register the package in lib/i18n/registry.ts:
import { jaLocale } from "./messages/ja";
registerLocale(jaLocale);LocalePlugin.id must be unique and non-empty. The label is displayed in the
language selector. getSupportedLocales() automatically exposes registered
packages to the UI.
The built-in Locale type currently lists en and zh-CN for compile-time
safety. When adding a built-in language, update that union in
lib/i18n/types.ts and update any locale validation that intentionally lists
the built-in identifiers.
Extend lib/i18n/registry.test.mjs when browser-language detection or registry
behavior changes. Extend lib/i18n/format.test.mjs when interpolation or
locale-aware formatting changes. A new language should at minimum verify its
registry id and a representative translation.
- Add a stable key to both
lib/i18n/messages/en.tsandlib/i18n/messages/zh-CN.ts. - Put keys under a feature namespace such as
chat.*,files.*,settings.*, orcommon.*. - Use
t("namespace.key")in visible text,title,aria-label, andplaceholdervalues. - Use interpolation for counts, names, paths, and other runtime values.
- Keep product names, model names, provider names, commands, file paths, user content, tool output, and server-provided error details unchanged.
- Do not translate an API error by matching its English text. Translate only the local UI fallback around it.
Keep established technical terms in English when translating them would make
the interface harder to map back to commands, configuration fields, or Pi
documentation. This includes terms such as Agent, API Key, Provider,
Token, worktree, Diff, CWD, Shell, Git, HEAD, OAuth, and
Mermaid; preserve their conventional casing. Translate the surrounding
actions and status text. Common concepts with clear localized forms, such as
model, plugin, skill, prompt, context, and cache, can remain localized.
For example:
// en.ts
"files.uploadedCount": "{count} uploaded"
// zh-CN.ts
"files.uploadedCount": "已上传 {count} 个文件"<span>{t("files.uploadedCount", { count: uploadedCount })}</span>If a key is missing from the selected language, the formatter first tries the English package and finally returns the key itself. Missing keys also produce a development warning, which makes incomplete translations visible during development.
Run the following commands from the repository root:
node_modules/.bin/tsc --noEmit
npm run lint
node_modules/.bin/jiti lib/i18n/registry.test.mjs
node_modules/.bin/jiti lib/i18n/format.test.mjs
node --test lib/*.test.mjsDo not run next build during normal development. See the repository
development notes for the reason.
Pi Web is a user-facing web interface. Internationalization is therefore an accessibility and usability feature: people should be able to understand and operate the interface in a language they are comfortable with. Language support should lower the barrier to using Pi Web, not be treated as a measure of technical expertise or as a reason to exclude otherwise useful contributions.
The i18n layer is deliberately incremental. Contributors can add a language or improve a small group of UI messages without rewriting API contracts, changing Pi's output, or introducing a large translation framework.