Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions apps/controller/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1259,7 +1259,8 @@
"nullable": true,
"enum": [
"en",
"zh-CN"
"zh-CN",
"zh-TW"
]
},
"analyticsEnabled": {
Expand Down Expand Up @@ -1291,7 +1292,8 @@
"type": "string",
"enum": [
"en",
"zh-CN"
"zh-CN",
"zh-TW"
]
},
"analyticsEnabled": {
Expand All @@ -1315,7 +1317,8 @@
"nullable": true,
"enum": [
"en",
"zh-CN"
"zh-CN",
"zh-TW"
]
},
"analyticsEnabled": {
Expand Down Expand Up @@ -11691,4 +11694,4 @@
}
}
}
}
}
6 changes: 3 additions & 3 deletions apps/controller/src/routes/channel-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import type { ControllerBindings } from "../types.js";

const channelIdParamSchema = z.object({ channelId: z.string() });
const errorSchema = z.object({ message: z.string() });
type ControllerLocale = "en" | "zh-CN";
type ControllerLocale = "en" | "zh-CN" | "zh-TW";

function getOpenclawOrigin(container: ControllerContainer): string | null {
try {
Expand All @@ -60,12 +60,12 @@ function localizeChannelConnectMessage(
locale: ControllerLocale,
): string {
if (!isChannelConnectError(error)) {
return locale === "zh-CN"
return locale !== "en"
? "连接失败,请稍后重试。"
: "Connection failed. Please try again.";
}

if (locale === "zh-CN") {
if (locale !== "en") {
switch (error.code) {
case "invalid_credentials":
return "凭证无效,请检查后重试。";
Expand Down
8 changes: 5 additions & 3 deletions apps/controller/src/routes/desktop-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ const fallbackEventsQuerySchema = z.object({
});

const desktopPreferencesResponseSchema = z.object({
locale: z.enum(["en", "zh-CN"]).nullable(),
locale: z.enum(["en", "zh-CN", "zh-TW"]).nullable(),
analyticsEnabled: z.boolean(),
});

const desktopPreferencesUpdateSchema = z
.object({
locale: z.enum(["en", "zh-CN"]).optional(),
locale: z.enum(["en", "zh-CN", "zh-TW"]).optional(),
analyticsEnabled: z.boolean().optional(),
})
.refine(
Expand Down Expand Up @@ -346,7 +346,9 @@ export function registerDesktopRoutes(
const message =
locale === "en"
? "⏳ Compacting conversation history, estimated ~30s..."
: "⏳ 正在整理对话记录,预计30秒内完成...";
: locale === "zh-TW"
? "⏳ 正在整理對話記錄,預計 30 秒內完成..."
: "⏳ 正在整理对话记录,预计30秒内完成...";

try {
await container.gatewayService.sendChannelMessage({
Expand Down
2 changes: 1 addition & 1 deletion apps/controller/src/runtime/credit-guard-state-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ControllerEnv } from "../app/env.js";
export class CreditGuardStateWriter {
constructor(private readonly env: ControllerEnv) {}

async write(locale: "en" | "zh-CN"): Promise<void> {
async write(locale: "en" | "zh-CN" | "zh-TW"): Promise<void> {
await mkdir(path.dirname(this.env.creditGuardStatePath), {
recursive: true,
});
Expand Down
4 changes: 3 additions & 1 deletion apps/controller/src/runtime/slimclaw-runtime-model-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ export interface OpenClawRuntimeModelState {
}

const RUNTIME_MODEL_FALLBACK = "anthropic/claude-opus-4-6";
export type NoModelMessageLocale = "en" | "zh-CN";
export type NoModelMessageLocale = "en" | "zh-CN" | "zh-TW";

const NO_MODEL_CONFIGURED_MESSAGES: Record<NoModelMessageLocale, string> = {
en: "No model is available right now. Please sign in to your Nexu Official account, or add your own API key or OAuth provider under Settings → Models to enable a model.",
"zh-CN":
"当前没有可用的模型。请登录 Nexu 官方账号,或在 设置 → 模型 中配置您自己的 API Key 或 OAuth 服务商以启用模型。",
"zh-TW":
"當前沒有可用的模型。請登入 Nexu 官方帳號,或在「設定 → 模型」中設定你自己的 API Key 或 OAuth 服務商以啟用模型。",
};

export const NO_MODEL_CONFIGURED_MESSAGE = NO_MODEL_CONFIGURED_MESSAGES.en;
Expand Down
4 changes: 2 additions & 2 deletions apps/controller/src/services/channel-fallback-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export interface ChannelFallbackMessageSender {
}

export interface ChannelFallbackLocaleProvider {
getLocale(): Promise<"en" | "zh-CN"> | "en" | "zh-CN";
getLocale(): Promise<"en" | "zh-CN" | "zh-TW"> | "en" | "zh-CN" | "zh-TW";
}

export class ChannelFallbackService {
Expand Down Expand Up @@ -203,7 +203,7 @@ export class ChannelFallbackService {
const template = selectFallbackTemplate(
adapter.getTemplateMap() as Record<
FallbackErrorCode,
Partial<Record<"en" | "zh-CN", string>>
Partial<Record<"en" | "zh-CN" | "zh-TW", string>>
>,
normalized.errorCode as FallbackErrorCode,
lang,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,28 @@ const FEISHU_FALLBACK_TEMPLATES: FallbackTemplateMap = {
en: "🤖 Sorry, I can't handle your request right now. Please try again later, or contact the NexU team for support: https://docs.nexu.io/guide/contact",
"zh-CN":
"🤖 抱歉,我暂时无法处理你的请求,请稍后重试,或联系 NexU 工作人员获取支持:https://docs.nexu.io/zh/guide/contact",
"zh-TW":
"🤖 抱歉,我暫時無法處理你的請求,請稍後再試,或聯絡 NexU 團隊取得支援:https://docs.nexu.io/zh/guide/contact",
},
internal_error: {
en: "Sorry, I hit an internal error while replying. Please try again in a moment.",
"zh-CN": "抱歉,我刚刚回复时遇到内部错误。请稍后再试。",
"zh-TW": "抱歉,我剛剛回覆時遇到內部錯誤。請稍後再試。",
},
reply_delivery_failed: {
en: "Sorry, I couldn't deliver the previous reply successfully. Please try again in a moment.",
"zh-CN": "抱歉,我刚刚没有成功送达上一条回复。请稍后再试。",
"zh-TW": "抱歉,我剛剛沒有成功送達上一條回覆。請稍後再試。",
},
no_final_reply: {
en: "Sorry, I couldn't finish the previous reply. Please try again in a moment.",
"zh-CN": "抱歉,我刚刚没有完整完成上一条回复。请稍后再试。",
"zh-TW": "抱歉,我剛剛沒有完整完成上一條回覆。請稍後再試。",
},
synthetic_pre_llm_failure: {
en: "Sorry, Nexu intentionally interrupted this reply for diagnostics.",
"zh-CN": "抱歉,这条回复被 Nexu 为诊断目的主动中断。",
"zh-TW": "抱歉,這條回覆被 Nexu 為了診斷目的主動中斷。",
},
};

Expand Down Expand Up @@ -94,7 +100,7 @@ export class FeishuFallbackAdapter

toSendInput(input: {
normalized: NormalizedFallback<FallbackErrorCode>;
lang: "en" | "zh-CN";
lang: "en" | "zh-CN" | "zh-TW";
message: string;
}) {
const message = appendOptionalDiagnosticHint(
Expand All @@ -118,7 +124,7 @@ function appendOptionalDiagnosticHint(
message: string,
hint: string | undefined,
errorCode: FallbackErrorCode,
lang: "en" | "zh-CN",
lang: "en" | "zh-CN" | "zh-TW",
): string {
if (errorCode !== "unknown") {
return message;
Expand All @@ -133,6 +139,9 @@ function appendOptionalDiagnosticHint(
case "zh-CN":
suffix = `诊断提示:${trimmedHint}`;
break;
case "zh-TW":
suffix = `診斷提示:${trimmedHint}`;
break;
default:
suffix = `Diagnostic hint: ${trimmedHint}`;
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SendChannelMessageInput } from "../../openclaw-gateway-service.js";

export type FallbackLang = "en" | "zh-CN";
export type FallbackLang = "en" | "zh-CN" | "zh-TW";

export type FallbackErrorCode =
| "unknown"
Expand Down
7 changes: 5 additions & 2 deletions apps/controller/src/services/openclaw-sync-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,11 @@ export class OpenClawSyncService {
// Write locale state for the credit-guard patch in OpenClaw runtime.
// Match the controller's own locale default: unset → "en" (not "zh-CN").
const locale =
(config.desktop as Record<string, unknown>).locale === "zh-CN"
? "zh-CN"
(config.desktop as Record<string, unknown>).locale === "zh-CN" ||
(config.desktop as Record<string, unknown>).locale === "zh-TW"
? ((config.desktop as Record<string, unknown>).locale as
| "zh-CN"
| "zh-TW")
: "en";
if (runtimeModelRef) {
await this.runtimeModelWriter.write(runtimeModelRef);
Expand Down
15 changes: 11 additions & 4 deletions apps/controller/src/store/nexu-config-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,16 @@ function readDesktopCloudSessions(
);
}

function readDesktopLocale(config: NexuConfig): "en" | "zh-CN" | null {
function readDesktopLocale(
config: NexuConfig,
): "en" | "zh-CN" | "zh-TW" | null {
const desktop = config.desktop as Record<string, unknown>;
if (desktop.locale === "zh-CN") {
return "zh-CN";
}
if (desktop.locale === "zh-TW") {
return "zh-TW";
}
if (desktop.locale === "en") {
return "en";
}
Expand Down Expand Up @@ -2024,16 +2029,18 @@ export class NexuConfigStore {
return this.getDesktopRewardsStatus();
}

async getStoredDesktopLocale(): Promise<"en" | "zh-CN" | null> {
async getStoredDesktopLocale(): Promise<"en" | "zh-CN" | "zh-TW" | null> {
const config = await this.getConfig();
return readDesktopLocale(config);
}

async getDesktopLocale(): Promise<"en" | "zh-CN"> {
async getDesktopLocale(): Promise<"en" | "zh-CN" | "zh-TW"> {
return (await this.getStoredDesktopLocale()) ?? "en";
}

async setDesktopLocale(locale: "en" | "zh-CN"): Promise<"en" | "zh-CN"> {
async setDesktopLocale(
locale: "en" | "zh-CN" | "zh-TW",
): Promise<"en" | "zh-CN" | "zh-TW"> {
await this.store.update((config) => ({
...config,
desktop: {
Expand Down
2 changes: 1 addition & 1 deletion apps/controller/src/store/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ const nexuConfigObjectSchema = z.object({
.object({
localProfile: z.unknown().optional(),
cloud: z.unknown().optional(),
locale: z.enum(["en", "zh-CN"]).optional(),
locale: z.enum(["en", "zh-CN", "zh-TW"]).optional(),
analyticsEnabled: z.boolean().optional(),
})
.catchall(z.unknown())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ function loadState() {

// ── i18n messages ───────────────────────────────────────────────────

const CONTACT_LABEL = { "zh-CN": "联系我们", en: "Contact us" };
const CONTACT_LABEL = {
"zh-CN": "联系我们",
"zh-TW": "聯絡我們",
en: "Contact us",
};

function t(locale, zhMsg, enMsg, contactUrl) {
const msg = locale === "en" ? enMsg : zhMsg;
Expand Down
8 changes: 4 additions & 4 deletions apps/web/lib/api/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ export type GetApiInternalDesktopPreferencesResponses = {
* Desktop preferences
*/
200: {
locale: 'en' | 'zh-CN';
locale: 'en' | 'zh-CN' | 'zh-TW';
analyticsEnabled: boolean;
};
};
Expand All @@ -500,7 +500,7 @@ export type GetApiInternalDesktopPreferencesResponse = GetApiInternalDesktopPref

export type PatchApiInternalDesktopPreferencesData = {
body: {
locale?: 'en' | 'zh-CN';
locale?: 'en' | 'zh-CN' | 'zh-TW';
analyticsEnabled?: boolean;
};
path?: never;
Expand All @@ -513,7 +513,7 @@ export type PatchApiInternalDesktopPreferencesResponses = {
* Updated desktop preferences
*/
200: {
locale: 'en' | 'zh-CN';
locale: 'en' | 'zh-CN' | 'zh-TW';
analyticsEnabled: boolean;
};
};
Expand Down Expand Up @@ -3965,4 +3965,4 @@ export type PutApiInternalWorkspaceTemplatesByNameResponse = PutApiInternalWorks

export type ClientOptions = {
baseUrl: `${string}://${string}` | (string & {});
};
};
3 changes: 2 additions & 1 deletion apps/web/src/components/language-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export function LanguageSwitcher({ variant = "light", size = "sm" }: Props) {

const options: Array<{ value: Locale; label: string }> = [
{ value: "en", label: "English" },
{ value: "zh", label: "中文" },
{ value: "zh-CN", label: "简体中文" },
{ value: "zh-TW", label: "繁體中文" },
];

const currentLabel =
Expand Down
26 changes: 19 additions & 7 deletions apps/web/src/hooks/use-locale.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
patchApiInternalDesktopPreferences,
} from "../../lib/api/sdk.gen";

export type Locale = "en" | "zh";
export type Locale = "en" | "zh-CN" | "zh-TW";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update downstream locale checks for new zh- codes*

Changing the locale domain to "zh-CN" | "zh-TW" breaks existing consumers that still expect the legacy "zh" value, which causes user-visible regressions in Chinese UI paths. For example, apps/web/src/lib/skill-translations.ts only translates tags when locale === "zh", so skills pages now always show raw English tags for both Chinese locales. Please normalize or update these checks when introducing the new locale codes.

Useful? React with 👍 / 👎.


interface LocaleCtx {
locale: Locale;
Expand All @@ -26,12 +26,20 @@ const STORAGE_KEY = "nexu_locale";
function detectDefault(): Locale {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "en" || stored === "zh") return stored;
if (stored === "en" || stored === "zh-CN" || stored === "zh-TW") {
return stored;
}
if (stored === "zh") {
return "zh-CN";
}
} catch {
/* ignore */
}
const lang = navigator.language || "";
return lang.startsWith("zh") ? "zh" : "en";
if (/^zh-(TW|HK|MO)$/i.test(lang) || /Hant/i.test(lang)) {
return "zh-TW";
}
return lang.startsWith("zh") ? "zh-CN" : "en";
}

const LocaleContext = createContext<LocaleCtx>({
Expand All @@ -49,7 +57,7 @@ export function LocaleProvider({ children }: { children: ReactNode }) {
const userInteractedRef = useRef(false);

useEffect(() => {
document.documentElement.lang = locale === "zh" ? "zh-CN" : "en";
document.documentElement.lang = locale;
}, [locale]);

useEffect(() => {
Expand Down Expand Up @@ -94,7 +102,7 @@ export function useLocale() {
async function syncDesktopLocale(locale: Locale): Promise<void> {
await patchApiInternalDesktopPreferences({
body: {
locale: locale === "zh" ? "zh-CN" : "en",
locale,
},
}).catch(() => {
// Best-effort sync only; local UI language should still work offline.
Expand All @@ -118,8 +126,12 @@ async function bootstrapLocale(
return;
}

if (storedLocale === "en" || storedLocale === "zh-CN") {
const nextLocale = storedLocale === "zh-CN" ? "zh" : "en";
if (
storedLocale === "en" ||
storedLocale === "zh-CN" ||
storedLocale === "zh-TW"
) {
const nextLocale = storedLocale;
await i18n.changeLanguage(nextLocale);
setLocaleState(nextLocale);
try {
Expand Down
Loading