From cfdfec8fe61726ad2480ba8980e55329e1c8d597 Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Thu, 6 Nov 2025 17:36:07 +0800 Subject: [PATCH] feat(i18n): replace ad-hoc loader with rust-i18n backend bundles --- docs/CONTRIBUTING_i18n.md | 150 +++++++++++---------- src-tauri/Cargo.lock | 198 ++++++++++++++++++++++++++-- src-tauri/Cargo.toml | 1 + src-tauri/locales/ar.yml | 52 ++++++++ src-tauri/locales/de.yml | 52 ++++++++ src-tauri/locales/en.yml | 52 ++++++++ src-tauri/locales/es.yml | 52 ++++++++ src-tauri/locales/fa.yml | 52 ++++++++ src-tauri/locales/id.yml | 52 ++++++++ src-tauri/locales/jp.yml | 52 ++++++++ src-tauri/locales/ko.yml | 52 ++++++++ src-tauri/locales/ru.yml | 52 ++++++++ src-tauri/locales/tr.yml | 52 ++++++++ src-tauri/locales/tt.yml | 52 ++++++++ src-tauri/locales/zh.yml | 52 ++++++++ src-tauri/locales/zhtw.yml | 52 ++++++++ src-tauri/src/cmd/service.rs | 7 +- src-tauri/src/core/service.rs | 12 +- src-tauri/src/core/tray/menu_def.rs | 62 +++++---- src-tauri/src/core/tray/mod.rs | 16 ++- src-tauri/src/lib.rs | 3 + src-tauri/src/utils/i18n.rs | 138 +++++++------------ src-tauri/src/utils/notification.rs | 53 ++++---- src-tauri/tauri.conf.json | 2 +- 24 files changed, 1064 insertions(+), 254 deletions(-) create mode 100644 src-tauri/locales/ar.yml create mode 100644 src-tauri/locales/de.yml create mode 100644 src-tauri/locales/en.yml create mode 100644 src-tauri/locales/es.yml create mode 100644 src-tauri/locales/fa.yml create mode 100644 src-tauri/locales/id.yml create mode 100644 src-tauri/locales/jp.yml create mode 100644 src-tauri/locales/ko.yml create mode 100644 src-tauri/locales/ru.yml create mode 100644 src-tauri/locales/tr.yml create mode 100644 src-tauri/locales/tt.yml create mode 100644 src-tauri/locales/zh.yml create mode 100644 src-tauri/locales/zhtw.yml diff --git a/docs/CONTRIBUTING_i18n.md b/docs/CONTRIBUTING_i18n.md index de4b1069..466c9837 100644 --- a/docs/CONTRIBUTING_i18n.md +++ b/docs/CONTRIBUTING_i18n.md @@ -1,23 +1,45 @@ # CONTRIBUTING — i18n -Thank you for considering contributing to our localization work — your help is appreciated. +Thank you for contributing to Clash Verge Rev localization! This guide reflects the current project layout and tooling so you can land translation improvements smoothly. ## Quick workflow -- Start small: fix typos, improve phrasing, or refine tone and consistency. -- Use `scripts/cleanup-unused-i18n.mjs` (see below) to keep locale files aligned and free of dead keys. -- 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. +- Focus fixes on the language folders inside `src/locales//` and use `src/locales/en/` as the baseline for key shape and intent. +- Keep pull requests small and open draft PRs early if you would like feedback. +- Run `pnpm format:i18n` to align JSON structure and `pnpm i18n:types` to refresh generated typings before pushing. +- Preview changes with `pnpm dev` (desktop shell) or `pnpm web:dev` (web only) to verify context and layout. +- Report missing context, untranslated UI strings, or script bugs via issues so we can track them. + +## Locale layout + +Every language lives in its own directory: + +``` +src/locales/ + en/ + connections.json + home.json + … + shared.json + tests.json + index.ts + zh/ + … +``` + +- Each JSON file maps to a namespace (`home` → `home.*`, `shared` → `shared.*`, etc.). Keep keys scoped to their file. +- `shared.json` stores reusable copy (buttons, labels, validation messages). Feature-specific content remains in the relevant namespace file (`profiles.json`, `settings.json`, …). +- `index.ts` re-exports a `resources` object that aggregates the namespace JSON. When adding files, mirror the pattern used in `src/locales/en/index.ts`. +- Do **not** edit `src-tauri/resources/locales`; those files are copied from `src/locales` during packaging by `pnpm prebuild`. +- Rust/Tauri uses a separate set of YAML bundles in `src-tauri/locales/` for system tray text and native notifications. Update those when backend-facing strings change. ## Locale maintenance script The repository ships with `scripts/cleanup-unused-i18n.mjs`, a TypeScript-aware analyzer that: -- Scans `src/` and `src-tauri/` for `t("...")` usages (including dynamic prefixes) to identify which locale keys are referenced. -- Reports unused keys per locale and optionally removes them. -- Compares every locale against the baseline (default: `en.json`) to produce missing/extra key lists. -- Aligns locale structure/order with the baseline so diffs stay predictable. +- Scans `src/` and `src-tauri/` for `t("...")` usage (including dynamic prefixes) to determine which keys are referenced. +- Compares locales to the baseline (`en`) to list missing or extra keys. +- Optionally removes unused keys and aligns key ordering/structure. - Emits optional JSON reports for CI or manual review. ### Typical commands @@ -33,89 +55,71 @@ pnpm node scripts/cleanup-unused-i18n.mjs --apply --align pnpm node scripts/cleanup-unused-i18n.mjs --report ./i18n-report.json ``` -Shorthand task runner aliases: +Aliases and flags: - `pnpm format:i18n` → `node scripts/cleanup-unused-i18n.mjs --align --apply` -- `pnpm node scripts/cleanup-unused-i18n.mjs -- --help` — view all flags (`--baseline`, `--src`, `--keep-extra`, `--no-backup`, `--report`, `--apply`, `--align`). +- `pnpm node scripts/cleanup-unused-i18n.mjs -- --help` shows all options (`--baseline`, `--src`, `--keep-extra`, `--no-backup`, `--report`, `--apply`, `--align`, …). -### Recommended steps before submitting translations +### Before submitting translations -1. Run the script in dry-run mode to review unused/missing key output. -2. Apply removals/alignment locally if your changes introduce new keys or delete UI. -3. Inspect `.bak` backups (created by default) when applying changes to confirm nothing important disappeared. -4. For dynamic key patterns, add explicit references or update the whitelist if the script misidentifies usage. +1. Run the script in dry-run mode to inspect missing/unused keys. +2. Apply alignment if you added or removed keys so diffs stay minimal. +3. Review `.bak` backups (generated when `--apply` runs) to ensure important strings were not removed; delete the backups once confirmed. +4. For dynamic keys, add explicit references in code or update the script whitelist so the analyzer recognizes them. -PR checklist +## Typings & runtime integration -- Keep JSON files UTF-8 encoded. -- Follow the repo’s locale file structure and naming conventions. -- Run `pnpm format:i18n` to align with the baseline file 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. +- `pnpm i18n:types` regenerates `src/types/generated/i18n-keys.ts` and `src/types/generated/i18n-resources.ts`. Run it whenever keys change so TypeScript enforces valid usages. +- Supported runtime languages are defined in `src/services/i18n.ts`. Update the `supportedLanguages` array when you add an additional locale. +- The app defaults to Chinese (`zh`) and lazily loads other bundles. If a bundle fails to load, it falls back to `zh`. +- Packing (`pnpm build`, `pnpm prebuild`) copies `src/locales` into `src-tauri/resources/locales` so keep the source tree authoritative. +- Backend (Tauri) strings such as tray menu labels and native notifications use the YAML bundles under `src-tauri/locales/.yml` via `rust-i18n`. Keep the English file (`en.yml`) aligned with the Simplified Chinese semantics and mirror updates across the remaining languages (繁體 `zhtw` translates the Chinese copy; other locales can temporarily duplicate English until translators step in). +- When adding a new language to the backend, create a matching `.yml` in `src-tauri/locales/`, populate the keys used in the existing files, and ensure `src/services/i18n.ts` includes the language code so the frontend can request it. -Notes +## Adding a new language -- The script expects simple top-level JSON key/value maps in each locale file. -- `.bak` snapshots are created automatically when applying fixes; remove them once you confirm the changes. -- Alignment keeps key order stable across locales, which makes reviews easier. +1. Duplicate `src/locales/en/` into `src/locales//` (match the folder name to the language code you intend to serve, e.g. `pt-br`). +2. Translate the JSON files while preserving the key hierarchy. `shared.json` should stay aligned with the baseline. +3. Update the new locale’s `index.ts` to import every JSON namespace (use the English file as reference). +4. Append the language code to `supportedLanguages` in `src/services/i18n.ts`. Adjust `crowdin.yml` if the locale code needs a mapping. +5. Run `pnpm format:i18n`, `pnpm i18n:types`, and optionally `pnpm node scripts/cleanup-unused-i18n.mjs` (dry-run) to verify structure. +6. Execute `pnpm dev` to confirm UI translations load, and `pnpm prebuild` if you want to double-check Tauri resource syncing. -## Locale Key Structure Guidelines +## Authoring guidelines -The locale files now follow a two-namespace layout designed to mirror the React/Rust feature tree: - -- **`shared.*`** — cross-cutting vocabulary (buttons, statuses, validation hints, window chrome, etc.). - - Buckets to prefer: `actions`, `labels`, `statuses`, `messages`, `placeholders`, `units`, `validation`, `window`, `editorModes`. -- Add to `shared` only when the copy is used (or is expected to be reused) by two or more features. Otherwise keep it in the owning feature namespace. -- **`.*`** — route-level or domain-level strings scoped to a single feature. - - Top-level keys mirror folders under `src/pages`, `src/components`, or service domains (`settings`, `proxies`, `profiles`, `home`, `unlock`, `layout`, …). - - Within a feature namespace, prefer consistent buckets like `page`, `sections`, `forms`, `fields`, `actions`, `tooltips`, `notifications`, `errors`, `dialogs`, `tables`, `components`. Choose the minimum depth needed to describe the UI. - -### Authoring guidelines - -1. **Follow the shared/feature split** — before inventing a new key, check whether an equivalent exists under `shared.*`. -2. **Use camelCase leaf keys** — keep names semantic (`systemProxy`, `updateInterval`) and avoid positional names (`item1`, `btn_ok`). -3. **Group by UI responsibility** — for example: - - `settings.dns.fields.listen` - - `settings.dns.dialog.title` - - `settings.dns.sections.general` -4. **Component-specific copy** — nest under `components.` or `dialogs.` to keep implementation-specific strings organized but still discoverable. -5. **Dynamic placeholders** — continue using `{{placeholder}}` syntax and document required params in code when possible. - -### Minimal example +- **Reuse shared vocabulary**: before creating a new label, check `shared.json` (`actions`, `labels`, `statuses`, `placeholders`, `validation`, `window`, `editorModes`, etc.). Only introduce feature-specific copy when it is unique to that area. +- **Keep keys semantic**: use camelCase leaves that describe intent (`systemProxy`, `updateInterval`, `autoRefresh`). Avoid positional keys like `item1` or `dialogTitle2`. +- **Organize by UI responsibility** inside each namespace. Common buckets include: + - `page`, `sections`, `forms`, `fields`, `actions`, `tooltips`, `notifications`, `errors`, `dialogs`, `tables`, `components`, `statuses`. +- **Document dynamic placeholders**: continue using the `{{placeholder}}` syntax and ensure code comments/context explain required parameters. +- **Example structure** (from `src/locales/en/home.json`): ```json { - "shared": { - "actions": { - "save": "Save", - "cancel": "Cancel" + "page": { + "title": "Home", + "tooltips": { + "settings": "Home Settings" } }, - "profiles": { - "page": { - "title": "Profiles", - "actions": { - "import": "Import", - "updateAll": "Update All Profiles" - }, - "notifications": { - "importSuccess": "Profile imported successfully" - } - }, - "components": { - "batchDialog": { - "title": "Batch Operations", - "items": "items" + "components": { + "proxyTun": { + "status": { + "systemProxyEnabled": "System Proxy Enabled" } } } } ``` -Whenever you need a common verb or label, reference `shared.*` directly in the code (`shared.actions.save`, `shared.labels.name`, …) instead of duplicating the copy in a feature namespace. +## Testing & QA -## Feedback & Contributions +- Launch the desktop shell with `pnpm dev` (or `pnpm web:dev` for browser-only checks) to confirm strings display correctly and spacing still works in the UI. +- Run `pnpm test` if you touched code that relies on translations or formatting logic. +- Note uncovered scenarios or language-specific concerns (pluralization, truncated text) in your PR description. -- For tool usage issues or feedback: please open an Issue in this repository 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. +## Feedback & support + +- Open an issue for tooling problems, missing context, or translation bugs so we can track them. +- For localization contributions (translations, fixes, context notes, etc.), submit a PR with screenshots when layout changes might be impacted. +- If you need a second pair of eyes, leave a comment on your PR and the team will follow up. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d433be36..42c0c516 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7cb1cf3d..124f4684 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/locales/ar.yml b/src-tauri/locales/ar.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/ar.yml @@ -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 diff --git a/src-tauri/locales/de.yml b/src-tauri/locales/de.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/de.yml @@ -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 diff --git a/src-tauri/locales/en.yml b/src-tauri/locales/en.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/en.yml @@ -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 diff --git a/src-tauri/locales/es.yml b/src-tauri/locales/es.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/es.yml @@ -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 diff --git a/src-tauri/locales/fa.yml b/src-tauri/locales/fa.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/fa.yml @@ -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 diff --git a/src-tauri/locales/id.yml b/src-tauri/locales/id.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/id.yml @@ -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 diff --git a/src-tauri/locales/jp.yml b/src-tauri/locales/jp.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/jp.yml @@ -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 diff --git a/src-tauri/locales/ko.yml b/src-tauri/locales/ko.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/ko.yml @@ -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 diff --git a/src-tauri/locales/ru.yml b/src-tauri/locales/ru.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/ru.yml @@ -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 diff --git a/src-tauri/locales/tr.yml b/src-tauri/locales/tr.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/tr.yml @@ -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 diff --git a/src-tauri/locales/tt.yml b/src-tauri/locales/tt.yml new file mode 100644 index 00000000..330cf3b5 --- /dev/null +++ b/src-tauri/locales/tt.yml @@ -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 diff --git a/src-tauri/locales/zh.yml b/src-tauri/locales/zh.yml new file mode 100644 index 00000000..10c2e636 --- /dev/null +++ b/src-tauri/locales/zh.yml @@ -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: 订阅 diff --git a/src-tauri/locales/zhtw.yml b/src-tauri/locales/zhtw.yml new file mode 100644 index 00000000..040226b2 --- /dev/null +++ b/src-tauri/locales/zhtw.yml @@ -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: 訂閱 diff --git a/src-tauri/src/cmd/service.rs b/src-tauri/src/cmd/service.rs index 75d1a544..7e5b7d2c 100644 --- a/src-tauri/src/cmd/service.rs +++ b/src-tauri/src/cmd/service.rs @@ -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(()) } diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 28e0dc97..e14e043d 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -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}""# ); diff --git a/src-tauri/src/core/tray/menu_def.rs b/src-tauri/src/core/tray/menu_def.rs index 10fe3835..78c76e6c 100644 --- a/src-tauri/src/core/tray/menu_def.rs +++ b/src-tauri/src/core/tray/menu_def.rs @@ -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 { + 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", } diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index e19491f3..81f732ea 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -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> { 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> = profile_menu_items .iter() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 746df76c..01573ef4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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 = OnceCell::new(); /// Application initialization helper functions mod app_init { diff --git a/src-tauri/src/utils/i18n.rs b/src-tauri/src/utils/i18n.rs index b1c55232..6aefbc84 100644 --- a/src-tauri/src/utils/i18n.rs +++ b/src-tauri/src/utils/i18n.rs @@ -1,43 +1,46 @@ -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>); +fn supported_languages_internal() -> Vec<&'static str> { + rust_i18n::available_locales!() +} -fn get_locales_dir() -> Option { - dirs::app_resources_dir() - .map(|resource_path| resource_path.join("locales")) - .ok() +fn is_supported(language: &str) -> bool { + let normalized = language.to_lowercase(); + supported_languages_internal() + .iter() + .any(|&lang| lang.eq_ignore_ascii_case(&normalized)) +} + +const fn fallback_language() -> &'static str { + DEFAULT_LANGUAGE +} + +fn system_language() -> String { + sys_locale::get_locale() + .map(|locale| locale.to_lowercase()) + .and_then(|locale| locale.split(['_', '-']).next().map(str::to_string)) + .filter(|lang| is_supported(lang)) + .unwrap_or_else(|| fallback_language().to_string()) } pub fn get_supported_languages() -> Vec { - 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 normalized = language.to_lowercase(); + let lang = if is_supported(&normalized) { + normalized + } else { + fallback_language().to_string() + }; + rust_i18n::set_locale(&lang); } pub async fn current_language() -> String { @@ -45,70 +48,19 @@ 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()) + .map(|lang| lang.to_lowercase()) + .filter(|lang| is_supported(lang)) + .unwrap_or_else(system_language) } -static TRANSLATIONS: Lazy> = 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>> { - 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::>(&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 { - 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(¤t_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() } diff --git a/src-tauri/src/utils/notification.rs b/src-tauri/src/utils/notification.rs index f737560e..5370132d 100644 --- a/src-tauri/src/utils/notification.rs +++ b/src-tauri/src/utils/notification.rs @@ -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) -} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8e2c72ce..8dbd1654 100755 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -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",