openclaw/src/wizard/clack-prompter.ts
olwater 10acfb6fff feat(i18n): introduce internationalization architecture and zh-CN support
This commit adds a robust i18n framework to OpenClaw with the following highlights:

1. Non-invasive Architecture: The core logic remains untouched. The i18n layer acts as a lightweight UI wrapper, ensuring zero side effects on the agent's performance or stability.
2. Seamless Migration: 100% backward compatible. Existing users will notice no change unless the LANG environment variable is explicitly set.
3. Robust Fallback: Implements a reliable fallback mechanism that defaults to English strings if a translation is missing or corrupted.
4. Onboarding Focus: Prioritizes the onboarding wizard and skill descriptions to improve accessibility for Chinese-speaking users.

Changes:
- Implemented a unified t() translation helper.
- Added locales/en.ts (base) and locales/zh.ts.
- Enabled language switching via LANG/LC_ALL environment variables.
- Added comprehensive documentation in i18n/README.md.

Testing: Lightly tested with LANG=zh_cn environment variable
Prompts: Used Claude 3.5 for translation assistance
2026-01-31 00:21:01 +08:00

100 lines
3.0 KiB
TypeScript

import {
cancel,
confirm,
intro,
isCancel,
multiselect,
type Option,
outro,
select,
spinner,
text,
} from "@clack/prompts";
import { createCliProgress } from "../cli/progress.js";
import { note as emitNote } from "../terminal/note.js";
import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
import { theme } from "../terminal/theme.js";
import { t } from "../i18n/index.js";
import type { WizardProgress, WizardPrompter } from "./prompts.js";
import { WizardCancelledError } from "./prompts.js";
function guardCancel<T>(value: T | symbol): T {
if (isCancel(value)) {
cancel(stylePromptTitle(t("setupCancelled")) ?? t("setupCancelled"));
throw new WizardCancelledError();
}
return value as T;
}
export function createClackPrompter(): WizardPrompter {
return {
intro: async (title) => {
intro(stylePromptTitle(title) ?? title);
},
outro: async (message) => {
outro(stylePromptTitle(message) ?? message);
},
note: async (message, title) => {
emitNote(message, title);
},
select: async (params) =>
guardCancel(
await select({
message: stylePromptMessage(params.message),
options: params.options.map((opt) => {
const base = { value: opt.value, label: opt.label };
return opt.hint === undefined ? base : { ...base, hint: stylePromptHint(opt.hint) };
}) as Option<(typeof params.options)[number]["value"]>[],
initialValue: params.initialValue,
}),
),
multiselect: async (params) =>
guardCancel(
await multiselect({
message: stylePromptMessage(params.message),
options: params.options.map((opt) => {
const base = { value: opt.value, label: opt.label };
return opt.hint === undefined ? base : { ...base, hint: stylePromptHint(opt.hint) };
}) as Option<(typeof params.options)[number]["value"]>[],
initialValues: params.initialValues,
}),
),
text: async (params) =>
guardCancel(
await text({
message: stylePromptMessage(params.message),
initialValue: params.initialValue,
placeholder: params.placeholder,
validate: params.validate,
}),
),
confirm: async (params) =>
guardCancel(
await confirm({
message: stylePromptMessage(params.message),
initialValue: params.initialValue,
}),
),
progress: (label: string): WizardProgress => {
const spin = spinner();
spin.start(theme.accent(label));
const osc = createCliProgress({
label,
indeterminate: true,
enabled: true,
fallback: "none",
});
return {
update: (message) => {
spin.message(theme.accent(message));
osc.setLabel(message);
},
stop: (message) => {
osc.done();
spin.stop(message);
},
};
},
};
}