441 lines
13 KiB
JavaScript
441 lines
13 KiB
JavaScript
/* global module, require */
|
|
|
|
const { I18N_CATALOG } = require("./i18nCatalog");
|
|
|
|
const DEFAULT_UI_LANGUAGE = "zh-Hans";
|
|
const UI_LANGUAGE_VALUES = Object.freeze(["zh-Hans", "zh-Hant", "en", "ja", "ko"]);
|
|
const UI_LANGUAGE_VALUE_SET = new Set(UI_LANGUAGE_VALUES);
|
|
const UI_LANGUAGE_FALLBACKS = Object.freeze({
|
|
ja: "en",
|
|
ko: "en"
|
|
});
|
|
/**
|
|
* 主题内部值顺序与 shared/Web 保持一致:
|
|
* 1. 设置页下拉顺序直接复用这份稳定数组;
|
|
* 2. 已重命名的老主题继续保留多语言展示名;
|
|
* 3. 新增主题若未提供本地化名,则回退内部值显示,和 Web 保持同口径。
|
|
*/
|
|
const THEME_PRESET_VALUES = Object.freeze([
|
|
"tide",
|
|
"暮砂",
|
|
"霓潮",
|
|
"苔暮",
|
|
"焰岩",
|
|
"岩陶",
|
|
"靛雾",
|
|
"绛霓",
|
|
"玫蓝",
|
|
"珊湾",
|
|
"苔荧",
|
|
"铜暮",
|
|
"炽潮",
|
|
"藕夜",
|
|
"沙海",
|
|
"珀岚",
|
|
"炫虹",
|
|
"鎏霓",
|
|
"珊汐",
|
|
"黛苔",
|
|
"霜绯"
|
|
]);
|
|
const THEME_PRESET_LABELS = Object.freeze({
|
|
"zh-Hans": Object.freeze({
|
|
tide: "潮汐",
|
|
暮砂: "沙丘",
|
|
霓潮: "棱光",
|
|
苔暮: "苔影",
|
|
焰岩: "余烬",
|
|
岩陶: "陶土",
|
|
靛雾: "岚雾",
|
|
绛霓: "绛霓",
|
|
玫蓝: "玫蓝",
|
|
珊湾: "珊湾",
|
|
苔荧: "苔荧",
|
|
铜暮: "铜暮",
|
|
炽潮: "炽潮",
|
|
藕夜: "藕夜",
|
|
沙海: "沙海",
|
|
珀岚: "珀岚",
|
|
炫虹: "炫虹",
|
|
鎏霓: "鎏霓",
|
|
珊汐: "珊汐",
|
|
黛苔: "黛苔",
|
|
霜绯: "霜绯"
|
|
}),
|
|
"zh-Hant": Object.freeze({
|
|
tide: "潮汐",
|
|
暮砂: "沙丘",
|
|
霓潮: "稜光",
|
|
苔暮: "苔影",
|
|
焰岩: "餘燼",
|
|
岩陶: "陶土",
|
|
靛雾: "嵐霧",
|
|
绛霓: "絳霓",
|
|
玫蓝: "玫藍",
|
|
珊湾: "珊灣",
|
|
苔荧: "苔螢",
|
|
铜暮: "銅暮",
|
|
炽潮: "熾潮",
|
|
藕夜: "藕夜",
|
|
沙海: "沙海",
|
|
珀岚: "珀嵐",
|
|
炫虹: "炫虹",
|
|
鎏霓: "鎏霓",
|
|
珊汐: "珊汐",
|
|
黛苔: "黛苔",
|
|
霜绯: "霜緋"
|
|
}),
|
|
en: Object.freeze({
|
|
tide: "Tide",
|
|
暮砂: "Dune",
|
|
霓潮: "Prism",
|
|
苔暮: "Moss",
|
|
焰岩: "Ember",
|
|
岩陶: "Clay",
|
|
靛雾: "Haze",
|
|
绛霓: "Crimson",
|
|
玫蓝: "Rose",
|
|
珊湾: "Bay",
|
|
苔荧: "Glow",
|
|
铜暮: "Copper",
|
|
炽潮: "Blaze",
|
|
藕夜: "Lotus",
|
|
沙海: "Sandsea",
|
|
珀岚: "Amber",
|
|
炫虹: "Neon",
|
|
鎏霓: "Gilded",
|
|
珊汐: "Coral",
|
|
黛苔: "Slate",
|
|
霜绯: "Frost"
|
|
}),
|
|
ja: Object.freeze({
|
|
tide: "潮汐",
|
|
暮砂: "砂丘",
|
|
霓潮: "プリズム",
|
|
苔暮: "苔影",
|
|
焰岩: "残り火",
|
|
岩陶: "陶土",
|
|
靛雾: "藍霧",
|
|
绛霓: "深紅",
|
|
玫蓝: "薔薇",
|
|
珊湾: "珊湾",
|
|
苔荧: "苔光",
|
|
铜暮: "銅暮",
|
|
炽潮: "炎潮",
|
|
藕夜: "蓮夜",
|
|
沙海: "砂海",
|
|
珀岚: "琥珀",
|
|
炫虹: "ネオン",
|
|
鎏霓: "金彩",
|
|
珊汐: "珊汐",
|
|
黛苔: "黛苔",
|
|
霜绯: "霜緋"
|
|
}),
|
|
ko: Object.freeze({
|
|
tide: "조류",
|
|
暮砂: "사구",
|
|
霓潮: "프리즘",
|
|
苔暮: "이끼 그림자",
|
|
焰岩: "잿불",
|
|
岩陶: "도토",
|
|
靛雾: "남빛 안개",
|
|
绛霓: "크림슨",
|
|
玫蓝: "로즈",
|
|
珊湾: "코럴",
|
|
苔荧: "글로우",
|
|
铜暮: "코퍼",
|
|
炽潮: "블레이즈",
|
|
藕夜: "로터스",
|
|
沙海: "샌드씨",
|
|
珀岚: "앰버",
|
|
炫虹: "네온",
|
|
鎏霓: "길디드",
|
|
珊汐: "산호",
|
|
黛苔: "슬레이트",
|
|
霜绯: "프로스트"
|
|
})
|
|
});
|
|
|
|
function normalizeUiLanguage(value) {
|
|
const normalized = String(value || "").trim();
|
|
return UI_LANGUAGE_VALUE_SET.has(normalized) ? normalized : DEFAULT_UI_LANGUAGE;
|
|
}
|
|
|
|
/**
|
|
* 新增语种先走英文基线兜底:
|
|
* 1. 保证所有现有页面能稳定显示,不会回落成简体中文;
|
|
* 2. 后续补专门词典时,只需在 catalog 中加同名语言节点即可覆盖。
|
|
*/
|
|
function resolveCatalogLanguage(language) {
|
|
const normalized = normalizeUiLanguage(language);
|
|
return I18N_CATALOG[normalized] ? normalized : UI_LANGUAGE_FALLBACKS[normalized] || DEFAULT_UI_LANGUAGE;
|
|
}
|
|
|
|
function resolveCatalog(language) {
|
|
return I18N_CATALOG[resolveCatalogLanguage(language)] || I18N_CATALOG[DEFAULT_UI_LANGUAGE];
|
|
}
|
|
|
|
function deepClone(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => deepClone(item));
|
|
}
|
|
if (value && typeof value === "object") {
|
|
const next = {};
|
|
Object.keys(value).forEach((key) => {
|
|
next[key] = deepClone(value[key]);
|
|
});
|
|
return next;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function readByPath(source, path) {
|
|
if (!source || typeof source !== "object") return undefined;
|
|
const segments = String(path || "")
|
|
.split(".")
|
|
.filter((item) => !!item);
|
|
let current = source;
|
|
for (let i = 0; i < segments.length; i += 1) {
|
|
const key = segments[i];
|
|
if (!current || typeof current !== "object" || !(key in current)) {
|
|
return undefined;
|
|
}
|
|
current = current[key];
|
|
}
|
|
return current;
|
|
}
|
|
|
|
function formatTemplate(template, params) {
|
|
const source = params && typeof params === "object" ? params : {};
|
|
return String(template || "").replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
if (!(key in source)) return "";
|
|
return String(source[key] == null ? "" : source[key]);
|
|
});
|
|
}
|
|
|
|
function t(language, key, params) {
|
|
const value = readByPath(resolveCatalog(language), key);
|
|
if (typeof value !== "string") {
|
|
return key ? String(key) : "";
|
|
}
|
|
return params ? formatTemplate(value, params) : value;
|
|
}
|
|
|
|
function buildPageCopy(language, namespace) {
|
|
const value = readByPath(resolveCatalog(language), namespace);
|
|
return value && typeof value === "object" ? deepClone(value) : {};
|
|
}
|
|
|
|
function getStatusLabel(language, status) {
|
|
const normalized = String(status || "").trim();
|
|
return t(language, `common.statusLabels.${normalized}`) || normalized;
|
|
}
|
|
|
|
function getRuntimeStateLabel(language, status) {
|
|
const normalized = String(status || "").trim();
|
|
return t(language, `common.runtimeStateLabels.${normalized}`) || normalized;
|
|
}
|
|
|
|
function getUiLanguageOptions() {
|
|
return [
|
|
{ label: "简体中文", value: "zh-Hans" },
|
|
{ label: "繁體中文", value: "zh-Hant" },
|
|
{ label: "English", value: "en" },
|
|
{ label: "日本語", value: "ja" },
|
|
{ label: "한국어", value: "ko" }
|
|
];
|
|
}
|
|
|
|
function buildThemeModeOptions(language) {
|
|
return [
|
|
{ label: t(language, "settings.options.themeMode.dark"), value: "dark" },
|
|
{ label: t(language, "settings.options.themeMode.light"), value: "light" }
|
|
];
|
|
}
|
|
|
|
function buildSettingsAuthTypeOptions(language) {
|
|
return [
|
|
{ label: t(language, "settings.options.authType.password"), value: "password" },
|
|
{ label: t(language, "settings.options.authType.key"), value: "key" }
|
|
];
|
|
}
|
|
|
|
function buildAiCodexSandboxOptions(language) {
|
|
return [
|
|
{ label: t(language, "settings.options.aiCodexSandboxMode.readOnly"), value: "read-only" },
|
|
{ label: t(language, "settings.options.aiCodexSandboxMode.workspaceWrite"), value: "workspace-write" },
|
|
{
|
|
label: t(language, "settings.options.aiCodexSandboxMode.dangerFullAccess"),
|
|
value: "danger-full-access"
|
|
}
|
|
];
|
|
}
|
|
|
|
function buildAiProviderOptions(language) {
|
|
return [
|
|
{ label: t(language, "settings.options.aiDefaultProvider.codex"), value: "codex" },
|
|
{ label: t(language, "settings.options.aiDefaultProvider.copilot"), value: "copilot" }
|
|
];
|
|
}
|
|
|
|
function buildAiCopilotPermissionOptions(language) {
|
|
return [
|
|
{ label: t(language, "settings.options.aiCopilotPermissionMode.default"), value: "default" },
|
|
{
|
|
label: t(language, "settings.options.aiCopilotPermissionMode.experimental"),
|
|
value: "experimental"
|
|
},
|
|
{ label: t(language, "settings.options.aiCopilotPermissionMode.allowAll"), value: "allow-all" }
|
|
];
|
|
}
|
|
|
|
function buildServerAuthTypeOptions(language) {
|
|
return [
|
|
{ label: t(language, "serverSettings.options.authType.password"), value: "password" },
|
|
{ label: t(language, "serverSettings.options.authType.privateKey"), value: "privateKey" },
|
|
{ label: t(language, "serverSettings.options.authType.certificate"), value: "certificate" }
|
|
];
|
|
}
|
|
|
|
function buildSettingsTabs(language) {
|
|
return [
|
|
{ id: "ui", label: t(language, "settings.tabs.ui") },
|
|
{ id: "shell", label: t(language, "settings.tabs.shell") },
|
|
{ id: "connection", label: t(language, "settings.tabs.connection") },
|
|
{ id: "log", label: t(language, "settings.tabs.log") }
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 主题预设内部值保持稳定,界面层再按语言映射为用户可读展示名。
|
|
*/
|
|
function getThemePresetLabel(language, value) {
|
|
const normalizedLanguage = normalizeUiLanguage(language);
|
|
return THEME_PRESET_LABELS[normalizedLanguage]?.[value] || String(value || "");
|
|
}
|
|
|
|
function buildThemePresetOptions(language) {
|
|
return THEME_PRESET_VALUES.map((value) => ({
|
|
value,
|
|
label: getThemePresetLabel(language, value)
|
|
}));
|
|
}
|
|
|
|
function localizeServerValidationMessage(language, message) {
|
|
const raw = String(message || "").trim();
|
|
const localizedLanguage = resolveCatalogLanguage(language);
|
|
if (!raw) return raw;
|
|
const map = {
|
|
主机不能为空: {
|
|
"zh-Hant": "主機不能為空",
|
|
en: "Host is required",
|
|
ja: "ホストは必須です",
|
|
ko: "호스트는 필수입니다"
|
|
},
|
|
用户名不能为空: {
|
|
"zh-Hant": "使用者名稱不能為空",
|
|
en: "Username is required",
|
|
ja: "ユーザー名は必須です",
|
|
ko: "사용자명은 필수입니다"
|
|
},
|
|
密码不能为空: {
|
|
"zh-Hant": "密碼不能為空",
|
|
en: "Password is required",
|
|
ja: "パスワードは必須です",
|
|
ko: "비밀번호는 필수입니다"
|
|
},
|
|
私钥内容不能为空: {
|
|
"zh-Hant": "私鑰內容不能為空",
|
|
en: "Private key is required",
|
|
ja: "秘密鍵は必須です",
|
|
ko: "개인 키는 필수입니다"
|
|
},
|
|
证书模式下私钥不能为空: {
|
|
"zh-Hant": "憑證模式下私鑰不能為空",
|
|
en: "Private key is required for certificate mode",
|
|
ja: "証明書モードでは秘密鍵が必須です",
|
|
ko: "인증서 모드에서는 개인 키가 필요합니다"
|
|
},
|
|
证书模式下证书不能为空: {
|
|
"zh-Hant": "憑證模式下憑證不能為空",
|
|
en: "Certificate is required for certificate mode",
|
|
ja: "証明書モードでは証明書が必須です",
|
|
ko: "인증서 모드에서는 인증서가 필요합니다"
|
|
},
|
|
跳板机主机不能为空: {
|
|
"zh-Hant": "跳板機主機不能為空",
|
|
en: "Jump host is required",
|
|
ja: "踏み台ホストは必須です",
|
|
ko: "점프 호스트는 필수입니다"
|
|
},
|
|
跳板机用户名不能为空: {
|
|
"zh-Hant": "跳板機使用者名稱不能為空",
|
|
en: "Jump host username is required",
|
|
ja: "踏み台ユーザー名は必須です",
|
|
ko: "점프 호스트 사용자명은 필수입니다"
|
|
},
|
|
跳板机密码不能为空: {
|
|
"zh-Hant": "跳板機密碼不能為空",
|
|
en: "Jump host password is required",
|
|
ja: "踏み台パスワードは必須です",
|
|
ko: "점프 호스트 비밀번호는 필수입니다"
|
|
},
|
|
跳板机私钥不能为空: {
|
|
"zh-Hant": "跳板機私鑰不能為空",
|
|
en: "Jump host private key is required",
|
|
ja: "踏み台秘密鍵は必須です",
|
|
ko: "점프 호스트 개인 키는 필수입니다"
|
|
},
|
|
跳板机证书模式下私钥不能为空: {
|
|
"zh-Hant": "跳板機憑證模式下私鑰不能為空",
|
|
en: "Jump host private key is required for certificate mode",
|
|
ja: "踏み台の証明書モードでは秘密鍵が必須です",
|
|
ko: "점프 호스트 인증서 모드에서는 개인 키가 필요합니다"
|
|
},
|
|
跳板机证书模式下证书不能为空: {
|
|
"zh-Hant": "跳板機憑證模式下憑證不能為空",
|
|
en: "Jump host certificate is required for certificate mode",
|
|
ja: "踏み台の証明書モードでは証明書が必須です",
|
|
ko: "점프 호스트 인증서 모드에서는 인증서가 필요합니다"
|
|
}
|
|
};
|
|
if (map[raw] && map[raw][localizedLanguage]) {
|
|
return map[raw][localizedLanguage];
|
|
}
|
|
if (/^SSH 端口需为 1-65535 的整数$/.test(raw)) {
|
|
if (localizedLanguage === "zh-Hant") return "SSH 埠需為 1-65535 的整數";
|
|
if (localizedLanguage === "en") return "SSH port must be an integer between 1 and 65535";
|
|
if (localizedLanguage === "ja") return "SSH ポートは 1〜65535 の整数で入力してください";
|
|
if (localizedLanguage === "ko") return "SSH 포트는 1~65535 사이 정수여야 합니다";
|
|
}
|
|
if (/^跳板机端口需为 1-65535 的整数$/.test(raw)) {
|
|
if (localizedLanguage === "zh-Hant") return "跳板機埠需為 1-65535 的整數";
|
|
if (localizedLanguage === "en") return "Jump host port must be an integer between 1 and 65535";
|
|
if (localizedLanguage === "ja") return "踏み台ポートは 1〜65535 の整数で入力してください";
|
|
if (localizedLanguage === "ko") return "점프 호스트 포트는 1~65535 사이 정수여야 합니다";
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULT_UI_LANGUAGE,
|
|
UI_LANGUAGE_VALUES,
|
|
buildAiCodexSandboxOptions,
|
|
buildAiCopilotPermissionOptions,
|
|
buildAiProviderOptions,
|
|
buildPageCopy,
|
|
buildServerAuthTypeOptions,
|
|
buildSettingsAuthTypeOptions,
|
|
buildSettingsTabs,
|
|
buildThemeModeOptions,
|
|
buildThemePresetOptions,
|
|
formatTemplate,
|
|
getThemePresetLabel,
|
|
getRuntimeStateLabel,
|
|
getStatusLabel,
|
|
getUiLanguageOptions,
|
|
localizeServerValidationMessage,
|
|
normalizeUiLanguage,
|
|
t
|
|
};
|