chore: i18n (#5276)

* chore: notice i18n

* feat: add script to clean up unused i18n keys

* chore: cleanup i18n keys

* refactor(i18n/proxies): migrate proxies UI to structured locale keys

* chore: i18n for rule module

* chore: i18n for profile module

* chore: i18n for connections module

* chore: i18n for settings module

* chore: i18n for verge settings

* chore: i18n for theme settings

* chore: i18n for theme

* chore(i18n): components.home.*

* chore(i18n): remove unused i18n keys

* chore(i18n): components.profile.*

* chore(i18n): components.connection

* chore(i18n): pages.logs.*

* chore(i18n): pages.*.provider

* chore(i18n): components.settings.externalCors.*

* chore(i18n): components.settings.clash.*

* chore(i18n): components.settings.liteMode.*

* chore(i18n): components.settings.backup.*

* chore(i18n): components.settings.clash.port.*

* chore(i18n): components.settings.misc.*

* chore(i18n): components.settings.update.*

* chore(i18n): components.settings.sysproxy.*

* chore(i18n): components.settings.sysproxy.*

* chore(i18n): pages.profiles.notices/components.providers.notices

* refactor(notice): unify showNotice usage

* refactor(notice): add typed showNotice shortcuts, centralize defaults, and simplify subscriptions

* refactor: unify showNotice usage

* refactor(notice): unify showNotice API

* refactor(notice): unify showNotice usage

* chore(i18n): components.test.*

* chore(i18n): components.settings.dns.*

* chore(i18n): components.home.clashInfo.*

* chore(i18n): components.home.systemInfo.*

* chore(i18n): components.home.ipInfo/traffic.*

* chore(i18n): navigation.*

* refactor(i18n): remove pages.* namespace and migrate route texts under module-level page keys

* chore(i18n): common.*

* chore(i18n): common.*

* fix: change error handling in patch_profiles_config to return false when a switch is in progress

* fix: improve error handling in patch_profiles_config to prevent requests during profile switching

* fix: change error handling in patch_profiles_config to return false when a switch is in progress

fix: ensure CURRENT_SWITCHING_PROFILE is reset after config updates in perform_config_update and patch_profiles_config

* chore(i18n): restructure root-level locale keys into namespaces

* chore(i18n): add missing i18n keys

* docs: i18n guide

* chore: adjust i18n

* refactor(i18n): align UI actions and status labels with common keys

* refactor(i18n): unify two-name locale namespaces

* refactor(i18n/components): unify locale keys and update component references

* chore(i18n): add shared and entities namespaces to all locale files

* refactor(i18n): consolidate shared and entity namespaces across features

* chore(deps): update npm dependencies to ^7.3.5 (#5310)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* refactor(i18n): migrate shared editor modes and consolidate entities namespaces

* tmp

* refactor(i18n): flatten locales and move theme/validation strings

* docs: CONTRIBUTING_i18n.md

* refactor(i18n): restructure feedback and profile namespaces for better organization

* refactor(i18n): unify settings locale structure and update references

* refactor(i18n): reorganize locale keys for home, proxies, rules, connections, logs, unlock, and tests

* refactor(i18n/feedback/layout): unify shared toasts & normalize layout namespace

* refactor(i18n): centralize common UI strings in shared

* refactor(i18n): flatten headers and unify locale schema

* refactor(i18n): consolidate duplicate per-feature translations into shared namespace

* refactor(i18n): split locales into per-namespace files

* style: lint

* refactor(i18n): unify unlock UI translations under tests namespace

* feat(i18n): add type-checked translation keys

* style: eslint import order

* feat(i18n): replace ad-hoc loader with rust-i18n backend bundles

* chore(prebuild): remove locale-copy step

* fix(i18n, notice): propagate runtime params and update cleanup script path

* fix(i18n,notice): make locale formatting idempotent and guard early notice translations

* fix(i18n): resolve locale aliases and match OS codes correctly

* fix(unlock): use i18next-compatible double-brace interpolation in failure notice

* fix(i18n): route unlock error notices through translation keys

* fix(i18n): i18n types

* feat(i18n): localize upgrade notice for Clash core viewer

* fix(notice): ensure runtime overrides apply to prefix translations

* chore(i18n): replace literal notices with translation keys

* chore(i18n): types

* chore(i18n): regen typings before formatting to keep keys in sync

* chore(i18n): simply labels

* chore(i18n): adjust translation

* chore: remove eslint-plugin-i18next

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend (#5341)

* chore(i18n): translate settings.json (missed in previous pass) (#5343)

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend

* fix(i18n-tauri): quote placeholder-leading value in ko.yml to prevent rust_i18n parse panic

* chore(i18n): translate settings.json (forgot to include previously)

---------

Co-authored-by: rozan <34974262+thelojan@users.noreply.github.com>
This commit is contained in:
Sline
2025-11-08 19:40:38 +08:00
committed by GitHub
co-authored by renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> rozan
parent c1a2e99f63
commit c8aa72186e
286 changed files with 24243 additions and 8867 deletions
+5 -11
View File
@@ -314,28 +314,22 @@ export async function getAppDir() {
}
export async function openAppDir() {
return invoke<void>("open_app_dir").catch((err) =>
showNotice("error", err?.message || err.toString()),
);
return invoke<void>("open_app_dir").catch((err) => showNotice.error(err));
}
export async function openCoreDir() {
return invoke<void>("open_core_dir").catch((err) =>
showNotice("error", err?.message || err.toString()),
);
return invoke<void>("open_core_dir").catch((err) => showNotice.error(err));
}
export async function openLogsDir() {
return invoke<void>("open_logs_dir").catch((err) =>
showNotice("error", err?.message || err.toString()),
);
return invoke<void>("open_logs_dir").catch((err) => showNotice.error(err));
}
export const openWebUrl = async (url: string) => {
try {
await invoke("open_web_url", { url });
} catch (err: any) {
showNotice("error", err.toString());
showNotice.error(err);
}
};
@@ -377,7 +371,7 @@ export async function cmdTestDelay(url: string) {
export async function invoke_uwp_tool() {
return invoke<void>("invoke_uwp_tool").catch((err) =>
showNotice("error", err?.message || err.toString(), 1500),
showNotice.error(err, 1500),
);
}
+37 -6
View File
@@ -17,6 +17,24 @@ export const supportedLanguages = [
"zhtw",
];
const FALLBACK_LANGUAGE = "zh";
type LocaleModule = {
default: Record<string, unknown>;
};
const localeModules = import.meta.glob<LocaleModule>("@/locales/*/index.ts");
const localeLoaders = Object.entries(localeModules).reduce<
Record<string, () => Promise<LocaleModule>>
>((acc, [path, loader]) => {
const match = path.match(/[/\\]locales[/\\]([^/\\]+)[/\\]index\.ts$/);
if (match) {
acc[match[1]] = loader;
}
return acc;
}, {});
export const languages: Record<string, any> = supportedLanguages.reduce(
(acc, lang) => {
acc[lang] = {};
@@ -27,14 +45,27 @@ export const languages: Record<string, any> = supportedLanguages.reduce(
export const loadLanguage = async (language: string) => {
try {
const module = await import(`@/locales/${language}.json`);
const loader = localeLoaders[language];
if (!loader) {
throw new Error(`Locale loader not found for language "${language}"`);
}
const module = await loader();
return module.default;
} catch (error) {
console.warn(
`Failed to load language ${language}, fallback to zh, ${error}`,
);
const fallback = await import("@/locales/zh.json");
return fallback.default;
if (language !== FALLBACK_LANGUAGE) {
console.warn(
`Failed to load language ${language}, fallback to ${FALLBACK_LANGUAGE}, ${error}`,
);
const fallbackLoader = localeLoaders[FALLBACK_LANGUAGE];
if (!fallbackLoader) {
throw new Error(
`Fallback language "${FALLBACK_LANGUAGE}" resources are missing.`,
);
}
const fallback = await fallbackLoader();
return fallback.default;
}
throw error;
}
};
+311 -51
View File
@@ -1,81 +1,341 @@
import { ReactNode } from "react";
import i18n from "i18next";
import { ReactNode, isValidElement } from "react";
type NoticeType = "success" | "error" | "info";
export interface NoticeTranslationDescriptor {
key: string;
params?: Record<string, unknown>;
}
interface NoticeItem {
id: number;
type: "success" | "error" | "info";
message: ReactNode;
duration: number;
readonly id: number;
readonly type: NoticeType;
readonly duration: number;
readonly message?: ReactNode;
readonly i18n?: NoticeTranslationDescriptor;
timerId?: ReturnType<typeof setTimeout>;
}
type Listener = (notices: NoticeItem[]) => void;
type NoticeContent = unknown;
type NoticeExtra = unknown;
type NoticeShortcut = (
message: NoticeContent,
...extras: NoticeExtra[]
) => number;
type ShowNotice = ((
type: NoticeType,
message: NoticeContent,
...extras: NoticeExtra[]
) => number) & {
success: NoticeShortcut;
error: NoticeShortcut;
info: NoticeShortcut;
};
type NoticeSubscriber = () => void;
const DEFAULT_DURATIONS: Readonly<Record<NoticeType, number>> = {
success: 3000,
info: 5000,
error: 8000,
};
const TRANSLATION_KEY_PATTERN = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+$/;
let nextId = 0;
let notices: NoticeItem[] = [];
const listeners: Set<Listener> = new Set();
const subscribers: Set<NoticeSubscriber> = new Set();
function notifyListeners() {
listeners.forEach((listener) => listener([...notices])); // Pass a copy
function notifySubscribers() {
subscribers.forEach((subscriber) => subscriber());
}
// Shows a notification.
interface ParsedNoticeExtras {
params?: Record<string, unknown>;
raw?: unknown;
duration?: number;
}
export function showNotice(
type: "success" | "error" | "info",
message: ReactNode,
duration?: number,
): number {
const id = nextId++;
const effectiveDuration =
duration ?? (type === "error" ? 8000 : type === "info" ? 5000 : 3000); // Longer defaults
function parseNoticeExtras(extras: NoticeExtra[]): ParsedNoticeExtras {
let params: Record<string, unknown> | undefined;
let raw: unknown;
let duration: number | undefined;
const newNotice: NoticeItem = {
// Prioritize objects as translation params, then as raw payloads, while the first number wins as duration.
for (const extra of extras) {
if (extra === undefined) continue;
if (typeof extra === "number" && duration === undefined) {
duration = extra;
continue;
}
if (isPlainRecord(extra)) {
if (!params) {
params = extra;
continue;
}
if (!raw) {
raw = extra;
continue;
}
}
if (!raw) {
raw = extra;
continue;
}
if (!params && isPlainRecord(extra)) {
params = extra;
continue;
}
if (duration === undefined && typeof extra === "number") {
duration = extra;
}
}
return { params, raw, duration };
}
function resolveDuration(type: NoticeType, override?: number) {
return override ?? DEFAULT_DURATIONS[type];
}
function buildNotice(
id: number,
type: NoticeType,
duration: number,
payload: { message?: ReactNode; i18n?: NoticeTranslationDescriptor },
timerId?: ReturnType<typeof setTimeout>,
): NoticeItem {
return {
id,
type,
message,
duration: effectiveDuration,
duration,
timerId,
...payload,
};
// Auto-hide timer (only if duration is not null/0)
if (effectiveDuration > 0) {
newNotice.timerId = setTimeout(() => {
hideNotice(id);
}, effectiveDuration);
}
notices = [...notices, newNotice];
notifyListeners();
return id;
}
// Hides a specific notification by its ID.
function isMaybeTranslationDescriptor(
message: unknown,
): message is NoticeTranslationDescriptor {
if (
typeof message === "object" &&
message !== null &&
!Array.isArray(message) &&
!isValidElement(message)
) {
return typeof (message as Record<string, unknown>).key === "string";
}
return false;
}
function isPlainRecord(value: unknown): value is Record<string, unknown> {
if (
typeof value !== "object" ||
value === null ||
Array.isArray(value) ||
value instanceof Error ||
isValidElement(value)
) {
return false;
}
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
function createRawDescriptor(message: string): NoticeTranslationDescriptor {
return {
key: "shared.feedback.notices.raw",
params: { message },
};
}
function isLikelyTranslationKey(key: string) {
return TRANSLATION_KEY_PATTERN.test(key);
}
function shouldUseTranslationKey(
key: string,
params?: Record<string, unknown>,
) {
if (params && Object.keys(params).length > 0) return true;
if (isLikelyTranslationKey(key)) return true;
if (i18n.isInitialized) {
return i18n.exists(key);
}
return false;
}
function extractDisplayText(input: unknown): string | undefined {
if (input === null || input === undefined) return undefined;
if (typeof input === "string") return input;
if (typeof input === "number" || typeof input === "boolean") {
return String(input);
}
if (input instanceof Error) {
return input.message || input.name;
}
if (typeof input === "object" && input !== null) {
const maybeMessage = (input as { message?: unknown }).message;
if (typeof maybeMessage === "string") return maybeMessage;
}
try {
return JSON.stringify(input);
} catch {
return String(input);
}
}
function normalizeNoticeMessage(
message: NoticeContent,
params?: Record<string, unknown>,
raw?: unknown,
): { message?: ReactNode; i18n?: NoticeTranslationDescriptor } {
const rawText = raw !== undefined ? extractDisplayText(raw) : undefined;
if (isValidElement(message)) {
return { message };
}
if (isMaybeTranslationDescriptor(message)) {
const originalParams = message.params ?? {};
const mergedParams = Object.keys(params ?? {}).length
? { ...originalParams, ...params }
: { ...originalParams };
if (rawText !== undefined) {
return {
i18n: {
key: "shared.feedback.notices.prefixedRaw",
params: {
...mergedParams,
prefixKey: message.key,
prefixParams: originalParams,
message: rawText,
},
},
};
}
return {
i18n: {
key: message.key,
params: Object.keys(mergedParams).length ? mergedParams : undefined,
},
};
}
if (typeof message === "string") {
if (rawText !== undefined) {
if (shouldUseTranslationKey(message, params)) {
return {
i18n: {
key: "shared.feedback.notices.prefixedRaw",
params: {
...(params ?? {}),
prefixKey: message,
message: rawText,
},
},
};
}
// Prefer showing the original string while still surfacing the raw details below.
return {
i18n: {
key: "shared.feedback.notices.prefixedRaw",
params: {
...(params ?? {}),
prefix: message,
message: rawText,
},
},
};
}
if (shouldUseTranslationKey(message, params)) {
return {
i18n: {
key: message,
params: params && Object.keys(params).length ? params : undefined,
},
};
}
return { i18n: createRawDescriptor(message) };
}
if (rawText !== undefined) {
return { i18n: createRawDescriptor(rawText) };
}
const extracted = extractDisplayText(message);
if (extracted !== undefined) {
return { i18n: createRawDescriptor(extracted) };
}
return { i18n: createRawDescriptor("") };
}
const baseShowNotice = (
type: NoticeType,
message: NoticeContent,
...extras: NoticeExtra[]
): number => {
const id = nextId++;
const { params, raw, duration } = parseNoticeExtras(extras);
const effectiveDuration = resolveDuration(type, duration);
const timerId =
effectiveDuration > 0
? setTimeout(() => hideNotice(id), effectiveDuration)
: undefined;
const normalizedMessage = normalizeNoticeMessage(message, params, raw);
const notice = buildNotice(
id,
type,
effectiveDuration,
normalizedMessage,
timerId,
);
notices = [...notices, notice];
notifySubscribers();
return id;
};
export const showNotice: ShowNotice = Object.assign(baseShowNotice, {
success: (message: NoticeContent, ...extras: NoticeExtra[]) =>
baseShowNotice("success", message, ...extras),
error: (message: NoticeContent, ...extras: NoticeExtra[]) =>
baseShowNotice("error", message, ...extras),
info: (message: NoticeContent, ...extras: NoticeExtra[]) =>
baseShowNotice("info", message, ...extras),
});
export function hideNotice(id: number) {
const notice = notices.find((n) => n.id === id);
const notice = notices.find((candidate) => candidate.id === id);
if (notice?.timerId) {
clearTimeout(notice.timerId); // Clear timeout if manually closed
clearTimeout(notice.timerId);
}
notices = notices.filter((n) => n.id !== id);
notifyListeners();
notices = notices.filter((candidate) => candidate.id !== id);
notifySubscribers();
}
// Subscribes a listener function to notice state changes.
export function subscribeNotices(listener: () => void) {
listeners.add(listener);
export function subscribeNotices(subscriber: NoticeSubscriber) {
subscribers.add(subscriber);
return () => {
listeners.delete(listener);
subscribers.delete(subscriber);
};
}
export function getSnapshotNotices() {
return notices;
}
// Function to clear all notices at once
export function clearAllNotices() {
notices.forEach((n) => {
if (n.timerId) clearTimeout(n.timerId);
});
notices = [];
notifyListeners();
}
-888
View File
@@ -1,888 +0,0 @@
type Platform =
| "aix"
| "android"
| "darwin"
| "freebsd"
| "haiku"
| "linux"
| "openbsd"
| "sunos"
| "win32"
| "cygwin"
| "netbsd";
/**
* defines in `vite.config.ts`
*/
declare const OS_PLATFORM: Platform;
/**
* Some interface for clash api
*/
interface IConfigData {
port: number;
mode: string;
ipv6: boolean;
"socket-port": number;
"allow-lan": boolean;
"log-level": string;
"mixed-port": number;
"redir-port": number;
"socks-port": number;
"tproxy-port": number;
"external-controller": string;
"external-controller-cors": {
"allow-private-network": boolean;
"allow-origins": string[];
};
secret: string;
"unified-delay": boolean;
tun: {
stack: string;
device: string;
"auto-route": boolean;
"auto-detect-interface": boolean;
"dns-hijack": string[];
"strict-route": boolean;
mtu: number;
};
dns?: {
enable?: boolean;
listen?: string;
"enhanced-mode"?: "fake-ip" | "redir-host";
"fake-ip-range"?: string;
"fake-ip-filter"?: string[];
"fake-ip-filter-mode"?: "blacklist" | "whitelist";
"prefer-h3"?: boolean;
"respect-rules"?: boolean;
nameserver?: string[];
fallback?: string[];
"default-nameserver"?: string[];
"proxy-server-nameserver"?: string[];
"direct-nameserver"?: string[];
"direct-nameserver-follow-policy"?: boolean;
"nameserver-policy"?: Record<string, any>;
"use-hosts"?: boolean;
"use-system-hosts"?: boolean;
"fallback-filter"?: {
geoip?: boolean;
"geoip-code"?: string;
ipcidr?: string[];
domain?: string[];
};
};
}
interface IRuleItem {
type: string;
payload: string;
proxy: string;
}
interface IProxyItem {
name: string;
type: string;
udp: boolean;
xudp: boolean;
tfo: boolean;
mptcp: boolean;
smux: boolean;
history: {
time: string;
delay: number;
}[];
testUrl?: string;
all?: string[];
now?: string;
hidden?: boolean;
icon?: string;
provider?: string; // 记录是否来自provider
fixed?: string; // 记录固定(优先)的节点
}
type IProxyGroupItem = Omit<IProxyItem, "all"> & {
all: IProxyItem[];
};
interface IProxyProviderItem {
name: string;
type: string;
proxies: IProxyItem[];
updatedAt: string;
vehicleType: string;
subscriptionInfo?: {
Upload: number;
Download: number;
Total: number;
Expire: number;
};
}
interface IRuleProviderItem {
name: string;
behavior: string;
format: string;
ruleCount: number;
type: string;
updatedAt: string;
vehicleType: string;
}
interface ITrafficItem {
up: number;
down: number;
up_rate?: number;
down_rate?: number;
last_updated?: number;
}
interface IFormattedTrafficData {
up_rate_formatted: string;
down_rate_formatted: string;
total_up_formatted: string;
total_down_formatted: string;
is_fresh: boolean;
}
interface IFormattedMemoryData {
inuse_formatted: string;
oslimit_formatted: string;
usage_percent: number;
is_fresh: boolean;
}
// 增强的类型安全接口定义,确保所有字段必需
interface ISystemMonitorOverview {
traffic: {
raw: {
up: number;
down: number;
up_rate: number;
down_rate: number;
};
formatted: {
up_rate: string;
down_rate: string;
total_up: string;
total_down: string;
};
is_fresh: boolean;
};
memory: {
raw: {
inuse: number;
oslimit: number;
usage_percent: number;
};
formatted: {
inuse: string;
oslimit: string;
usage_percent: number;
};
is_fresh: boolean;
};
overall_status: "active" | "inactive" | "error" | "unknown" | "healthy";
}
// 类型安全的数据验证器
interface ISystemMonitorOverviewValidator {
validate(data: any): data is ISystemMonitorOverview;
sanitize(data: any): ISystemMonitorOverview;
}
interface ILogItem {
type: string;
time?: string;
payload: string;
}
interface IConnectionsItem {
id: string;
metadata: {
network: string;
type: string;
host: string;
sourceIP: string;
sourcePort: string;
destinationPort: string;
destinationIP?: string;
remoteDestination?: string;
process?: string;
processPath?: string;
};
upload: number;
download: number;
start: string;
chains: string[];
rule: string;
rulePayload: string;
curUpload?: number; // upload speed, calculate at runtime
curDownload?: number; // download speed, calculate at runtime
}
interface IConnections {
downloadTotal: number;
uploadTotal: number;
connections: IConnectionsItem[];
}
/**
* Some interface for command
*/
interface IClashInfo {
// status: string;
mixed_port?: number; // clash mixed port
socks_port?: number; // clash socks port
redir_port?: number; // clash redir port
tproxy_port?: number; // clash tproxy port
port?: number; // clash http port
server?: string; // external-controller
secret?: string;
}
interface IProfileItem {
uid: string;
type?: "local" | "remote" | "merge" | "script";
name?: string;
desc?: string;
file?: string;
url?: string;
updated?: number;
selected?: {
name?: string;
now?: string;
}[];
extra?: {
upload: number;
download: number;
total: number;
expire: number;
};
option?: IProfileOption;
home?: string;
}
interface IProfileOption {
user_agent?: string;
with_proxy?: boolean;
self_proxy?: boolean;
update_interval?: number;
timeout_seconds?: number;
danger_accept_invalid_certs?: boolean;
allow_auto_update?: boolean;
merge?: string;
script?: string;
rules?: string;
proxies?: string;
groups?: string;
}
interface IProfilesConfig {
current?: string;
items?: IProfileItem[];
}
interface IVergeTestItem {
uid: string;
name?: string;
icon?: string;
url: string;
}
interface IAddress {
V4?: {
ip: string;
broadcast?: string;
netmask?: string;
};
V6?: {
ip: string;
broadcast?: string;
netmask?: string;
};
}
interface INetworkInterface {
name: string;
addr: IAddress[];
mac_addr?: string;
index: number;
}
interface ISeqProfileConfig {
prepend: [];
append: [];
delete: [];
}
interface IProxyGroupConfig {
name: string;
type: "select" | "url-test" | "fallback" | "load-balance" | "relay";
proxies?: string[];
use?: string[];
url?: string;
interval?: number;
lazy?: boolean;
timeout?: number;
"max-failed-times"?: number;
"disable-udp"?: boolean;
"interface-name": string;
"routing-mark"?: number;
"include-all"?: boolean;
"include-all-proxies"?: boolean;
"include-all-providers"?: boolean;
filter?: string;
"exclude-filter"?: string;
"exclude-type"?: string;
"expected-status"?: string;
hidden?: boolean;
icon?: string;
}
interface WsOptions {
path?: string;
headers?: {
[key: string]: string;
};
"max-early-data"?: number;
"early-data-header-name"?: string;
"v2ray-http-upgrade"?: boolean;
"v2ray-http-upgrade-fast-open"?: boolean;
}
interface HttpOptions {
method?: string;
path?: string[];
headers?: {
[key: string]: string[];
};
}
interface H2Options {
path?: string;
host?: string;
}
interface GrpcOptions {
"grpc-service-name"?: string;
}
interface RealityOptions {
"public-key"?: string;
"short-id"?: string;
}
type ClientFingerprint =
| "chrome"
| "firefox"
| "safari"
| "iOS"
| "android"
| "edge"
| "360"
| "qq"
| "random";
type NetworkType = "ws" | "http" | "h2" | "grpc" | "tcp";
type CipherType =
| "none"
| "auto"
| "dummy"
| "aes-128-gcm"
| "aes-192-gcm"
| "aes-256-gcm"
| "lea-128-gcm"
| "lea-192-gcm"
| "lea-256-gcm"
| "aes-128-gcm-siv"
| "aes-256-gcm-siv"
| "2022-blake3-aes-128-gcm"
| "2022-blake3-aes-256-gcm"
| "aes-128-cfb"
| "aes-192-cfb"
| "aes-256-cfb"
| "aes-128-ctr"
| "aes-192-ctr"
| "aes-256-ctr"
| "chacha20"
| "chacha20-ietf"
| "chacha20-ietf-poly1305"
| "2022-blake3-chacha20-poly1305"
| "rabbit128-poly1305"
| "xchacha20-ietf-poly1305"
| "xchacha20"
| "aegis-128l"
| "aegis-256"
| "aez-384"
| "deoxys-ii-256-128"
| "rc4-md5";
// base
interface IProxyBaseConfig {
tfo?: boolean;
mptcp?: boolean;
"interface-name"?: string;
"routing-mark"?: number;
"ip-version"?: "dual" | "ipv4" | "ipv6" | "ipv4-prefer" | "ipv6-prefer";
"dialer-proxy"?: string;
}
// direct
interface IProxyDirectConfig extends IProxyBaseConfig {
name: string;
type: "direct";
}
// dns
interface IProxyDnsConfig extends IProxyBaseConfig {
name: string;
type: "dns";
}
// http
interface IProxyHttpConfig extends IProxyBaseConfig {
name: string;
type: "http";
server?: string;
port?: number;
username?: string;
password?: string;
tls?: boolean;
sni?: string;
"skip-cert-verify"?: boolean;
fingerprint?: string;
headers?: {
[key: string]: string;
};
}
// socks5
interface IProxySocks5Config extends IProxyBaseConfig {
name: string;
type: "socks5";
server?: string;
port?: number;
username?: string;
password?: string;
tls?: boolean;
udp?: boolean;
"skip-cert-verify"?: boolean;
fingerprint?: string;
}
// ssh
interface IProxySshConfig extends IProxyBaseConfig {
name: string;
type: "ssh";
server?: string;
port?: number;
username?: string;
password?: string;
"private-key"?: string;
"private-key-passphrase"?: string;
"host-key"?: string;
"host-key-algorithms"?: string;
}
// trojan
interface IProxyTrojanConfig extends IProxyBaseConfig {
name: string;
type: "trojan";
server?: string;
port?: number;
password?: string;
alpn?: string[];
sni?: string;
"skip-cert-verify"?: boolean;
fingerprint?: string;
udp?: boolean;
network?: NetworkType;
"reality-opts"?: RealityOptions;
"grpc-opts"?: GrpcOptions;
"ws-opts"?: WsOptions;
"ss-opts"?: {
enabled?: boolean;
method?: string;
password?: string;
};
"client-fingerprint"?: ClientFingerprint;
}
// tuic
interface IProxyTuicConfig extends IProxyBaseConfig {
name: string;
type: "tuic";
server?: string;
port?: number;
token?: string;
uuid?: string;
password?: string;
ip?: string;
"heartbeat-interval"?: number;
alpn?: string[];
"reduce-rtt"?: boolean;
"request-timeout"?: number;
"udp-relay-mode"?: string;
"congestion-controller"?: string;
"disable-sni"?: boolean;
"max-udp-relay-packet-size"?: number;
"fast-open"?: boolean;
"max-open-streams"?: number;
cwnd?: number;
"skip-cert-verify"?: boolean;
fingerprint?: string;
ca?: string;
"ca-str"?: string;
"recv-window-conn"?: number;
"recv-window"?: number;
"disable-mtu-discovery"?: boolean;
"max-datagram-frame-size"?: number;
sni?: string;
"udp-over-stream"?: boolean;
"udp-over-stream-version"?: number;
}
// vless
interface IProxyVlessConfig extends IProxyBaseConfig {
name: string;
type: "vless";
server?: string;
port?: number;
uuid?: string;
flow?: string;
tls?: boolean;
alpn?: string[];
udp?: boolean;
"packet-addr"?: boolean;
xudp?: boolean;
"packet-encoding"?: string;
network?: NetworkType;
"reality-opts"?: RealityOptions;
"http-opts"?: HttpOptions;
"h2-opts"?: H2Options;
"grpc-opts"?: GrpcOptions;
"ws-opts"?: WsOptions;
"ws-path"?: string;
"ws-headers"?: {
[key: string]: string;
};
"skip-cert-verify"?: boolean;
fingerprint?: string;
servername?: string;
"client-fingerprint"?: ClientFingerprint;
smux?: boolean;
}
// vmess
interface IProxyVmessConfig extends IProxyBaseConfig {
name: string;
type: "vmess";
server?: string;
port?: number;
uuid?: string;
alterId?: number;
cipher?: CipherType;
udp?: boolean;
network?: NetworkType;
tls?: boolean;
alpn?: string[];
"skip-cert-verify"?: boolean;
fingerprint?: string;
servername?: string;
"reality-opts"?: RealityOptions;
"http-opts"?: HttpOptions;
"h2-opts"?: H2Options;
"grpc-opts"?: GrpcOptions;
"ws-opts"?: WsOptions;
"packet-addr"?: boolean;
xudp?: boolean;
"packet-encoding"?: string;
"global-padding"?: boolean;
"authenticated-length"?: boolean;
"client-fingerprint"?: ClientFingerprint;
smux?: boolean;
}
interface WireGuardPeerOptions {
server?: string;
port?: number;
"public-key"?: string;
"pre-shared-key"?: string;
reserved?: number[];
"allowed-ips"?: string[];
}
// wireguard
interface IProxyWireguardConfig extends IProxyBaseConfig, WireGuardPeerOptions {
name: string;
type: "wireguard";
ip?: string;
ipv6?: string;
"private-key"?: string;
workers?: number;
mtu?: number;
udp?: boolean;
"persistent-keepalive"?: number;
peers?: WireGuardPeerOptions[];
"remote-dns-resolve"?: boolean;
dns?: string[];
"refresh-server-ip-interval"?: number;
}
// hysteria
interface IProxyHysteriaConfig extends IProxyBaseConfig {
name: string;
type: "hysteria";
server?: string;
port?: number;
ports?: string;
protocol?: string;
"obfs-protocol"?: string;
up?: string;
"up-speed"?: number;
down?: string;
"down-speed"?: number;
auth?: string;
"auth-str"?: string;
obfs?: string;
sni?: string;
"skip-cert-verify"?: boolean;
fingerprint?: string;
alpn?: string[];
ca?: string;
"ca-str"?: string;
"recv-window-conn"?: number;
"recv-window"?: number;
"disable-mtu-discovery"?: boolean;
"fast-open"?: boolean;
"hop-interval"?: number;
}
// hysteria2
interface IProxyHysteria2Config extends IProxyBaseConfig {
name: string;
type: "hysteria2";
server?: string;
port?: number;
ports?: string;
"hop-interval"?: number;
protocol?: string;
"obfs-protocol"?: string;
up?: string;
down?: string;
password?: string;
obfs?: string;
"obfs-password"?: string;
sni?: string;
"skip-cert-verify"?: boolean;
fingerprint?: string;
alpn?: string[];
ca?: string;
"ca-str"?: string;
cwnd?: number;
"udp-mtu"?: number;
}
// shadowsocks
interface IProxyShadowsocksConfig extends IProxyBaseConfig {
name: string;
type: "ss";
server?: string;
port?: number;
password?: string;
cipher?: CipherType;
udp?: boolean;
plugin?: "obfs" | "v2ray-plugin" | "shadow-tls" | "restls";
"plugin-opts"?: {
mode?: string;
host?: string;
password?: string;
path?: string;
tls?: string;
fingerprint?: string;
headers?: {
[key: string]: string;
};
"skip-cert-verify"?: boolean;
version?: number;
mux?: boolean;
"v2ray-http-upgrade"?: boolean;
"v2ray-http-upgrade-fast-open"?: boolean;
"version-hint"?: string;
"restls-script"?: string;
};
"udp-over-tcp"?: boolean;
"udp-over-tcp-version"?: number;
"client-fingerprint"?: ClientFingerprint;
smux?: boolean;
}
// shadowsocksR
interface IProxyshadowsocksRConfig extends IProxyBaseConfig {
name: string;
type: "ssr";
server?: string;
port?: number;
password?: string;
cipher?: CipherType;
obfs?: string;
"obfs-param"?: string;
protocol?: string;
"protocol-param"?: string;
udp?: boolean;
}
// sing-mux
interface IProxySmuxConfig {
smux?: {
enabled?: boolean;
protocol?: "smux" | "yamux" | "h2mux";
"max-connections"?: number;
"min-streams"?: number;
"max-streams"?: number;
padding?: boolean;
statistic?: boolean;
"only-tcp"?: boolean;
"brutal-opts"?: {
enabled?: boolean;
up?: string;
down?: string;
};
};
}
// snell
interface IProxySnellConfig extends IProxyBaseConfig {
name: string;
type: "snell";
server?: string;
port?: number;
psk?: string;
udp?: boolean;
version?: number;
}
interface IProxyConfig
extends IProxyBaseConfig,
IProxyDirectConfig,
IProxyDnsConfig,
IProxyHttpConfig,
IProxySocks5Config,
IProxySshConfig,
IProxyTrojanConfig,
IProxyTuicConfig,
IProxyVlessConfig,
IProxyVmessConfig,
IProxyWireguardConfig,
IProxyHysteriaConfig,
IProxyHysteria2Config,
IProxyShadowsocksConfig,
IProxyshadowsocksRConfig,
IProxySmuxConfig,
IProxySnellConfig {
type:
| "ss"
| "ssr"
| "direct"
| "dns"
| "snell"
| "http"
| "trojan"
| "hysteria"
| "hysteria2"
| "tuic"
| "wireguard"
| "ssh"
| "socks5"
| "vmess"
| "vless";
}
interface IVergeConfig {
app_log_level?: "trace" | "debug" | "info" | "warn" | "error" | string;
app_log_max_size?: number; // KB
app_log_max_count?: number;
language?: string;
tray_event?:
| "main_window"
| "tray_menu"
| "system_proxy"
| "tun_mode"
| string;
env_type?: "bash" | "cmd" | "powershell" | "fish" | string;
startup_script?: string;
start_page?: string;
clash_core?: string;
theme_mode?: "light" | "dark" | "system";
traffic_graph?: boolean;
enable_memory_usage?: boolean;
enable_group_icon?: boolean;
menu_icon?: "monochrome" | "colorful" | "disable";
menu_order?: string[];
tray_icon?: "monochrome" | "colorful";
common_tray_icon?: boolean;
sysproxy_tray_icon?: boolean;
tun_tray_icon?: boolean;
// enable_tray_speed?: boolean;
// enable_tray_icon?: boolean;
tray_inline_proxy_groups?: boolean;
enable_tun_mode?: boolean;
enable_auto_light_weight_mode?: boolean;
auto_light_weight_minutes?: number;
enable_auto_launch?: boolean;
enable_silent_start?: boolean;
enable_system_proxy?: boolean;
enable_global_hotkey?: boolean;
enable_dns_settings?: boolean;
proxy_auto_config?: boolean;
pac_file_content?: string;
proxy_host?: string;
enable_random_port?: boolean;
verge_mixed_port?: number;
verge_socks_port?: number;
verge_redir_port?: number;
verge_tproxy_port?: number;
verge_port?: number;
verge_redir_enabled?: boolean;
verge_tproxy_enabled?: boolean;
verge_socks_enabled?: boolean;
verge_http_enabled?: boolean;
enable_proxy_guard?: boolean;
enable_bypass_check?: boolean;
use_default_bypass?: boolean;
proxy_guard_duration?: number;
system_proxy_bypass?: string;
web_ui_list?: string[];
hotkeys?: string[];
theme_setting?: {
primary_color?: string;
secondary_color?: string;
primary_text?: string;
secondary_text?: string;
info_color?: string;
error_color?: string;
warning_color?: string;
success_color?: string;
font_family?: string;
css_injection?: string;
background_image?: string;
background_blend_mode?: string;
background_opacity?: number;
};
auto_close_connection?: boolean;
auto_check_update?: boolean;
default_latency_test?: string;
default_latency_timeout?: number;
enable_auto_delay_detection?: boolean;
enable_builtin_enhanced?: boolean;
auto_log_clean?: 0 | 1 | 2 | 3 | 4;
proxy_layout_column?: number;
test_list?: IVergeTestItem[];
webdav_url?: string;
webdav_username?: string;
webdav_password?: string;
home_cards?: Record<string, boolean>;
enable_hover_jump_navigator?: boolean;
hover_jump_navigator_delay?: number;
enable_external_controller?: boolean;
}
interface IWebDavFile {
filename: string;
href: string;
last_modified: string;
content_length: number;
content_type: string;
tag: string;
}
interface ILocalBackupFile {
filename: string;
path: string;
last_modified: string;
content_length: number;
}
interface IWebDavConfig {
url: string;
username: string;
password: string;
}