Compare commits

..

106 Commits

289 changed files with 24291 additions and 8899 deletions

View File

@@ -9,6 +9,19 @@ if ! command -v pnpm >/dev/null 2>&1; then
exit 1
fi
LOCALE_DIFF="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '^src/locales/' || true)"
if [ -n "$LOCALE_DIFF" ]; then
echo "[pre-commit] Locale changes detected. Regenerating i18n types..."
pnpm i18n:types
if [ -d src/types/generated ]; then
echo "[pre-commit] Staging regenerated i18n type artifacts..."
git add src/types/generated
fi
fi
echo "[pre-commit] Running pnpm format before lint..."
pnpm format
echo "[pre-commit] Running lint-staged for JS/TS files..."
pnpm exec lint-staged

View File

@@ -1,4 +0,0 @@
files:
- source: /src/locales/en.json
translation: /src/locales
multilingual: 1

View File

@@ -1,81 +1,79 @@
# CONTRIBUTING — i18n
Thank you for considering contributing to our localization work — your help is appreciated.
Thanks for helping localize Clash Verge Rev. This guide reflects the current architecture, where the React frontend and the Tauri backend keep their translation bundles separate. Follow the steps below to keep both sides in sync without stepping on each other.
Quick overview
## Quick workflow
- cvr-i18 is a CLI that helps manage simple top-level JSON locale files:
- Detect duplicated top-level keys
- Find keys missing versus a base file (default: en.json)
- Export missing entries for translators
- Reorder keys to match the base file for predictable diffs
- Operate on a directory or a single file
- Update the language folder under `src/locales/<lang>/`; use `src/locales/en/` as the canonical reference for keys and intent.
- Run `pnpm format:i18n` to align structure and `pnpm i18n:types` to refresh generated typings.
- If you touch backend copy, edit the matching YAML file in `src-tauri/locales/<lang>.yml`.
- Preview UI changes with `pnpm dev` (desktop shell) or `pnpm web:dev` (web only).
- Keep PRs focused and add screenshots whenever layout could be affected by text length.
Get the CLI (No binary provided yet)
## Frontend locale structure
```bash
git clone https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
cd clash-verge-rev-i18n-cli
cargo install --path .
# or
cargo install --git https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
Each locale folder mirrors the namespaces under `src/locales/en/`:
```
src/locales/
en/
connections.json
home.json
shared.json
...
index.ts
zh/
...
```
Common commands
- JSON files map to namespaces (for example `home.json``home.*`). Keep keys scoped to the file they belong to.
- `shared.json` stores reusable vocabulary (buttons, validations, etc.); feature-specific wording should live in the relevant namespace.
- `index.ts` re-exports a `resources` object that aggregates the namespace JSON files. When adding or removing namespaces, mirror the pattern from `src/locales/en/index.ts`.
- Frontend bundles are lazy-loaded by `src/services/i18n.ts`. Only languages listed in `supportedLanguages` are fetched at runtime, so append new codes there when you add a locale.
- Show help: `cvr-i18`
- Directory (auto-detects `./locales` or `./src/locales`): `cvr-i18 -d /path/to/locales`
- Check duplicates: `cvr-i18 -k`
- Check missing keys: `cvr-i18 -m`
- Export missing keys: `cvr-i18 -m -e ./exports`
- Sort keys to base file: `cvr-i18 -s`
- Use a base file: `cvr-i18 -b base.json`
- Single file: `cvr-i18 -f locales/zh.json`
Because backend translations now live in their own directory, you no longer need to run `pnpm prebuild` just to sync locales—the frontend folder is the sole source of truth for web bundles.
Options (short)
## Tooling for frontend contributors
- `-d, --directory <DIR>`
- `-f, --file <FILE>`
- `-k, --duplicated-key`
- `-m, --missing-key`
- `-e, --export <DIR>`
- `-s, --sort`
- `-b, --base <FILE>`
- `pnpm format:i18n``node scripts/cleanup-unused-i18n.mjs --align --apply`. It aligns key ordering, removes unused entries, and keeps all locales in lock-step with English.
- `pnpm node scripts/cleanup-unused-i18n.mjs` (without flags) performs a dry-run audit. Use it to inspect missing or extra keys before committing.
- `pnpm i18n:types` regenerates `src/types/generated/i18n-keys.ts` and `src/types/generated/i18n-resources.ts`, ensuring TypeScript catches invalid key usage.
- For dynamic keys that the analyzer cannot statically detect, add explicit references in code or update the script whitelist to avoid false positives.
Exit codes
## Backend (Tauri) locale bundles
- `0` — success (no issues)
- `1` — issues found (duplicates/missing)
- `2` — error (IO/parse/runtime)
Native UI strings (tray menu, notifications, dialogs) use `rust-i18n` with YAML bundles stored in `src-tauri/locales/<lang>.yml`. These files are completely independent from the frontend JSON modules.
How to contribute (recommended steps)
- Keep `en.yml` semantically aligned with the Simplified Chinese baseline (`zh.yml`). Other locales may temporarily copy English if no translation is available yet.
- When a backend feature introduces new strings, update every YAML file to keep the key set consistent. Missing keys fall back to the default language (`zh`), so catching gaps early avoids mixed-language output.
- Rust code resolves the active language through `src-tauri/src/utils/i18n.rs`. No additional build step is required after editing YAML files; `tauri dev` and `tauri build` pick them up automatically.
- Start small: fix typos, improve phrasing, or refine tone and consistency.
- Run the CLI against your locale files to detect duplicates or missing keys.
- Export starter JSONs for translators with `-m -e <DIR>`.
- Prefer incremental PRs or draft PRs; leave a comment on the issue if you want guidance.
- Open an issue to report missing strings, UI context, or localization bugs.
- Add or improve docs and tests to make future contributions easier.
## Adding a new language
PR checklist
1. Duplicate `src/locales/en/` into `src/locales/<new-lang>/` and translate the JSON files while preserving key structure.
2. Update the locales `index.ts` to import every namespace. Matching the English file is the easiest way to avoid missing exports.
3. Append the language code to `supportedLanguages` in `src/services/i18n.ts`.
4. If the backend should expose the language, create `src-tauri/locales/<new-lang>.yml` and translate the keys used in existing YAML files.
5. Adjust `crowdin.yml` if the locale requires a special mapping for Crowdin.
6. Run `pnpm format:i18n`, `pnpm i18n:types`, and (optionally) `pnpm node scripts/cleanup-unused-i18n.mjs` in dry-run mode to confirm structure.
- Keep JSON files UTF-8 encoded.
- Follow the repos locale file structure and naming conventions.
- Reorder keys to match the base file (`-s`) for minimal diffs.
- Test translations in a local dev build before opening a PR.
- Reference related issues and explain any context for translations or changes.
## Authoring guidelines
Notes
- **Reuse shared vocabulary** before introducing new phrases—check `shared.json` for common actions, statuses, and labels.
- **Prefer semantic keys** (`systemProxy`, `updateInterval`, `autoRefresh`) over positional ones (`item1`, `dialogTitle2`).
- **Document placeholders** using `{{placeholder}}` and ensure components supply the required values.
- **Group keys by UI responsibility** inside each namespace (`page`, `sections`, `forms`, `actions`, `tooltips`, `notifications`, `errors`, `tables`, `statuses`, etc.).
- **Keep strings concise** to avoid layout issues. If a translation needs more context, leave a PR note so reviewers can verify the UI.
- The tool expects simple top-level JSON key/value maps.
- Exported JSONs are starter files for translators (fill in values, keep keys).
- Sorting keeps diffs consistent and reviewable.
## Testing & QA
Repository
https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli
- Launch the desktop shell with `pnpm dev` (or `pnpm web:dev`) and navigate through the affected views to confirm translations load and layouts behave.
- Run `pnpm test` if you touched code that consumes translations or adjusts formatting logic.
- For backend changes, trigger the relevant tray actions or notifications to verify the updated copy.
- Note any remaining untranslated sections or layout concerns in your PR description so maintainers can follow up.
## Feedback & Contributions
## Feedback & support
- For tool usage issues or feedback: please open an Issue in the [repository](https://github.com/clash-verge-rev/clash-verge-rev-i18n-cli) so it can be tracked and addressed.
- For localization contributions (translations, fixes, context notes, etc.): submit a PR or Issue in this repository and include examples, context, and testing instructions when possible.
- If you need help or a review, leave a comment on your submission requesting assistance.
- File an issue for missing context, tooling bugs, or localization gaps so we can track them.
- PRs that touch UI should include screenshots or GIFs whenever text length may affect layout.
- Mention the commands you ran (formatting, type generation, tests) in the PR checklist. If you need extra context or review help, request it via a PR comment.

View File

@@ -17,6 +17,7 @@ export default defineConfig([
plugins: {
js: eslintJS,
// @ts-expect-error -- https://github.com/typescript-eslint/typescript-eslint/issues/11543
"react-hooks": pluginReactHooks,
// @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/421
"import-x": pluginImportX,
@@ -132,4 +133,14 @@ export default defineConfig([
"prettier/prettier": "warn",
},
},
{
files: ["scripts/**/*.{js,mjs,cjs}", "scripts-workflow/**/*.{js,mjs,cjs}"],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
]);

View File

@@ -30,6 +30,8 @@
"lint:fix": "eslint -c eslint.config.ts --max-warnings=0 --cache --cache-location .eslintcache --fix src",
"format": "prettier --write .",
"format:check": "prettier --check .",
"format:i18n": "node scripts/cleanup-unused-i18n.mjs --align --apply",
"i18n:types": "node scripts/generate-i18n-keys.mjs",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
@@ -57,7 +59,7 @@
"axios": "^1.13.2",
"dayjs": "1.11.19",
"foxact": "^0.2.49",
"i18next": "^25.6.1",
"i18next": "^25.6.0",
"js-yaml": "^4.1.0",
"json-schema": "^0.4.0",
"lodash-es": "^4.17.21",
@@ -96,6 +98,7 @@
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-i18next": "^6.1.3",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react-hooks": "^7.0.1",

34
pnpm-lock.yaml generated
View File

@@ -78,8 +78,8 @@ importers:
specifier: ^0.2.49
version: 0.2.49(react@19.2.0)
i18next:
specifier: ^25.6.1
version: 25.6.1(typescript@5.9.3)
specifier: ^25.6.0
version: 25.6.0(typescript@5.9.3)
js-yaml:
specifier: ^4.1.0
version: 4.1.0
@@ -112,7 +112,7 @@ importers:
version: 7.66.0(react@19.2.0)
react-i18next:
specifier: 16.2.4
version: 16.2.4(i18next@25.6.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
version: 16.2.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
react-markdown:
specifier: 10.1.0
version: 10.1.0(@types/react@19.2.2)(react@19.2.0)
@@ -189,6 +189,9 @@ importers:
eslint-import-resolver-typescript:
specifier: ^4.4.4
version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)))(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-i18next:
specifier: ^6.1.3
version: 6.1.3
eslint-plugin-import-x:
specifier: ^4.16.1
version: 4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))
@@ -2566,6 +2569,10 @@ packages:
eslint-import-resolver-webpack:
optional: true
eslint-plugin-i18next@6.1.3:
resolution: {integrity: sha512-z/h4oBRd9wI1ET60HqcLSU6XPeAh/EPOrBBTyCdkWeMoYrWAaUVA+DOQkWTiNIyCltG4NTmy62SQisVXxoXurw==}
engines: {node: '>=18.10.0'}
eslint-plugin-import-x@4.16.1:
resolution: {integrity: sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2943,8 +2950,8 @@ packages:
engines: {node: '>=18'}
hasBin: true
i18next@25.6.1:
resolution: {integrity: sha512-yUWvdXtalZztmKrKw3yz/AvSP3yKyqIkVPx/wyvoYy9lkLmwzItLxp0iHZLG5hfVQ539Jor4XLO+U+NHIXg7pw==}
i18next@25.6.0:
resolution: {integrity: sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==}
peerDependencies:
typescript: ^5
peerDependenciesMeta:
@@ -3724,6 +3731,10 @@ packages:
remark-rehype@11.1.2:
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
requireindex@1.1.0:
resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==}
engines: {node: '>=0.10.5'}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
@@ -6853,6 +6864,11 @@ snapshots:
- supports-color
optional: true
eslint-plugin-i18next@6.1.3:
dependencies:
lodash: 4.17.21
requireindex: 1.1.0
eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@typescript-eslint/types': 8.46.3
@@ -7359,7 +7375,7 @@ snapshots:
husky@9.1.7: {}
i18next@25.6.1(typescript@5.9.3):
i18next@25.6.0(typescript@5.9.3):
dependencies:
'@babel/runtime': 7.28.4
optionalDependencies:
@@ -8183,11 +8199,11 @@ snapshots:
dependencies:
react: 19.2.0
react-i18next@16.2.4(i18next@25.6.1(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
react-i18next@16.2.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
dependencies:
'@babel/runtime': 7.28.4
html-parse-stringify: 3.0.1
i18next: 25.6.1(typescript@5.9.3)
i18next: 25.6.0(typescript@5.9.3)
react: 19.2.0
use-sync-external-store: 1.6.0(react@19.2.0)
optionalDependencies:
@@ -8310,6 +8326,8 @@ snapshots:
unified: 11.0.5
vfile: 6.0.3
requireindex@1.1.0: {}
reselect@5.1.1: {}
resize-observer-polyfill@1.5.1: {}

View File

@@ -1,102 +0,0 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const LOCALES_DIR = path.resolve(__dirname, "../src/locales");
const SRC_DIRS = [
path.resolve(__dirname, "../src"),
path.resolve(__dirname, "../src-tauri"),
];
const exts = [".js", ".ts", ".tsx", ".jsx", ".vue", ".rs"];
// 递归获取所有文件
function getAllFiles(dir, exts) {
let files = [];
fs.readdirSync(dir).forEach((file) => {
const full = path.join(dir, file);
if (fs.statSync(full).isDirectory()) {
files = files.concat(getAllFiles(full, exts));
} else if (exts.includes(path.extname(full))) {
files.push(full);
}
});
return files;
}
// 读取所有源码内容为一个大字符串
function getAllSourceContent() {
const files = SRC_DIRS.flatMap((dir) => getAllFiles(dir, exts));
return files.map((f) => fs.readFileSync(f, "utf8")).join("\n");
}
// 白名单 key不检查这些 key 是否被使用
const WHITELIST_KEYS = [
"theme.light",
"theme.dark",
"theme.system",
"Already Using Latest Core Version",
];
// 主流程
function processI18nFile(i18nPath, lang, allSource) {
const i18n = JSON.parse(fs.readFileSync(i18nPath, "utf8"));
const keys = Object.keys(i18n);
const used = {};
const unused = [];
let checked = 0;
const total = keys.length;
keys.forEach((key) => {
if (WHITELIST_KEYS.includes(key)) {
used[key] = i18n[key];
} else {
// 只查找一次
const regex = new RegExp(`["'\`]${key}["'\`]`);
if (regex.test(allSource)) {
used[key] = i18n[key];
} else {
unused.push(key);
}
}
checked++;
if (checked % 20 === 0 || checked === total) {
const percent = ((checked / total) * 100).toFixed(1);
process.stdout.write(
`\r[${lang}] Progress: ${checked}/${total} (${percent}%)`,
);
if (checked === total) process.stdout.write("\n");
}
});
// 输出未使用的 key
console.log(`\n[${lang}] Unused keys:`, unused);
// 备份原文件
const oldPath = i18nPath + ".old";
fs.renameSync(i18nPath, oldPath);
// 写入精简后的 i18n 文件(保留原文件名)
fs.writeFileSync(i18nPath, JSON.stringify(used, null, 2), "utf8");
console.log(
`[${lang}] Cleaned i18n file written to src/locales/${path.basename(i18nPath)}`,
);
console.log(`[${lang}] Original file backed up as ${path.basename(oldPath)}`);
}
function main() {
// 支持 zhtw.json、zh-tw.json、zh_CN.json 等
const files = fs
.readdirSync(LOCALES_DIR)
.filter((f) => /^[a-z0-9\-_]+\.json$/i.test(f) && !f.endsWith(".old"));
const allSource = getAllSourceContent();
files.forEach((file) => {
const lang = path.basename(file, ".json");
processI18nFile(path.join(LOCALES_DIR, file), lang, allSource);
});
}
main();

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import { exec } from "child_process";
import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
import { promisify } from "util";
/**
* 为Alpha版本重命名版本号

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env node
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, "..");
const LOCALE_DIR = path.resolve(ROOT_DIR, "src/locales/en");
const KEY_OUTPUT = path.resolve(ROOT_DIR, "src/types/generated/i18n-keys.ts");
const RESOURCE_OUTPUT = path.resolve(
ROOT_DIR,
"src/types/generated/i18n-resources.ts",
);
const isPlainObject = (value) =>
typeof value === "object" && value !== null && !Array.isArray(value);
const flattenKeys = (data, prefix = "") => {
const keys = [];
for (const [key, value] of Object.entries(data)) {
const nextPrefix = prefix ? `${prefix}.${key}` : key;
if (isPlainObject(value)) {
keys.push(...flattenKeys(value, nextPrefix));
} else {
keys.push(nextPrefix);
}
}
return keys;
};
const buildType = (data, indent = 0) => {
if (!isPlainObject(data)) {
return "string";
}
const entries = Object.entries(data).sort(([a], [b]) => a.localeCompare(b));
const pad = " ".repeat(indent);
const inner = entries
.map(([key, value]) => {
const typeStr = buildType(value, indent + 2);
return `${" ".repeat(indent + 2)}${JSON.stringify(key)}: ${typeStr};`;
})
.join("\n");
return entries.length
? `{
${inner}
${pad}}`
: "{}";
};
const loadNamespaceJson = async () => {
const dirents = await fs.readdir(LOCALE_DIR, { withFileTypes: true });
const namespaces = [];
for (const dirent of dirents) {
if (!dirent.isFile() || !dirent.name.endsWith(".json")) continue;
const name = dirent.name.replace(/\.json$/, "");
const filePath = path.join(LOCALE_DIR, dirent.name);
const raw = await fs.readFile(filePath, "utf8");
const json = JSON.parse(raw);
namespaces.push({ name, json });
}
namespaces.sort((a, b) => a.name.localeCompare(b.name));
return namespaces;
};
const buildKeysFile = (keys) => {
const arrayLiteral = keys.map((key) => ` "${key}"`).join(",\n");
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport const translationKeys = [\n${arrayLiteral}\n] as const;\n\nexport type TranslationKey = typeof translationKeys[number];\n`;
};
const buildResourcesFile = (namespaces) => {
const namespaceEntries = namespaces
.map(({ name, json }) => {
const typeStr = buildType(json, 4);
return ` ${JSON.stringify(name)}: ${typeStr};`;
})
.join("\n");
return `// This file is auto-generated by scripts/generate-i18n-keys.mjs\n// Do not edit this file manually.\n\nexport interface TranslationResources {\n translation: {\n${namespaceEntries}\n };\n}\n`;
};
const main = async () => {
const namespaces = await loadNamespaceJson();
const keys = namespaces.flatMap(({ name, json }) => flattenKeys(json, name));
const keysContent = buildKeysFile(keys);
const resourcesContent = buildResourcesFile(namespaces);
await fs.mkdir(path.dirname(KEY_OUTPUT), { recursive: true });
await fs.writeFile(KEY_OUTPUT, keysContent, "utf8");
await fs.writeFile(RESOURCE_OUTPUT, resourcesContent, "utf8");
console.log(`Generated ${keys.length} translation keys.`);
};
main().catch((error) => {
console.error("Failed to generate i18n metadata:", error);
process.exitCode = 1;
});

View File

@@ -1,9 +1,10 @@
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
import AdmZip from "adm-zip";
import { createRequire } from "module";
import path from "path";
import { getOctokit, context } from "@actions/github";
import AdmZip from "adm-zip";
const target = process.argv.slice(2)[0];
const alpha = process.argv.slice(2)[1];
@@ -79,11 +80,11 @@ async function resolvePortable() {
tag,
});
let assets = release.assets.filter((x) => {
const assets = release.assets.filter((x) => {
return x.name === zipFile;
});
if (assets.length > 0) {
let id = assets[0].id;
const id = assets[0].id;
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: id,

View File

@@ -1,8 +1,9 @@
import fs from "fs";
import path from "path";
import AdmZip from "adm-zip";
import { createRequire } from "module";
import fsp from "fs/promises";
import { createRequire } from "module";
import path from "path";
import AdmZip from "adm-zip";
const target = process.argv.slice(2)[0];
const ARCH_MAP = {

View File

@@ -1,14 +1,16 @@
import AdmZip from "adm-zip";
import { execSync } from "child_process";
import { createHash } from "crypto";
import fs from "fs";
import fsp from "fs/promises";
import path from "path";
import zlib from "zlib";
import AdmZip from "adm-zip";
import { glob } from "glob";
import { HttpsProxyAgent } from "https-proxy-agent";
import fetch from "node-fetch";
import path from "path";
import { extract } from "tar";
import zlib from "zlib";
import { log_debug, log_error, log_info, log_success } from "./utils.mjs";
/**
@@ -55,7 +57,7 @@ const ARCH_MAP = {
const arg1 = process.argv.slice(2)[0];
const arg2 = process.argv.slice(2)[1];
let target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
const target = arg1 === "--force" || arg1 === "-f" ? arg2 : arg1;
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process;
@@ -113,7 +115,7 @@ async function calculateFileHash(filePath) {
const hashSum = createHash("sha256");
hashSum.update(fileBuffer);
return hashSum.digest("hex");
} catch (err) {
} catch (ignoreErr) {
return null;
}
}
@@ -547,9 +549,9 @@ const resolveServicePermission = async () => {
const hashCache = await loadHashCache();
let hasChanges = false;
for (let f of serviceExecutables) {
for (const f of serviceExecutables) {
const files = glob.sync(path.join(resDir, f));
for (let filePath of files) {
for (const filePath of files) {
if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath);
const cacheKey = `${filePath}_chmod`;
@@ -573,52 +575,29 @@ const resolveServicePermission = async () => {
}
};
// resolve locales (从 src/locales 复制到 resources/locales并使用 hash 检查)
async function resolveLocales() {
const srcLocalesDir = path.join(cwd, "src/locales");
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
try {
await fsp.mkdir(targetLocalesDir, { recursive: true });
const files = await fsp.readdir(srcLocalesDir);
for (const file of files) {
const srcPath = path.join(srcLocalesDir, file);
const targetPath = path.join(targetLocalesDir, file);
if (!(await hasFileChanged(srcPath, targetPath))) continue;
await fsp.copyFile(srcPath, targetPath);
await updateHashCache(targetPath);
log_success(`Copied locale file: ${file}`);
}
log_success("All locale files processed successfully");
} catch (err) {
log_error("Error copying locale files:", err.message);
throw err;
}
}
// =======================
// Other resource resolvers (service, mmdb, geosite, geoip, enableLoopback, sysproxy)
// =======================
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service-ipc/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
const ext = platform === "win32" ? ".exe" : "";
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
});
};
const resolveInstall = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
const ext = platform === "win32" ? ".exe" : "";
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service-install" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service-install${ext}`,
});
};
const resolveUninstall = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
const ext = platform === "win32" ? ".exe" : "";
const suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
return resolveResource({
file: "clash-verge-service-uninstall" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service-uninstall${ext}`,
@@ -715,7 +694,6 @@ const tasks = [
retry: 5,
macosOnly: true,
},
{ name: "locales", func: resolveLocales, retry: 2 },
];
async function runTask() {

View File

@@ -30,10 +30,11 @@
*/
import { execSync } from "child_process";
import { program } from "commander";
import fs from "fs/promises";
import path from "path";
import { program } from "commander";
/**
* 获取当前 git 短 commit hash
* @returns {string}

View File

@@ -1,5 +1,7 @@
import axios from "axios";
import { readFileSync } from "fs";
import axios from "axios";
import { log_error, log_info, log_success } from "./utils.mjs";
const CHAT_ID_RELEASE = "@clash_verge_re"; // 正式发布频道

View File

@@ -58,7 +58,7 @@ export async function resolveUpdateLogDefault() {
const reEnd = /^---/;
let isCapturing = false;
let content = [];
const content = [];
let firstTag = "";
for (const line of data.split("\n")) {

View File

@@ -1,5 +1,6 @@
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import fetch from "node-fetch";
import { resolveUpdateLog } from "./updatelog.mjs";
const UPDATE_TAG_NAME = "updater";
@@ -113,7 +114,7 @@ async function resolveUpdater() {
});
// delete the old assets
for (let asset of updateRelease.assets) {
for (const asset of updateRelease.assets) {
if (asset.name === UPDATE_JSON_FILE) {
await github.rest.repos.deleteReleaseAsset({
...options,

View File

@@ -1,5 +1,6 @@
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import fetch from "node-fetch";
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
// Add stable update JSON filenames
@@ -259,7 +260,7 @@ async function processRelease(github, options, tag, isAlpha) {
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
// Delete existing assets with these names
for (let asset of updateRelease.assets) {
for (const asset of updateRelease.assets) {
if (asset.name === jsonFile) {
await github.rest.repos.deleteReleaseAsset({
...options,

198
src-tauri/Cargo.lock generated
View File

@@ -572,6 +572,12 @@ dependencies = [
"tokio",
]
[[package]]
name = "base62"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111"
[[package]]
name = "base64"
version = "0.13.1"
@@ -822,6 +828,16 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "bstr"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.19.0"
@@ -1131,6 +1147,7 @@ dependencies = [
"reqwest",
"reqwest_dav",
"runas",
"rust-i18n",
"scopeguard",
"serde",
"serde_json",
@@ -2848,7 +2865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
dependencies = [
"heck 0.4.1",
"proc-macro-crate 2.0.2",
"proc-macro-crate 2.0.0",
"proc-macro-error",
"proc-macro2",
"quote",
@@ -2889,6 +2906,30 @@ dependencies = [
"xkeysym",
]
[[package]]
name = "globset"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "globwalk"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
"bitflags 1.3.2",
"ignore",
"walkdir",
]
[[package]]
name = "gobject-sys"
version = "0.18.0"
@@ -3513,6 +3554,22 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "ignore"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]]
name = "image"
version = "0.25.8"
@@ -3702,6 +3759,15 @@ dependencies = [
"waker-fn",
]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.12.1"
@@ -4433,6 +4499,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "normpath"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "notify-rust"
version = "4.11.7"
@@ -5555,11 +5630,10 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "2.0.2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
dependencies = [
"toml_datetime 0.6.3",
"toml_edit 0.20.2",
]
@@ -6231,6 +6305,60 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "rust-i18n"
version = "3.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332"
dependencies = [
"globwalk",
"once_cell",
"regex",
"rust-i18n-macro",
"rust-i18n-support",
"smallvec",
]
[[package]]
name = "rust-i18n-macro"
version = "3.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965"
dependencies = [
"glob",
"once_cell",
"proc-macro2",
"quote",
"rust-i18n-support",
"serde",
"serde_json",
"serde_yaml",
"syn 2.0.108",
]
[[package]]
name = "rust-i18n-support"
version = "3.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19"
dependencies = [
"arc-swap",
"base62",
"globwalk",
"itertools 0.11.0",
"lazy_static",
"normpath",
"once_cell",
"proc-macro2",
"regex",
"serde",
"serde_json",
"serde_yaml",
"siphasher 1.0.1",
"toml 0.8.23",
"triomphe",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
@@ -6673,6 +6801,19 @@ dependencies = [
"syn 2.0.108",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.12.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "serde_yaml_ng"
version = "0.10.0"
@@ -7186,7 +7327,7 @@ dependencies = [
"cfg-expr",
"heck 0.5.0",
"pkg-config",
"toml 0.8.2",
"toml 0.8.23",
"version-compare",
]
@@ -8127,14 +8268,14 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.2"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.3",
"toml_edit 0.20.2",
"toml_datetime 0.6.11",
"toml_edit 0.22.27",
]
[[package]]
@@ -8154,9 +8295,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.3"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
@@ -8177,7 +8318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.12.0",
"toml_datetime 0.6.3",
"toml_datetime 0.6.11",
"winnow 0.5.40",
]
@@ -8186,12 +8327,24 @@ name = "toml_edit"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap 2.12.0",
"toml_datetime 0.6.11",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap 2.12.0",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.3",
"winnow 0.5.40",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.13",
]
[[package]]
@@ -8215,6 +8368,12 @@ dependencies = [
"winnow 0.7.13",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.4"
@@ -8514,6 +8673,17 @@ dependencies = [
"petgraph",
]
[[package]]
name = "triomphe"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39"
dependencies = [
"arc-swap",
"serde",
"stable_deref_trait",
]
[[package]]
name = "try-lock"
version = "0.2.5"

View File

@@ -87,6 +87,7 @@ clash_verge_service_ipc = { version = "2.0.21", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.7.1"
rust-i18n = "3.1.5"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"

52
src-tauri/locales/ar.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/de.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/en.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/es.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/fa.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/id.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/jp.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/ko.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/ru.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/tr.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/tt.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: Dashboard
body: Dashboard visibility has been updated.
clashModeChanged:
title: Mode Switch
body: Switched to {mode}.
systemProxyToggled:
title: System Proxy
body: System proxy status has been updated.
tunModeToggled:
title: TUN Mode
body: TUN mode status has been updated.
lightweightModeEntered:
title: Lightweight Mode
body: Entered lightweight mode.
appQuit:
title: About to Exit
body: Clash Verge is about to exit.
appHidden:
title: Application Hidden
body: Clash Verge is running in the background.
service:
adminPrompt: Installing the service requires administrator privileges.
tray:
dashboard: Dashboard
ruleMode: Rule Mode
globalMode: Global Mode
directMode: Direct Mode
profiles: Profiles
proxies: Proxies
systemProxy: System Proxy
tunMode: TUN Mode
closeAllConnections: Close All Connections
lightweightMode: Lightweight Mode
copyEnv: Copy Environment Variables
confDir: Configuration Directory
coreDir: Core Directory
logsDir: Log Directory
openDir: Open Directory
appLog: Application Log
coreLog: Core Log
restartClash: Restart Clash Core
restartApp: Restart Application
vergeVersion: Verge Version
more: More
exit: Exit
tooltip:
systemProxy: System Proxy
tun: TUN
profile: Profile

52
src-tauri/locales/zh.yml Normal file
View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: 仪表板
body: 仪表板显示状态已更新。
clashModeChanged:
title: 模式切换
body: 已切换至 {mode}。
systemProxyToggled:
title: 系统代理
body: 系统代理状态已更新。
tunModeToggled:
title: TUN 模式
body: TUN 模式状态已更新。
lightweightModeEntered:
title: 轻量模式
body: 已进入轻量模式。
appQuit:
title: 即将退出
body: Clash Verge 即将退出。
appHidden:
title: 应用已隐藏
body: Clash Verge 正在后台运行。
service:
adminPrompt: 安装服务需要管理员权限
tray:
dashboard: 仪表板
ruleMode: 规则模式
globalMode: 全局模式
directMode: 直连模式
profiles: 订阅
proxies: 代理
systemProxy: 系统代理
tunMode: TUN 模式
closeAllConnections: 关闭所有连接
lightweightMode: 轻量模式
copyEnv: 复制环境变量
confDir: 配置目录
coreDir: 内核目录
logsDir: 日志目录
openDir: 打开目录
appLog: 应用日志
coreLog: 内核日志
restartClash: 重启 Clash 内核
restartApp: 重启应用
vergeVersion: Verge 版本
more: 更多
exit: 退出
tooltip:
systemProxy: 系统代理
tun: TUN
profile: 订阅

View File

@@ -0,0 +1,52 @@
_version: 1
notifications:
dashboardToggled:
title: 儀表板
body: 儀表板顯示狀態已更新。
clashModeChanged:
title: 模式切換
body: 已切換至 {mode}。
systemProxyToggled:
title: 系統代理
body: 系統代理狀態已更新。
tunModeToggled:
title: TUN 模式
body: TUN 模式狀態已更新。
lightweightModeEntered:
title: 輕量模式
body: 已進入輕量模式。
appQuit:
title: 即將退出
body: Clash Verge 即將退出。
appHidden:
title: 應用已隱藏
body: Clash Verge 正在背景執行。
service:
adminPrompt: 安裝服務需要管理員權限
tray:
dashboard: 儀表板
ruleMode: 規則模式
globalMode: 全域模式
directMode: 直連模式
profiles: 訂閱
proxies: 代理
systemProxy: 系統代理
tunMode: TUN 模式
closeAllConnections: 關閉所有連線
lightweightMode: 輕量模式
copyEnv: 複製環境變數
confDir: 設定目錄
coreDir: 核心目錄
logsDir: 日誌目錄
openDir: 開啟目錄
appLog: 應用程式日誌
coreLog: 核心日誌
restartClash: 重新啟動 Clash 核心
restartApp: 重新啟動應用程式
vergeVersion: Verge 版本
more: 更多
exit: 離開
tooltip:
systemProxy: 系統代理
tun: TUN
profile: 訂閱

View File

@@ -1,8 +1,5 @@
use super::{CmdResult, StringifyErr};
use crate::{
core::service::{self, SERVICE_MANAGER, ServiceStatus},
utils::i18n::t,
};
use crate::core::service::{self, SERVICE_MANAGER, ServiceStatus};
use smartstring::SmartString;
async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) -> CmdResult {
@@ -13,7 +10,7 @@ async fn execute_service_operation_sync(status: ServiceStatus, op_type: &str) ->
.await
{
let emsg = format!("{} Service failed: {}", op_type, e);
return Err(SmartString::from(&*t(emsg.as_str()).await));
return Err(SmartString::from(emsg));
}
Ok(())
}

View File

@@ -12,7 +12,7 @@ use std::{
mpsc,
},
thread,
time::{Duration, Instant},
time::Instant,
};
use tauri::{Emitter, WebviewWindow};
@@ -92,15 +92,12 @@ impl NotificationSystem {
}
fn worker_loop(rx: mpsc::Receiver<FrontendEvent>) {
loop {
let handle = Handle::global();
if handle.is_exiting() {
break;
}
match rx.recv_timeout(Duration::from_millis(1_000)) {
let handle = Handle::global();
while !handle.is_exiting() {
match rx.try_recv() {
Ok(event) => Self::process_event(handle, event),
Err(mpsc::RecvTimeoutError::Timeout) => (),
Err(mpsc::RecvTimeoutError::Disconnected) => break,
Err(mpsc::TryRecvError::Disconnected) => break,
Err(mpsc::TryRecvError::Empty) => break,
}
}
}

View File

@@ -220,8 +220,6 @@ async fn reinstall_service() -> Result<()> {
#[cfg(target_os = "macos")]
async fn uninstall_service() -> Result<()> {
use crate::utils::i18n::t;
logging!(info, Type::Service, "uninstall service");
let binary_path = dirs::service_path()?;
@@ -233,7 +231,9 @@ async fn uninstall_service() -> Result<()> {
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
let prompt = t("Service Administrator Prompt").await;
crate::utils::i18n::sync_locale().await;
let prompt = rust_i18n::t!("service.adminPrompt").to_string();
let command = format!(
r#"do shell script "sudo '{uninstall_shell}'" with administrator privileges with prompt "{prompt}""#
);
@@ -256,8 +256,6 @@ async fn uninstall_service() -> Result<()> {
#[cfg(target_os = "macos")]
async fn install_service() -> Result<()> {
use crate::utils::i18n::t;
logging!(info, Type::Service, "install service");
let binary_path = dirs::service_path()?;
@@ -269,7 +267,9 @@ async fn install_service() -> Result<()> {
let install_shell: String = install_path.to_string_lossy().into_owned();
let prompt = t("Service Administrator Prompt").await;
crate::utils::i18n::sync_locale().await;
let prompt = rust_i18n::t!("service.adminPrompt").to_string();
let command = format!(
r#"do shell script "sudo '{install_shell}'" with administrator privileges with prompt "{prompt}""#
);

View File

@@ -1,5 +1,12 @@
use crate::utils::i18n::t;
use std::sync::Arc;
use rust_i18n::t;
use std::{borrow::Cow, sync::Arc};
fn to_arc_str(value: Cow<'static, str>) -> Arc<str> {
match value {
Cow::Borrowed(s) => Arc::from(s),
Cow::Owned(s) => Arc::from(s.into_boxed_str()),
}
}
macro_rules! define_menu {
($($field:ident => $const_name:ident, $id:expr, $text:expr),+ $(,)?) => {
@@ -11,9 +18,10 @@ macro_rules! define_menu {
pub struct MenuIds;
impl MenuTexts {
pub async fn new() -> Self {
let ($($field,)+) = futures::join!($(t($text),)+);
Self { $($field,)+ }
pub fn new() -> Self {
Self {
$($field: to_arc_str(t!($text)),)+
}
}
}
@@ -24,26 +32,26 @@ macro_rules! define_menu {
}
define_menu! {
dashboard => DASHBOARD, "tray_dashboard", "Dashboard",
rule_mode => RULE_MODE, "tray_rule_mode", "Rule Mode",
global_mode => GLOBAL_MODE, "tray_global_mode", "Global Mode",
direct_mode => DIRECT_MODE, "tray_direct_mode", "Direct Mode",
profiles => PROFILES, "tray_profiles", "Profiles",
proxies => PROXIES, "tray_proxies", "Proxies",
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "System Proxy",
tun_mode => TUN_MODE, "tray_tun_mode", "TUN Mode",
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "Close All Connections",
lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "LightWeight Mode",
copy_env => COPY_ENV, "tray_copy_env", "Copy Env",
conf_dir => CONF_DIR, "tray_conf_dir", "Conf Dir",
core_dir => CORE_DIR, "tray_core_dir", "Core Dir",
logs_dir => LOGS_DIR, "tray_logs_dir", "Logs Dir",
open_dir => OPEN_DIR, "tray_open_dir", "Open Dir",
app_log => APP_LOG, "tray_app_log", "Open App Log",
core_log => CORE_LOG, "tray_core_log", "Open Core Log",
restart_clash => RESTART_CLASH, "tray_restart_clash", "Restart Clash Core",
restart_app => RESTART_APP, "tray_restart_app", "Restart App",
verge_version => VERGE_VERSION, "tray_verge_version", "Verge Version",
more => MORE, "tray_more", "More",
exit => EXIT, "tray_exit", "Exit",
dashboard => DASHBOARD, "tray_dashboard", "tray.dashboard",
rule_mode => RULE_MODE, "tray_rule_mode", "tray.ruleMode",
global_mode => GLOBAL_MODE, "tray_global_mode", "tray.globalMode",
direct_mode => DIRECT_MODE, "tray_direct_mode", "tray.directMode",
profiles => PROFILES, "tray_profiles", "tray.profiles",
proxies => PROXIES, "tray_proxies", "tray.proxies",
system_proxy => SYSTEM_PROXY, "tray_system_proxy", "tray.systemProxy",
tun_mode => TUN_MODE, "tray_tun_mode", "tray.tunMode",
close_all_connections => CLOSE_ALL_CONNECTIONS, "tray_close_all_connections", "tray.closeAllConnections",
lightweight_mode => LIGHTWEIGHT_MODE, "tray_lightweight_mode", "tray.lightweightMode",
copy_env => COPY_ENV, "tray_copy_env", "tray.copyEnv",
conf_dir => CONF_DIR, "tray_conf_dir", "tray.confDir",
core_dir => CORE_DIR, "tray_core_dir", "tray.coreDir",
logs_dir => LOGS_DIR, "tray_logs_dir", "tray.logsDir",
open_dir => OPEN_DIR, "tray_open_dir", "tray.openDir",
app_log => APP_LOG, "tray_app_log", "tray.appLog",
core_log => CORE_LOG, "tray_core_log", "tray.coreLog",
restart_clash => RESTART_CLASH, "tray_restart_clash", "tray.restartClash",
restart_app => RESTART_APP, "tray_restart_app", "tray.restartApp",
verge_version => VERGE_VERSION, "tray_verge_version", "tray.vergeVersion",
more => MORE, "tray_more", "tray.more",
exit => EXIT, "tray_exit", "tray.exit",
}

View File

@@ -15,7 +15,7 @@ use crate::{
feat, logging,
module::lightweight::is_in_lightweight_mode,
singleton_lazy,
utils::{dirs::find_target_icons, i18n::t},
utils::{dirs::find_target_icons, i18n},
};
use super::handle;
@@ -440,6 +440,8 @@ impl Tray {
let app_handle = handle::Handle::app_handle();
i18n::sync_locale().await;
let verge = Config::verge().await.latest_arc();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
@@ -466,9 +468,9 @@ impl Tray {
}
// Get localized strings before using them
let sys_proxy_text = t("SysProxy").await;
let tun_text = t("TUN").await;
let profile_text = t("Profile").await;
let sys_proxy_text = rust_i18n::t!("tray.tooltip.systemProxy");
let tun_text = rust_i18n::t!("tray.tooltip.tun");
let profile_text = rust_i18n::t!("tray.tooltip.profile");
let v = env!("CARGO_PKG_VERSION");
let reassembled_version = v.split_once('+').map_or_else(
@@ -639,7 +641,7 @@ async fn create_profile_menu_item(
CheckMenuItem::with_id(
&app_handle,
format!("profiles_{profile_uid}"),
t(profile_name).await,
profile_name.as_str(),
true,
is_current_profile,
None::<&str>,
@@ -834,6 +836,8 @@ async fn create_tray_menu(
) -> Result<tauri::menu::Menu<Wry>> {
let current_proxy_mode = mode.unwrap_or("");
i18n::sync_locale().await;
// 获取当前配置文件的选中代理组信息
let current_profile_selected = {
let profiles_config = Config::profiles().await;
@@ -894,7 +898,7 @@ async fn create_tray_menu(
create_profile_menu_item(app_handle, profile_uid_and_name).await?;
// Pre-fetch all localized strings
let texts = &MenuTexts::new().await;
let texts = MenuTexts::new();
// Convert to references only when needed
let profile_menu_items_refs: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
.iter()

View File

@@ -25,12 +25,15 @@ use crate::{
use anyhow::Result;
use config::Config;
use once_cell::sync::OnceCell;
use rust_i18n::i18n;
use tauri::{AppHandle, Manager};
#[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
use utils::logging::Type;
i18n!("locales", fallback = "zh");
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
/// Application initialization helper functions
mod app_init {

View File

@@ -1,43 +1,80 @@
use crate::{config::Config, utils::dirs};
use once_cell::sync::Lazy;
use smartstring::alias::String;
use std::{
collections::HashMap,
fs,
path::PathBuf,
sync::{Arc, RwLock},
};
use crate::config::Config;
use sys_locale;
const DEFAULT_LANGUAGE: &str = "zh";
type TranslationMap = (String, HashMap<String, Arc<str>>);
fn supported_languages_internal() -> Vec<&'static str> {
rust_i18n::available_locales!()
}
fn get_locales_dir() -> Option<PathBuf> {
dirs::app_resources_dir()
.map(|resource_path| resource_path.join("locales"))
.ok()
const fn fallback_language() -> &'static str {
DEFAULT_LANGUAGE
}
fn locale_alias(locale: &str) -> Option<&'static str> {
match locale {
"ja" | "ja-jp" | "jp" => Some("jp"),
"zh" | "zh-cn" | "zh-hans" | "zh-sg" | "zh-my" | "zh-chs" => Some("zh"),
"zh-tw" | "zh-hk" | "zh-hant" | "zh-mo" | "zh-cht" => Some("zhtw"),
_ => None,
}
}
fn resolve_supported_language(language: &str) -> Option<String> {
if language.is_empty() {
return None;
}
let normalized = language.to_lowercase().replace('_', "-");
let mut candidates: Vec<String> = Vec::new();
let mut push_candidate = |candidate: String| {
if !candidate.is_empty()
&& !candidates
.iter()
.any(|existing| existing.eq_ignore_ascii_case(&candidate))
{
candidates.push(candidate);
}
};
let segments: Vec<&str> = normalized.split('-').collect();
for i in (1..=segments.len()).rev() {
let prefix = segments[..i].join("-");
if let Some(alias) = locale_alias(&prefix) {
push_candidate(alias.to_string());
}
push_candidate(prefix);
}
let supported = supported_languages_internal();
candidates.into_iter().find(|candidate| {
supported
.iter()
.any(|&lang| lang.eq_ignore_ascii_case(candidate))
})
}
fn system_language() -> String {
sys_locale::get_locale()
.as_deref()
.and_then(resolve_supported_language)
.unwrap_or_else(|| fallback_language().to_string())
}
pub fn get_supported_languages() -> Vec<String> {
let mut languages = Vec::new();
supported_languages_internal()
.into_iter()
.map(|lang| lang.to_string())
.collect()
}
if let Some(locales_dir) = get_locales_dir()
&& let Ok(entries) = fs::read_dir(locales_dir)
{
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str()
&& let Some(lang) = file_name.strip_suffix(".json")
{
languages.push(lang.into());
}
}
}
if languages.is_empty() {
languages.push(DEFAULT_LANGUAGE.into());
}
languages
pub fn set_locale(language: &str) {
let lang =
resolve_supported_language(language).unwrap_or_else(|| fallback_language().to_string());
rust_i18n::set_locale(&lang);
}
pub async fn current_language() -> String {
@@ -45,70 +82,18 @@ pub async fn current_language() -> String {
.await
.latest_arc()
.language
.as_deref()
.map(String::from)
.unwrap_or_else(get_system_language)
.clone()
.filter(|lang| !lang.is_empty())
.and_then(|lang| resolve_supported_language(&lang))
.unwrap_or_else(system_language)
}
static TRANSLATIONS: Lazy<RwLock<TranslationMap>> = Lazy::new(|| {
let lang = get_system_language();
let map = load_lang_file(&lang).unwrap_or_default();
RwLock::new((lang, map))
});
fn load_lang_file(lang: &str) -> Option<HashMap<String, Arc<str>>> {
let locales_dir = get_locales_dir()?;
let file_path = locales_dir.join(format!("{lang}.json"));
fs::read_to_string(file_path)
.ok()
.and_then(|content| serde_json::from_str::<HashMap<String, String>>(&content).ok())
.map(|map| {
map.into_iter()
.map(|(k, v)| (k, Arc::from(v.as_str())))
.collect()
})
pub async fn sync_locale() -> String {
let language = current_language().await;
set_locale(&language);
language
}
fn get_system_language() -> String {
sys_locale::get_locale()
.map(|locale| locale.to_lowercase())
.and_then(|locale| locale.split(['_', '-']).next().map(String::from))
.filter(|lang| get_supported_languages().contains(lang))
.unwrap_or_else(|| DEFAULT_LANGUAGE.into())
}
pub async fn t(key: &str) -> Arc<str> {
let current_lang = current_language().await;
{
if let Ok(cache) = TRANSLATIONS.read()
&& cache.0 == current_lang
&& let Some(text) = cache.1.get(key)
{
return Arc::clone(text);
}
}
if let Some(new_map) = load_lang_file(&current_lang)
&& let Ok(mut cache) = TRANSLATIONS.write()
{
*cache = (current_lang.clone(), new_map);
if let Some(text) = cache.1.get(key) {
return Arc::clone(text);
}
}
if current_lang != DEFAULT_LANGUAGE
&& let Some(default_map) = load_lang_file(DEFAULT_LANGUAGE)
&& let Ok(mut cache) = TRANSLATIONS.write()
{
*cache = (DEFAULT_LANGUAGE.into(), default_map);
if let Some(text) = cache.1.get(key) {
return Arc::clone(text);
}
}
Arc::from(key)
pub const fn default_language() -> &'static str {
fallback_language()
}

View File

@@ -1,5 +1,4 @@
use crate::{core::handle, utils::i18n::t};
use crate::{core::handle, utils::i18n};
use tauri_plugin_notification::NotificationExt;
pub enum NotificationEvent<'a> {
@@ -27,48 +26,44 @@ fn notify(title: &str, body: &str) {
}
pub async fn notify_event<'a>(event: NotificationEvent<'a>) {
i18n::sync_locale().await;
match event {
NotificationEvent::DashboardToggled => {
notify(
&t("DashboardToggledTitle").await,
&t("DashboardToggledBody").await,
);
let title = rust_i18n::t!("notifications.dashboardToggled.title").to_string();
let body = rust_i18n::t!("notifications.dashboardToggled.body").to_string();
notify(&title, &body);
}
NotificationEvent::ClashModeChanged { mode } => {
notify(
&t("ClashModeChangedTitle").await,
&t_with_args("ClashModeChangedBody", mode).await,
);
let title = rust_i18n::t!("notifications.clashModeChanged.title").to_string();
let body = rust_i18n::t!("notifications.clashModeChanged.body").replace("{mode}", mode);
notify(&title, &body);
}
NotificationEvent::SystemProxyToggled => {
notify(
&t("SystemProxyToggledTitle").await,
&t("SystemProxyToggledBody").await,
);
let title = rust_i18n::t!("notifications.systemProxyToggled.title").to_string();
let body = rust_i18n::t!("notifications.systemProxyToggled.body").to_string();
notify(&title, &body);
}
NotificationEvent::TunModeToggled => {
notify(
&t("TunModeToggledTitle").await,
&t("TunModeToggledBody").await,
);
let title = rust_i18n::t!("notifications.tunModeToggled.title").to_string();
let body = rust_i18n::t!("notifications.tunModeToggled.body").to_string();
notify(&title, &body);
}
NotificationEvent::LightweightModeEntered => {
notify(
&t("LightweightModeEnteredTitle").await,
&t("LightweightModeEnteredBody").await,
);
let title = rust_i18n::t!("notifications.lightweightModeEntered.title").to_string();
let body = rust_i18n::t!("notifications.lightweightModeEntered.body").to_string();
notify(&title, &body);
}
NotificationEvent::AppQuit => {
notify(&t("AppQuitTitle").await, &t("AppQuitBody").await);
let title = rust_i18n::t!("notifications.appQuit.title").to_string();
let body = rust_i18n::t!("notifications.appQuit.body").to_string();
notify(&title, &body);
}
#[cfg(target_os = "macos")]
NotificationEvent::AppHidden => {
notify(&t("AppHiddenTitle").await, &t("AppHiddenBody").await);
let title = rust_i18n::t!("notifications.appHidden.title").to_string();
let body = rust_i18n::t!("notifications.appHidden.body").to_string();
notify(&title, &body);
}
}
}
// 辅助函数带参数的i18n
async fn t_with_args(key: &str, mode: &str) -> String {
t(key).await.replace("{mode}", mode)
}

View File

@@ -1,5 +1,3 @@
use std::time::Duration;
use anyhow::{Result, bail};
use percent_encoding::percent_decode_str;
use smartstring::alias::String;
@@ -75,23 +73,24 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
"failed to parse profile from url: {:?}",
e
);
// TODO 通知系统疑似损坏,前端无法显示通知事件
handle::Handle::notice_message("import_sub_url::error", e.to_string());
return Ok(());
}
};
let uid = item.uid.clone().unwrap_or_default();
// TODO 通过 deep link 导入后需要正确调用前端刷新订阅页面,以及通知结果
match profiles::profiles_append_item_safe(&mut item).await {
Ok(_) => {
Config::profiles().await.apply();
let _ = Config::profiles().await.data_arc().save_file().await;
// TODO 通知系统疑似损坏,前端无法显示通知事件
handle::Handle::notice_message(
"import_sub_url::ok",
"", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
item.uid.clone().unwrap_or_default(),
);
handle::Handle::refresh_verge();
handle::Handle::notify_profile_changed(uid.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
// TODO fuck me this shit is fucking broken as fucked
handle::Handle::notify_profile_changed(uid);
}
Err(e) => {
@@ -102,10 +101,14 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
e
);
Config::profiles().await.discard();
// TODO 通知系统疑似损坏,前端无法显示通知事件
handle::Handle::notice_message("import_sub_url::error", e.to_string());
return Ok(());
}
}
handle::Handle::refresh_verge();
handle::Handle::refresh_clash();
Ok(())
}

View File

@@ -11,7 +11,7 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": ["resources", "resources/locales/*"],
"resources": ["resources"],
"publisher": "Clash Verge Rev",
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"copyright": "GNU General Public License v3.0",

View File

@@ -1,14 +1,17 @@
import { CloseRounded } from "@mui/icons-material";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import React, { useSyncExternalStore } from "react";
import { useTranslation } from "react-i18next";
import {
subscribeNotices,
hideNotice,
getSnapshotNotices,
} from "@/services/noticeService";
import type { TranslationKey } from "@/types/generated/i18n-keys";
export const NoticeManager: React.FC = () => {
const { t } = useTranslation();
const currentNotices = useSyncExternalStore(
subscribeNotices,
getSnapshotNotices,
@@ -60,7 +63,61 @@ export const NoticeManager: React.FC = () => {
</IconButton>
}
>
{notice.message}
{notice.i18n
? (() => {
const params = (notice.i18n.params ?? {}) as Record<
string,
unknown
>;
const {
prefixKey,
prefixParams,
prefix,
message,
...restParams
} = params;
const prefixKeyParams =
prefixParams &&
typeof prefixParams === "object" &&
prefixParams !== null
? (prefixParams as Record<string, unknown>)
: undefined;
const resolvedPrefix =
typeof prefixKey === "string"
? t(prefixKey as TranslationKey, {
defaultValue: prefixKey,
...(prefixKeyParams ?? {}),
...restParams,
})
: typeof prefix === "string"
? prefix
: undefined;
const finalParams: Record<string, unknown> = {
...restParams,
};
if (resolvedPrefix !== undefined) {
finalParams.prefix = resolvedPrefix;
}
if (typeof message === "string") {
finalParams.message = message;
}
const defaultValue =
resolvedPrefix && typeof message === "string"
? `${resolvedPrefix} ${message}`
: typeof message === "string"
? message
: undefined;
return t(notice.i18n.key as TranslationKey, {
defaultValue,
...finalParams,
});
})()
: notice.message}
</Alert>
</Snackbar>
))}

View File

@@ -1,16 +1,25 @@
import { InboxRounded } from "@mui/icons-material";
import { alpha, Box, Typography } from "@mui/material";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import type { TranslationKey } from "@/types/generated/i18n-keys";
interface Props {
text?: React.ReactNode;
extra?: React.ReactNode;
text?: ReactNode;
textKey?: TranslationKey;
extra?: ReactNode;
}
export const BaseEmpty = (props: Props) => {
const { text = "Empty", extra } = props;
export const BaseEmpty = ({
text,
textKey = "shared.statuses.empty",
extra,
}: Props) => {
const { t } = useTranslation();
const resolvedText: ReactNode = text !== undefined ? text : t(textKey);
return (
<Box
sx={({ palette }) => ({
@@ -24,7 +33,7 @@ export const BaseEmpty = (props: Props) => {
})}
>
<InboxRounded sx={{ fontSize: "4em" }} />
<Typography sx={{ fontSize: "1.25em" }}>{t(`${text}`)}</Typography>
<Typography sx={{ fontSize: "1.25em" }}>{resolvedText}</Typography>
{extra}
</Box>
);

View File

@@ -135,7 +135,7 @@ export const BaseSearchBox = ({
if (useRegularExpression && value) {
const isValid = validateRegex(value);
if (!isValid) {
setErrorMessage(t("Invalid regular expression"));
setErrorMessage(t("shared.validation.invalidRegex"));
}
}
@@ -156,7 +156,7 @@ export const BaseSearchBox = ({
} else {
const value = inputRef.current?.value ?? "";
if (value && !validateRegex(value)) {
setErrorMessage(t("Invalid regular expression"));
setErrorMessage(t("shared.validation.invalidRegex"));
}
}
return next;
@@ -173,7 +173,7 @@ export const BaseSearchBox = ({
size="small"
variant="outlined"
spellCheck="false"
placeholder={placeholder ?? t("Filter conditions")}
placeholder={placeholder ?? t("shared.placeholders.filter")}
sx={{ input: { py: 0.65, px: 1.25 } }}
onChange={onChange}
error={!!errorMessage}
@@ -182,7 +182,7 @@ export const BaseSearchBox = ({
sx: { pr: 1 },
endAdornment: (
<Box display="flex">
<Tooltip title={t("Match Case")}>
<Tooltip title={t("shared.placeholders.matchCase")}>
<div>
<SvgIcon
component={matchCaseIcon}
@@ -192,7 +192,7 @@ export const BaseSearchBox = ({
/>
</div>
</Tooltip>
<Tooltip title={t("Match Whole Word")}>
<Tooltip title={t("shared.placeholders.matchWholeWord")}>
<div>
<SvgIcon
component={matchWholeWordIcon}
@@ -202,7 +202,7 @@ export const BaseSearchBox = ({
/>
</div>
</Tooltip>
<Tooltip title={t("Use Regular Expression")}>
<Tooltip title={t("shared.placeholders.useRegex")}>
<div>
<SvgIcon
component={useRegularExpressionIcon}

View File

@@ -12,7 +12,7 @@ export const BaseStyledTextField = styled((props: TextFieldProps) => {
size="small"
variant="outlined"
spellCheck="false"
placeholder={t("Filter conditions")}
placeholder={t("shared.placeholders.filter")}
sx={{ input: { py: 0.65, px: 1.25 } }}
{...props}
/>

View File

@@ -177,7 +177,7 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
<ErrorOutlineRounded sx={{ fontSize: 48, mb: 2, color: "error.main" }} />
<Typography variant="h6" gutterBottom>
{t("Traffic Statistics Error")}
{t("shared.feedback.errors.trafficStats")}
</Typography>
<Typography
@@ -186,18 +186,17 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
textAlign="center"
sx={{ mb: 2 }}
>
{t(
"The traffic statistics component encountered an error and has been disabled to prevent crashes.",
)}
{t("shared.feedback.errors.trafficStatsDescription")}
</Typography>
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
<Typography variant="body2">
<strong>Error:</strong> {error?.message || "Unknown error"}
<strong>Error:</strong>{" "}
{error instanceof Error ? error.message : "Unknown error"}
</Typography>
{retryCount > 0 && (
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
{t("Retry attempts")}: {retryCount}/{maxRetries}
{t("shared.labels.retryAttempts")}: {retryCount}/{maxRetries}
</Typography>
)}
</Alert>
@@ -211,12 +210,12 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
onClick={onRetry}
size="small"
>
{t("Retry")}
{t("shared.actions.retry")}
</Button>
)}
<Button variant="outlined" onClick={onRefresh} size="small">
{t("Refresh Page")}
{t("shared.actions.refreshPage")}
</Button>
<Button
@@ -225,7 +224,9 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
onClick={onToggleDetails}
size="small"
>
{showDetails ? t("Hide Details") : t("Show Details")}
{showDetails
? t("shared.actions.hideDetails")
: t("shared.actions.showDetails")}
</Button>
</Box>

View File

@@ -1,8 +1,8 @@
import { Box, Button, Snackbar, useTheme } from "@mui/material";
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { t } from "i18next";
import { useImperativeHandle, useState, type Ref } from "react";
import { useTranslation } from "react-i18next";
import { closeConnections } from "tauri-plugin-mihomo-api";
import parseTraffic from "@/utils/parse-traffic";
@@ -55,6 +55,7 @@ interface InnerProps {
}
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
const { t } = useTranslation();
const { metadata, rulePayload } = data;
const theme = useTheme();
const chains = [...data.chains].reverse().join(" / ");
@@ -67,34 +68,52 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
: metadata.remoteDestination;
const information = [
{ label: t("Host"), value: host },
{ label: t("Downloaded"), value: parseTraffic(data.download).join(" ") },
{ label: t("Uploaded"), value: parseTraffic(data.upload).join(" ") },
{ label: t("connections.components.fields.host"), value: host },
{
label: t("DL Speed"),
label: t("shared.labels.downloaded"),
value: parseTraffic(data.download).join(" "),
},
{
label: t("shared.labels.uploaded"),
value: parseTraffic(data.upload).join(" "),
},
{
label: t("connections.components.fields.dlSpeed"),
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
},
{
label: t("UL Speed"),
label: t("connections.components.fields.ulSpeed"),
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
},
{
label: t("Chains"),
label: t("connections.components.fields.chains"),
value: chains,
},
{ label: t("Rule"), value: rule },
{ label: t("connections.components.fields.rule"), value: rule },
{
label: t("Process"),
label: t("connections.components.fields.process"),
value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ""}`,
},
{ label: t("Time"), value: dayjs(data.start).fromNow() },
{
label: t("Source"),
label: t("connections.components.fields.time"),
value: dayjs(data.start).fromNow(),
},
{
label: t("connections.components.fields.source"),
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
},
{ label: t("Destination"), value: Destination },
{ label: t("DestinationPort"), value: `${metadata.destinationPort}` },
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
{
label: t("connections.components.fields.destination"),
value: Destination,
},
{
label: t("connections.components.fields.destinationPort"),
value: `${metadata.destinationPort}`,
},
{
label: t("connections.components.fields.type"),
value: `${metadata.type}(${metadata.network})`,
},
];
const onDelete = useLockFn(async () => closeConnections(data.id));
@@ -118,13 +137,13 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
<Box sx={{ textAlign: "right" }}>
<Button
variant="contained"
title={t("Close Connection")}
title={t("connections.components.actions.closeConnection")}
onClick={() => {
onDelete();
onClose?.();
}}
>
{t("Close Connection")}
{t("connections.components.actions.closeConnection")}
</Button>
</Box>
</Box>

View File

@@ -9,6 +9,7 @@ import {
} from "@mui/material";
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { useTranslation } from "react-i18next";
import { closeConnections } from "tauri-plugin-mihomo-api";
import parseTraffic from "@/utils/parse-traffic";
@@ -33,6 +34,7 @@ export const ConnectionItem = (props: Props) => {
const { value, onShowDetail } = props;
const { id, metadata, chains, start, curUpload, curDownload } = value;
const { t } = useTranslation();
const onDelete = useLockFn(async () => closeConnections(id));
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
@@ -42,7 +44,13 @@ export const ConnectionItem = (props: Props) => {
dense
sx={{ borderBottom: "1px solid var(--divider-color)" }}
secondaryAction={
<IconButton edge="end" color="inherit" onClick={onDelete}>
<IconButton
edge="end"
color="inherit"
onClick={onDelete}
title={t("connections.components.actions.closeConnection")}
aria-label={t("connections.components.actions.closeConnection")}
>
<CloseRounded />
</IconButton>
}

View File

@@ -162,13 +162,13 @@ export const ConnectionTable = (props: Props) => {
return [
{
field: "host",
headerName: t("Host"),
headerName: t("connections.components.fields.host"),
width: columnWidths["host"] || 220,
minWidth: 180,
},
{
field: "download",
headerName: t("Downloaded"),
headerName: t("shared.labels.downloaded"),
width: columnWidths["download"] || 88,
align: "right",
headerAlign: "right",
@@ -176,7 +176,7 @@ export const ConnectionTable = (props: Props) => {
},
{
field: "upload",
headerName: t("Uploaded"),
headerName: t("shared.labels.uploaded"),
width: columnWidths["upload"] || 88,
align: "right",
headerAlign: "right",
@@ -184,7 +184,7 @@ export const ConnectionTable = (props: Props) => {
},
{
field: "dlSpeed",
headerName: t("DL Speed"),
headerName: t("connections.components.fields.dlSpeed"),
width: columnWidths["dlSpeed"] || 88,
align: "right",
headerAlign: "right",
@@ -192,7 +192,7 @@ export const ConnectionTable = (props: Props) => {
},
{
field: "ulSpeed",
headerName: t("UL Speed"),
headerName: t("connections.components.fields.ulSpeed"),
width: columnWidths["ulSpeed"] || 88,
align: "right",
headerAlign: "right",
@@ -200,25 +200,25 @@ export const ConnectionTable = (props: Props) => {
},
{
field: "chains",
headerName: t("Chains"),
headerName: t("connections.components.fields.chains"),
width: columnWidths["chains"] || 340,
minWidth: 180,
},
{
field: "rule",
headerName: t("Rule"),
headerName: t("connections.components.fields.rule"),
width: columnWidths["rule"] || 280,
minWidth: 180,
},
{
field: "process",
headerName: t("Process"),
headerName: t("connections.components.fields.process"),
width: columnWidths["process"] || 220,
minWidth: 180,
},
{
field: "time",
headerName: t("Time"),
headerName: t("connections.components.fields.time"),
width: columnWidths["time"] || 120,
minWidth: 100,
align: "right",
@@ -229,19 +229,19 @@ export const ConnectionTable = (props: Props) => {
},
{
field: "source",
headerName: t("Source"),
headerName: t("connections.components.fields.source"),
width: columnWidths["source"] || 200,
minWidth: 130,
},
{
field: "remoteDestination",
headerName: t("Destination"),
headerName: t("connections.components.fields.destination"),
width: columnWidths["remoteDestination"] || 200,
minWidth: 130,
},
{
field: "type",
headerName: t("Type"),
headerName: t("connections.components.fields.type"),
width: columnWidths["type"] || 160,
minWidth: 100,
},
@@ -250,7 +250,6 @@ export const ConnectionTable = (props: Props) => {
const handleColumnResize = (params: GridColumnResizeParams) => {
const { colDef, width } = params;
console.log("Column resize:", colDef.field, width);
setColumnWidths((prev) => ({
...prev,
[colDef.field]: width,

View File

@@ -32,7 +32,7 @@ export const ClashInfoCard = () => {
<Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Core Version")}
{t("home.components.clashInfo.fields.coreVersion")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{clashVersion || "-"}
@@ -41,7 +41,7 @@ export const ClashInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("System Proxy Address")}
{t("home.components.clashInfo.fields.systemProxyAddress")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{systemProxyAddress}
@@ -50,7 +50,7 @@ export const ClashInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Mixed Port")}
{t("home.components.clashInfo.fields.mixedPort")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{clashConfig.mixedPort || "-"}
@@ -59,7 +59,7 @@ export const ClashInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Uptime")}
{t("home.components.clashInfo.fields.uptime")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{formattedUptime}
@@ -68,7 +68,7 @@ export const ClashInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Rules Count")}
{t("home.components.clashInfo.fields.rulesCount")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{rules.length}
@@ -87,7 +87,7 @@ export const ClashInfoCard = () => {
return (
<EnhancedCard
title={t("Clash Info")}
title={t("home.components.clashInfo.title")}
icon={<DeveloperBoardOutlined />}
iconColor="warning"
action={null}

View File

@@ -12,6 +12,31 @@ import { closeAllConnections } from "tauri-plugin-mihomo-api";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import { patchClashMode } from "@/services/cmds";
import type { TranslationKey } from "@/types/generated/i18n-keys";
const CLASH_MODES = ["rule", "global", "direct"] as const;
type ClashMode = (typeof CLASH_MODES)[number];
const isClashMode = (mode: string): mode is ClashMode =>
(CLASH_MODES as readonly string[]).includes(mode);
const MODE_META: Record<
ClashMode,
{ label: TranslationKey; description: TranslationKey }
> = {
rule: {
label: "home.components.clashMode.labels.rule",
description: "home.components.clashMode.descriptions.rule",
},
global: {
label: "home.components.clashMode.labels.global",
description: "home.components.clashMode.descriptions.global",
},
direct: {
label: "home.components.clashMode.labels.direct",
description: "home.components.clashMode.descriptions.direct",
},
};
export const ClashModeCard = () => {
const { t } = useTranslation();
@@ -19,19 +44,21 @@ export const ClashModeCard = () => {
const { clashConfig, refreshClashConfig } = useAppData();
// 支持的模式列表
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
const modeList = CLASH_MODES;
// 直接使用API返回的模式不维护本地状态
const currentMode = clashConfig?.mode?.toLowerCase();
const currentModeKey =
typeof currentMode === "string" && isClashMode(currentMode)
? currentMode
: undefined;
const modeDescription = useMemo(() => {
if (typeof currentMode === "string" && currentMode.length > 0) {
return t(
`${currentMode[0].toLocaleUpperCase()}${currentMode.slice(1)} Mode Description`,
);
if (currentModeKey) {
return t(MODE_META[currentModeKey].description);
}
return t("Core communication error");
}, [currentMode, t]);
return t("home.components.clashMode.errors.communication");
}, [currentModeKey, t]);
// 模式图标映射
const modeIcons = useMemo(
@@ -44,8 +71,8 @@ export const ClashModeCard = () => {
);
// 切换模式的处理函数
const onChangeMode = useLockFn(async (mode: string) => {
if (mode === currentMode) return;
const onChangeMode = useLockFn(async (mode: ClashMode) => {
if (mode === currentModeKey) return;
if (verge?.auto_close_connection) {
closeAllConnections();
}
@@ -60,7 +87,7 @@ export const ClashModeCard = () => {
});
// 按钮样式
const buttonStyles = (mode: string) => ({
const buttonStyles = (mode: ClashMode) => ({
cursor: "pointer",
px: 2,
py: 1.2,
@@ -68,8 +95,8 @@ export const ClashModeCard = () => {
alignItems: "center",
justifyContent: "center",
gap: 1,
bgcolor: mode === currentMode ? "primary.main" : "background.paper",
color: mode === currentMode ? "primary.contrastText" : "text.primary",
bgcolor: mode === currentModeKey ? "primary.main" : "background.paper",
color: mode === currentModeKey ? "primary.contrastText" : "text.primary",
borderRadius: 1.5,
transition: "all 0.2s ease-in-out",
position: "relative",
@@ -82,7 +109,7 @@ export const ClashModeCard = () => {
transform: "translateY(1px)",
},
"&::after":
mode === currentMode
mode === currentModeKey
? {
content: '""',
position: "absolute",
@@ -128,7 +155,7 @@ export const ClashModeCard = () => {
{modeList.map((mode) => (
<Paper
key={mode}
elevation={mode === currentMode ? 2 : 0}
elevation={mode === currentModeKey ? 2 : 0}
onClick={() => onChangeMode(mode)}
sx={buttonStyles(mode)}
>
@@ -137,10 +164,10 @@ export const ClashModeCard = () => {
variant="body2"
sx={{
textTransform: "capitalize",
fontWeight: mode === currentMode ? 600 : 400,
fontWeight: mode === currentModeKey ? 600 : 400,
}}
>
{t(mode)}
{t(MODE_META[mode].label)}
</Typography>
</Paper>
))}

View File

@@ -811,11 +811,11 @@ export const CurrentProxyCard = () => {
const getSortTooltip = (): string => {
switch (sortType) {
case 0:
return t("Sort by default");
return t("proxies.page.tooltips.sortDefault");
case 1:
return t("Sort by delay");
return t("proxies.page.tooltips.sortDelay");
case 2:
return t("Sort by name");
return t("proxies.page.tooltips.sortName");
default:
return "";
}
@@ -823,7 +823,7 @@ export const CurrentProxyCard = () => {
return (
<EnhancedCard
title={t("Current Node")}
title={t("home.components.currentProxy.title")}
icon={
<Tooltip
title={
@@ -840,7 +840,9 @@ export const CurrentProxyCard = () => {
iconColor={currentProxy ? "primary" : undefined}
action={
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Tooltip title={t("Delay check")}>
<Tooltip
title={t("home.components.currentProxy.actions.refreshDelay")}
>
<span>
<IconButton
size="small"
@@ -868,7 +870,7 @@ export const CurrentProxyCard = () => {
sx={{ borderRadius: 1.5 }}
endIcon={<ChevronRight fontSize="small" />}
>
{t("Label-Proxies")}
{t("layout.components.navigation.tabs.proxies")}
</Button>
</Box>
}
@@ -906,7 +908,7 @@ export const CurrentProxyCard = () => {
{isGlobalMode && (
<Chip
size="small"
label={t("Global Mode")}
label={t("home.components.currentProxy.labels.globalMode")}
color="primary"
sx={{ mr: 0.5 }}
/>
@@ -914,7 +916,7 @@ export const CurrentProxyCard = () => {
{isDirectMode && (
<Chip
size="small"
label={t("Direct Mode")}
label={t("home.components.currentProxy.labels.directMode")}
color="success"
sx={{ mr: 0.5 }}
/>
@@ -954,12 +956,14 @@ export const CurrentProxyCard = () => {
size="small"
sx={{ mb: 1.5 }}
>
<InputLabel id="proxy-group-select-label">{t("Group")}</InputLabel>
<InputLabel id="proxy-group-select-label">
{t("home.components.currentProxy.labels.group")}
</InputLabel>
<Select
labelId="proxy-group-select-label"
value={state.selection.group}
onChange={handleGroupChange}
label={t("Group")}
label={t("home.components.currentProxy.labels.group")}
disabled={isGlobalMode || isDirectMode}
>
{state.proxyData.groups.map((group) => (
@@ -972,12 +976,14 @@ export const CurrentProxyCard = () => {
{/* 代理节点选择器 */}
<FormControl fullWidth variant="outlined" size="small" sx={{ mb: 0 }}>
<InputLabel id="proxy-select-label">{t("Proxy")}</InputLabel>
<InputLabel id="proxy-select-label">
{t("home.components.currentProxy.labels.proxy")}
</InputLabel>
<Select
labelId="proxy-select-label"
value={state.selection.proxy}
onChange={handleProxyChange}
label={t("Proxy")}
label={t("home.components.currentProxy.labels.proxy")}
disabled={isDirectMode}
renderValue={renderProxyValue}
MenuProps={{
@@ -1033,7 +1039,7 @@ export const CurrentProxyCard = () => {
) : (
<Box sx={{ textAlign: "center", py: 4 }}>
<Typography variant="body1" color="text.secondary">
{t("No active proxy node")}
{t("home.components.currentProxy.labels.noActiveNode")}
</Typography>
</Box>
)}

View File

@@ -851,7 +851,9 @@ export const EnhancedCanvasTrafficGraph = memo(
// 获取时间范围文本
const getTimeRangeText = useCallback(() => {
return t("{{time}} Minutes", { time: timeRange });
return t("home.components.traffic.patterns.minutes", {
time: timeRange,
});
}, [timeRange, t]);
return (
@@ -934,7 +936,7 @@ export const EnhancedCanvasTrafficGraph = memo(
textAlign: "right",
}}
>
{t("Upload")}
{t("home.components.traffic.legends.upload")}
</Box>
<Box
sx={{
@@ -944,7 +946,7 @@ export const EnhancedCanvasTrafficGraph = memo(
textAlign: "right",
}}
>
{t("Download")}
{t("home.components.traffic.legends.download")}
</Box>
</Box>

View File

@@ -219,42 +219,42 @@ export const EnhancedTrafficStats = () => {
() => [
{
icon: <ArrowUpwardRounded fontSize="small" />,
title: t("Upload Speed"),
title: t("home.components.traffic.metrics.uploadSpeed"),
value: parsedData.up,
unit: `${parsedData.upUnit}/s`,
color: "secondary" as const,
},
{
icon: <ArrowDownwardRounded fontSize="small" />,
title: t("Download Speed"),
title: t("home.components.traffic.metrics.downloadSpeed"),
value: parsedData.down,
unit: `${parsedData.downUnit}/s`,
color: "primary" as const,
},
{
icon: <LinkRounded fontSize="small" />,
title: t("Active Connections"),
title: t("home.components.traffic.metrics.activeConnections"),
value: parsedData.connectionsCount,
unit: "",
color: "success" as const,
},
{
icon: <CloudUploadRounded fontSize="small" />,
title: t("Uploaded"),
title: t("shared.labels.uploaded"),
value: parsedData.uploadTotal,
unit: parsedData.uploadTotalUnit,
color: "secondary" as const,
},
{
icon: <CloudDownloadRounded fontSize="small" />,
title: t("Downloaded"),
title: t("shared.labels.downloaded"),
value: parsedData.downloadTotal,
unit: parsedData.downloadTotalUnit,
color: "primary" as const,
},
{
icon: <MemoryRounded fontSize="small" />,
title: t("Memory Usage"),
title: t("home.components.traffic.metrics.memoryUsage"),
value: parsedData.inuse,
unit: parsedData.inuseUnit,
color: "error" as const,

View File

@@ -111,7 +111,7 @@ const ProfileDetails = ({
noWrap
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ flexShrink: 0 }}>{t("From")}: </span>
<span style={{ flexShrink: 0 }}>{t("shared.labels.from")}: </span>
{current.home ? (
<Link
component="button"
@@ -186,7 +186,7 @@ const ProfileDetails = ({
sx={{ cursor: "pointer" }}
onClick={onUpdateProfile}
>
{t("Update Time")}:{" "}
{t("shared.labels.updateTime")}:{" "}
<Box component="span" fontWeight="medium">
{dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")}
</Box>
@@ -199,7 +199,7 @@ const ProfileDetails = ({
<Stack direction="row" alignItems="center" spacing={1}>
<SpeedOutlined fontSize="small" color="action" />
<Typography variant="body2" color="text.secondary">
{t("Used / Total")}:{" "}
{t("shared.labels.usedTotal")}:{" "}
<Box component="span" fontWeight="medium">
{parseTraffic(usedTraffic)} /{" "}
{parseTraffic(current.extra.total)}
@@ -211,7 +211,7 @@ const ProfileDetails = ({
<Stack direction="row" alignItems="center" spacing={1}>
<EventOutlined fontSize="small" color="action" />
<Typography variant="body2" color="text.secondary">
{t("Expire Time")}:{" "}
{t("shared.labels.expireTime")}:{" "}
<Box component="span" fontWeight="medium">
{parseExpire(current.extra.expire)}
</Box>
@@ -266,10 +266,10 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
sx={{ fontSize: 60, color: "primary.main", mb: 2 }}
/>
<Typography variant="h6" gutterBottom>
{t("Import")} {t("Profiles")}
{t("profiles.page.actions.import")} {t("profiles.page.title")}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Click to import subscription")}
{t("profiles.components.card.labels.clickToImport")}
</Typography>
</Box>
);
@@ -296,8 +296,8 @@ export const HomeProfileCard = ({
// 刷新首页数据
refreshAll();
} catch (err: any) {
showNotice("error", err.message || err.toString(), 3000);
} catch (err) {
showNotice.error(err, 3000);
} finally {
setUpdating(false);
}
@@ -310,7 +310,7 @@ export const HomeProfileCard = ({
// 卡片标题
const cardTitle = useMemo(() => {
if (!current) return t("Profiles");
if (!current) return t("profiles.page.title");
if (!current.home) return current.name;
@@ -363,7 +363,7 @@ export const HomeProfileCard = ({
endIcon={<StorageOutlined fontSize="small" />}
sx={{ borderRadius: 1.5 }}
>
{t("Label-Profiles")}
{t("layout.components.navigation.tabs.proxies")}
</Button>
);
}, [current, goToProfiles, t]);

View File

@@ -68,8 +68,12 @@ export const IpInfoCard = () => {
const data = await getIpInfo();
setIpInfo(data);
setCountdown(IP_REFRESH_SECONDS);
} catch (err: any) {
setError(err.message || t("Failed to get IP info"));
} catch (err) {
setError(
err instanceof Error
? err.message
: t("home.components.ipInfo.errors.load"),
);
} finally {
setLoading(false);
}
@@ -114,7 +118,7 @@ export const IpInfoCard = () => {
if (loading) {
return (
<EnhancedCard
title={t("IP Information")}
title={t("home.components.ipInfo.title")}
icon={<LocationOnOutlined />}
iconColor="info"
action={
@@ -137,7 +141,7 @@ export const IpInfoCard = () => {
if (error) {
return (
<EnhancedCard
title={t("IP Information")}
title={t("home.components.ipInfo.title")}
icon={<LocationOnOutlined />}
iconColor="info"
action={
@@ -160,7 +164,7 @@ export const IpInfoCard = () => {
{error}
</Typography>
<Button onClick={fetchIpInfo} sx={{ mt: 2 }}>
{t("Retry")}
{t("shared.actions.retry")}
</Button>
</Box>
</EnhancedCard>
@@ -170,7 +174,7 @@ export const IpInfoCard = () => {
// 渲染正常数据
return (
<EnhancedCard
title={t("IP Information")}
title={t("home.components.ipInfo.title")}
icon={<LocationOnOutlined />}
iconColor="info"
action={
@@ -222,7 +226,7 @@ export const IpInfoCard = () => {
maxWidth: "100%",
}}
>
{ipInfo?.country || t("Unknown")}
{ipInfo?.country || t("home.components.ipInfo.labels.unknown")}
</Typography>
</Box>
@@ -232,7 +236,7 @@ export const IpInfoCard = () => {
color="text.secondary"
sx={{ flexShrink: 0 }}
>
{t("IP")}:
{t("home.components.ipInfo.labels.ip")}:
</Typography>
<Box
sx={{
@@ -266,20 +270,29 @@ export const IpInfoCard = () => {
</Box>
<InfoItem
label={t("ASN")}
label={t("home.components.ipInfo.labels.asn")}
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
/>
</Box>
{/* 右侧组织、ISP和位置信息 */}
<Box sx={{ width: "60%", overflow: "auto" }}>
<InfoItem label={t("ISP")} value={ipInfo?.isp} />
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
<InfoItem
label={t("Location")}
label={t("home.components.ipInfo.labels.isp")}
value={ipInfo?.isp}
/>
<InfoItem
label={t("home.components.ipInfo.labels.org")}
value={ipInfo?.asn_organization}
/>
<InfoItem
label={t("home.components.ipInfo.labels.location")}
value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
/>
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
<InfoItem
label={t("home.components.ipInfo.labels.timezone")}
value={ipInfo?.timezone}
/>
</Box>
</Box>
@@ -297,7 +310,7 @@ export const IpInfoCard = () => {
}}
>
<Typography variant="caption">
{t("Auto refresh")}: {countdown}s
{t("home.components.ipInfo.labels.autoRefresh")}: {countdown}s
</Typography>
<Typography
variant="caption"

View File

@@ -147,8 +147,8 @@ export const ProxyTunCard: FC = () => {
const { enable_tun_mode } = verge ?? {};
const handleError = (err: Error) => {
showNotice("error", err.message || err.toString());
const handleError = (err: unknown) => {
showNotice.error(err);
};
const handleTabChange = (tab: string) => {
@@ -160,18 +160,18 @@ export const ProxyTunCard: FC = () => {
if (activeTab === "system") {
return {
text: systemProxyActualState
? t("System Proxy Enabled")
: t("System Proxy Disabled"),
tooltip: t("System Proxy Info"),
? t("home.components.proxyTun.status.systemProxyEnabled")
: t("home.components.proxyTun.status.systemProxyDisabled"),
tooltip: t("home.components.proxyTun.tooltips.systemProxy"),
};
} else {
return {
text: !isTunModeAvailable
? t("TUN Mode Service Required")
? t("home.components.proxyTun.status.tunModeServiceRequired")
: enable_tun_mode
? t("TUN Mode Enabled")
: t("TUN Mode Disabled"),
tooltip: t("TUN Mode Intercept Info"),
? t("home.components.proxyTun.status.tunModeEnabled")
: t("home.components.proxyTun.status.tunModeDisabled"),
tooltip: t("home.components.proxyTun.tooltips.tunMode"),
};
}
}, [
@@ -198,14 +198,14 @@ export const ProxyTunCard: FC = () => {
isActive={activeTab === "system"}
onClick={() => handleTabChange("system")}
icon={ComputerRounded}
label={t("System Proxy")}
label={t("settings.sections.system.toggles.systemProxy")}
hasIndicator={systemProxyActualState}
/>
<TabButton
isActive={activeTab === "tun"}
onClick={() => handleTabChange("tun")}
icon={TroubleshootRounded}
label={t("Tun Mode")}
label={t("settings.sections.system.toggles.tunMode")}
hasIndicator={enable_tun_mode && isTunModeAvailable}
/>
</Stack>
@@ -236,7 +236,11 @@ export const ProxyTunCard: FC = () => {
>
<ProxyControlSwitches
onError={handleError}
label={activeTab === "system" ? t("System Proxy") : t("Tun Mode")}
label={
activeTab === "system"
? t("settings.sections.system.toggles.systemProxy")
: t("settings.sections.system.toggles.tunMode")
}
noRightPadding={true}
/>
</Box>

View File

@@ -174,13 +174,15 @@ export const SystemInfoCard = () => {
try {
const info = await checkUpdate();
if (!info?.available) {
showNotice("success", t("Currently on the Latest Version"));
showNotice.success(
"settings.components.verge.advanced.notifications.latestVersion",
);
} else {
showNotice("info", t("Update Available"), 2000);
showNotice.info("shared.feedback.notifications.updateAvailable", 2000);
goToSettings();
}
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -217,11 +219,11 @@ export const SystemInfoCard = () => {
<>
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")}
titleAccess={t("home.components.systemInfo.badges.adminMode")}
/>
<DnsOutlined
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
titleAccess={t("Service Mode")}
titleAccess={t("home.components.systemInfo.badges.serviceMode")}
/>
</>
);
@@ -229,21 +231,21 @@ export const SystemInfoCard = () => {
return (
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")}
titleAccess={t("home.components.systemInfo.badges.adminMode")}
/>
);
} else if (isSidecarMode) {
return (
<ExtensionOutlined
sx={{ color: "info.main", fontSize: 16 }}
titleAccess={t("Sidecar Mode")}
titleAccess={t("home.components.systemInfo.badges.sidecarMode")}
/>
);
} else {
return (
<DnsOutlined
sx={{ color: "success.main", fontSize: 16 }}
titleAccess={t("Service Mode")}
titleAccess={t("home.components.systemInfo.badges.serviceMode")}
/>
);
}
@@ -254,13 +256,13 @@ export const SystemInfoCard = () => {
if (isAdminMode) {
// 判断是否同时处于服务模式
if (!isSidecarMode) {
return t("Administrator + Service Mode");
return t("home.components.systemInfo.badges.adminServiceMode");
}
return t("Administrator Mode");
return t("home.components.systemInfo.badges.adminMode");
} else if (isSidecarMode) {
return t("Sidecar Mode");
return t("home.components.systemInfo.badges.sidecarMode");
} else {
return t("Service Mode");
return t("home.components.systemInfo.badges.serviceMode");
}
};
@@ -269,11 +271,15 @@ export const SystemInfoCard = () => {
return (
<EnhancedCard
title={t("System Info")}
title={t("home.components.systemInfo.title")}
icon={<InfoOutlined />}
iconColor="error"
action={
<IconButton size="small" onClick={goToSettings} title={t("Settings")}>
<IconButton
size="small"
onClick={goToSettings}
title={t("home.components.systemInfo.actions.settings")}
>
<SettingsOutlined fontSize="small" />
</IconButton>
}
@@ -281,7 +287,7 @@ export const SystemInfoCard = () => {
<Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("OS Info")}
{t("home.components.systemInfo.fields.osInfo")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{systemState.osInfo}
@@ -294,19 +300,23 @@ export const SystemInfoCard = () => {
alignItems="center"
>
<Typography variant="body2" color="text.secondary">
{t("Auto Launch")}
{t("home.components.systemInfo.fields.autoLaunch")}
</Typography>
<Stack direction="row" spacing={1} alignItems="center">
{isAdminMode && (
<Tooltip
title={t("Administrator mode may not support auto launch")}
title={t("home.components.systemInfo.tooltips.autoLaunchAdmin")}
>
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
</Tooltip>
)}
<Chip
size="small"
label={autoLaunchEnabled ? t("Enabled") : t("Disabled")}
label={
autoLaunchEnabled
? t("shared.statuses.enabled")
: t("shared.statuses.disabled")
}
color={autoLaunchEnabled ? "success" : "default"}
variant={autoLaunchEnabled ? "filled" : "outlined"}
onClick={toggleAutoLaunch}
@@ -321,7 +331,7 @@ export const SystemInfoCard = () => {
alignItems="center"
>
<Typography variant="body2" color="text.secondary">
{t("Running Mode")}
{t("home.components.systemInfo.fields.runningMode")}
</Typography>
<Typography
variant="body2"
@@ -336,7 +346,7 @@ export const SystemInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Last Check Update")}
{t("home.components.systemInfo.fields.lastCheckUpdate")}
</Typography>
<Typography
variant="body2"
@@ -354,7 +364,7 @@ export const SystemInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Verge Version")}
{t("home.components.systemInfo.fields.vergeVersion")}
</Typography>
<Typography variant="body2" fontWeight="medium">
v{appVersion}

View File

@@ -173,16 +173,16 @@ export const TestCard = () => {
return (
<EnhancedCard
title={t("Website Tests")}
title={t("home.components.tests.title")}
icon={<NetworkCheck />}
action={
<Box sx={{ display: "flex", gap: 1 }}>
<Tooltip title={t("Test All")} arrow>
<Tooltip title={t("tests.page.actions.testAll")} arrow>
<IconButton size="small" onClick={handleTestAll}>
<NetworkCheck fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t("Create Test")} arrow>
<Tooltip title={t("tests.modals.test.title.create")} arrow>
<IconButton size="small" onClick={handleCreateTest}>
<Add fontSize="small" />
</IconButton>

View File

@@ -87,7 +87,7 @@ export const LayoutTraffic = () => {
<Box display="flex" flexDirection="column" gap={0.75}>
<Box
title={`${t("Upload Speed")}`}
title={`${t("home.components.traffic.metrics.uploadSpeed")}`}
{...boxStyle}
sx={{
...boxStyle.sx,
@@ -105,7 +105,7 @@ export const LayoutTraffic = () => {
</Box>
<Box
title={`${t("Download Speed")}`}
title={`${t("home.components.traffic.metrics.downloadSpeed")}`}
{...boxStyle}
sx={{
...boxStyle.sx,
@@ -124,7 +124,7 @@ export const LayoutTraffic = () => {
{displayMemory && (
<Box
title={`${t("Memory Usage")} `}
title={`${t("home.components.traffic.metrics.memoryUsage")} `}
{...boxStyle}
sx={{
cursor: "auto",

View File

@@ -35,10 +35,10 @@ export const ConfirmViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
{t("shared.actions.cancel")}
</Button>
<Button onClick={onConfirm} variant="contained">
{t("Confirm")}
{t("shared.actions.confirm")}
</Button>
</DialogActions>
</Dialog>

View File

@@ -97,7 +97,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
onClose,
} = props;
const resolvedTitle = title ?? t("Edit File");
const resolvedTitle = title ?? t("profiles.components.menu.editFile");
const resolvedInitialData = useMemo(
() => initialData ?? Promise.resolve(""),
[initialData],
@@ -132,8 +132,8 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
try {
currData.current = value;
onChange?.(prevData.current, currData.current);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -143,16 +143,16 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
onSave?.(prevData.current, currData.current);
}
onClose();
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
const handleClose = useLockFn(async () => {
try {
onClose();
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -202,7 +202,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
},
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
readOnly: readOnly, // 只读模式
readOnlyMessage: { value: t("ReadOnlyMessage") }, // 只读模式尝试编辑时的提示信息
readOnlyMessage: {
value: t("profiles.modals.editor.messages.readOnly"),
}, // 只读模式尝试编辑时的提示信息
renderValidationDecorations: "on", // 只读模式下显示校验信息
quickSuggestions: {
strings: true, // 字符串类型的建议
@@ -231,7 +233,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
size="medium"
color="inherit"
sx={{ display: readOnly ? "none" : "" }}
title={t("Format document")}
title={t("profiles.modals.editor.actions.format")}
onClick={() =>
editorRef.current
?.getAction("editor.action.formatDocument")
@@ -243,7 +245,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
<IconButton
size="medium"
color="inherit"
title={t(isMaximized ? "Minimize" : "Maximize")}
title={t(
isMaximized ? "shared.window.minimize" : "shared.window.maximize",
)}
onClick={() => appWindow.toggleMaximize().then(editorResize)}
>
{isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
@@ -253,11 +257,11 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t(readOnly ? "Close" : "Cancel")}
{t(readOnly ? "shared.actions.close" : "shared.actions.cancel")}
</Button>
{!readOnly && (
<Button onClick={handleSave} variant="contained">
{t("Save")}
{t("shared.actions.save")}
</Button>
)}
</DialogActions>

View File

@@ -42,7 +42,7 @@ export const FileInput = (props: Props) => {
sx={{ flex: "none" }}
onClick={() => inputRef.current?.click()}
>
{t("Choose File")}
{t("profiles.components.fileInput.chooseFile")}
</Button>
<input

View File

@@ -57,6 +57,7 @@ import {
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { useThemeMode } from "@/services/states";
import type { TranslationKey } from "@/types/generated/i18n-keys";
import getSystem from "@/utils/get-system";
import { BaseSearchBox } from "../base/base-search-box";
@@ -73,6 +74,24 @@ interface Props {
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
const PROXY_STRATEGY_LABEL_KEYS: Record<string, TranslationKey> = {
select: "proxies.components.enums.strategies.select",
"url-test": "proxies.components.enums.strategies.url-test",
fallback: "proxies.components.enums.strategies.fallback",
"load-balance": "proxies.components.enums.strategies.load-balance",
relay: "proxies.components.enums.strategies.relay",
};
const PROXY_POLICY_LABEL_KEYS: Record<string, TranslationKey> =
builtinProxyPolicies.reduce(
(acc, policy) => {
acc[policy] =
`proxies.components.enums.policies.${policy}` as TranslationKey;
return acc;
},
{} as Record<string, TranslationKey>,
);
const normalizeDeleteSeq = (input?: unknown): string[] => {
if (!Array.isArray(input)) {
return [];
@@ -121,6 +140,20 @@ export const GroupsEditorViewer = (props: Props) => {
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
props;
const { t } = useTranslation();
const translateStrategy = useCallback(
(value: string) =>
PROXY_STRATEGY_LABEL_KEYS[value]
? t(PROXY_STRATEGY_LABEL_KEYS[value])
: value,
[t],
);
const translatePolicy = useCallback(
(value: string) =>
PROXY_POLICY_LABEL_KEYS[value]
? t(PROXY_POLICY_LABEL_KEYS[value])
: value,
[t],
);
const themeMode = useThemeMode();
const [prevData, setPrevData] = useState("");
const [currData, setCurrData] = useState("");
@@ -369,7 +402,7 @@ export const GroupsEditorViewer = (props: Props) => {
const validateGroup = () => {
const group = formIns.getValues();
if (group.name === "") {
throw new Error(t("Group Name Required"));
throw new Error(t("profiles.modals.groupsEditor.errors.nameRequired"));
}
};
@@ -384,12 +417,12 @@ export const GroupsEditorViewer = (props: Props) => {
}
await saveProfileFile(property, nextData);
showNotice("success", t("Saved Successfully"));
showNotice.success("shared.feedback.notifications.saved");
setPrevData(nextData);
onSave?.(prevData, nextData);
onClose();
} catch (err: any) {
showNotice("error", err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -398,7 +431,7 @@ export const GroupsEditorViewer = (props: Props) => {
<DialogTitle>
{
<Box display="flex" justifyContent="space-between">
{t("Edit Groups")}
{t("profiles.modals.groupsEditor.title")}
<Box>
<Button
variant="contained"
@@ -407,7 +440,9 @@ export const GroupsEditorViewer = (props: Props) => {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
{visualization
? t("shared.editorModes.advanced")
: t("shared.editorModes.visualization")}
</Button>
</Box>
</Box>
@@ -436,7 +471,9 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Group Type")} />
<ListItemText
primary={t("profiles.modals.groupsEditor.fields.type")}
/>
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
@@ -448,9 +485,10 @@ export const GroupsEditorViewer = (props: Props) => {
"relay",
]}
value={field.value}
getOptionLabel={translateStrategy}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
<li {...props} title={translateStrategy(option)}>
{translateStrategy(option)}
</li>
)}
onChange={(_, value) => value && field.onChange(value)}
@@ -464,7 +502,9 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Group Name")} />
<ListItemText
primary={t("profiles.modals.groupsEditor.fields.name")}
/>
<TextField
autoComplete="new-password"
size="small"
@@ -481,7 +521,9 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Proxy Group Icon")} />
<ListItemText
primary={t("profiles.modals.groupsEditor.fields.icon")}
/>
<TextField
autoComplete="new-password"
size="small"
@@ -496,7 +538,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Use Proxies")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.proxies",
)}
/>
<Autocomplete
size="small"
sx={{
@@ -508,10 +554,11 @@ export const GroupsEditorViewer = (props: Props) => {
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
<li {...props} title={translatePolicy(option)}>
{translatePolicy(option)}
</li>
)}
getOptionLabel={translatePolicy}
/>
</Item>
)}
@@ -521,7 +568,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Use Provider")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.provider",
)}
/>
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
@@ -539,7 +590,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Health Check Url")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.healthCheckUrl",
)}
/>
<TextField
autoComplete="new-password"
placeholder="https://cp.cloudflare.com/generate_204"
@@ -555,7 +610,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Expected Status")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.expectedStatus",
)}
/>
<TextField
autoComplete="new-password"
placeholder="*"
@@ -573,7 +632,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Interval")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.interval",
)}
/>
<TextField
autoComplete="new-password"
placeholder="300"
@@ -587,7 +650,7 @@ export const GroupsEditorViewer = (props: Props) => {
input: {
endAdornment: (
<InputAdornment position="end">
{t("seconds")}
{t("shared.units.seconds")}
</InputAdornment>
),
},
@@ -601,7 +664,7 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Timeout")} />
<ListItemText primary={t("shared.labels.timeout")} />
<TextField
autoComplete="new-password"
placeholder="5000"
@@ -615,7 +678,7 @@ export const GroupsEditorViewer = (props: Props) => {
input: {
endAdornment: (
<InputAdornment position="end">
{t("millis")}
{t("shared.units.milliseconds")}
</InputAdornment>
),
},
@@ -629,7 +692,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Max Failed Times")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.maxFailedTimes",
)}
/>
<TextField
autoComplete="new-password"
placeholder="5"
@@ -648,7 +715,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Interface Name")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.interfaceName",
)}
/>
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
@@ -665,7 +736,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Routing Mark")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.routingMark",
)}
/>
<TextField
autoComplete="new-password"
type="number"
@@ -683,7 +758,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Filter")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.filter",
)}
/>
<TextField
autoComplete="new-password"
size="small"
@@ -698,7 +777,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Exclude Filter")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.excludeFilter",
)}
/>
<TextField
autoComplete="new-password"
size="small"
@@ -713,7 +796,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Exclude Type")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.excludeType",
)}
/>
<Autocomplete
multiple
options={[
@@ -759,7 +846,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Include All")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.includeAll",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -769,7 +860,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Include All Proxies")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.includeAllProxies",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -779,7 +874,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Include All Providers")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.includeAllProviders",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -789,7 +888,9 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Lazy")} />
<ListItemText
primary={t("profiles.modals.groupsEditor.toggles.lazy")}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -799,7 +900,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Disable UDP")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.toggles.disableUdp",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -809,7 +914,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Hidden")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.toggles.hidden",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -825,16 +934,18 @@ export const GroupsEditorViewer = (props: Props) => {
validateGroup();
for (const item of [...prependSeq, ...groupList]) {
if (item.name === formIns.getValues().name) {
throw new Error(t("Group Name Already Exists"));
throw new Error(
t("profiles.modals.groupsEditor.errors.nameExists"),
);
}
}
setPrependSeq([formIns.getValues(), ...prependSeq]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
}}
>
{t("Prepend Group")}
{t("profiles.modals.groupsEditor.actions.prepend")}
</Button>
</Item>
<Item>
@@ -847,16 +958,18 @@ export const GroupsEditorViewer = (props: Props) => {
validateGroup();
for (const item of [...appendSeq, ...groupList]) {
if (item.name === formIns.getValues().name) {
throw new Error(t("Group Name Already Exists"));
throw new Error(
t("profiles.modals.groupsEditor.errors.nameExists"),
);
}
}
setAppendSeq([...appendSeq, formIns.getValues()]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
}}
>
{t("Append Group")}
{t("profiles.modals.groupsEditor.actions.append")}
</Button>
</Item>
</List>
@@ -1007,11 +1120,11 @@ export const GroupsEditorViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
{t("shared.actions.cancel")}
</Button>
<Button onClick={handleSave} variant="contained">
{t("Save")}
{t("shared.actions.save")}
</Button>
</DialogActions>
</Dialog>

View File

@@ -26,7 +26,7 @@ export const LogViewer = (props: Props) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t("Script Console")}</DialogTitle>
<DialogTitle>{t("profiles.modals.logViewer.title")}</DialogTitle>
<DialogContent
sx={{
@@ -62,7 +62,7 @@ export const LogViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Close")}
{t("shared.actions.close")}
</Button>
</DialogActions>
</Dialog>

View File

@@ -36,6 +36,7 @@ import {
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { useLoadingCache, useSetLoadingCache } from "@/services/states";
import type { TranslationKey } from "@/types/generated/i18n-keys";
import parseTraffic from "@/utils/parse-traffic";
import { ProfileBox } from "./profile-box";
@@ -120,34 +121,46 @@ export const ProfileItem = (props: Props) => {
// 如果已经过期,显示"更新失败"
if (nextUpdateDate.isBefore(now)) {
setNextUpdateTime(t("Last Update failed"));
setNextUpdateTime(
t("profiles.components.profileItem.status.lastUpdateFailed"),
);
} else {
// 否则显示剩余时间
const diffMinutes = nextUpdateDate.diff(now, "minute");
if (diffMinutes < 60) {
if (diffMinutes <= 0) {
setNextUpdateTime(`${t("Next Up")} <1m`);
setNextUpdateTime(
`${t("profiles.components.profileItem.status.nextUp")} <1m`,
);
} else {
setNextUpdateTime(`${t("Next Up")} ${diffMinutes}m`);
setNextUpdateTime(
`${t("profiles.components.profileItem.status.nextUp")} ${diffMinutes}m`,
);
}
} else {
const hours = Math.floor(diffMinutes / 60);
const mins = diffMinutes % 60;
setNextUpdateTime(`${t("Next Up")} ${hours}h ${mins}m`);
setNextUpdateTime(
`${t("profiles.components.profileItem.status.nextUp")} ${hours}h ${mins}m`,
);
}
}
} else {
console.log(`返回的下次更新时间为空`);
setNextUpdateTime(t("No schedule"));
setNextUpdateTime(
t("profiles.components.profileItem.status.noSchedule"),
);
}
} catch (err) {
console.error(`获取下次更新时间出错:`, err);
setNextUpdateTime(t("Unknown"));
setNextUpdateTime(t("profiles.components.profileItem.status.unknown"));
}
} else {
console.log(`该配置未设置更新间隔或间隔为0`);
setNextUpdateTime(t("Auto update disabled"));
setNextUpdateTime(
t("profiles.components.profileItem.status.autoUpdateDisabled"),
);
}
});
@@ -312,8 +325,8 @@ export const ProfileItem = (props: Props) => {
setAnchorEl(null);
try {
await viewProfile(itemData.uid);
} catch (err: any) {
showNotice("error", err?.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -354,42 +367,95 @@ export const ProfileItem = (props: Props) => {
}
});
const urlModeMenu = (
hasHome ? [{ label: "Home", handler: onOpenHome, disabled: false }] : []
).concat([
{ label: "Select", handler: onForceSelect, disabled: false },
{ label: "Edit Info", handler: onEditInfo, disabled: false },
{ label: "Edit File", handler: onEditFile, disabled: false },
type ContextMenuItem = {
label: string;
handler: () => void;
disabled: boolean;
};
const menuLabels: Record<string, TranslationKey> = {
home: "profiles.components.menu.home",
select: "profiles.components.menu.select",
editInfo: "profiles.components.menu.editInfo",
editFile: "profiles.components.menu.editFile",
editRules: "profiles.components.menu.editRules",
editProxies: "profiles.components.menu.editProxies",
editGroups: "profiles.components.menu.editGroups",
extendConfig: "profiles.components.menu.extendConfig",
extendScript: "profiles.components.menu.extendScript",
openFile: "profiles.components.menu.openFile",
update: "profiles.components.menu.update",
updateViaProxy: "profiles.components.menu.updateViaProxy",
delete: "shared.actions.delete",
} as const;
const urlModeMenu: ContextMenuItem[] = [
...(hasHome
? [
{
label: menuLabels.home,
handler: onOpenHome,
disabled: false,
} satisfies ContextMenuItem,
]
: []),
{
label: "Edit Rules",
label: menuLabels.select,
handler: onForceSelect,
disabled: false,
},
{
label: menuLabels.editInfo,
handler: onEditInfo,
disabled: false,
},
{
label: menuLabels.editFile,
handler: onEditFile,
disabled: false,
},
{
label: menuLabels.editRules,
handler: onEditRules,
disabled: !option?.rules,
},
{
label: "Edit Proxies",
label: menuLabels.editProxies,
handler: onEditProxies,
disabled: !option?.proxies,
},
{
label: "Edit Groups",
label: menuLabels.editGroups,
handler: onEditGroups,
disabled: !option?.groups,
},
{
label: "Extend Config",
label: menuLabels.extendConfig,
handler: onEditMerge,
disabled: !option?.merge,
},
{
label: "Extend Script",
label: menuLabels.extendScript,
handler: onEditScript,
disabled: !option?.script,
},
{ label: "Open File", handler: onOpenFile, disabled: false },
{ label: "Update", handler: () => onUpdate(0), disabled: false },
{ label: "Update via proxy", handler: () => onUpdate(2), disabled: false },
{
label: "Delete",
label: menuLabels.openFile,
handler: onOpenFile,
disabled: false,
},
{
label: menuLabels.update,
handler: () => onUpdate(0),
disabled: false,
},
{
label: menuLabels.updateViaProxy,
handler: () => onUpdate(2),
disabled: false,
},
{
label: menuLabels.delete,
handler: () => {
setAnchorEl(null);
if (batchMode) {
@@ -403,39 +469,55 @@ export const ProfileItem = (props: Props) => {
},
disabled: false,
},
]);
const fileModeMenu = [
{ label: "Select", handler: onForceSelect, disabled: false },
{ label: "Edit Info", handler: onEditInfo, disabled: false },
{ label: "Edit File", handler: onEditFile, disabled: false },
];
const fileModeMenu: ContextMenuItem[] = [
{
label: "Edit Rules",
label: menuLabels.select,
handler: onForceSelect,
disabled: false,
},
{
label: menuLabels.editInfo,
handler: onEditInfo,
disabled: false,
},
{
label: menuLabels.editFile,
handler: onEditFile,
disabled: false,
},
{
label: menuLabels.editRules,
handler: onEditRules,
disabled: !option?.rules,
},
{
label: "Edit Proxies",
label: menuLabels.editProxies,
handler: onEditProxies,
disabled: !option?.proxies,
},
{
label: "Edit Groups",
label: menuLabels.editGroups,
handler: onEditGroups,
disabled: !option?.groups,
},
{
label: "Extend Config",
label: menuLabels.extendConfig,
handler: onEditMerge,
disabled: !option?.merge,
},
{
label: "Extend Script",
label: menuLabels.extendScript,
handler: onEditScript,
disabled: !option?.script,
},
{ label: "Open File", handler: onOpenFile, disabled: false },
{
label: "Delete",
label: menuLabels.openFile,
handler: onOpenFile,
disabled: false,
},
{
label: menuLabels.delete,
handler: () => {
setAnchorEl(null);
if (batchMode) {
@@ -599,7 +681,7 @@ export const ProfileItem = (props: Props) => {
{/* only if has url can it be updated */}
{hasUrl && (
<IconButton
title={t("Refresh")}
title={t("shared.actions.refresh")}
sx={{
position: "absolute",
p: "3px",
@@ -637,7 +719,10 @@ export const ProfileItem = (props: Props) => {
</Typography>
) : (
hasUrl && (
<Typography noWrap title={`${t("From")} ${from}`}>
<Typography
noWrap
title={`${t("shared.labels.from")} ${from}`}
>
{from}
</Typography>
)
@@ -657,8 +742,8 @@ export const ProfileItem = (props: Props) => {
textAlign="right"
title={
showNextUpdate
? t("Click to show last update time")
: `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`
? t("profiles.components.profileItem.tooltips.showLast")
: `${t("shared.labels.updateTime")}: ${parseExpire(updated)}\n${t("profiles.components.profileItem.tooltips.showNext")}`
}
sx={{
cursor: "pointer",
@@ -686,14 +771,16 @@ export const ProfileItem = (props: Props) => {
{/* the third line show extra info or last updated time */}
{hasExtra ? (
<Box sx={{ ...boxStyle, fontSize: 14 }}>
<span title={t("Used / Total")}>
<span title={t("shared.labels.usedTotal")}>
{parseTraffic(upload + download)} / {parseTraffic(total)}
</span>
<span title={t("Expire Time")}>{expire}</span>
<span title={t("shared.labels.expireTime")}>{expire}</span>
</Box>
) : (
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: "flex-end" }}>
<span title={t("Update Time")}>{parseExpire(updated)}</span>
<span title={t("shared.labels.updateTime")}>
{parseExpire(updated)}
</span>
</Box>
)}
<LinearProgress
@@ -728,7 +815,7 @@ export const ProfileItem = (props: Props) => {
(theme) => {
return {
color:
item.label === "Delete"
item.label === menuLabels.delete
? theme.palette.error.main
: undefined,
};
@@ -813,8 +900,8 @@ export const ProfileItem = (props: Props) => {
)}
<ConfirmViewer
title={t("Confirm deletion")}
message={t("This operation is not reversible")}
title={t("profiles.modals.confirmDelete.title")}
message={t("profiles.modals.confirmDelete.message")}
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onConfirm={() => {

View File

@@ -47,16 +47,26 @@ export const ProfileMore = (props: Props) => {
setAnchorEl(null);
try {
await viewProfile(id);
} catch (err: any) {
showNotice("error", err?.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
const hasError = entries.some(([level]) => level === "exception");
const globalTitles: Record<Props["id"], string> = {
Merge: "profiles.components.more.global.merge",
Script: "profiles.components.more.global.script",
};
const chipLabels: Record<Props["id"], string> = {
Merge: "profiles.components.more.chips.merge",
Script: "profiles.components.more.chips.script",
};
const itemMenu = [
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "profiles.components.menu.editFile", handler: onEditFile },
{ label: "profiles.components.menu.openFile", handler: onOpenFile },
];
const boxStyle = {
@@ -89,13 +99,13 @@ export const ProfileMore = (props: Props) => {
variant="h6"
component="h2"
noWrap
title={t(`Global ${id}`)}
title={t(globalTitles[id])}
>
{t(`Global ${id}`)}
{t(globalTitles[id])}
</Typography>
<Chip
label={id}
label={t(chipLabels[id])}
color="primary"
size="small"
variant="outlined"
@@ -111,7 +121,7 @@ export const ProfileMore = (props: Props) => {
size="small"
edge="start"
color="error"
title={t("Script Console")}
title={t("profiles.modals.logViewer.title")}
onClick={() => setLogOpen(true)}
>
<FeaturedPlayListRounded fontSize="inherit" />
@@ -122,7 +132,7 @@ export const ProfileMore = (props: Props) => {
size="small"
edge="start"
color="inherit"
title={t("Script Console")}
title={t("profiles.modals.logViewer.title")}
onClick={() => setLogOpen(true)}
>
<FeaturedPlayListRounded fontSize="inherit" />
@@ -170,7 +180,7 @@ export const ProfileMore = (props: Props) => {
{fileOpen && (
<EditorViewer
open={true}
title={`${t("Global " + id)}`}
title={t(globalTitles[id])}
initialData={readProfileFile(id)}
language={id === "Merge" ? "yaml" : "javascript"}
schema={id === "Merge" ? "clash" : undefined}

View File

@@ -144,9 +144,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
}
} catch {
// 首次创建/更新失败,尝试使用自身代理
showNotice(
"info",
t("Profile creation failed, retrying with Clash proxy..."),
showNotice.info(
"profiles.modals.profileForm.feedback.notifications.creationRetry",
);
// 使用自身代理的配置
@@ -170,9 +169,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
await patchProfile(form.uid, { option: originalOptions });
}
showNotice(
"success",
t("Profile creation succeeded with Clash proxy"),
showNotice.success(
"profiles.modals.profileForm.feedback.notifications.creationSuccess",
);
}
}
@@ -186,8 +184,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
setTimeout(() => {
onChange(isActivating);
}, 0);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
} finally {
setLoading(false);
}
@@ -220,10 +218,14 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
return (
<BaseDialog
open={open}
title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
title={
openType === "new"
? t("profiles.modals.profileForm.title.create")
: t("profiles.modals.profileForm.title.edit")
}
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={handleClose}
onCancel={handleClose}
onOk={handleOk}
@@ -234,8 +236,14 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
control={control}
render={({ field }) => (
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
<InputLabel>{t("Type")}</InputLabel>
<Select {...field} autoFocus label={t("Type")}>
<InputLabel>
{t("profiles.modals.profileForm.fields.type")}
</InputLabel>
<Select
{...field}
autoFocus
label={t("profiles.modals.profileForm.fields.type")}
>
<MenuItem value="remote">Remote</MenuItem>
<MenuItem value="local">Local</MenuItem>
</Select>
@@ -247,7 +255,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
name="name"
control={control}
render={({ field }) => (
<TextField {...text} {...field} label={t("Name")} />
<TextField {...text} {...field} label={t("shared.labels.name")} />
)}
/>
@@ -255,7 +263,11 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
name="desc"
control={control}
render={({ field }) => (
<TextField {...text} {...field} label={t("Descriptions")} />
<TextField
{...text}
{...field}
label={t("profiles.modals.profileForm.fields.description")}
/>
)}
/>
@@ -269,7 +281,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
{...text}
{...field}
multiline
label={t("Subscription URL")}
label={t("profiles.modals.profileForm.fields.subscriptionUrl")}
/>
)}
/>
@@ -296,12 +308,12 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
{...field}
type="number"
placeholder="60"
label={t("HTTP Request Timeout")}
label={t("profiles.modals.profileForm.fields.httpTimeout")}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
{t("seconds")}
{t("shared.units.seconds")}
</InputAdornment>
),
},
@@ -321,11 +333,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
{...text}
{...field}
type="number"
label={t("Update Interval")}
label={t("profiles.modals.profileForm.fields.updateInterval")}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">{t("mins")}</InputAdornment>
<InputAdornment position="end">
{t("shared.units.minutes")}
</InputAdornment>
),
},
}}
@@ -350,7 +364,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
control={control}
render={({ field }) => (
<StyledBox>
<InputLabel>{t("Use System Proxy")}</InputLabel>
<InputLabel>
{t("profiles.modals.profileForm.fields.useSystemProxy")}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
</StyledBox>
)}
@@ -361,7 +377,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
control={control}
render={({ field }) => (
<StyledBox>
<InputLabel>{t("Use Clash Proxy")}</InputLabel>
<InputLabel>
{t("profiles.modals.profileForm.fields.useClashProxy")}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
</StyledBox>
)}
@@ -372,7 +390,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
control={control}
render={({ field }) => (
<StyledBox>
<InputLabel>{t("Accept Invalid Certs (Danger)")}</InputLabel>
<InputLabel>
{t("profiles.modals.profileForm.fields.acceptInvalidCerts")}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
</StyledBox>
)}
@@ -383,7 +403,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
control={control}
render={({ field }) => (
<StyledBox>
<InputLabel>{t("Allow Auto Update")}</InputLabel>
<InputLabel>
{t("profiles.modals.profileForm.fields.allowAutoUpdate")}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
</StyledBox>
)}

View File

@@ -163,11 +163,11 @@ export const ProxiesEditorViewer = (props: Props) => {
proxies.push(proxy);
names.push(proxy.name);
}
} catch (err: any) {
} catch (err) {
console.warn(
"[ProxiesEditorViewer] parseUri failed for line:",
uri,
err?.message || err,
err,
);
// 不阻塞主流程
}
@@ -263,11 +263,11 @@ export const ProxiesEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
showNotice("success", t("Saved Successfully"));
showNotice.success("shared.feedback.notifications.saved");
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
showNotice("error", err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -276,7 +276,7 @@ export const ProxiesEditorViewer = (props: Props) => {
<DialogTitle>
{
<Box display="flex" justifyContent="space-between">
{t("Edit Proxies")}
{t("profiles.modals.proxiesEditor.title")}
<Box>
<Button
variant="contained"
@@ -285,7 +285,9 @@ export const ProxiesEditorViewer = (props: Props) => {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
{visualization
? t("shared.editorModes.advanced")
: t("shared.editorModes.visualization")}
</Button>
</Box>
</Box>
@@ -312,7 +314,9 @@ export const ProxiesEditorViewer = (props: Props) => {
<Item>
<TextField
autoComplete="new-password"
placeholder={t("Use newlines for multiple uri")}
placeholder={t(
"profiles.modals.proxiesEditor.placeholders.multiUri",
)}
fullWidth
rows={9}
multiline
@@ -332,7 +336,7 @@ export const ProxiesEditorViewer = (props: Props) => {
});
}}
>
{t("Prepend Proxy")}
{t("profiles.modals.proxiesEditor.actions.prepend")}
</Button>
</Item>
<Item>
@@ -346,7 +350,7 @@ export const ProxiesEditorViewer = (props: Props) => {
});
}}
>
{t("Append Proxy")}
{t("profiles.modals.proxiesEditor.actions.append")}
</Button>
</Item>
</List>
@@ -497,11 +501,11 @@ export const ProxiesEditorViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
{t("shared.actions.cancel")}
</Button>
<Button onClick={handleSave} variant="contained">
{t("Save")}
{t("shared.actions.save")}
</Button>
</DialogActions>
</Dialog>

View File

@@ -240,8 +240,24 @@ const rules: {
},
];
const RULE_TYPE_LABEL_KEYS: Record<string, string> = Object.fromEntries(
rules.map((rule) => [
rule.name,
`rules.modals.editor.ruleTypes.${rule.name}`,
]),
);
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
const PROXY_POLICY_LABEL_KEYS: Record<string, string> =
builtinProxyPolicies.reduce(
(acc, policy) => {
acc[policy] = `proxy.policies.${policy}`;
return acc;
},
{} as Record<string, string>,
);
export const RulesEditorViewer = (props: Props) => {
const { groupsUid, mergeUid, profileUid, property, open, onClose, onSave } =
props;
@@ -350,8 +366,8 @@ export const RulesEditorViewer = (props: Props) => {
{ forceQuotes: true },
),
);
} catch (e: any) {
showNotice("error", e?.message || e?.toString() || "YAML dump error");
} catch (error) {
showNotice.error(error ?? "YAML dump error");
}
};
let idleId: number | undefined;
@@ -462,10 +478,12 @@ export const RulesEditorViewer = (props: Props) => {
const validateRule = () => {
if ((ruleType.required ?? true) && !ruleContent) {
throw new Error(t("Rule Condition Required"));
throw new Error(
t("rules.modals.editor.form.validation.conditionRequired"),
);
}
if (ruleType.validator && !ruleType.validator(ruleContent)) {
throw new Error(t("Invalid Rule"));
throw new Error(t("rules.modals.editor.form.validation.invalidRule"));
}
const condition = (ruleType.required ?? true) ? ruleContent : "";
@@ -477,11 +495,11 @@ export const RulesEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
showNotice("success", t("Saved Successfully"));
showNotice.success("shared.feedback.notifications.saved");
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
showNotice("error", err.toString());
showNotice.error(err);
}
});
@@ -490,7 +508,7 @@ export const RulesEditorViewer = (props: Props) => {
<DialogTitle>
{
<Box display="flex" justifyContent="space-between">
{t("Edit Rules")}
{t("rules.modals.editor.title")}
<Box>
<Button
variant="contained"
@@ -499,7 +517,9 @@ export const RulesEditorViewer = (props: Props) => {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
{visualization
? t("shared.editorModes.advanced")
: t("shared.editorModes.visualization")}
</Button>
</Box>
</Box>
@@ -518,26 +538,37 @@ export const RulesEditorViewer = (props: Props) => {
}}
>
<Item>
<ListItemText primary={t("Rule Type")} />
<ListItemText
primary={t("rules.modals.editor.form.labels.type")}
/>
<Autocomplete
size="small"
sx={{ minWidth: "240px" }}
renderInput={(params) => <TextField {...params} />}
options={rules}
value={ruleType}
getOptionLabel={(option) => option.name}
renderOption={(props, option) => (
<li {...props} title={t(option.name)}>
{option.name}
</li>
)}
getOptionLabel={(option) =>
t(RULE_TYPE_LABEL_KEYS[option.name] ?? option.name)
}
renderOption={(props, option) => {
const label = t(
RULE_TYPE_LABEL_KEYS[option.name] ?? option.name,
);
return (
<li {...props} title={label}>
{label}
</li>
);
}}
onChange={(_, value) => value && setRuleType(value)}
/>
</Item>
<Item
sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}
>
<ListItemText primary={t("Rule Content")} />
<ListItemText
primary={t("rules.modals.editor.form.labels.content")}
/>
{ruleType.name === "RULE-SET" && (
<Autocomplete
@@ -574,24 +605,34 @@ export const RulesEditorViewer = (props: Props) => {
)}
</Item>
<Item>
<ListItemText primary={t("Proxy Policy")} />
<ListItemText
primary={t("rules.modals.editor.form.labels.proxyPolicy")}
/>
<Autocomplete
size="small"
sx={{ minWidth: "240px" }}
renderInput={(params) => <TextField {...params} />}
options={proxyPolicyList}
value={proxyPolicy}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
</li>
)}
getOptionLabel={(option) =>
t(PROXY_POLICY_LABEL_KEYS[option] ?? option)
}
renderOption={(props, option) => {
const label = t(PROXY_POLICY_LABEL_KEYS[option] ?? option);
return (
<li {...props} title={label}>
{label}
</li>
);
}}
onChange={(_, value) => value && setProxyPolicy(value)}
/>
</Item>
{ruleType.noResolve && (
<Item>
<ListItemText primary={t("No Resolve")} />
<ListItemText
primary={t("rules.modals.editor.form.toggles.noResolve")}
/>
<Switch
checked={noResolve}
onChange={() => setNoResolve(!noResolve)}
@@ -609,11 +650,11 @@ export const RulesEditorViewer = (props: Props) => {
if (prependSeq.includes(raw)) return;
setPrependSeq([raw, ...prependSeq]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
}}
>
{t("Prepend Rule")}
{t("rules.modals.editor.form.actions.prependRule")}
</Button>
</Item>
<Item>
@@ -627,11 +668,11 @@ export const RulesEditorViewer = (props: Props) => {
if (appendSeq.includes(raw)) return;
setAppendSeq([...appendSeq, raw]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
}}
>
{t("Append Rule")}
{t("rules.modals.editor.form.actions.appendRule")}
</Button>
</Item>
</List>
@@ -776,11 +817,11 @@ export const RulesEditorViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
{t("shared.actions.cancel")}
</Button>
<Button onClick={handleSave} variant="contained">
{t("Save")}
{t("shared.actions.save")}
</Button>
</DialogActions>
</Dialog>

View File

@@ -66,12 +66,17 @@ export const ProviderButton = () => {
await refreshProxy();
await refreshProxyProviders();
showNotice("success", `${name} 更新成功`);
} catch (err: any) {
showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
showNotice.success(
"proxies.feedback.notifications.provider.updateSuccess",
{
name,
},
);
} catch (err) {
showNotice.error("proxies.feedback.notifications.provider.updateFailed", {
name,
message: String(err),
});
} finally {
// 清除更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
@@ -84,7 +89,7 @@ export const ProviderButton = () => {
// 获取所有provider的名称
const allProviders = Object.keys(proxyProviders || {});
if (allProviders.length === 0) {
showNotice("info", "没有可更新的代理提供者");
showNotice.info("proxies.feedback.notifications.provider.none");
return;
}
@@ -114,9 +119,11 @@ export const ProviderButton = () => {
await refreshProxy();
await refreshProxyProviders();
showNotice("success", "全部代理提供者更新成功");
} catch (err: any) {
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
showNotice.success("proxies.feedback.notifications.provider.allUpdated");
} catch (err) {
showNotice.error("proxies.feedback.notifications.provider.genericError", {
message: String(err),
});
} finally {
// 清除所有更新状态
setUpdating({});
@@ -138,7 +145,7 @@ export const ProviderButton = () => {
onClick={() => setOpen(true)}
sx={{ mr: 1 }}
>
{t("Proxy Provider")}
{t("proxies.page.provider.title")}
</Button>
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
@@ -148,14 +155,17 @@ export const ProviderButton = () => {
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Proxy Provider")}</Typography>
<Typography variant="h6">
{t("proxies.page.provider.title")}
</Typography>
<Box>
<Button
variant="contained"
size="small"
onClick={updateAllProviders}
aria-label={t("proxies.page.provider.actions.updateAll")}
>
{t("Update All")}
{t("proxies.page.provider.actions.updateAll")}
</Button>
</Box>
</Box>
@@ -246,7 +256,7 @@ export const ProviderButton = () => {
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
<small>{t("shared.labels.updateAt")}: </small>
{time.fromNow()}
</Typography>
</Box>
@@ -264,11 +274,17 @@ export const ProviderButton = () => {
justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}>
<span
title={t("shared.labels.usedTotal") as string}
>
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span>
<span title={t("Expire Time") as string}>
<span
title={
t("shared.labels.expireTime") as string
}
>
{parseExpire(expire)}
</span>
</Box>
@@ -313,7 +329,8 @@ export const ProviderButton = () => {
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
title={t("proxies.page.provider.actions.update")}
aria-label={t("proxies.page.provider.actions.update")}
>
<RefreshRounded />
</IconButton>
@@ -326,7 +343,7 @@ export const ProviderButton = () => {
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t("Close")}
{t("shared.actions.close")}
</Button>
</DialogActions>
</Dialog>

View File

@@ -156,7 +156,11 @@ const SortableItem = ({ proxy, index, onRemove }: SortableItemProps) => {
{proxy.delay !== undefined && (
<Chip
label={proxy.delay > 0 ? `${proxy.delay}ms` : t("timeout") || "超时"}
label={
proxy.delay > 0
? `${proxy.delay}ms`
: t("shared.labels.timeout") || "超时"
}
size="small"
color={
proxy.delay > 0 && proxy.delay < 200
@@ -299,7 +303,7 @@ export const ProxyChain = ({
// onUpdateChain([]);
} catch (error) {
console.error("Failed to disconnect from proxy chain:", error);
alert(t("Failed to disconnect from proxy chain") || "断开链式代理失败");
alert(t("proxies.page.chain.disconnectFailed") || "断开链式代理失败");
} finally {
setIsConnecting(false);
}
@@ -307,9 +311,7 @@ export const ProxyChain = ({
}
if (proxyChain.length < 2) {
alert(
t("Chain proxy requires at least 2 nodes") || "链式代理至少需要2个节点",
);
alert(t("proxies.page.chain.minimumNodes") || "链式代理至少需要2个节点");
return;
}
@@ -341,7 +343,7 @@ export const ProxyChain = ({
console.log("Successfully connected to proxy chain");
} catch (error) {
console.error("Failed to connect to proxy chain:", error);
alert(t("Failed to connect to proxy chain") || "连接链式代理失败");
alert(t("proxies.page.chain.connectFailed") || "连接链式代理失败");
} finally {
setIsConnecting(false);
}
@@ -471,7 +473,7 @@ export const ProxyChain = ({
mb: 2,
}}
>
<Typography variant="h6">{t("Chain Proxy Config")}</Typography>
<Typography variant="h6">{t("proxies.page.chain.header")}</Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{proxyChain.length > 0 && (
<IconButton
@@ -486,7 +488,9 @@ export const ProxyChain = ({
backgroundColor: theme.palette.error.light + "20",
},
}}
title={t("Delete Chain Config") || "删除链式配置"}
title={
t("proxies.page.actions.clearChainConfig") || "删除链式配置"
}
>
<DeleteIcon fontSize="small" />
</IconButton>
@@ -507,16 +511,16 @@ export const ProxyChain = ({
}}
title={
proxyChain.length < 2
? t("Chain proxy requires at least 2 nodes") ||
? t("proxies.page.chain.minimumNodes") ||
"链式代理至少需要2个节点"
: undefined
}
>
{isConnecting
? t("Connecting...") || "连接中..."
? t("proxies.page.actions.connecting") || "连接中..."
: isConnected
? t("Disconnect") || "断开"
: t("Connect") || "连接"}
? t("proxies.page.actions.disconnect") || "断开"
: t("proxies.page.actions.connect") || "连接"}
</Button>
</Box>
</Box>
@@ -526,10 +530,9 @@ export const ProxyChain = ({
sx={{ mb: 2 }}
>
{proxyChain.length === 1
? t(
"Chain proxy requires at least 2 nodes. Please add one more node.",
) || "链式代理至少需要2个节点请再添加一个节点。"
: t("Click nodes in order to add to proxy chain") ||
? t("proxies.page.chain.minimumNodesHint") ||
"链式代理至少需要2个节点请再添加一个节点。"
: t("proxies.page.chain.instruction") ||
"按顺序点击节点添加到代理链中"}
</Alert>
@@ -544,7 +547,7 @@ export const ProxyChain = ({
color: theme.palette.text.secondary,
}}
>
<Typography>{t("No proxy chain configured")}</Typography>
<Typography>{t("proxies.page.chain.empty")}</Typography>
</Box>
) : (
<DndContext

View File

@@ -239,7 +239,7 @@ export const ProxyGroups = (props: Props) => {
setProxyChain((prev) => {
// 检查是否已经存在相同名称的代理,防止重复添加
if (prev.some((item) => item.name === proxy.name)) {
const warningMessage = t("Proxy node already exists in chain");
const warningMessage = t("proxies.page.chain.duplicateNode");
setDuplicateWarning({
open: true,
message: warningMessage,
@@ -372,7 +372,7 @@ export const ProxyGroups = (props: Props) => {
}, [renderList]);
if (mode === "direct") {
return <BaseEmpty text={t("clash_mode_direct")} />;
return <BaseEmpty textKey="proxies.page.messages.directMode" />;
}
if (isChainMode) {
@@ -403,7 +403,7 @@ export const ProxyGroups = (props: Props) => {
variant="h6"
sx={{ fontWeight: 600, fontSize: "16px" }}
>
{t("Proxy Rules")}
{t("proxies.page.rules.title")}
</Typography>
{currentGroup && (
<Box
@@ -442,7 +442,7 @@ export const ProxyGroups = (props: Props) => {
variant="body2"
sx={{ mr: 0.5, fontSize: "12px" }}
>
{t("Select Rules")}
{t("proxies.page.rules.select")}
</Typography>
<ExpandMoreRounded fontSize="small" />
</IconButton>

View File

@@ -67,7 +67,7 @@ export const ProxyHead = ({
<IconButton
size="small"
color="inherit"
title={t("locate")}
title={t("proxies.page.tooltips.locate")}
onClick={onLocation}
>
<MyLocationRounded />
@@ -76,7 +76,7 @@ export const ProxyHead = ({
<IconButton
size="small"
color="inherit"
title={t("Delay check")}
title={t("proxies.page.tooltips.delayCheck")}
onClick={() => {
console.log(`[ProxyHead] 点击延迟测试按钮,组: ${groupName}`);
// Remind the user that it is custom test url
@@ -94,9 +94,11 @@ export const ProxyHead = ({
size="small"
color="inherit"
title={
[t("Sort by default"), t("Sort by delay"), t("Sort by name")][
sortType
]
[
t("proxies.page.tooltips.sortDefault"),
t("proxies.page.tooltips.sortDelay"),
t("proxies.page.tooltips.sortName"),
][sortType]
}
onClick={() =>
onHeadState({ sortType: ((sortType + 1) % 3) as ProxySortType })
@@ -110,7 +112,7 @@ export const ProxyHead = ({
<IconButton
size="small"
color="inherit"
title={t("Delay check URL")}
title={t("proxies.page.tooltips.delayCheckUrl")}
onClick={() =>
onHeadState({ textState: textState === "url" ? null : "url" })
}
@@ -125,7 +127,11 @@ export const ProxyHead = ({
<IconButton
size="small"
color="inherit"
title={showType ? t("Proxy basic") : t("Proxy detail")}
title={
showType
? t("proxies.page.tooltips.showBasic")
: t("proxies.page.tooltips.showDetail")
}
onClick={() => onHeadState({ showType: !showType })}
>
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
@@ -134,7 +140,7 @@ export const ProxyHead = ({
<IconButton
size="small"
color="inherit"
title={t("Filter")}
title={t("proxies.page.tooltips.filter")}
onClick={() =>
onHeadState({ textState: textState === "filter" ? null : "filter" })
}
@@ -154,7 +160,7 @@ export const ProxyHead = ({
value={filterText}
size="small"
variant="outlined"
placeholder={t("Filter conditions")}
placeholder={t("shared.placeholders.filter")}
onChange={(e) => onHeadState({ filterText: e.target.value })}
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
/>
@@ -169,7 +175,7 @@ export const ProxyHead = ({
value={testUrl}
size="small"
variant="outlined"
placeholder={t("Delay check URL")}
placeholder={t("proxies.page.placeholders.delayCheckUrl")}
onChange={(e) => onHeadState({ testUrl: e.target.value })}
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
/>

View File

@@ -271,7 +271,9 @@ export const ProxyItemMini = (props: Props) => {
<span
className={proxy.name === group.now ? "the-pin" : "the-unpin"}
title={
group.type === "URLTest" ? t("Delay check to cancel fixed") : ""
group.type === "URLTest"
? t("proxies.page.labels.delayCheckReset")
: ""
}
>
📌

View File

@@ -160,7 +160,7 @@ export const ProxyRender = (props: RenderProps) => {
}}
/>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Tooltip title={t("Proxy Count")} arrow>
<Tooltip title={t("proxies.page.labels.proxyCount")} arrow>
<Chip
size="small"
label={`${group.all.length}`}

View File

@@ -58,12 +58,17 @@ export const ProviderButton = () => {
await refreshRules();
await refreshRuleProviders();
showNotice("success", `${name} 更新成功`);
} catch (err: any) {
showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
showNotice.success(
"rules.feedback.notifications.provider.updateSuccess",
{
name,
},
);
} catch (err) {
showNotice.error("rules.feedback.notifications.provider.updateFailed", {
name,
message: String(err),
});
} finally {
// 清除更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
@@ -76,7 +81,7 @@ export const ProviderButton = () => {
// 获取所有provider的名称
const allProviders = Object.keys(ruleProviders || {});
if (allProviders.length === 0) {
showNotice("info", "没有可更新的规则提供者");
showNotice.info("rules.feedback.notifications.provider.none");
return;
}
@@ -106,9 +111,11 @@ export const ProviderButton = () => {
await refreshRules();
await refreshRuleProviders();
showNotice("success", "全部规则提供者更新成功");
} catch (err: any) {
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
showNotice.success("rules.feedback.notifications.provider.allUpdated");
} catch (err) {
showNotice.error("rules.feedback.notifications.provider.genericError", {
message: String(err),
});
} finally {
// 清除所有更新状态
setUpdating({});
@@ -129,7 +136,7 @@ export const ProviderButton = () => {
startIcon={<StorageOutlined />}
onClick={() => setOpen(true)}
>
{t("Rule Provider")}
{t("rules.page.provider.trigger")}
</Button>
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
@@ -139,13 +146,15 @@ export const ProviderButton = () => {
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Rule Providers")}</Typography>
<Typography variant="h6">
{t("rules.page.provider.dialogTitle")}
</Typography>
<Button
variant="contained"
size="small"
onClick={updateAllProviders}
>
{t("Update All")}
{t("rules.page.provider.actions.updateAll")}
</Button>
</Box>
</DialogTitle>
@@ -216,7 +225,7 @@ export const ProviderButton = () => {
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
<small>{t("shared.labels.updateAt")}: </small>
{time.fromNow()}
</Typography>
</Box>
@@ -246,6 +255,7 @@ export const ProviderButton = () => {
color="primary"
onClick={() => updateProvider(key)}
disabled={isUpdating}
aria-label={t("rules.page.provider.actions.update")}
sx={{
animation: isUpdating
? "spin 1s linear infinite"
@@ -255,7 +265,7 @@ export const ProviderButton = () => {
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
title={t("rules.page.provider.actions.update")}
>
<RefreshRounded />
</IconButton>
@@ -268,7 +278,7 @@ export const ProviderButton = () => {
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t("Close")}
{t("shared.actions.close")}
</Button>
</DialogActions>
</Dialog>

View File

@@ -84,22 +84,22 @@ export const BackupConfigViewer = memo(
if (!url) {
urlRef.current?.focus();
showNotice("error", t("WebDAV URL Required"));
throw new Error(t("WebDAV URL Required"));
showNotice.error("settings.modals.backup.messages.webdavUrlRequired");
throw new Error(t("settings.modals.backup.messages.webdavUrlRequired"));
} else if (!isValidUrl(url)) {
urlRef.current?.focus();
showNotice("error", t("Invalid WebDAV URL"));
throw new Error(t("Invalid WebDAV URL"));
showNotice.error("settings.modals.backup.messages.invalidWebdavUrl");
throw new Error(t("settings.modals.backup.messages.invalidWebdavUrl"));
}
if (!username) {
usernameRef.current?.focus();
showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Username Required"));
showNotice.error("settings.modals.backup.messages.usernameRequired");
throw new Error(t("settings.modals.backup.messages.usernameRequired"));
}
if (!password) {
passwordRef.current?.focus();
showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Password Required"));
showNotice.error("settings.modals.backup.messages.passwordRequired");
throw new Error(t("settings.modals.backup.messages.passwordRequired"));
}
};
@@ -112,11 +112,17 @@ export const BackupConfigViewer = memo(
data.username.trim(),
data.password,
).then(() => {
showNotice("success", t("WebDAV Config Saved"));
showNotice.success(
"settings.modals.backup.messages.webdavConfigSaved",
);
onSaveSuccess();
});
} catch (error) {
showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
showNotice.error(
"settings.modals.backup.messages.webdavConfigSaveFailed",
{ error },
3000,
);
} finally {
setLoading(false);
}
@@ -127,11 +133,13 @@ export const BackupConfigViewer = memo(
try {
setLoading(true);
await createWebdavBackup().then(async () => {
showNotice("success", t("Backup Created"));
showNotice.success("settings.modals.backup.messages.backupCreated");
await onBackupSuccess();
});
} catch (error) {
showNotice("error", t("Backup Failed", { error }));
showNotice.error("settings.modals.backup.messages.backupFailed", {
error,
});
} finally {
setLoading(false);
}
@@ -145,7 +153,7 @@ export const BackupConfigViewer = memo(
<Grid size={{ xs: 12 }}>
<TextField
fullWidth
label={t("WebDAV Server URL")}
label={t("settings.modals.backup.fields.webdavUrl")}
variant="outlined"
size="small"
{...register("url")}
@@ -157,7 +165,7 @@ export const BackupConfigViewer = memo(
</Grid>
<Grid size={{ xs: 6 }}>
<TextField
label={t("Username")}
label={t("settings.modals.backup.fields.username")}
variant="outlined"
size="small"
{...register("username")}
@@ -169,7 +177,7 @@ export const BackupConfigViewer = memo(
</Grid>
<Grid size={{ xs: 6 }}>
<TextField
label={t("Password")}
label={t("shared.labels.password")}
type={showPassword ? "text" : "password"}
variant="outlined"
size="small"
@@ -214,7 +222,7 @@ export const BackupConfigViewer = memo(
type="button"
onClick={handleSubmit(save)}
>
{t("Save")}
{t("shared.actions.save")}
</Button>
) : (
<>
@@ -225,7 +233,7 @@ export const BackupConfigViewer = memo(
type="button"
size="large"
>
{t("Backup")}
{t("settings.modals.backup.actions.backup")}
</Button>
<Button
variant="outlined"
@@ -233,7 +241,7 @@ export const BackupConfigViewer = memo(
type="button"
size="large"
>
{t("Refresh")}
{t("shared.actions.refresh")}
</Button>
</>
)}

View File

@@ -75,7 +75,7 @@ export const BackupTableViewer = memo(
const handleRestore = useLockFn(async (filename: string) => {
await onRestore(filename).then(() => {
showNotice("success", t("Restore Success, App will restart in 1s"));
showNotice.success("settings.modals.backup.messages.restoreSuccess");
});
await restartApp();
});
@@ -92,10 +92,14 @@ export const BackupTableViewer = memo(
return;
}
await onExport(filename, savePath);
showNotice("success", t("Local Backup Exported"));
showNotice.success(
"settings.modals.backup.messages.localBackupExported",
);
} catch (error) {
console.error(error);
showNotice("error", t("Local Backup Export Failed"));
showNotice.error(
"settings.modals.backup.messages.localBackupExportFailed",
);
}
});
@@ -104,9 +108,15 @@ export const BackupTableViewer = memo(
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Filename")}</TableCell>
<TableCell>{t("Backup Time")}</TableCell>
<TableCell align="right">{t("Actions")}</TableCell>
<TableCell>
{t("settings.modals.backup.table.filename")}
</TableCell>
<TableCell>
{t("settings.modals.backup.table.backupTime")}
</TableCell>
<TableCell align="right">
{t("settings.modals.backup.table.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -140,9 +150,13 @@ export const BackupTableViewer = memo(
<>
<IconButton
color="primary"
aria-label={t("Export")}
aria-label={t(
"settings.modals.backup.actions.export",
)}
size="small"
title={t("Export Backup")}
title={t(
"settings.modals.backup.actions.exportBackup",
)}
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
await handleExport(file.filename);
@@ -159,13 +173,17 @@ export const BackupTableViewer = memo(
)}
<IconButton
color="secondary"
aria-label={t("Delete")}
aria-label={t("shared.actions.delete")}
size="small"
title={t("Delete Backup")}
title={t(
"settings.modals.backup.actions.deleteBackup",
)}
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
const confirmed = await confirmAsync(
t("Confirm to delete this backup file?"),
t(
"settings.modals.backup.messages.confirmDelete",
),
);
if (confirmed) {
await handleDelete(file.filename);
@@ -181,14 +199,20 @@ export const BackupTableViewer = memo(
/>
<IconButton
color="primary"
aria-label={t("Restore")}
aria-label={t(
"settings.modals.backup.actions.restore",
)}
size="small"
title={t("Restore Backup")}
title={t(
"settings.modals.backup.actions.restoreBackup",
)}
disabled={!file.allow_apply}
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
const confirmed = await confirmAsync(
t("Confirm to restore this backup file?"),
t(
"settings.modals.backup.messages.confirmRestore",
),
);
if (confirmed) {
await handleRestore(file.filename);
@@ -219,7 +243,7 @@ export const BackupTableViewer = memo(
color="textSecondary"
align="center"
>
{t("No Backups")}
{t("settings.modals.backup.table.noBackups")}
</Typography>
</Box>
</TableCell>
@@ -234,7 +258,7 @@ export const BackupTableViewer = memo(
rowsPerPage={DEFAULT_ROWS_PER_PAGE}
page={page}
onPageChange={onPageChange}
labelRowsPerPage={t("Rows per page")}
labelRowsPerPage={t("settings.modals.backup.table.rowsPerPage")}
/>
</TableContainer>
);

View File

@@ -127,7 +127,6 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
setBackupFiles([]);
setTotal(0);
console.error(error);
// Notice.error(t("Failed to fetch backup files"));
} finally {
setIsLoading(false);
}
@@ -247,7 +246,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
return (
<BaseDialog
open={open}
title={t("Backup Setting")}
title={t("settings.modals.backup.title")}
contentSx={{
minWidth: { xs: 320, sm: 620 },
maxWidth: "unset",
@@ -278,11 +277,14 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Tabs
value={source}
onChange={handleChangeSource}
aria-label={t("Select Backup Target")}
aria-label={t("settings.modals.backup.actions.selectTarget")}
sx={{ mb: 2 }}
>
<Tab value="local" label={t("Local Backup")} />
<Tab value="webdav" label={t("WebDAV Backup")} />
<Tab value="local" label={t("settings.modals.backup.tabs.local")} />
<Tab
value="webdav"
label={t("settings.modals.backup.tabs.webdav")}
/>
</Tabs>
{source === "local" ? (
<LocalBackupActions
@@ -342,7 +344,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
backgroundColor: (theme) => theme.palette.background.paper,
}}
>
{t("Close")}
{t("shared.actions.close")}
</Button>
</Box>,
dialogPaper,

View File

@@ -24,8 +24,16 @@ import { changeClashCore, restartCore } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
const VALID_CORE = [
{ name: "Mihomo", core: "verge-mihomo", chip: "Release Version" },
{ name: "Mihomo Alpha", core: "verge-mihomo-alpha", chip: "Alpha Version" },
{
name: "Mihomo",
core: "verge-mihomo",
chipKey: "settings.modals.clashCore.variants.release",
},
{
name: "Mihomo Alpha",
core: "verge-mihomo-alpha",
chipKey: "settings.modals.clashCore.variants.alpha",
},
];
export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
@@ -54,7 +62,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
const errorMsg = await changeClashCore(core);
if (errorMsg) {
showNotice("error", errorMsg);
showNotice.error(errorMsg);
setChangingCore(null);
return;
}
@@ -65,9 +73,9 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
mutate("getVersion");
setChangingCore(null);
}, 500);
} catch (err: any) {
} catch (err) {
setChangingCore(null);
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
});
@@ -75,11 +83,13 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
try {
setRestarting(true);
await restartCore();
showNotice("success", t(`Clash Core Restarted`));
showNotice.success(
t("settings.feedback.notifications.clash.restartSuccess"),
);
setRestarting(false);
} catch (err: any) {
} catch (err) {
setRestarting(false);
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
});
@@ -88,14 +98,16 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
setUpgrading(true);
await upgradeCore();
setUpgrading(false);
showNotice("success", t(`Core Version Updated`));
showNotice.success(
t("settings.feedback.notifications.clash.versionUpdated"),
);
} catch (err: any) {
setUpgrading(false);
const errMsg = err.response?.data?.message || err.toString();
const errMsg = err?.response?.data?.message ?? String(err);
const showMsg = errMsg.includes("already using latest version")
? "Already Using Latest Core Version"
? t("settings.feedback.notifications.clash.alreadyLatestVersion")
: errMsg;
showNotice("error", t(showMsg));
showNotice.info(showMsg);
}
});
@@ -104,7 +116,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
open={open}
title={
<Box display="flex" justifyContent="space-between">
{t("Clash Core")}
{t("settings.sections.clash.form.fields.clashCore")}
<Box>
<LoadingButton
variant="contained"
@@ -116,7 +128,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
sx={{ marginRight: "8px" }}
onClick={onUpgrade}
>
{t("Upgrade")}
{t("shared.actions.upgrade")}
</LoadingButton>
<LoadingButton
variant="contained"
@@ -127,7 +139,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
disabled={upgrading}
onClick={onRestart}
>
{t("Restart")}
{t("shared.actions.restart")}
</LoadingButton>
</Box>
</Box>
@@ -141,7 +153,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
marginTop: "-8px",
}}
disableOk
cancelBtn={t("Close")}
cancelBtn={t("shared.actions.close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
@@ -157,7 +169,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
{changingCore === each.core ? (
<CircularProgress size={20} sx={{ mr: 1 }} />
) : (
<Chip label={t(`${each.chip}`)} size="small" />
<Chip label={t(each.chipKey)} size="small" />
)}
</ListItemButton>
))}

View File

@@ -69,11 +69,13 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
manual: true,
onSuccess: () => {
setOpen(false);
showNotice("success", t("Port settings saved"));
showNotice.success("settings.modals.clashPort.messages.saved");
},
onError: (e) => {
showNotice("error", e.message || t("Failed to save port settings"));
// showNotice("error", t("Failed to save port settings"));
onError: (error) => {
showNotice.error(
"settings.modals.clashPort.messages.saveFailed",
error,
);
},
},
);
@@ -150,7 +152,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
return (
<BaseDialog
open={open}
title={t("Port Config")}
title={t("settings.modals.clashPort.title")}
contentSx={{
width: 400,
}}
@@ -158,13 +160,13 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
loading ? (
<Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} />
{t("Saving...")}
{t("shared.statuses.saving")}
</Stack>
) : (
t("Save")
t("shared.actions.save")
)
}
cancelBtn={t("Cancel")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
@@ -172,7 +174,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<List sx={{ width: "100%" }}>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Mixed Port")}
primary={t("settings.modals.clashPort.fields.mixed")}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
@@ -188,7 +190,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<IconButton
size="small"
onClick={() => setMixedPort(generateRandomPort())}
title={t("Random Port")}
title={t("settings.modals.clashPort.actions.random")}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
@@ -204,7 +206,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Socks Port")}
primary={t("settings.modals.clashPort.fields.socks")}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
@@ -221,7 +223,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<IconButton
size="small"
onClick={() => setSocksPort(generateRandomPort())}
title={t("Random Port")}
title={t("settings.modals.clashPort.actions.random")}
disabled={!socksEnabled}
sx={{ mr: 0.5 }}
>
@@ -238,7 +240,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Http Port")}
primary={t("settings.modals.clashPort.fields.http")}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
@@ -255,7 +257,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<IconButton
size="small"
onClick={() => setHttpPort(generateRandomPort())}
title={t("Random Port")}
title={t("settings.modals.clashPort.actions.random")}
disabled={!httpEnabled}
sx={{ mr: 0.5 }}
>
@@ -273,7 +275,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
{OS !== "windows" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Redir Port")}
primary={t("settings.modals.clashPort.fields.redir")}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
@@ -290,7 +292,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<IconButton
size="small"
onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")}
title={t("settings.modals.clashPort.actions.random")}
disabled={!redirEnabled}
sx={{ mr: 0.5 }}
>
@@ -309,7 +311,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Tproxy Port")}
primary={t("settings.modals.clashPort.fields.tproxy")}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
@@ -326,7 +328,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<IconButton
size="small"
onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")}
title={t("settings.modals.clashPort.actions.random")}
disabled={!tproxyEnabled}
sx={{ mr: 0.5 }}
>

View File

@@ -27,8 +27,8 @@ export const ConfigViewer = forwardRef<DialogRef>((_, ref) => {
open={true}
title={
<Box display="flex" alignItems="center" gap={2}>
{t("Runtime Config")}
<Chip label={t("ReadOnly")} size="small" />
{t("settings.components.verge.advanced.fields.runtimeConfig")}
<Chip label={t("shared.labels.readOnly")} size="small" />
</Box>
}
initialData={Promise.resolve(runtimeConfig)}

View File

@@ -56,12 +56,16 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
// 如果启用了外部控制器,则保存控制器地址和密钥
if (enableController) {
if (!controller.trim()) {
showNotice("error", t("Controller address cannot be empty"));
showNotice.error(
"settings.sections.externalController.messages.addressRequired",
);
return;
}
if (!secret.trim()) {
showNotice("error", t("Secret cannot be empty"));
showNotice.error(
"settings.sections.externalController.messages.secretRequired",
);
return;
}
@@ -71,12 +75,12 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
await patchInfo({ "external-controller": "" });
}
showNotice("success", t("Configuration saved successfully"));
showNotice.success("shared.feedback.notifications.common.saveSuccess");
setOpen(false);
} catch (err: any) {
showNotice(
"error",
err.message || t("Failed to save configuration"),
} catch (err) {
showNotice.error(
"shared.feedback.notifications.common.saveFailed",
err,
4000,
);
} finally {
@@ -93,7 +97,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
setTimeout(() => setCopySuccess(null));
} catch (err) {
console.warn("[ControllerViewer] copy to clipboard failed:", err);
showNotice("error", t("Failed to copy"));
showNotice.error(
"settings.sections.externalController.messages.copyFailed",
);
}
},
);
@@ -101,19 +107,19 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
return (
<BaseDialog
open={open}
title={t("External Controller")}
title={t("settings.sections.externalController.title")}
contentSx={{ width: 400 }}
okBtn={
isSaving ? (
<Box display="flex" alignItems="center" gap={1}>
<CircularProgress size={16} color="inherit" />
{t("Saving...")}
{t("shared.statuses.saving")}
</Box>
) : (
t("Save")
t("shared.actions.save")
)
}
cancelBtn={t("Cancel")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
@@ -126,7 +132,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
justifyContent: "space-between",
}}
>
<ListItemText primary={t("Enable External Controller")} />
<ListItemText
primary={t("settings.sections.externalController.fields.enable")}
/>
<Switch
edge="end"
checked={enableController}
@@ -142,7 +150,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
justifyContent: "space-between",
}}
>
<ListItemText primary={t("External Controller")} />
<ListItemText
primary={t("settings.sections.externalController.fields.address")}
/>
<Box display="flex" alignItems="center" gap={1}>
<TextField
size="small"
@@ -152,11 +162,15 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
pointerEvents: enableController ? "auto" : "none",
}}
value={controller}
placeholder="Required"
placeholder={t(
"settings.sections.externalController.placeholders.address",
)}
onChange={(e) => setController(e.target.value)}
disabled={isSaving || !enableController}
/>
<Tooltip title={t("Copy to clipboard")}>
<Tooltip
title={t("settings.sections.externalController.tooltips.copy")}
>
<IconButton
size="small"
onClick={() => handleCopyToClipboard(controller, "controller")}
@@ -176,7 +190,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
justifyContent: "space-between",
}}
>
<ListItemText primary={t("Core Secret")} />
<ListItemText
primary={t("settings.sections.externalController.fields.secret")}
/>
<Box display="flex" alignItems="center" gap={1}>
<TextField
size="small"
@@ -186,11 +202,15 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
pointerEvents: enableController ? "auto" : "none",
}}
value={secret}
placeholder={t("Recommended")}
placeholder={t(
"settings.sections.externalController.placeholders.secret",
)}
onChange={(e) => setSecret(e.target.value)}
disabled={isSaving || !enableController}
/>
<Tooltip title={t("Copy to clipboard")}>
<Tooltip
title={t("settings.sections.externalController.tooltips.copy")}
>
<IconButton
size="small"
onClick={() => handleCopyToClipboard(secret, "secret")}
@@ -211,8 +231,10 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
>
<Alert severity="success">
{copySuccess === "controller"
? t("Controller address copied to clipboard")
: t("Secret copied to clipboard")}
? t(
"settings.sections.externalController.messages.controllerCopied",
)
: t("settings.sections.externalController.messages.secretCopied")}
</Alert>
</Snackbar>
</BaseDialog>

View File

@@ -424,9 +424,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
skipYamlSyncRef.current = true;
updateValuesFromConfig(parsedYaml);
} catch {
showNotice("error", t("Invalid YAML format"));
showNotice.error("settings.modals.dns.errors.invalidYaml");
}
}, [yamlContent, t, updateValuesFromConfig]);
}, [yamlContent, updateValuesFromConfig]);
useEffect(() => {
if (skipYamlSyncRef.current) {
@@ -509,7 +509,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
// 使用YAML编辑器的值
const parsedConfig = yaml.load(yamlContent);
if (typeof parsedConfig !== "object" || parsedConfig === null) {
throw new Error(t("Invalid configuration"));
throw new Error(t("settings.modals.dns.errors.invalid"));
}
config = parsedConfig as Record<string, any>;
}
@@ -547,9 +547,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
}
}
showNotice(
"error",
t("DNS configuration error") + ": " + cleanErrorMsg,
showNotice.error(
"settings.modals.dns.messages.configError",
cleanErrorMsg,
);
return;
}
@@ -561,9 +561,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
}
setOpen(false);
showNotice("success", t("DNS settings saved"));
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.success("settings.modals.dns.messages.saved");
} catch (err) {
showNotice.error(err);
}
});
@@ -613,7 +613,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
open={open}
title={
<Box display="flex" justifyContent="space-between" alignItems="center">
{t("DNS Overwrite")}
{t("settings.modals.dns.dialog.title")}
<Box display="flex" alignItems="center" gap={1}>
<Button
variant="outlined"
@@ -622,7 +622,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
startIcon={<RestartAltRounded />}
onClick={resetToDefaults}
>
{t("Reset to Default")}
{t("shared.actions.resetToDefault")}
</Button>
<Button
variant="contained"
@@ -631,7 +631,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
{visualization
? t("shared.editorModes.advanced")
: t("shared.editorModes.visualization")}
</Button>
</Box>
</Box>
@@ -643,8 +645,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
? {}
: { padding: "0 24px", display: "flex", flexDirection: "column" }),
}}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
@@ -655,7 +657,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
color="warning.main"
sx={{ mb: 2, mt: 0, fontStyle: "italic" }}
>
{t("DNS Settings Warning")}
{t("settings.modals.dns.dialog.warning")}
</Typography>
{visualization ? (
@@ -664,11 +666,11 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="subtitle1"
sx={{ mt: 1, mb: 1, fontWeight: "bold" }}
>
{t("DNS Settings")}
{t("settings.modals.dns.sections.general")}
</Typography>
<Item>
<ListItemText primary={t("Enable DNS")} />
<ListItemText primary={t("settings.modals.dns.fields.enable")} />
<Switch
edge="end"
checked={values.enable}
@@ -677,7 +679,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Item>
<Item>
<ListItemText primary={t("DNS Listen")} />
<ListItemText primary={t("settings.modals.dns.fields.listen")} />
<TextField
size="small"
autoComplete="off"
@@ -689,7 +691,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Item>
<Item>
<ListItemText primary={t("Enhanced Mode")} />
<ListItemText
primary={t("settings.modals.dns.fields.enhancedMode")}
/>
<FormControl size="small" sx={{ width: 150 }}>
<Select
value={values.enhancedMode}
@@ -702,7 +706,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Item>
<Item>
<ListItemText primary={t("Fake IP Range")} />
<ListItemText
primary={t("settings.modals.dns.fields.fakeIpRange")}
/>
<TextField
size="small"
autoComplete="off"
@@ -714,7 +720,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Item>
<Item>
<ListItemText primary={t("Fake IP Filter Mode")} />
<ListItemText
primary={t("settings.modals.dns.fields.fakeIpFilterMode")}
/>
<FormControl size="small" sx={{ width: 150 }}>
<Select
value={values.fakeIpFilterMode}
@@ -728,8 +736,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("IPv6")}
secondary={t("Enable IPv6 DNS resolution")}
primary={t("settings.modals.dns.fields.ipv6.label")}
secondary={t("settings.modals.dns.fields.ipv6.description")}
/>
<Switch
edge="end"
@@ -740,8 +748,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("Prefer H3")}
secondary={t("DNS DOH使用HTTP/3")}
primary={t("settings.modals.dns.fields.preferH3.label")}
secondary={t("settings.modals.dns.fields.preferH3.description")}
/>
<Switch
edge="end"
@@ -752,8 +760,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("Respect Rules")}
secondary={t("DNS connections follow routing rules")}
primary={t("settings.modals.dns.fields.respectRules.label")}
secondary={t(
"settings.modals.dns.fields.respectRules.description",
)}
/>
<Switch
edge="end"
@@ -764,8 +774,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("Use Hosts")}
secondary={t("Enable to resolve hosts through hosts file")}
primary={t("settings.modals.dns.fields.useHosts.label")}
secondary={t("settings.modals.dns.fields.useHosts.description")}
/>
<Switch
edge="end"
@@ -776,8 +786,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("Use System Hosts")}
secondary={t("Enable to resolve hosts through system hosts file")}
primary={t("settings.modals.dns.fields.useSystemHosts.label")}
secondary={t(
"settings.modals.dns.fields.useSystemHosts.description",
)}
/>
<Switch
edge="end"
@@ -788,8 +800,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("Direct Nameserver Follow Policy")}
secondary={t("Whether to follow nameserver policy")}
primary={t("settings.modals.dns.fields.directPolicy.label")}
secondary={t(
"settings.modals.dns.fields.directPolicy.description",
)}
/>
<Switch
edge="end"
@@ -800,8 +814,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Default Nameserver")}
secondary={t("Default DNS servers used to resolve DNS servers")}
primary={t("settings.modals.dns.fields.defaultNameserver.label")}
secondary={t(
"settings.modals.dns.fields.defaultNameserver.description",
)}
/>
<TextField
fullWidth
@@ -817,8 +833,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Nameserver")}
secondary={t("List of DNS servers")}
primary={t("settings.modals.dns.fields.nameserver.label")}
secondary={t("settings.modals.dns.fields.nameserver.description")}
/>
<TextField
fullWidth
@@ -834,8 +850,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fallback")}
secondary={t("List of fallback DNS servers")}
primary={t("settings.modals.dns.fields.fallback.label")}
secondary={t("settings.modals.dns.fields.fallback.description")}
/>
<TextField
fullWidth
@@ -851,8 +867,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Proxy Server Nameserver")}
secondary={t("Proxy Node Nameserver")}
primary={t("settings.modals.dns.fields.proxy.label")}
secondary={t("settings.modals.dns.fields.proxy.description")}
/>
<TextField
fullWidth
@@ -868,8 +884,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Direct Nameserver")}
secondary={t("Direct outbound Nameserver")}
primary={t("settings.modals.dns.fields.directNameserver.label")}
secondary={t(
"settings.modals.dns.fields.directNameserver.description",
)}
/>
<TextField
fullWidth
@@ -885,8 +903,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fake IP Filter")}
secondary={t("Domains that skip fake IP resolution")}
primary={t("settings.modals.dns.fields.fakeIpFilter.label")}
secondary={t(
"settings.modals.dns.fields.fakeIpFilter.description",
)}
/>
<TextField
fullWidth
@@ -902,8 +922,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Nameserver Policy")}
secondary={t("Domain-specific DNS server")}
primary={t("settings.modals.dns.fields.nameserverPolicy.label")}
secondary={t(
"settings.modals.dns.fields.nameserverPolicy.description",
)}
/>
<TextField
fullWidth
@@ -921,13 +943,15 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="subtitle2"
sx={{ mt: 2, mb: 1, fontWeight: "bold" }}
>
{t("Fallback Filter Settings")}
{t("settings.modals.dns.sections.fallbackFilter")}
</Typography>
<Item>
<ListItemText
primary={t("GeoIP Filtering")}
secondary={t("Enable GeoIP filtering for fallback")}
primary={t("settings.modals.dns.fields.geoipFiltering.label")}
secondary={t(
"settings.modals.dns.fields.geoipFiltering.description",
)}
/>
<Switch
edge="end"
@@ -937,7 +961,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Item>
<Item>
<ListItemText primary={t("GeoIP Code")} />
<ListItemText primary={t("settings.modals.dns.fields.geoipCode")} />
<TextField
size="small"
autoComplete="off"
@@ -950,8 +974,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fallback IP CIDR")}
secondary={t("IP CIDRs not using fallback servers")}
primary={t("settings.modals.dns.fields.fallbackIpCidr.label")}
secondary={t(
"settings.modals.dns.fields.fallbackIpCidr.description",
)}
/>
<TextField
fullWidth
@@ -967,8 +993,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fallback Domain")}
secondary={t("Domains using fallback servers")}
primary={t("settings.modals.dns.fields.fallbackDomain.label")}
secondary={t(
"settings.modals.dns.fields.fallbackDomain.description",
)}
/>
<TextField
fullWidth
@@ -987,13 +1015,13 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="subtitle1"
sx={{ mt: 3, mb: 0, fontWeight: "bold" }}
>
{t("Hosts Settings")}
{t("settings.modals.dns.sections.hosts")}
</Typography>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Hosts")}
secondary={t("Custom domain to IP or domain mapping")}
primary={t("settings.modals.dns.fields.hosts.label")}
secondary={t("settings.modals.dns.fields.hosts.description")}
/>
<TextField
fullWidth

View File

@@ -140,10 +140,12 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
manual: true,
onSuccess: () => {
setOpen(false);
showNotice("success", t("Configuration saved successfully"));
showNotice.success(
"shared.feedback.notifications.common.saveSuccess",
);
},
onError: () => {
showNotice("error", t("Failed to save configuration"));
showNotice.error("shared.feedback.notifications.common.saveFailed");
},
},
);
@@ -181,10 +183,10 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
return (
<BaseDialog
open={open}
title={t("External Cors Configuration")}
title={t("settings.sections.externalCors.title")}
contentSx={{ width: 500 }}
okBtn={loading ? t("Saving...") : t("Save")}
cancelBtn={t("Cancel")}
okBtn={loading ? t("shared.statuses.saving") : t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={handleSave}
@@ -198,7 +200,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
width="100%"
>
<span style={{ fontWeight: "normal" }}>
{t("Allow private network access")}
{t("settings.sections.externalCors.fields.allowPrivateNetwork")}
</span>
<Switch
edge="end"
@@ -218,7 +220,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
<ListItem sx={{ padding: "8px 0" }}>
<div style={{ width: "100%" }}>
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
{t("Allowed Origins")}
{t("settings.sections.externalCors.fields.allowedOrigins")}
</div>
{originEntries.map(({ origin, index, key }) => (
<div
@@ -235,7 +237,9 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
sx={{ fontSize: 14, marginRight: 2 }}
value={origin}
onChange={(e) => handleUpdateOrigin(index, e.target.value)}
placeholder={t("Please enter a valid url")}
placeholder={t(
"settings.sections.externalCors.placeholders.origin",
)}
inputProps={{ style: { fontSize: 14 } }}
/>
<Button
@@ -256,7 +260,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
onClick={handleAddOrigin}
sx={addButtonStyle}
>
{t("Add")}
{t("settings.sections.externalCors.actions.add")}
</Button>
<div
@@ -270,7 +274,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
<div
style={{ color: "#666", fontSize: 12, fontStyle: "italic" }}
>
{t("Always included origins: {{urls}}", {
{t("settings.sections.externalCors.messages.alwaysIncluded", {
urls: DEV_URLS.join(", "),
})}
</div>

View File

@@ -102,7 +102,7 @@ export const HotkeyInput = (props: Props) => {
<IconButton
size="small"
title={t("Delete")}
title={t("shared.actions.delete")}
color="inherit"
onClick={() => {
onChange([]);

View File

@@ -24,7 +24,19 @@ const HOTKEY_FUNC = [
"toggle_system_proxy",
"toggle_tun_mode",
"entry_lightweight_mode",
];
] as const;
const HOTKEY_FUNC_LABELS: Record<(typeof HOTKEY_FUNC)[number], string> = {
open_or_close_dashboard:
"settings.modals.hotkey.functions.openOrCloseDashboard",
clash_mode_rule: "settings.modals.hotkey.functions.rule",
clash_mode_global: "settings.modals.hotkey.functions.global",
clash_mode_direct: "settings.modals.hotkey.functions.direct",
toggle_system_proxy: "settings.modals.hotkey.functions.toggleSystemProxy",
toggle_tun_mode: "settings.modals.hotkey.functions.toggleTunMode",
entry_lightweight_mode:
"settings.modals.hotkey.functions.entryLightweightMode",
};
export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
@@ -81,24 +93,26 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
enable_global_hotkey: enableGlobalHotkey,
});
setOpen(false);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
return (
<BaseDialog
open={open}
title={t("Hotkey Setting")}
title={t("settings.modals.hotkey.title")}
contentSx={{ width: 450, maxHeight: 380 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<ItemWrapper style={{ marginBottom: 16 }}>
<Typography>{t("Enable Global Hotkey")}</Typography>
<Typography>
{t("settings.modals.hotkey.toggles.enableGlobal")}
</Typography>
<Switch
edge="end"
checked={enableGlobalHotkey}
@@ -108,7 +122,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
{HOTKEY_FUNC.map((func) => (
<ItemWrapper key={func}>
<Typography>{t(func)}</Typography>
<Typography>{t(HOTKEY_FUNC_LABELS[func])}</Typography>
<HotkeyInput
value={hotkeyMap[func] ?? []}
onChange={(v) => setHotkeyMap((m) => ({ ...m, [func]: v }))}

View File

@@ -104,7 +104,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
const onSwitchFormat = (_e: any, value: boolean) => value;
const onError = (err: any) => {
showNotice("error", err.message || err.toString());
showNotice.error(err);
};
const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
@@ -113,16 +113,20 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
return (
<BaseDialog
open={open}
title={t("Layout Setting")}
title={t("settings.components.verge.layout.title")}
contentSx={{ width: 450 }}
disableOk
cancelBtn={t("Close")}
cancelBtn={t("shared.actions.close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<List>
<Item>
<ListItemText primary={t("Prefer System Titlebar")} />
<ListItemText
primary={t(
"settings.components.verge.layout.fields.preferSystemTitlebar",
)}
/>
<GuardState
value={decorated}
valueProps="checked"
@@ -137,7 +141,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
<Item>
<ListItemText primary={t("Traffic Graph")} />
<ListItemText
primary={t("settings.components.verge.layout.fields.trafficGraph")}
/>
<GuardState
value={verge?.traffic_graph ?? true}
valueProps="checked"
@@ -151,7 +157,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
<Item>
<ListItemText primary={t("Memory Usage")} />
<ListItemText
primary={t("settings.components.verge.layout.fields.memoryUsage")}
/>
<GuardState
value={verge?.enable_memory_usage ?? true}
valueProps="checked"
@@ -165,7 +173,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
<Item>
<ListItemText primary={t("Proxy Group Icon")} />
<ListItemText
primary={t(
"settings.components.verge.layout.fields.proxyGroupIcon",
)}
/>
<GuardState
value={verge?.enable_group_icon ?? true}
valueProps="checked"
@@ -182,9 +194,13 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
<ListItemText
primary={
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
<span>{t("Hover Jump Navigator")}</span>
<span>
{t("settings.components.verge.layout.fields.hoverNavigator")}
</span>
<TooltipIcon
title={t("Hover Jump Navigator Info")}
title={t(
"settings.components.verge.layout.tooltips.hoverNavigator",
)}
sx={{ opacity: "0.7" }}
/>
</Box>
@@ -206,9 +222,15 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
<ListItemText
primary={
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
<span>{t("Hover Jump Navigator Delay")}</span>
<span>
{t(
"settings.components.verge.layout.fields.hoverNavigatorDelay",
)}
</span>
<TooltipIcon
title={t("Hover Jump Navigator Delay Info")}
title={t(
"settings.components.verge.layout.tooltips.hoverNavigatorDelay",
)}
sx={{ opacity: "0.7" }}
/>
</Box>
@@ -241,7 +263,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
input: {
endAdornment: (
<InputAdornment position="end">
{t("millis")}
{t("shared.units.milliseconds")}
</InputAdornment>
),
},
@@ -256,7 +278,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
<Item>
<ListItemText primary={t("Nav Icon")} />
<ListItemText
primary={t("settings.components.verge.layout.fields.navIcon")}
/>
<GuardState
value={verge?.menu_icon ?? "monochrome"}
onCatch={onError}
@@ -265,16 +289,24 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
onGuard={(value) => patchVerge({ menu_icon: value })}
>
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
<MenuItem value="monochrome">{t("Monochrome")}</MenuItem>
<MenuItem value="colorful">{t("Colorful")}</MenuItem>
<MenuItem value="disable">{t("Disable")}</MenuItem>
<MenuItem value="monochrome">
{t("settings.components.verge.layout.options.icon.monochrome")}
</MenuItem>
<MenuItem value="colorful">
{t("settings.components.verge.layout.options.icon.colorful")}
</MenuItem>
<MenuItem value="disable">
{t("settings.components.verge.layout.options.icon.disable")}
</MenuItem>
</Select>
</GuardState>
</Item>
{OS === "macos" && (
<Item>
<ListItemText primary={t("Tray Icon")} />
<ListItemText
primary={t("settings.components.verge.layout.fields.trayIcon")}
/>
<GuardState
value={verge?.tray_icon ?? "monochrome"}
onCatch={onError}
@@ -286,15 +318,21 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
size="small"
sx={{ width: 140, "> div": { py: "7.5px" } }}
>
<MenuItem value="monochrome">{t("Monochrome")}</MenuItem>
<MenuItem value="colorful">{t("Colorful")}</MenuItem>
<MenuItem value="monochrome">
{t(
"settings.components.verge.layout.options.icon.monochrome",
)}
</MenuItem>
<MenuItem value="colorful">
{t("settings.components.verge.layout.options.icon.colorful")}
</MenuItem>
</Select>
</GuardState>
</Item>
)}
{/* {OS === "macos" && (
<Item>
<ListItemText primary={t("Enable Tray Speed")} />
<ListItemText primary={t("settings.components.verge.layout.fields.enableTraySpeed")} />
<GuardState
value={verge?.enable_tray_speed ?? false}
valueProps="checked"
@@ -309,7 +347,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
)} */}
{/* {OS === "macos" && (
<Item>
<ListItemText primary={t("Enable Tray Icon")} />
<ListItemText primary={t("settings.components.verge.layout.fields.enableTrayIcon")} />
<GuardState
value={
verge?.enable_tray_icon === false &&
@@ -328,7 +366,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
)} */}
<Item>
<ListItemText primary={t("Show Proxy Groups Inline")} />
<ListItemText
primary={t(
"settings.components.verge.layout.fields.showProxyGroupsInline",
)}
/>
<GuardState
value={verge?.tray_inline_proxy_groups ?? false}
valueProps="checked"
@@ -342,7 +384,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
<Item>
<ListItemText primary={t("Common Tray Icon")} />
<ListItemText
primary={t(
"settings.components.verge.layout.fields.commonTrayIcon",
)}
/>
<GuardState
value={verge?.common_tray_icon}
onCatch={onError}
@@ -384,13 +430,19 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
}
}}
>
{verge?.common_tray_icon ? t("Clear") : t("Browse")}
{verge?.common_tray_icon
? t("shared.actions.clear")
: t("settings.components.verge.basic.actions.browse")}
</Button>
</GuardState>
</Item>
<Item>
<ListItemText primary={t("System Proxy Tray Icon")} />
<ListItemText
primary={t(
"settings.components.verge.layout.fields.systemProxyTrayIcon",
)}
/>
<GuardState
value={verge?.sysproxy_tray_icon}
onCatch={onError}
@@ -430,13 +482,17 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
}
}}
>
{verge?.sysproxy_tray_icon ? t("Clear") : t("Browse")}
{verge?.sysproxy_tray_icon
? t("shared.actions.clear")
: t("settings.components.verge.basic.actions.browse")}
</Button>
</GuardState>
</Item>
<Item>
<ListItemText primary={t("Tun Tray Icon")} />
<ListItemText
primary={t("settings.components.verge.layout.fields.tunTrayIcon")}
/>
<GuardState
value={verge?.tun_tray_icon}
onCatch={onError}
@@ -474,7 +530,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
}
}}
>
{verge?.tun_tray_icon ? t("Clear") : t("Browse")}
{verge?.tun_tray_icon
? t("shared.actions.clear")
: t("settings.components.verge.basic.actions.browse")}
</Button>
</GuardState>
</Item>

View File

@@ -45,25 +45,27 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
auto_light_weight_minutes: values.autoEnterLiteModeDelay,
});
setOpen(false);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
return (
<BaseDialog
open={open}
title={t("LightWeight Mode Settings")}
title={t("settings.modals.liteMode.title")}
contentSx={{ width: 450 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Enter LightWeight Mode Now")} />
<ListItemText
primary={t("settings.modals.liteMode.actions.enterNow")}
/>
<Typography
variant="button"
sx={{
@@ -73,17 +75,17 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
}}
onClick={async () => await entry_lightweight_mode()}
>
{t("Enable")}
{t("shared.actions.enable")}
</Typography>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Auto Enter LightWeight Mode")}
primary={t("settings.modals.liteMode.toggles.autoEnter")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon
title={t("Auto Enter LightWeight Mode Info")}
title={t("settings.modals.liteMode.tooltips.autoEnter")}
sx={{ opacity: "0.7" }}
/>
<Switch
@@ -99,7 +101,9 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
{values.autoEnterLiteMode && (
<>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Enter LightWeight Mode Delay")} />
<ListItemText
primary={t("settings.modals.liteMode.fields.delay")}
/>
<TextField
autoComplete="off"
size="small"
@@ -119,7 +123,7 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
input: {
endAdornment: (
<InputAdornment position="end">
{t("mins")}
{t("shared.units.minutes")}
</InputAdornment>
),
},
@@ -133,10 +137,9 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
color="text.secondary"
sx={{ fontStyle: "italic" }}
>
{t(
"When closing the window, LightWeight Mode will be automatically activated after _n minutes",
{ n: values.autoEnterLiteModeDelay },
)}
{t("settings.modals.liteMode.messages.autoEnterHint", {
n: values.autoEnterLiteModeDelay,
})}
</Typography>
</ListItem>
</>

View File

@@ -20,11 +20,13 @@ export const LocalBackupActions = memo(
try {
setLoading(true);
await createLocalBackup();
showNotice("success", t("Local Backup Created"));
showNotice.success(
"settings.modals.backup.messages.localBackupCreated",
);
await onBackupSuccess();
} catch (error) {
console.error(error);
showNotice("error", t("Local Backup Failed"));
showNotice.error("settings.modals.backup.messages.localBackupFailed");
} finally {
setLoading(false);
}
@@ -43,7 +45,7 @@ export const LocalBackupActions = memo(
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 9 }}>
<Typography variant="body2" color="text.secondary">
{t("Local Backup Info")}
{t("settings.modals.backup.fields.info")}
</Typography>
</Grid>
<Grid size={{ xs: 12, sm: 3 }}>
@@ -60,7 +62,7 @@ export const LocalBackupActions = memo(
type="button"
size="large"
>
{t("Backup")}
{t("settings.modals.backup.actions.backup")}
</Button>
<Button
variant="outlined"
@@ -68,7 +70,7 @@ export const LocalBackupActions = memo(
type="button"
size="large"
>
{t("Refresh")}
{t("shared.actions.refresh")}
</Button>
</Stack>
</Grid>

View File

@@ -69,25 +69,27 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
auto_log_clean: values.autoLogClean as any,
});
setOpen(false);
} catch (err: any) {
showNotice("error", err.toString());
} catch (err) {
showNotice.error(err);
}
});
return (
<BaseDialog
open={open}
title={t("Miscellaneous")}
title={t("settings.modals.misc.title")}
contentSx={{ width: 450 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("App Log Level")} />
<ListItemText
primary={t("settings.modals.misc.fields.appLogLevel")}
/>
<Select
size="small"
sx={{ width: 100, "> div": { py: "7.5px" } }}
@@ -109,7 +111,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("App Log Max Size")}
primary={t("settings.modals.misc.fields.appLogMaxSize")}
sx={{ maxWidth: "fit-content" }}
/>
<TextField
@@ -130,7 +132,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">{t("KB")}</InputAdornment>
<InputAdornment position="end">
{t("shared.units.kilobytes")}
</InputAdornment>
),
},
}}
@@ -139,7 +143,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("App Log Max Count")}
primary={t("settings.modals.misc.fields.appLogMaxCount")}
sx={{ maxWidth: "fit-content" }}
/>
<TextField
@@ -160,7 +164,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">{t("Files")}</InputAdornment>
<InputAdornment position="end">
{t("shared.units.files")}
</InputAdornment>
),
},
}}
@@ -169,11 +175,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Auto Close Connections")}
primary={t("settings.modals.misc.fields.autoCloseConnections")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon
title={t("Auto Close Connections Info")}
title={t("settings.modals.misc.tooltips.autoCloseConnections")}
sx={{ opacity: "0.7" }}
/>
<Switch
@@ -187,7 +193,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Check Update")} />
<ListItemText
primary={t("settings.modals.misc.fields.autoCheckUpdate")}
/>
<Switch
edge="end"
checked={values.autoCheckUpdate}
@@ -199,11 +207,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Enable Builtin Enhanced")}
primary={t("settings.modals.misc.fields.enableBuiltinEnhanced")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon
title={t("Enable Builtin Enhanced Info")}
title={t("settings.modals.misc.tooltips.enableBuiltinEnhanced")}
sx={{ opacity: "0.7" }}
/>
<Switch
@@ -217,7 +225,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Proxy Layout Columns")} />
<ListItemText
primary={t("settings.modals.misc.fields.proxyLayoutColumns")}
/>
<Select
size="small"
sx={{ width: 160, "> div": { py: "7.5px" } }}
@@ -230,7 +240,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
}
>
<MenuItem value={6} key={6}>
{t("Auto Columns")}
{t("settings.modals.misc.options.proxyLayoutColumns.auto")}
</MenuItem>
{[1, 2, 3, 4, 5].map((i) => (
<MenuItem value={i} key={i}>
@@ -241,7 +251,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Log Clean")} />
<ListItemText
primary={t("settings.modals.misc.fields.autoLogClean")}
/>
<Select
size="small"
sx={{ width: 160, "> div": { py: "7.5px" } }}
@@ -255,11 +267,34 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
>
{/* 1: 1天, 2: 7天, 3: 30天, 4: 90天*/}
{[
{ key: t("Never Clean"), value: 0 },
{ key: t("Retain _n Days", { n: 1 }), value: 1 },
{ key: t("Retain _n Days", { n: 7 }), value: 2 },
{ key: t("Retain _n Days", { n: 30 }), value: 3 },
{ key: t("Retain _n Days", { n: 90 }), value: 4 },
{
key: t("settings.modals.misc.options.autoLogClean.never"),
value: 0,
},
{
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
n: 1,
}),
value: 1,
},
{
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
n: 7,
}),
value: 2,
},
{
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
n: 30,
}),
value: 3,
},
{
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
n: 90,
}),
value: 4,
},
].map((i) => (
<MenuItem key={i.value} value={i.value}>
{i.key}
@@ -270,11 +305,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Auto Delay Detection")}
primary={t("settings.modals.misc.fields.autoDelayDetection")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon
title={t("Auto Delay Detection Info")}
title={t("settings.modals.misc.tooltips.autoDelayDetection")}
sx={{ opacity: "0.7" }}
/>
<Switch
@@ -289,11 +324,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Default Latency Test")}
primary={t("settings.modals.misc.fields.defaultLatencyTest")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon
title={t("Default Latency Test Info")}
title={t("settings.modals.misc.tooltips.defaultLatencyTest")}
sx={{ opacity: "0.7" }}
/>
<TextField
@@ -312,7 +347,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Default Latency Timeout")} />
<ListItemText
primary={t("settings.modals.misc.fields.defaultLatencyTimeout")}
/>
<TextField
autoComplete="new-password"
size="small"
@@ -332,7 +369,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">{t("millis")}</InputAdornment>
<InputAdornment position="end">
{t("shared.units.milliseconds")}
</InputAdornment>
),
},
}}

View File

@@ -35,7 +35,7 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
open={open}
title={
<Box display="flex" justifyContent="space-between">
{t("Network Interface")}
{t("settings.modals.networkInterface.title")}
<Box>
<Button
variant="contained"
@@ -51,7 +51,7 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
}
contentSx={{ width: 450 }}
disableOk
cancelBtn={t("Close")}
cancelBtn={t("shared.actions.close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
@@ -66,13 +66,17 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
address.V4 && (
<AddressDisplay
key={address.V4.ip}
label={t("Ip Address")}
label={t(
"settings.modals.networkInterface.fields.ipAddress",
)}
content={address.V4.ip}
/>
),
)}
<AddressDisplay
label={t("Mac Address")}
label={t(
"settings.modals.networkInterface.fields.macAddress",
)}
content={item.mac_addr ?? ""}
/>
</>
@@ -84,13 +88,17 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
address.V6 && (
<AddressDisplay
key={address.V6.ip}
label={t("Ip Address")}
label={t(
"settings.modals.networkInterface.fields.ipAddress",
)}
content={address.V6.ip}
/>
),
)}
<AddressDisplay
label={t("Mac Address")}
label={t(
"settings.modals.networkInterface.fields.macAddress",
)}
content={item.mac_addr ?? ""}
/>
</>
@@ -109,8 +117,6 @@ const AddressDisplay = ({
label: string;
content: string;
}) => {
const { t } = useTranslation();
return (
<Box
sx={{
@@ -135,7 +141,9 @@ const AddressDisplay = ({
size="small"
onClick={async () => {
await writeText(content);
showNotice("success", t("Copy Success"));
showNotice.success(
"shared.feedback.notifications.common.copySuccess",
);
}}
>
<ContentCopyRounded sx={{ fontSize: "18px" }} />

View File

@@ -21,13 +21,15 @@ export const PasswordInput = (props: Props) => {
return (
<Dialog open={true} maxWidth="xs" fullWidth>
<DialogTitle>{t("Please enter your root password")}</DialogTitle>
<DialogTitle>
{t("settings.modals.password.prompts.enterRoot")}
</DialogTitle>
<DialogContent>
<TextField
sx={{ mt: 1 }}
autoFocus
label={t("Password")}
label={t("shared.labels.password")}
fullWidth
size="small"
type="password"
@@ -42,7 +44,7 @@ export const PasswordInput = (props: Props) => {
onClick={async () => await onConfirm(passwd)}
variant="contained"
>
{t("Confirm")}
{t("shared.actions.confirm")}
</Button>
</DialogActions>
</Dialog>

View File

@@ -160,8 +160,8 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
mutate("getAutotemProxy"),
]);
}
} catch (err: any) {
showNotice("error", err.toString());
} catch (err) {
showNotice.error(err);
}
};
@@ -277,14 +277,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const onSave = useLockFn(async () => {
if (value.duration < 1) {
showNotice(
"error",
t("Proxy Daemon Duration Cannot be Less than 1 Second"),
);
showNotice.error("settings.modals.sysproxy.messages.durationTooShort");
return;
}
if (value.bypass && !validReg.test(value.bypass)) {
showNotice("error", t("Invalid Bypass Format"));
showNotice.error("settings.modals.sysproxy.messages.invalidBypass");
return;
}
@@ -301,7 +298,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
!ipv6Regex.test(value.proxy_host) &&
!hostnameRegex.test(value.proxy_host)
) {
showNotice("error", t("Invalid Proxy Host Format"));
showNotice.error("settings.modals.sysproxy.messages.invalidProxyHost");
return;
}
@@ -401,10 +398,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
console.warn("代理状态更新失败:", err);
}
}, 50);
} catch (err: any) {
} catch (err) {
console.error("配置保存失败:", err);
mutateVerge();
showNotice("error", err.toString());
showNotice.error(err);
// setOpen(true);
}
});
@@ -413,10 +410,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
return (
<BaseDialog
open={open}
title={t("System Proxy Setting")}
title={t("settings.modals.sysproxy.title")}
contentSx={{ width: 450, maxHeight: 565 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
@@ -424,28 +421,37 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
disableOk={saving}
>
<List>
<BaseFieldset label={t("Current System Proxy")} padding="15px 10px">
<BaseFieldset
label={t("settings.modals.sysproxy.fieldsets.currentStatus")}
padding="15px 10px"
>
<FlexBox>
<Typography className="label">{t("Enable status")}</Typography>
<Typography className="label">
{t("settings.modals.sysproxy.fields.enableStatus")}
</Typography>
<Typography className="value">
{value.pac
? autoproxy?.enable
? t("Enabled")
: t("Disabled")
? t("shared.statuses.enabled")
: t("shared.statuses.disabled")
: sysproxy?.enable
? t("Enabled")
: t("Disabled")}
? t("shared.statuses.enabled")
: t("shared.statuses.disabled")}
</Typography>
</FlexBox>
{!value.pac && (
<FlexBox>
<Typography className="label">{t("Server Addr")}</Typography>
<Typography className="label">
{t("settings.modals.sysproxy.fields.serverAddr")}
</Typography>
<Typography className="value">{getSystemProxyAddress}</Typography>
</FlexBox>
)}
{value.pac && (
<FlexBox>
<Typography className="label">{t("PAC URL")}</Typography>
<Typography className="label">
{t("settings.modals.sysproxy.fields.pacUrl")}
</Typography>
<Typography className="value">
{getCurrentPacUrl || "-"}
</Typography>
@@ -453,7 +459,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
)}
</BaseFieldset>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Proxy Host")} />
<ListItemText
primary={t("settings.modals.sysproxy.fields.proxyHost")}
/>
<Autocomplete
size="small"
sx={{ width: 150 }}
@@ -478,7 +486,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Use PAC Mode")} />
<ListItemText
primary={t("settings.modals.sysproxy.fields.usePacMode")}
/>
<Switch
edge="end"
disabled={!enabled}
@@ -489,10 +499,13 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Proxy Guard")}
primary={t("settings.modals.sysproxy.fields.proxyGuard")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Proxy Guard Info")} sx={{ opacity: "0.7" }} />
<TooltipIcon
title={t("settings.modals.sysproxy.tooltips.proxyGuard")}
sx={{ opacity: "0.7" }}
/>
<Switch
edge="end"
disabled={!enabled}
@@ -503,7 +516,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Guard Duration")} />
<ListItemText
primary={t("settings.modals.sysproxy.fields.guardDuration")}
/>
<TextField
disabled={!enabled}
size="small"
@@ -524,7 +539,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
{!value.pac && (
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Always use Default Bypass")} />
<ListItemText
primary={t(
"settings.modals.sysproxy.fields.alwaysUseDefaultBypass",
)}
/>
<Switch
edge="end"
disabled={!enabled}
@@ -543,7 +562,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
{!value.pac && !value.use_default && (
<>
<ListItemText primary={t("Proxy Bypass")} />
<ListItemText
primary={t("settings.modals.sysproxy.fields.proxyBypass")}
/>
<TextField
error={value.bypass ? !validReg.test(value.bypass) : false}
disabled={!enabled}
@@ -561,7 +582,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
{!value.pac && value.use_default && (
<>
<ListItemText primary={t("Bypass")} />
<ListItemText
primary={t("settings.modals.sysproxy.fields.bypass")}
/>
<FlexBox>
<TextField
disabled={true}
@@ -578,7 +601,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
{value.pac && (
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
<ListItemText
primary={t("PAC Script Content")}
primary={t("settings.modals.sysproxy.fields.pacScriptContent")}
sx={{ padding: "3px 0" }}
/>
<Button
@@ -588,12 +611,12 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
setEditorOpen(true);
}}
>
{t("Edit")} PAC
{t("settings.modals.sysproxy.actions.editPac")}
</Button>
{editorOpen && (
<EditorViewer
open={true}
title={`${t("Edit")} PAC`}
title={t("settings.modals.sysproxy.actions.editPac")}
initialData={Promise.resolve(value.pac_content ?? "")}
language="javascript"
onSave={(_prev, curr) => {

View File

@@ -23,7 +23,7 @@ export const ThemeModeSwitch = (props: Props) => {
onClick={() => onChange?.(mode)}
sx={{ textTransform: "capitalize" }}
>
{t(`theme.${mode}`)}
{t(`settings.sections.appearance.${mode}`)}
</Button>
))}
</ButtonGroup>

View File

@@ -9,7 +9,7 @@ import {
useTheme,
} from "@mui/material";
import { useLockFn } from "ahooks";
import { useImperativeHandle, useState } from "react";
import { useImperativeHandle, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { BaseDialog, DialogRef } from "@/components/base";
@@ -50,8 +50,8 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
try {
await patchVerge({ theme_setting: theme });
setOpen(false);
} catch (err: any) {
showNotice("error", err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -62,9 +62,48 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
type ThemeKey = keyof typeof theme & keyof typeof defaultTheme;
const renderItem = (label: string, key: ThemeKey) => {
const fieldDefinitions: Array<{ labelKey: string; key: ThemeKey }> = useMemo(
() => [
{
labelKey: "settings.components.verge.theme.fields.primaryColor",
key: "primary_color",
},
{
labelKey: "settings.components.verge.theme.fields.secondaryColor",
key: "secondary_color",
},
{
labelKey: "settings.components.verge.theme.fields.primaryText",
key: "primary_text",
},
{
labelKey: "settings.components.verge.theme.fields.secondaryText",
key: "secondary_text",
},
{
labelKey: "settings.components.verge.theme.fields.infoColor",
key: "info_color",
},
{
labelKey: "settings.components.verge.theme.fields.warningColor",
key: "warning_color",
},
{
labelKey: "settings.components.verge.theme.fields.errorColor",
key: "error_color",
},
{
labelKey: "settings.components.verge.theme.fields.successColor",
key: "success_color",
},
],
[],
);
const renderItem = (labelKey: string, key: ThemeKey) => {
const label = t(labelKey);
return (
<Item>
<Item key={key}>
<ListItemText primary={label} />
<Round sx={{ background: theme[key] || dt[key] }} />
<TextField
@@ -81,33 +120,21 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
return (
<BaseDialog
open={open}
title={t("Theme Setting")}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
title={t("settings.components.verge.theme.title")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
contentSx={{ width: 400, maxHeight: 505, overflow: "auto", pb: 0 }}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List sx={{ pt: 0 }}>
{renderItem(t("Primary Color"), "primary_color")}
{renderItem(t("Secondary Color"), "secondary_color")}
{renderItem(t("Primary Text"), "primary_text")}
{renderItem(t("Secondary Text"), "secondary_text")}
{renderItem(t("Info Color"), "info_color")}
{renderItem(t("Warning Color"), "warning_color")}
{renderItem(t("Error Color"), "error_color")}
{renderItem(t("Success Color"), "success_color")}
{fieldDefinitions.map((field) => renderItem(field.labelKey, field.key))}
<Item>
<ListItemText primary={t("Font Family")} />
<ListItemText
primary={t("settings.components.verge.theme.fields.fontFamily")}
/>
<TextField
{...textProps}
value={theme.font_family ?? ""}
@@ -116,7 +143,9 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
/>
</Item>
<Item>
<ListItemText primary={t("CSS Injection")} />
<ListItemText
primary={t("settings.components.verge.theme.fields.cssInjection")}
/>
<Button
startIcon={<EditRounded />}
variant="outlined"
@@ -124,12 +153,12 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
setEditorOpen(true);
}}
>
{t("Edit")} CSS
{t("settings.components.verge.theme.actions.editCss")}
</Button>
{editorOpen && (
<EditorViewer
open={true}
title={`${t("Edit")} CSS`}
title={t("settings.components.verge.theme.dialogs.editCssTitle")}
initialData={Promise.resolve(theme.css_injection ?? "")}
language="css"
onSave={(_prev, curr) => {

View File

@@ -80,13 +80,13 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
);
try {
await enhanceProfiles();
showNotice("success", t("Settings Applied"));
showNotice.success("settings.modals.tun.messages.applied");
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
setOpen(false);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
});
@@ -95,7 +95,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
open={open}
title={
<Box display="flex" justifyContent="space-between" gap={1}>
<Typography variant="h6">{t("Tun Mode")}</Typography>
<Typography variant="h6">{t("settings.modals.tun.title")}</Typography>
<Button
variant="outlined"
size="small"
@@ -128,20 +128,20 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
);
}}
>
{t("Reset to Default")}
{t("shared.actions.resetToDefault")}
</Button>
</Box>
}
contentSx={{ width: 450 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Stack")} />
<ListItemText primary={t("settings.modals.tun.fields.stack")} />
<StackModeSwitch
value={values.stack}
onChange={(value) => {
@@ -154,7 +154,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Device")} />
<ListItemText primary={t("settings.modals.tun.fields.device")} />
<TextField
autoComplete="new-password"
size="small"
@@ -171,7 +171,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Route")} />
<ListItemText primary={t("settings.modals.tun.fields.autoRoute")} />
<Switch
edge="end"
checked={values.autoRoute}
@@ -180,7 +180,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Strict Route")} />
<ListItemText primary={t("settings.modals.tun.fields.strictRoute")} />
<Switch
edge="end"
checked={values.strictRoute}
@@ -189,7 +189,9 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Detect Interface")} />
<ListItemText
primary={t("settings.modals.tun.fields.autoDetectInterface")}
/>
<Switch
edge="end"
checked={values.autoDetectInterface}
@@ -200,7 +202,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("DNS Hijack")} />
<ListItemText primary={t("settings.modals.tun.fields.dnsHijack")} />
<TextField
autoComplete="new-password"
size="small"
@@ -209,7 +211,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
spellCheck="false"
sx={{ width: 250 }}
value={values.dnsHijack.join(",")}
placeholder="Please use , to separate multiple DNS servers"
placeholder={t("settings.modals.tun.tooltips.dnsHijack")}
onChange={(e) =>
setValues((v) => ({ ...v, dnsHijack: e.target.value.split(",") }))
}
@@ -217,7 +219,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("MTU")} />
<ListItemText primary={t("settings.modals.tun.fields.mtu")} />
<TextField
autoComplete="new-password"
size="small"

Some files were not shown because too many files have changed in this diff Show More