Compare commits

..

17 Commits

Author SHA1 Message Date
zhom 828c3bb984 chore: version bump 2026-05-04 02:01:36 +04:00
zhom ffe35c1672 chore: rand bump 2026-05-04 02:00:41 +04:00
zhom 4a4cf81255 refactor: don't block ui on clade check 2026-05-04 01:57:42 +04:00
zhom 77be8cadaf feat: vpn manipulation via the api 2026-05-04 01:57:05 +04:00
zhom 3207e4fbd3 chore: pnpm bump 2026-05-02 20:00:05 +04:00
dependabot[bot] c18e9625fd deps(rust)(deps): bump the rust-dependencies group (#331)
Bumps the rust-dependencies group in /src-tauri with 34 updates:

| Package | From | To |
| --- | --- | --- |
| [tauri](https://github.com/tauri-apps/tauri) | `2.10.3` | `2.11.0` |
| [reqwest](https://github.com/seanmonstar/reqwest) | `0.13.2` | `0.13.3` |
| [zip](https://github.com/zip-rs/zip2) | `8.5.1` | `8.6.0` |
| [bzip2](https://github.com/trifectatechfoundation/bzip2-rs) | `0.5.2` | `0.6.1` |
| [maxminddb](https://github.com/oschwald/maxminddb-rust) | `0.27.3` | `0.28.1` |
| [boringtun](https://github.com/cloudflare/boringtun) | `0.7.0` | `0.7.1` |
| [smoltcp](https://github.com/smoltcp-rs/smoltcp) | `0.13.0` | `0.13.1` |
| [tray-icon](https://github.com/tauri-apps/tray-icon) | `0.22.1` | `0.23.1` |
| [tauri-build](https://github.com/tauri-apps/tauri) | `2.5.6` | `2.6.0` |
| [cpubits](https://github.com/RustCrypto/utils) | `0.1.0` | `0.1.1` |
| [ctor](https://github.com/mmastrac/rust-ctor) | `0.2.9` | `0.8.0` |
| [fax](https://github.com/pdf-rs/fax) | `0.2.6` | `0.2.7` |
| [heapless](https://github.com/rust-embedded/heapless) | `0.9.2` | `0.9.3` |
| [idna_adapter](https://github.com/hsivonen/idna_adapter) | `1.2.1` | `1.2.2` |
| [imgref](https://github.com/kornelski/imgref) | `1.12.0` | `1.12.1` |
| [muda](https://github.com/tauri-apps/muda) | `0.17.2` | `0.19.1` |
| [plist](https://github.com/ebarnard/rust-plist) | `1.8.0` | `1.9.0` |
| [rustls](https://github.com/rustls/rustls) | `0.23.39` | `0.23.40` |
| [tauri-codegen](https://github.com/tauri-apps/tauri) | `2.5.5` | `2.6.0` |
| [tauri-macros](https://github.com/tauri-apps/tauri) | `2.5.5` | `2.6.0` |
| [tauri-plugin](https://github.com/tauri-apps/tauri) | `2.5.4` | `2.6.0` |
| [tauri-runtime](https://github.com/tauri-apps/tauri) | `2.10.1` | `2.11.0` |
| [tauri-runtime-wry](https://github.com/tauri-apps/tauri) | `2.10.1` | `2.11.0` |
| [tauri-utils](https://github.com/tauri-apps/tauri) | `2.8.3` | `2.9.0` |
| [tauri-winres](https://github.com/tauri-apps/winres) | `0.3.5` | `0.3.6` |
| [tendril](https://github.com/servo/html5ever) | `0.4.3` | `0.5.0` |
| [wasi](https://github.com/bytecodealliance/wasi-rs) | `0.9.0+wasi-snapshot-preview1` | `0.11.1+wasi-snapshot-preview1` |
| [wry](https://github.com/tauri-apps/wry) | `0.54.4` | `0.55.0` |
| [zbus](https://github.com/z-galaxy/zbus) | `5.14.0` | `5.15.0` |
| [zbus_macros](https://github.com/z-galaxy/zbus) | `5.14.0` | `5.15.0` |
| [zbus_names](https://github.com/z-galaxy/zbus) | `4.3.1` | `4.3.2` |
| [zvariant](https://github.com/z-galaxy/zbus) | `5.10.0` | `5.10.1` |
| [zvariant_derive](https://github.com/z-galaxy/zbus) | `5.10.0` | `5.10.1` |
| [zvariant_utils](https://github.com/z-galaxy/zbus) | `3.3.0` | `3.3.1` |


Updates `tauri` from 2.10.3 to 2.11.0
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.10.3...tauri-v2.11.0)

Updates `reqwest` from 0.13.2 to 0.13.3
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.13.2...v0.13.3)

Updates `zip` from 8.5.1 to 8.6.0
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zip-rs/zip2/compare/v8.5.1...v8.6.0)

Updates `bzip2` from 0.5.2 to 0.6.1
- [Release notes](https://github.com/trifectatechfoundation/bzip2-rs/releases)
- [Commits](https://github.com/trifectatechfoundation/bzip2-rs/compare/v0.5.2...v0.6.1)

Updates `maxminddb` from 0.27.3 to 0.28.1
- [Release notes](https://github.com/oschwald/maxminddb-rust/releases)
- [Changelog](https://github.com/oschwald/maxminddb-rust/blob/main/CHANGELOG.md)
- [Commits](https://github.com/oschwald/maxminddb-rust/compare/v0.27.3...v0.28.1)

Updates `boringtun` from 0.7.0 to 0.7.1
- [Release notes](https://github.com/cloudflare/boringtun/releases)
- [Changelog](https://github.com/cloudflare/boringtun/blob/master/CHANGELOG.md)
- [Commits](https://github.com/cloudflare/boringtun/compare/boringtun-0.7.0...boringtun-0.7.1)

Updates `smoltcp` from 0.13.0 to 0.13.1
- [Release notes](https://github.com/smoltcp-rs/smoltcp/releases)
- [Changelog](https://github.com/smoltcp-rs/smoltcp/blob/main/CHANGELOG.md)
- [Commits](https://github.com/smoltcp-rs/smoltcp/compare/v0.13.0...v0.13.1)

Updates `tray-icon` from 0.22.1 to 0.23.1
- [Release notes](https://github.com/tauri-apps/tray-icon/releases)
- [Changelog](https://github.com/tauri-apps/tray-icon/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/tauri-apps/tray-icon/compare/tray-icon-v0.22.1...tray-icon-v0.23.1)

Updates `tauri-build` from 2.5.6 to 2.6.0
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-build-v2.5.6...tauri-build-v2.6.0)

Updates `cpubits` from 0.1.0 to 0.1.1
- [Commits](https://github.com/RustCrypto/utils/compare/cpubits-v0.1.0...cpubits-v0.1.1)

Updates `ctor` from 0.2.9 to 0.8.0
- [Changelog](https://github.com/mmastrac/linktime/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mmastrac/rust-ctor/commits)

Updates `fax` from 0.2.6 to 0.2.7
- [Commits](https://github.com/pdf-rs/fax/commits)

Updates `heapless` from 0.9.2 to 0.9.3
- [Release notes](https://github.com/rust-embedded/heapless/releases)
- [Changelog](https://github.com/rust-embedded/heapless/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-embedded/heapless/compare/v0.9.2...v0.9.3)

Updates `idna_adapter` from 1.2.1 to 1.2.2
- [Commits](https://github.com/hsivonen/idna_adapter/compare/v1.2.1...v1.2.2)

Updates `imgref` from 1.12.0 to 1.12.1
- [Commits](https://github.com/kornelski/imgref/compare/v1.12.0...v1.12.1)

Updates `muda` from 0.17.2 to 0.19.1
- [Release notes](https://github.com/tauri-apps/muda/releases)
- [Changelog](https://github.com/tauri-apps/muda/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/tauri-apps/muda/compare/muda-v0.17.2...muda-v0.19.1)

Updates `plist` from 1.8.0 to 1.9.0
- [Release notes](https://github.com/ebarnard/rust-plist/releases)
- [Changelog](https://github.com/ebarnard/rust-plist/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ebarnard/rust-plist/compare/v1.8.0...v1.9.0)

Updates `rustls` from 0.23.39 to 0.23.40
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.39...v/0.23.40)

Updates `tauri-codegen` from 2.5.5 to 2.6.0
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-codegen-v2.5.5...tauri-codegen-v2.6.0)

Updates `tauri-macros` from 2.5.5 to 2.6.0
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-macros-v2.5.5...tauri-macros-v2.6.0)

Updates `tauri-plugin` from 2.5.4 to 2.6.0
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-plugin-v2.5.4...tauri-plugin-v2.6.0)

Updates `tauri-runtime` from 2.10.1 to 2.11.0
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-runtime-v2.10.1...tauri-runtime-v2.11.0)

Updates `tauri-runtime-wry` from 2.10.1 to 2.11.0
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-runtime-wry-v2.10.1...tauri-runtime-wry-v2.11.0)

Updates `tauri-utils` from 2.8.3 to 2.9.0
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-utils-v2.8.3...tauri-utils-v2.9.0)

Updates `tauri-winres` from 0.3.5 to 0.3.6
- [Release notes](https://github.com/tauri-apps/winres/releases)
- [Changelog](https://github.com/tauri-apps/winres/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tauri-apps/winres/compare/winres-v0.3.5...winres-v0.3.6)

Updates `tendril` from 0.4.3 to 0.5.0
- [Release notes](https://github.com/servo/html5ever/releases)
- [Commits](https://github.com/servo/html5ever/commits)

Updates `wasi` from 0.9.0+wasi-snapshot-preview1 to 0.11.1+wasi-snapshot-preview1
- [Commits](https://github.com/bytecodealliance/wasi-rs/commits/0.11.1)

Updates `wry` from 0.54.4 to 0.55.0
- [Release notes](https://github.com/tauri-apps/wry/releases)
- [Changelog](https://github.com/tauri-apps/wry/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/tauri-apps/wry/compare/wry-v0.54.4...wry-v0.55)

Updates `zbus` from 5.14.0 to 5.15.0
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zbus-5.14.0...zbus-5.15.0)

Updates `zbus_macros` from 5.14.0 to 5.15.0
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zbus_macros-5.14.0...zbus_macros-5.15.0)

Updates `zbus_names` from 4.3.1 to 4.3.2
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zbus_names-4.3.1...zbus_names-4.3.2)

Updates `zvariant` from 5.10.0 to 5.10.1
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zvariant-5.10.0...zvariant-5.10.1)

Updates `zvariant_derive` from 5.10.0 to 5.10.1
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zvariant_derive-5.10.0...zvariant_derive-5.10.1)

Updates `zvariant_utils` from 3.3.0 to 3.3.1
- [Release notes](https://github.com/z-galaxy/zbus/releases)
- [Changelog](https://github.com/z-galaxy/zbus/blob/main/release-plz.toml)
- [Commits](https://github.com/z-galaxy/zbus/compare/zvariant_utils-3.3.0...zvariant_utils-3.3.1)

---
updated-dependencies:
- dependency-name: tauri
  dependency-version: 2.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: reqwest
  dependency-version: 0.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zip
  dependency-version: 8.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: bzip2
  dependency-version: 0.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: maxminddb
  dependency-version: 0.28.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: boringtun
  dependency-version: 0.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: smoltcp
  dependency-version: 0.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tray-icon
  dependency-version: 0.23.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-build
  dependency-version: 2.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: cpubits
  dependency-version: 0.1.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: ctor
  dependency-version: 0.8.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: fax
  dependency-version: 0.2.7
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: heapless
  dependency-version: 0.9.3
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: idna_adapter
  dependency-version: 1.2.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: imgref
  dependency-version: 1.12.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: muda
  dependency-version: 0.19.1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: plist
  dependency-version: 1.9.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: rustls
  dependency-version: 0.23.40
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tauri-codegen
  dependency-version: 2.6.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-macros
  dependency-version: 2.6.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-plugin
  dependency-version: 2.6.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-runtime
  dependency-version: 2.11.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-runtime-wry
  dependency-version: 2.11.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-utils
  dependency-version: 2.9.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tauri-winres
  dependency-version: 0.3.6
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tendril
  dependency-version: 0.5.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: wasi
  dependency-version: 0.11.1+wasi-snapshot-preview1
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: wry
  dependency-version: 0.55.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zbus
  dependency-version: 5.15.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zbus_macros
  dependency-version: 5.15.0
  dependency-type: indirect
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: zbus_names
  dependency-version: 4.3.2
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zvariant
  dependency-version: 5.10.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zvariant_derive
  dependency-version: 5.10.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: zvariant_utils
  dependency-version: 3.3.1
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 10:38:08 +00:00
dependabot[bot] d06ddccd78 ci(deps): bump the github-actions group with 3 updates (#330)
Bumps the github-actions group with 3 updates: [pnpm/action-setup](https://github.com/pnpm/action-setup), [anomalyco/opencode](https://github.com/anomalyco/opencode) and [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `pnpm/action-setup` from 6.0.3 to 6.0.4
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/903f9c1a6ebcba6cf41d87230be49611ac97822e...26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a)

Updates `anomalyco/opencode` from 1.14.24 to 1.14.31
- [Release notes](https://github.com/anomalyco/opencode/releases)
- [Commits](https://github.com/anomalyco/opencode/compare/da6683fedcbb57a36c4ba54ba5ad00dd8bc2da65...557734bd130a68188454bc691e153f9f3731830e)

Updates `crate-ci/typos` from 1.45.1 to 1.46.0
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/cf5f1c29a8ac336af8568821ec41919923b05a83...bbaefadf97b0ec5fdc942684b647f1a6ab250274)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: anomalyco/opencode
  dependency-version: 1.14.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: crate-ci/typos
  dependency-version: 1.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 09:52:30 +00:00
github-actions[bot] 04297fc27d chore: update flake.nix for v0.22.5 [skip ci] (#328)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-29 21:54:30 +00:00
github-actions[bot] 1d404833ad docs: update CHANGELOG.md and README.md for v0.22.5 [skip ci] (#327)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-29 21:54:07 +00:00
andy f61a3905fa Merge pull request #326 from zhom/contributors-readme-action-EteKcTv4vm
docs(contributor): contributors readme action update
2026-04-29 22:26:06 +02:00
github-actions[bot] 79d8b83b57 docs(contributor): contrib-readme-action has updated readme 2026-04-29 20:23:54 +00:00
zhom e700b47b4c chore: version bump 2026-04-30 00:23:20 +04:00
zhom 57167b979f chore: copy 2026-04-30 00:23:20 +04:00
andy 571bfcb213 Merge pull request #325 from ThiagoMafra-Integrare/fix/missing-libxdo3-dependency
fix(deb,rpm): declare libxdo as runtime dependency
2026-04-29 19:20:42 +02:00
ThiagoMafra-Integrare 6721444822 fix(deb,rpm): declare libxdo as runtime dependency
Donut Browser uses libxdo at runtime (loaded via dlopen, not directly
linked), so Tauri's auto-dependency detection misses it. As a result,
the DEB/RPM packages install cleanly but launching the app fails
silently with:

  /usr/bin/donutbrowser: error while loading shared libraries:
  libxdo.so.3: cannot open shared object file: No such file or directory

Declare libxdo3 (Debian/Ubuntu) and libxdo (Fedora/openSUSE) explicitly
in bundle.linux.{deb,rpm}.depends so package managers pull the library
during install.
2026-04-29 10:49:33 -03:00
github-actions[bot] ef1dc3407f chore: update flake.nix for v0.22.4 [skip ci] (#324)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-28 21:24:36 +00:00
github-actions[bot] 1162f1e9f3 docs: update CHANGELOG.md and README.md for v0.22.4 [skip ci] (#323)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-28 21:24:17 +00:00
43 changed files with 1387 additions and 1512 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e #v6.0.3
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a #v6.0.4
with:
run_install: false
+1 -1
View File
@@ -327,7 +327,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Run opencode
uses: anomalyco/opencode/github@da6683fedcbb57a36c4ba54ba5ad00dd8bc2da65 #v1.14.24
uses: anomalyco/opencode/github@557734bd130a68188454bc691e153f9f3731830e #v1.14.31
env:
ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }}
TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e #v6.0.3
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a #v6.0.4
with:
run_install: false
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Set up pnpm package manager
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e #v6.0.3
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a #v6.0.4
with:
run_install: false
+1 -1
View File
@@ -108,7 +108,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e #v6.0.3
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a #v6.0.4
with:
run_install: false
+1 -1
View File
@@ -107,7 +107,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e #v6.0.3
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a #v6.0.4
with:
run_install: false
+1 -1
View File
@@ -23,4 +23,4 @@ jobs:
- name: Checkout Actions Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Spell Check Repo
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 #v1.45.1
uses: crate-ci/typos@bbaefadf97b0ec5fdc942684b647f1a6ab250274 #v1.46.0
+2 -2
View File
@@ -35,7 +35,7 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Install pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e #v6.0.3
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a #v6.0.4
with:
run_install: false
@@ -94,7 +94,7 @@ jobs:
done
- name: Install pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e #v6.0.3
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a #v6.0.4
with:
run_install: false
+2
View File
@@ -67,6 +67,8 @@ donutbrowser/
- Adding a new string means adding the key to ALL seven locale files in `src/i18n/locales/` (en, es, fr, ja, pt, ru, zh) — not just `en.json`. The English version alone is incomplete work.
- Reuse existing keys (`common.buttons.*`, `common.labels.*`, `createProfile.*`, etc.) before creating new namespaces. Check `en.json` first.
- Strings excluded from this rule: `console.log/warn/error`, dev-only debug labels, internal IDs, CSS class names, type names. If unsure whether a string renders to the user, assume it does and translate it.
- **Never use `t(key, "fallback")` with a default-value second argument.** The 2-arg form is forbidden — every key must exist in every locale file before the call site lands. Fallbacks mask missing translations: a key missing from `ru.json` will silently render the English fallback to Russian users, so the bug never surfaces in CI or review. Only call `t("namespace.key")`. If a translation is missing for any locale, that's a bug to fix at the JSON, not a hole to paper over at the call site.
- Empty-string values in non-English locales are also forbidden — a locale either has the right translation or it has the same content as English; never `""`. If a particular language doesn't need a particular phrase (e.g. a suffix that doesn't grammatically apply), refactor the JSX to use a single interpolated key (`t("foo.bar", { name })` with `"...{{name}}..."` in each locale) instead of splitting prefix/suffix.
## Singletons
+22
View File
@@ -1,6 +1,28 @@
# Changelog
## v0.22.5 (2026-04-29)
### Bug Fixes
- declare libxdo as runtime dependency
### Maintenance
- chore: version bump
- chore: copy
- chore: update flake.nix for v0.22.4 [skip ci] (#324)
## v0.22.4 (2026-04-28)
### Maintenance
- chore: version bump
- chore: i18n
- chore: update flake.nix for v0.22.3 [skip ci] (#321)
## v0.22.3 (2026-04-27)
### Bug Fixes
+12 -5
View File
@@ -51,7 +51,7 @@
| | Apple Silicon | Intel |
|---|---|---|
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut_0.22.3_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut_0.22.3_x64.dmg) |
| **DMG** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut_0.22.5_aarch64.dmg) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut_0.22.5_x64.dmg) |
Or install via Homebrew:
@@ -61,15 +61,15 @@ brew install --cask donut
### Windows
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut_0.22.3_x64-setup.exe) · [Portable (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut_0.22.3_x64-portable.zip)
[Download Windows Installer (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut_0.22.5_x64-setup.exe) · [Portable (x64)](https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut_0.22.5_x64-portable.zip)
### Linux
| Format | x86_64 | ARM64 |
|---|---|---|
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut_0.22.3_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut_0.22.3_arm64.deb) |
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut-0.22.3-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut-0.22.3-1.aarch64.rpm) |
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut_0.22.3_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut_0.22.3_aarch64.AppImage) |
| **deb** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut_0.22.5_amd64.deb) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut_0.22.5_arm64.deb) |
| **rpm** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut-0.22.5-1.x86_64.rpm) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut-0.22.5-1.aarch64.rpm) |
| **AppImage** | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut_0.22.5_amd64.AppImage) | [Download](https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut_0.22.5_aarch64.AppImage) |
<!-- install-links-end -->
Or install via package manager:
@@ -160,6 +160,13 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
<br />
<sub><b>Jory Severijnse</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ThiagoMafra-Integrare">
<img src="https://avatars.githubusercontent.com/u/222241596?v=4" width="100;" alt="ThiagoMafra-Integrare"/>
<br />
<sub><b>Thiago Mafra</b></sub>
</a>
</td>
</tr>
<tbody>
+5 -5
View File
@@ -94,17 +94,17 @@
pkgConfigPath = lib.makeSearchPath "lib/pkgconfig" (
pkgConfigLibs ++ map lib.getDev pkgConfigLibs
);
releaseVersion = "0.22.3";
releaseVersion = "0.22.5";
releaseAppImage =
if system == "x86_64-linux" then
pkgs.fetchurl {
url = "https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut_0.22.3_amd64.AppImage";
hash = "sha256-x2JDIdy6A0u4chw+bHedKsBaz2Ph8tSUR/TR9C3Ae5I=";
url = "https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut_0.22.5_amd64.AppImage";
hash = "sha256-709vcQ3SsFxsZEmDkuamlbHVsbFhGBAb3x59YvTehl4=";
}
else if system == "aarch64-linux" then
pkgs.fetchurl {
url = "https://github.com/zhom/donutbrowser/releases/download/v0.22.3/Donut_0.22.3_aarch64.AppImage";
hash = "sha256-Oee7kwx6vhv+ZifqxOcGoZ1K00tYY1j7JAHU8LNMt/w=";
url = "https://github.com/zhom/donutbrowser/releases/download/v0.22.5/Donut_0.22.5_aarch64.AppImage";
hash = "sha256-T7ZrRvo7gM5mnzmXfLQXVMekf28jVOgFlfAAi89huMY=";
}
else
null;
+4 -4
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.22.4",
"version": "0.22.6",
"type": "module",
"scripts": {
"dev": "next dev --turbopack -p 12341",
@@ -45,7 +45,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"@tauri-apps/api": "~2.10.1",
"@tauri-apps/api": "~2.11.0",
"@tauri-apps/plugin-deep-link": "^2.4.7",
"@tauri-apps/plugin-dialog": "^2.7.0",
"@tauri-apps/plugin-fs": "~2.5.0",
@@ -75,7 +75,7 @@
"devDependencies": {
"@biomejs/biome": "2.4.10",
"@tailwindcss/postcss": "^4.2.2",
"@tauri-apps/cli": "~2.10.1",
"@tauri-apps/cli": "~2.11.0",
"@types/color": "^4.2.1",
"@types/node": "^25.5.2",
"@types/react": "^19.2.14",
@@ -96,7 +96,7 @@
"fast-xml-parser@<5.7.0": ">=5.7.2"
}
},
"packageManager": "pnpm@10.33.0",
"packageManager": "pnpm@10.33.2",
"lint-staged": {
"**/*.{js,jsx,ts,tsx,json,css}": [
"biome check --fix"
+60 -60
View File
@@ -54,8 +54,8 @@ importers:
specifier: ^8.21.3
version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tauri-apps/api':
specifier: ~2.10.1
version: 2.10.1
specifier: ~2.11.0
version: 2.11.0
'@tauri-apps/plugin-deep-link':
specifier: ^2.4.7
version: 2.4.7
@@ -139,8 +139,8 @@ importers:
specifier: ^4.2.2
version: 4.2.2
'@tauri-apps/cli':
specifier: ~2.10.1
version: 2.10.1
specifier: ~2.11.0
version: 2.11.0
'@types/color':
specifier: ^4.2.1
version: 4.2.1
@@ -2678,82 +2678,82 @@ packages:
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
'@tauri-apps/api@2.10.1':
resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==}
'@tauri-apps/api@2.11.0':
resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==}
'@tauri-apps/cli-darwin-arm64@2.10.1':
resolution: {integrity: sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==}
'@tauri-apps/cli-darwin-arm64@2.11.0':
resolution: {integrity: sha512-UfMeDNlgIP252rm/KSTuu8yHatPua5TjtUEUf+jyIzVwBNcIl7Ywkdpfj+e5jVVg3EfCTp+4gwuL1dNpgF8clg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.10.1':
resolution: {integrity: sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==}
'@tauri-apps/cli-darwin-x64@2.11.0':
resolution: {integrity: sha512-lY1+aPlgyMN7vgjtCdQ3+WODfZkebAcxnrCrO0HjqDpKSXieDkrJbimqeaoM4RwhTSrCLRHfVYiYrfE5E131tg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.10.1':
resolution: {integrity: sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==}
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.0':
resolution: {integrity: sha512-5uCP0AusgN3NrKC8EpkuJwjek1k8pEffBdugJSpXPey/QGbPEb8vZ542n/giJ2mZPjMSllDkdhG2QIDpBY4PpQ==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.10.1':
resolution: {integrity: sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==}
'@tauri-apps/cli-linux-arm64-gnu@2.11.0':
resolution: {integrity: sha512-loDPqtRHMSbIcrH2VBd4GgHoQlF7jJnrZj7MxA2lj1cixS/jEgMAPFqj83U6Wvjete4HfYplbE/gCpSFifA9jw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.10.1':
resolution: {integrity: sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==}
'@tauri-apps/cli-linux-arm64-musl@2.11.0':
resolution: {integrity: sha512-DtSE8ZBlB9H+L+eHkfZ3myt00EVEyAB3e41juEHoE2qT88fgVlJvyrwa9SZYc/xTwCS9TnmK+R84tpg+ZsAg7Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.10.1':
resolution: {integrity: sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==}
'@tauri-apps/cli-linux-riscv64-gnu@2.11.0':
resolution: {integrity: sha512-5QdgS4LD+kntClI1aj2JmwjW38LosNXxwCe8viIHEwqYIWuMPdNEIau6/cLogI38Yzx9DnfCPRfEWLyI+5li8Q==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.10.1':
resolution: {integrity: sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==}
'@tauri-apps/cli-linux-x64-gnu@2.11.0':
resolution: {integrity: sha512-5UynPXo3Zq9khjVdAbD+YogeLltdVUeOah2ioSIM3tu6H7wY9vMy6rgGJhv9r5R8ZXmk9GttMippdqYJWrnLnA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.10.1':
resolution: {integrity: sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==}
'@tauri-apps/cli-linux-x64-musl@2.11.0':
resolution: {integrity: sha512-CNz7fHbApz1Zyhhq73jtGn9JqgNEV/lIWnTnUo6h6ujw+mHsTmkLszvJSM8W6JBaDjNpTTFr/RSNoVL5FMwcTg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.10.1':
resolution: {integrity: sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==}
'@tauri-apps/cli-win32-arm64-msvc@2.11.0':
resolution: {integrity: sha512-K+br+VXZ+Xx0n/9FdWohpW5Ugq+2FQUpJScqcPl1hTxXfh3fgjYgt4qA2NgrjlJo+zZPNrmUMl+NLvm0ufEqBQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.10.1':
resolution: {integrity: sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==}
'@tauri-apps/cli-win32-ia32-msvc@2.11.0':
resolution: {integrity: sha512-OFV+s3MLZnd75zl0ZAFU5riMpGK4waUEA8ZDuijDsnkU0btz/gHhqh5jVlOn8thyvgdtT3Xyoxqo099MMifH3g==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.10.1':
resolution: {integrity: sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==}
'@tauri-apps/cli-win32-x64-msvc@2.11.0':
resolution: {integrity: sha512-AeDTWBd2cOZ6TX133BWsoo+LutG9o0JRcgjMsIfLE13ZugpgCMv/2dJbUiBGeRvbPOGin5A3aYmsArPVV6ZSHQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.10.1':
resolution: {integrity: sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==}
'@tauri-apps/cli@2.11.0':
resolution: {integrity: sha512-W5Wbuqsb2pHFPTj4TaRNKTj5rwXhDShPiLSY9T18y4ouSR/NNCptAEFxFsBtyNRgL6Vs1a/q9LzfqqYzEwC+Jw==}
engines: {node: '>= 10'}
hasBin: true
@@ -8343,74 +8343,74 @@ snapshots:
'@tanstack/table-core@8.21.3': {}
'@tauri-apps/api@2.10.1': {}
'@tauri-apps/api@2.11.0': {}
'@tauri-apps/cli-darwin-arm64@2.10.1':
'@tauri-apps/cli-darwin-arm64@2.11.0':
optional: true
'@tauri-apps/cli-darwin-x64@2.10.1':
'@tauri-apps/cli-darwin-x64@2.11.0':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.10.1':
'@tauri-apps/cli-linux-arm-gnueabihf@2.11.0':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.10.1':
'@tauri-apps/cli-linux-arm64-gnu@2.11.0':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.10.1':
'@tauri-apps/cli-linux-arm64-musl@2.11.0':
optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.10.1':
'@tauri-apps/cli-linux-riscv64-gnu@2.11.0':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.10.1':
'@tauri-apps/cli-linux-x64-gnu@2.11.0':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.10.1':
'@tauri-apps/cli-linux-x64-musl@2.11.0':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.10.1':
'@tauri-apps/cli-win32-arm64-msvc@2.11.0':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.10.1':
'@tauri-apps/cli-win32-ia32-msvc@2.11.0':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.10.1':
'@tauri-apps/cli-win32-x64-msvc@2.11.0':
optional: true
'@tauri-apps/cli@2.10.1':
'@tauri-apps/cli@2.11.0':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.10.1
'@tauri-apps/cli-darwin-x64': 2.10.1
'@tauri-apps/cli-linux-arm-gnueabihf': 2.10.1
'@tauri-apps/cli-linux-arm64-gnu': 2.10.1
'@tauri-apps/cli-linux-arm64-musl': 2.10.1
'@tauri-apps/cli-linux-riscv64-gnu': 2.10.1
'@tauri-apps/cli-linux-x64-gnu': 2.10.1
'@tauri-apps/cli-linux-x64-musl': 2.10.1
'@tauri-apps/cli-win32-arm64-msvc': 2.10.1
'@tauri-apps/cli-win32-ia32-msvc': 2.10.1
'@tauri-apps/cli-win32-x64-msvc': 2.10.1
'@tauri-apps/cli-darwin-arm64': 2.11.0
'@tauri-apps/cli-darwin-x64': 2.11.0
'@tauri-apps/cli-linux-arm-gnueabihf': 2.11.0
'@tauri-apps/cli-linux-arm64-gnu': 2.11.0
'@tauri-apps/cli-linux-arm64-musl': 2.11.0
'@tauri-apps/cli-linux-riscv64-gnu': 2.11.0
'@tauri-apps/cli-linux-x64-gnu': 2.11.0
'@tauri-apps/cli-linux-x64-musl': 2.11.0
'@tauri-apps/cli-win32-arm64-msvc': 2.11.0
'@tauri-apps/cli-win32-ia32-msvc': 2.11.0
'@tauri-apps/cli-win32-x64-msvc': 2.11.0
'@tauri-apps/plugin-deep-link@2.4.7':
dependencies:
'@tauri-apps/api': 2.10.1
'@tauri-apps/api': 2.11.0
'@tauri-apps/plugin-dialog@2.7.0':
dependencies:
'@tauri-apps/api': 2.10.1
'@tauri-apps/api': 2.11.0
'@tauri-apps/plugin-fs@2.5.0':
dependencies:
'@tauri-apps/api': 2.10.1
'@tauri-apps/api': 2.11.0
'@tauri-apps/plugin-log@2.8.0':
dependencies:
'@tauri-apps/api': 2.10.1
'@tauri-apps/api': 2.11.0
'@tauri-apps/plugin-opener@2.5.3':
dependencies:
'@tauri-apps/api': 2.10.1
'@tauri-apps/api': 2.11.0
'@tokenizer/inflate@0.4.1':
dependencies:
@@ -11037,7 +11037,7 @@ snapshots:
tauri-plugin-macos-permissions-api@2.3.0:
dependencies:
'@tauri-apps/api': 2.10.1
'@tauri-apps/api': 2.11.0
terser-webpack-plugin@5.4.0(webpack@5.105.4):
dependencies:
+142 -589
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.22.4"
version = "0.22.6"
description = "Simple Yet Powerful Anti-Detect Browser"
authors = ["zhom@github"]
edition = "2021"
@@ -102,7 +102,7 @@ serde_yaml = "0.9"
thiserror = "2.0"
regex-lite = "0.1"
tempfile = "3"
maxminddb = "0.27"
maxminddb = "0.28"
quick-xml = { version = "0.39", features = ["serialize"] }
# VPN support
@@ -110,7 +110,7 @@ boringtun = "0.7"
smoltcp = { version = "0.13", default-features = false, features = ["std", "medium-ip", "proto-ipv4", "proto-ipv6", "socket-tcp", "socket-udp"] }
# Daemon dependencies (tray icon)
tray-icon = "0.22"
tray-icon = "0.23"
tao = "0.35"
image = "0.25"
dirs = "6"
+70 -1
View File
@@ -41,6 +41,7 @@ pub struct ApiProfile {
pub tags: Vec<String>,
pub is_running: bool,
pub proxy_bypass_rules: Vec<String>,
pub vpn_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
@@ -60,6 +61,7 @@ pub struct CreateProfileRequest {
pub browser: String,
pub version: String,
pub proxy_id: Option<String>,
pub vpn_id: Option<String>,
pub launch_hook: Option<String>,
pub release_type: Option<String>,
#[schema(value_type = Object)]
@@ -76,6 +78,7 @@ pub struct UpdateProfileRequest {
pub browser: Option<String>,
pub version: Option<String>,
pub proxy_id: Option<String>,
pub vpn_id: Option<String>,
pub launch_hook: Option<String>,
pub release_type: Option<String>,
#[schema(value_type = Object)]
@@ -140,6 +143,16 @@ struct ApiVpnResponse {
last_used: Option<i64>,
}
#[derive(Debug, Serialize, ToSchema)]
struct ApiVpnExportResponse {
id: String,
name: String,
/// Always "WireGuard"
vpn_type: String,
/// Raw `.conf` file content (decrypted)
config_data: String,
}
#[derive(Debug, Deserialize, ToSchema)]
struct ImportVpnRequest {
/// Raw WireGuard `.conf` file content
@@ -357,6 +370,7 @@ impl ApiServer {
.routes(routes!(get_proxy, update_proxy, delete_proxy))
.routes(routes!(get_vpns, create_vpn))
.routes(routes!(import_vpn))
.routes(routes!(export_vpn))
.routes(routes!(get_vpn, update_vpn, delete_vpn))
.routes(routes!(get_extensions))
.routes(routes!(delete_extension_api))
@@ -542,6 +556,7 @@ async fn get_profiles() -> Result<Json<ApiProfilesResponse>, StatusCode> {
tags: profile.tags.clone(),
is_running: profile.process_id.is_some(), // Simple check based on process_id
proxy_bypass_rules: profile.proxy_bypass_rules.clone(),
vpn_id: profile.vpn_id.clone(),
})
.collect();
@@ -598,6 +613,7 @@ async fn get_profile(
tags: profile.tags.clone(),
is_running: profile.process_id.is_some(), // Simple check based on process_id
proxy_bypass_rules: profile.proxy_bypass_rules.clone(),
vpn_id: profile.vpn_id.clone(),
},
}))
} else {
@@ -652,7 +668,7 @@ async fn create_profile(
&request.version,
request.release_type.as_deref().unwrap_or("stable"),
request.proxy_id.clone(),
None, // vpn_id
request.vpn_id.clone(),
camoufox_config,
wayfern_config,
request.group_id.clone(),
@@ -700,6 +716,7 @@ async fn create_profile(
tags: profile.tags,
is_running: false,
proxy_bypass_rules: profile.proxy_bypass_rules,
vpn_id: profile.vpn_id,
},
}))
}
@@ -733,6 +750,12 @@ async fn update_profile(
) -> Result<Json<ApiProfileResponse>, StatusCode> {
let profile_manager = ProfileManager::instance();
if request.proxy_id.as_deref().is_some_and(|s| !s.is_empty())
&& request.vpn_id.as_deref().is_some_and(|s| !s.is_empty())
{
return Err(StatusCode::BAD_REQUEST);
}
// Update profile fields
if let Some(new_name) = request.name {
if profile_manager
@@ -762,6 +785,21 @@ async fn update_profile(
}
}
if let Some(vpn_id) = request.vpn_id {
let normalized = if vpn_id.is_empty() {
None
} else {
Some(vpn_id)
};
if profile_manager
.update_profile_vpn(state.app_handle.clone(), &id, normalized)
.await
.is_err()
{
return Err(StatusCode::BAD_REQUEST);
}
}
if let Some(launch_hook) = request.launch_hook {
let normalized = if launch_hook.trim().is_empty() {
None
@@ -1308,6 +1346,37 @@ async fn get_vpn(
.ok_or(StatusCode::NOT_FOUND)
}
#[utoipa::path(
get,
path = "/v1/vpns/{id}/export",
params(("id" = String, Path, description = "VPN configuration ID")),
responses(
(status = 200, description = "Decrypted VPN configuration", body = ApiVpnExportResponse),
(status = 401, description = "Unauthorized"),
(status = 404, description = "VPN configuration not found"),
(status = 500, description = "Internal server error")
),
security(("bearer_auth" = [])),
tag = "vpns"
)]
async fn export_vpn(
Path(id): Path<String>,
State(_state): State<ApiServerState>,
) -> Result<Json<ApiVpnExportResponse>, StatusCode> {
let storage = crate::vpn::VPN_STORAGE
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match storage.load_config(&id) {
Ok(config) => Ok(Json(ApiVpnExportResponse {
id: config.id,
name: config.name,
vpn_type: config.vpn_type.to_string(),
config_data: config.config_data,
})),
Err(_) => Err(StatusCode::NOT_FOUND),
}
}
#[utoipa::path(
post,
path = "/v1/vpns/import",
+10 -4
View File
@@ -675,11 +675,17 @@ fn find_claude_cli() -> Option<std::path::PathBuf> {
}
#[tauri::command]
fn is_mcp_in_claude_code() -> Result<bool, String> {
async fn is_mcp_in_claude_code() -> Result<bool, String> {
let cli = find_claude_cli().ok_or("Claude Code CLI not found")?;
let output = std::process::Command::new(&cli)
// `claude mcp list` health-checks every registered MCP server, so a
// missing or stalled server can hang the call for many seconds. Cap it
// — for this dialog, a slow `claude` is treated the same as "not registered".
let fut = tokio::process::Command::new(&cli)
.args(["mcp", "list"])
.output()
.output();
let output = tokio::time::timeout(std::time::Duration::from_secs(2), fut)
.await
.map_err(|_| "claude mcp list timed out".to_string())?
.map_err(|e| format!("Failed to run claude: {e}"))?;
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout.contains("donut-browser"))
@@ -1232,7 +1238,7 @@ pub fn run() {
#[allow(unused_variables)]
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.title("Donut Browser")
.inner_size(800.0, 500.0)
.inner_size(840.0, 500.0)
.resizable(false)
.fullscreen(false)
.center()
+3 -3
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut",
"version": "0.22.4",
"version": "0.22.6",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
@@ -42,11 +42,11 @@
"linux": {
"deb": {
"desktopTemplate": "donutbrowser.desktop",
"depends": ["xdg-utils"]
"depends": ["xdg-utils", "libxdo3"]
},
"rpm": {
"desktopTemplate": "donutbrowser.desktop",
"depends": ["xdg-utils"]
"depends": ["xdg-utils", "libxdo"]
},
"appimage": {
"files": {
+1 -1
View File
@@ -1070,7 +1070,7 @@ export default function Home() {
return (
<div className="grid items-center justify-items-center min-h-screen gap-8 font-(family-name:--font-geist-sans) bg-background">
<main className="flex flex-col items-center w-full max-w-3xl">
<main className="flex flex-col items-center w-full max-w-4xl px-3">
<div className="w-full">
<HomeHeader
onCreateProfileDialogOpen={setCreateProfileDialogOpen}
+7 -5
View File
@@ -1,5 +1,6 @@
"use client";
import { useTranslation } from "react-i18next";
import { FaExternalLinkAlt, FaTimes } from "react-icons/fa";
import { LuCheckCheck } from "react-icons/lu";
import { Button } from "@/components/ui/button";
@@ -19,6 +20,7 @@ export function AppUpdateToast({
onDismiss,
updateReady = false,
}: AppUpdateToastProps) {
const { t } = useTranslation();
const handleRestartClick = async () => {
await onRestart();
};
@@ -43,10 +45,10 @@ export function AppUpdateToast({
<div className="flex flex-col gap-1">
<span className="text-sm font-semibold text-foreground">
{updateReady
? "Update ready, restart to apply"
? t("appUpdate.toast.updateReady")
: updateInfo.repo_update
? "Update available via package manager"
: "Manual download required"}
: t("appUpdate.toast.manualDownloadRequired")}
</span>
<div className="text-xs text-muted-foreground">
{updateInfo.current_version} {updateInfo.new_version}
@@ -71,7 +73,7 @@ export function AppUpdateToast({
className="flex gap-2 items-center text-xs"
>
<LuCheckCheck className="w-3 h-3" />
Restart Now
{t("appUpdate.toast.restartNow")}
</RippleButton>
) : (
!updateInfo.repo_update &&
@@ -82,7 +84,7 @@ export function AppUpdateToast({
className="flex gap-2 items-center text-xs"
>
<FaExternalLinkAlt className="w-3 h-3" />
View Release
{t("appUpdate.toast.viewRelease")}
</RippleButton>
)
)}
@@ -92,7 +94,7 @@ export function AppUpdateToast({
size="sm"
className="text-xs"
>
Later
{t("appUpdate.toast.later")}
</RippleButton>
</div>
</div>
+1 -1
View File
@@ -953,7 +953,7 @@ export function CreateProfileDialog({
<div className="flex gap-3 items-center">
<div className="w-4 h-4 rounded-full border-2 animate-spin border-muted/40 border-t-primary" />
<p className="text-sm text-muted-foreground">
Fetching available versions...
{t("createProfile.version.fetching")}
</p>
</div>
)}
+6 -4
View File
@@ -294,7 +294,9 @@ export function UnifiedToast(props: ToastProps) {
"completed_files" in progress && (
<div className="mt-1">
<p className="text-xs text-muted-foreground">
{progress.phase === "uploading" ? "Uploading" : "Downloading"}{" "}
{progress.phase === "uploading"
? t("appUpdate.toast.uploading")
: t("appUpdate.toast.downloading")}{" "}
{progress.completed_files}/{progress.total_files} files
{" \u2022 "}
{formatBytesCompact(progress.completed_bytes)} /{" "}
@@ -349,17 +351,17 @@ export function UnifiedToast(props: ToastProps) {
<>
{stage === "extracting" && (
<p className="mt-1 text-xs text-muted-foreground">
Extracting browser files... Please do not close the app.
{t("browserDownload.toast.extracting")}
</p>
)}
{stage === "verifying" && (
<p className="mt-1 text-xs text-muted-foreground">
Verifying browser files...
{t("browserDownload.toast.verifying")}
</p>
)}
{stage === "downloading (twilight rolling release)" && (
<p className="mt-1 text-xs text-muted-foreground">
Downloading rolling release build...
{t("browserDownload.toast.downloadingRolling")}
</p>
)}
</>
@@ -55,12 +55,12 @@ export function ExtensionGroupAssignmentDialog({
} catch (err) {
console.error("Failed to load extension groups:", err);
setError(
err instanceof Error ? err.message : "Failed to load extension groups",
err instanceof Error ? err.message : t("extensions.loadGroupsFailed"),
);
} finally {
setIsLoading(false);
}
}, []);
}, [t]);
const handleAssign = useCallback(async () => {
setIsAssigning(true);
@@ -79,7 +79,7 @@ export function ExtensionGroupAssignmentDialog({
} catch (err) {
console.error("Failed to assign extension group:", err);
const errorMessage =
err instanceof Error ? err.message : "Failed to assign extension group";
err instanceof Error ? err.message : t("extensions.assignGroupFailed");
setError(errorMessage);
toast.error(errorMessage);
} finally {
+208 -193
View File
@@ -50,36 +50,43 @@ type SyncStatus = "disabled" | "syncing" | "synced" | "error" | "waiting";
function getSyncStatusDot(
item: { sync_enabled?: boolean; last_sync?: number },
liveStatus: SyncStatus | undefined,
t: (key: string, options?: Record<string, unknown>) => string,
): { color: string; tooltip: string; animate: boolean } {
const status = liveStatus ?? (item.sync_enabled ? "synced" : "disabled");
switch (status) {
case "syncing":
return { color: "bg-warning", tooltip: "Syncing...", animate: true };
return {
color: "bg-warning",
tooltip: t("profileTable.syncTooltipSyncing"),
animate: true,
};
case "synced":
return {
color: "bg-success",
tooltip: item.last_sync
? `Synced ${new Date(item.last_sync * 1000).toLocaleString()}`
: "Synced",
? t("profileTable.syncTooltipSyncedAt", {
time: new Date(item.last_sync * 1000).toLocaleString(),
})
: t("profileTable.syncTooltipSynced"),
animate: false,
};
case "waiting":
return {
color: "bg-warning",
tooltip: "Waiting to sync",
tooltip: t("profileTable.syncTooltipWaiting"),
animate: false,
};
case "error":
return {
color: "bg-destructive",
tooltip: "Sync error",
tooltip: t("profileTable.syncTooltipError"),
animate: false,
};
default:
return {
color: "bg-muted-foreground",
tooltip: "Not synced",
tooltip: t("profileTable.syncTooltipNotSynced"),
animate: false,
};
}
@@ -674,6 +681,7 @@ export function ExtensionManagementDialog({
const syncDot = getSyncStatusDot(
ext,
extSyncStatus[ext.id],
t,
);
return (
<div
@@ -840,6 +848,7 @@ export function ExtensionManagementDialog({
const groupSyncDot = getSyncStatusDot(
group,
extSyncStatus[group.id],
t,
);
return (
@@ -995,7 +1004,7 @@ export function ExtensionManagementDialog({
}
}}
>
<DialogContent className="max-w-lg">
<DialogContent className="max-w-lg max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle>{t("extensions.editGroup")}</DialogTitle>
<DialogDescription>
@@ -1003,87 +1012,89 @@ export function ExtensionManagementDialog({
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("common.labels.name")}</Label>
<Input
value={editGroupName}
onChange={(e) => {
setEditGroupName(e.target.value);
}}
placeholder={t("extensions.groupNamePlaceholder")}
/>
</div>
{extensions.filter((e) => !editGroupExtensionIds.includes(e.id))
.length > 0 && (
<ScrollArea className="overflow-y-auto flex-1 -mx-6 px-6">
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("extensions.addToGroup")}</Label>
<Select
value=""
onValueChange={(extId) => {
setEditGroupExtensionIds((prev) => [...prev, extId]);
<Label>{t("common.labels.name")}</Label>
<Input
value={editGroupName}
onChange={(e) => {
setEditGroupName(e.target.value);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("extensions.addToGroup")} />
</SelectTrigger>
<SelectContent>
{extensions
.filter((e) => !editGroupExtensionIds.includes(e.id))
.map((ext) => (
<SelectItem key={ext.id} value={ext.id}>
<div className="flex items-center gap-2">
{renderExtensionIcon(ext, "sm")}
{ext.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
placeholder={t("extensions.groupNamePlaceholder")}
/>
</div>
)}
<div className="space-y-2">
<Label>{t("extensions.groupExtensions")}</Label>
{editGroupExtensionIds.length === 0 ? (
<div className="text-sm text-muted-foreground py-2">
{t("extensions.noExtensionsInGroup")}
</div>
) : (
<div className="space-y-1 max-h-[200px] overflow-y-auto">
{editGroupExtensionIds.map((extId) => {
const ext = extensions.find((e) => e.id === extId);
if (!ext) return null;
return (
<div
key={extId}
className="flex items-center gap-2 rounded-md border px-2 py-1.5"
>
{renderExtensionIcon(ext, "sm")}
<span className="text-sm flex-1 truncate min-w-0">
{ext.name}
</span>
{renderCompatIcons(ext.browser_compatibility)}
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 shrink-0"
onClick={() => {
setEditGroupExtensionIds((prev) =>
prev.filter((id) => id !== extId),
);
}}
>
<LuTrash2 className="w-3 h-3" />
</Button>
</div>
);
})}
{extensions.filter((e) => !editGroupExtensionIds.includes(e.id))
.length > 0 && (
<div className="space-y-2">
<Label>{t("extensions.addToGroup")}</Label>
<Select
value=""
onValueChange={(extId) => {
setEditGroupExtensionIds((prev) => [...prev, extId]);
}}
>
<SelectTrigger>
<SelectValue placeholder={t("extensions.addToGroup")} />
</SelectTrigger>
<SelectContent>
{extensions
.filter((e) => !editGroupExtensionIds.includes(e.id))
.map((ext) => (
<SelectItem key={ext.id} value={ext.id}>
<div className="flex items-center gap-2">
{renderExtensionIcon(ext, "sm")}
{ext.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<Label>{t("extensions.groupExtensions")}</Label>
{editGroupExtensionIds.length === 0 ? (
<div className="text-sm text-muted-foreground py-2">
{t("extensions.noExtensionsInGroup")}
</div>
) : (
<div className="space-y-1 max-h-[200px] overflow-y-auto">
{editGroupExtensionIds.map((extId) => {
const ext = extensions.find((e) => e.id === extId);
if (!ext) return null;
return (
<div
key={extId}
className="flex items-center gap-2 rounded-md border px-2 py-1.5"
>
{renderExtensionIcon(ext, "sm")}
<span className="text-sm flex-1 truncate min-w-0">
{ext.name}
</span>
{renderCompatIcons(ext.browser_compatibility)}
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 shrink-0"
onClick={() => {
setEditGroupExtensionIds((prev) =>
prev.filter((id) => id !== extId),
);
}}
>
<LuTrash2 className="w-3 h-3" />
</Button>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
</ScrollArea>
<DialogFooter>
<Button
@@ -1117,7 +1128,7 @@ export function ExtensionManagementDialog({
}
}}
>
<DialogContent className="max-w-lg">
<DialogContent className="max-w-lg max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle>{t("extensions.editExtension")}</DialogTitle>
<DialogDescription>
@@ -1125,123 +1136,127 @@ export function ExtensionManagementDialog({
</DialogDescription>
</DialogHeader>
{editingExtension && (
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("common.labels.name")}</Label>
<Input
value={editExtensionName}
onChange={(e) => {
setEditExtensionName(e.target.value);
}}
placeholder={t("extensions.namePlaceholder")}
onKeyDown={(e) => {
if (e.key === "Enter") void handleUpdateExtension();
}}
/>
</div>
<ScrollArea className="overflow-y-auto flex-1 -mx-6 px-6">
{editingExtension && (
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("common.labels.name")}</Label>
<Input
value={editExtensionName}
onChange={(e) => {
setEditExtensionName(e.target.value);
}}
placeholder={t("extensions.namePlaceholder")}
onKeyDown={(e) => {
if (e.key === "Enter") void handleUpdateExtension();
}}
/>
</div>
{/* Metadata from manifest.json */}
<div className="rounded-md border p-3 space-y-2">
<Label className="text-xs text-muted-foreground uppercase tracking-wide">
{t("extensions.metadata")}
</Label>
<div className="grid grid-cols-[auto,1fr] gap-x-3 gap-y-1.5 text-sm">
{editingExtension.version && (
<>
<span className="text-muted-foreground">
{t("extensions.version")}
</span>
<span>{editingExtension.version}</span>
</>
)}
{editingExtension.author && (
<>
<span className="text-muted-foreground">
{t("extensions.author")}
</span>
<span>{editingExtension.author}</span>
</>
)}
{editingExtension.description && (
<>
<span className="text-muted-foreground">
{t("common.labels.description")}
</span>
<span className="line-clamp-3">
{editingExtension.description}
</span>
</>
)}
<span className="text-muted-foreground">
{t("extensions.compatibility.label")}
</span>
<div className="flex items-center gap-1">
{renderCompatIcons(editingExtension.browser_compatibility)}
</div>
<span className="text-muted-foreground">
{t("common.labels.type")}
</span>
<span>.{editingExtension.file_type}</span>
{editingExtension.homepage_url && (
<>
<span className="text-muted-foreground">
{t("extensions.homepage")}
</span>
<a
href={editingExtension.homepage_url}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline flex items-center gap-1 truncate"
>
<span className="truncate">
{editingExtension.homepage_url}
{/* Metadata from manifest.json */}
<div className="rounded-md border p-3 space-y-2">
<Label className="text-xs text-muted-foreground uppercase tracking-wide">
{t("extensions.metadata")}
</Label>
<div className="grid grid-cols-[auto,1fr] gap-x-3 gap-y-1.5 text-sm">
{editingExtension.version && (
<>
<span className="text-muted-foreground">
{t("extensions.version")}
</span>
<LuExternalLink className="w-3 h-3 shrink-0" />
</a>
</>
)}
{!editingExtension.version &&
!editingExtension.author &&
!editingExtension.description &&
!editingExtension.homepage_url && (
<span className="col-span-2 text-muted-foreground text-xs">
{t("extensions.noMetadata")}
<span>{editingExtension.version}</span>
</>
)}
{editingExtension.author && (
<>
<span className="text-muted-foreground">
{t("extensions.author")}
</span>
<span>{editingExtension.author}</span>
</>
)}
{editingExtension.description && (
<>
<span className="text-muted-foreground">
{t("common.labels.description")}
</span>
<span className="line-clamp-3">
{editingExtension.description}
</span>
</>
)}
<span className="text-muted-foreground">
{t("extensions.compatibility.label")}
</span>
<div className="flex items-center gap-1">
{renderCompatIcons(
editingExtension.browser_compatibility,
)}
</div>
<span className="text-muted-foreground">
{t("common.labels.type")}
</span>
<span>.{editingExtension.file_type}</span>
{editingExtension.homepage_url && (
<>
<span className="text-muted-foreground">
{t("extensions.homepage")}
</span>
<a
href={editingExtension.homepage_url}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline flex items-center gap-1 truncate"
>
<span className="truncate">
{editingExtension.homepage_url}
</span>
<LuExternalLink className="w-3 h-3 shrink-0" />
</a>
</>
)}
{!editingExtension.version &&
!editingExtension.author &&
!editingExtension.description &&
!editingExtension.homepage_url && (
<span className="col-span-2 text-muted-foreground text-xs">
{t("extensions.noMetadata")}
</span>
)}
</div>
</div>
{/* Re-upload */}
<div className="space-y-2">
<Label>{t("extensions.reupload")}</Label>
<div className="flex gap-2 items-center">
<RippleButton
size="sm"
variant="outline"
onClick={() =>
document.getElementById("ext-edit-file-input")?.click()
}
>
<LuUpload className="w-3 h-3 mr-1" />
{t("extensions.selectFile")}
</RippleButton>
<input
id="ext-edit-file-input"
type="file"
accept=".xpi,.crx,.zip"
className="hidden"
onChange={handleEditFileSelect}
/>
{pendingUpdateFile && (
<span className="text-xs text-muted-foreground truncate max-w-[200px]">
{pendingUpdateFile.name}
</span>
)}
</div>
</div>
</div>
{/* Re-upload */}
<div className="space-y-2">
<Label>{t("extensions.reupload")}</Label>
<div className="flex gap-2 items-center">
<RippleButton
size="sm"
variant="outline"
onClick={() =>
document.getElementById("ext-edit-file-input")?.click()
}
>
<LuUpload className="w-3 h-3 mr-1" />
{t("extensions.selectFile")}
</RippleButton>
<input
id="ext-edit-file-input"
type="file"
accept=".xpi,.crx,.zip"
className="hidden"
onChange={handleEditFileSelect}
/>
{pendingUpdateFile && (
<span className="text-xs text-muted-foreground truncate max-w-[200px]">
{pendingUpdateFile.name}
</span>
)}
</div>
</div>
</div>
)}
)}
</ScrollArea>
<DialogFooter>
<Button
+1 -1
View File
@@ -139,7 +139,7 @@ export function GroupBadges({
return (
<div className="flex gap-2 mb-4">
<div className="flex items-center gap-2 px-4.5 py-1.5 text-xs">
Loading groups...
{t("groups.loading")}
</div>
</div>
);
+1 -1
View File
@@ -283,7 +283,7 @@ export function GroupManagementDialog({
{/* Groups list */}
{isLoading ? (
<div className="text-sm text-muted-foreground">
{t("common.loading")}
{t("common.buttons.loading")}
</div>
) : groups.length === 0 ? (
<div className="text-sm text-muted-foreground">
+3 -3
View File
@@ -543,9 +543,9 @@ export function ImportProfileDialog({
<div className="space-y-4">
<Alert>
<AlertDescription>
{t("importProfile.importedAsPrefix")}{" "}
<strong>{getBrowserDisplayName(currentMappedBrowser)}</strong>{" "}
{t("importProfile.importedAsSuffix")}
{t("importProfile.importedAs", {
browser: getBrowserDisplayName(currentMappedBrowser),
})}
</AlertDescription>
</Alert>
+34 -13
View File
@@ -536,7 +536,11 @@ const TagsCell = React.memo<{
onChange={(opts) => void handleChange(opts)}
creatable
selectFirstItem={false}
placeholder={effectiveTags.length === 0 ? "Add tags" : ""}
placeholder={
effectiveTags.length === 0
? translate("profileTable.addTagsPlaceholder")
: ""
}
className={cn(
"bg-transparent border-0! focus-within:ring-0!",
"[&_div:first-child]:border-0! [&_div:first-child]:ring-0! [&_div:first-child]:focus-within:ring-0!",
@@ -1846,6 +1850,7 @@ export function ProfilesDataTable({
},
{
id: "actions",
size: 100,
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
@@ -1964,7 +1969,7 @@ export function ProfilesDataTable({
size="sm"
disabled={!canLaunch || isLaunching || isStopping}
className={cn(
"min-w-[70px] h-7",
"min-w-[80px] h-7 px-3",
!canLaunch && "opacity-50 cursor-not-allowed",
canLaunch && "cursor-pointer",
isFollower && "border-accent",
@@ -1980,9 +1985,9 @@ export function ProfilesDataTable({
<div className="w-3 h-3 rounded-full border border-current animate-spin border-t-transparent" />
</div>
) : isRunning ? (
"Stop"
meta.t("profiles.actions.stop")
) : (
"Launch"
meta.t("profiles.actions.launch")
)}
</RippleButton>
</span>
@@ -1999,7 +2004,9 @@ export function ProfilesDataTable({
},
{
accessorKey: "name",
header: ({ column }) => {
size: 130,
header: ({ column, table }) => {
const meta = table.options.meta as TableMeta;
return (
<Button
variant="ghost"
@@ -2008,7 +2015,7 @@ export function ProfilesDataTable({
}}
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
>
Name
{meta.t("common.labels.name")}
{column.getIsSorted() === "asc" ? (
<LuChevronUp className="ml-2 w-4 h-4" />
) : column.getIsSorted() === "desc" ? (
@@ -2137,7 +2144,11 @@ export function ProfilesDataTable({
},
{
id: "tags",
header: "Tags",
size: 110,
header: ({ table }) => {
const meta = table.options.meta as TableMeta;
return meta.t("profileTable.tagsHeader");
},
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
@@ -2166,7 +2177,11 @@ export function ProfilesDataTable({
},
{
id: "note",
header: "Note",
size: 110,
header: ({ table }) => {
const meta = table.options.meta as TableMeta;
return meta.t("profileTable.noteHeader");
},
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
@@ -2193,7 +2208,11 @@ export function ProfilesDataTable({
},
{
id: "proxy",
header: "Proxy / VPN",
size: 130,
header: ({ table }) => {
const meta = table.options.meta as TableMeta;
return meta.t("profiles.table.proxy");
},
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
@@ -2231,7 +2250,7 @@ export function ProfilesDataTable({
? effectiveVpn.name
: effectiveProxy
? effectiveProxy.name
: "Not Selected";
: meta.t("profiles.table.notSelected");
const vpnBadge = effectiveVpn ? "WG" : null;
const tooltipText = hasAssignment ? displayName : null;
const isSelectorOpen = meta.openProxySelectorFor === profile.id;
@@ -2372,7 +2391,7 @@ export function ProfilesDataTable({
))}
</CommandGroup>
{meta.vpnConfigs.length > 0 && (
<CommandGroup heading="VPNs">
<CommandGroup heading={t("profileTable.vpnsHeading")}>
{meta.vpnConfigs.map((vpn) => (
<CommandItem
key={vpn.id}
@@ -2405,7 +2424,9 @@ export function ProfilesDataTable({
)}
{meta.canCreateLocationProxy &&
meta.countries.length > 0 && (
<CommandGroup heading="Create by country">
<CommandGroup
heading={t("profileTable.createByCountryHeading")}
>
{meta.countries
.filter(
(c) =>
@@ -2569,7 +2590,7 @@ export function ProfilesDataTable({
platform === "macos" ? "h-[340px]" : "h-[280px]",
)}
>
<Table className="overflow-visible">
<Table className="overflow-visible table-fixed">
<TableHeader className="overflow-visible">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="overflow-visible">
+11 -7
View File
@@ -1,7 +1,7 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { LoadingButton } from "@/components/loading-button";
import { Badge } from "@/components/ui/badge";
@@ -50,7 +50,15 @@ export function ProfileSelectorDialog({
}: ProfileSelectorDialogProps) {
const { t } = useTranslation();
// Use the centralized profile events hook
const { profiles, runningProfiles: hookRunningProfiles } = useProfileEvents();
const { profiles: rawProfiles, runningProfiles: hookRunningProfiles } =
useProfileEvents();
const profiles = useMemo(
() =>
[...rawProfiles].sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLowerCase()),
),
[rawProfiles],
);
// Use external runningProfiles if provided, otherwise use hook's runningProfiles
const runningProfiles = externalRunningProfiles ?? hookRunningProfiles;
@@ -148,11 +156,7 @@ export function ProfileSelectorDialog({
if (runningAvailableProfile) {
setSelectedProfile(runningAvailableProfile.name);
} else {
// Sort profiles by name and select first
const sortedProfiles = [...profiles].sort((a, b) =>
a.name.localeCompare(b.name),
);
setSelectedProfile(sortedProfiles[0].name);
setSelectedProfile(profiles[0].name);
}
}
}, [isOpen, profiles, selectedProfile, runningProfiles]);
+14 -31
View File
@@ -166,7 +166,7 @@ export function ProfileSyncDialog({
}, [profile, hasConfig, onSyncConfigOpen, onClose, t]);
const formatLastSync = (timestamp?: number) => {
if (!timestamp) return t("common.labels.never", "Never");
if (!timestamp) return t("common.labels.never");
const date = new Date(timestamp * 1000);
return date.toLocaleString();
};
@@ -177,7 +177,7 @@ export function ProfileSyncDialog({
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t("sync.mode.title", "Profile Sync")}</DialogTitle>
<DialogTitle>{t("sync.mode.title")}</DialogTitle>
<DialogDescription>
{t("sync.mode.description", {
name: profile.name,
@@ -194,9 +194,7 @@ export function ProfileSyncDialog({
<div className="grid gap-4 py-4">
{!hasConfig && (
<div className="p-3 text-sm rounded-md bg-muted">
<p className="mb-2">
{t("sync.mode.notConfigured", "Sync service not configured.")}
</p>
<p className="mb-2">{t("sync.mode.notConfigured")}</p>
<Button
variant="outline"
size="sm"
@@ -205,7 +203,7 @@ export function ProfileSyncDialog({
onClose();
}}
>
{t("sync.mode.configureService", "Configure Sync Service")}
{t("sync.mode.configureService")}
</Button>
</div>
)}
@@ -222,13 +220,10 @@ export function ProfileSyncDialog({
<RadioGroupItem value="Disabled" id="sync-disabled" />
<Label htmlFor="sync-disabled" className="cursor-pointer">
<span className="font-medium">
{t("sync.mode.disabled", "Disabled")}
{t("sync.mode.disabled")}
</span>
<p className="text-sm text-muted-foreground">
{t(
"sync.mode.disabledDescription",
"No sync for this profile",
)}
{t("sync.mode.disabledDescription")}
</p>
</Label>
</div>
@@ -237,13 +232,10 @@ export function ProfileSyncDialog({
<RadioGroupItem value="Regular" id="sync-regular" />
<Label htmlFor="sync-regular" className="cursor-pointer">
<span className="font-medium">
{t("sync.mode.regular", "Regular Sync")}
{t("sync.mode.regular")}
</span>
<p className="text-sm text-muted-foreground">
{t(
"sync.mode.regularDescription",
"Fast sync, unencrypted",
)}
{t("sync.mode.regularDescription")}
</p>
</Label>
</div>
@@ -263,18 +255,12 @@ export function ProfileSyncDialog({
}
>
<span className="font-medium">
{t("sync.mode.encrypted", "E2E Encrypted Sync")}
{t("sync.mode.encrypted")}
</span>
<p className="text-sm text-muted-foreground">
{canUseEncryption
? t(
"sync.mode.encryptedDescription",
"Encrypted before upload. Server never sees plaintext data.",
)
: t(
"settings.encryption.requiresProOrOwner",
"Profile encryption is available for Pro users and team owners.",
)}
? t("sync.mode.encryptedDescription")
: t("settings.encryption.requiresProOrOwner")}
</p>
</Label>
</div>
@@ -284,15 +270,12 @@ export function ProfileSyncDialog({
!hasE2ePassword &&
userChangedMode && (
<div className="p-3 text-sm rounded-md bg-destructive/10 text-destructive">
{t(
"sync.mode.noPasswordWarning",
"E2E password not set. Please set a password in Settings.",
)}
{t("sync.mode.noPasswordWarning")}
</div>
)}
<div className="space-y-2">
<Label>{t("sync.mode.lastSynced", "Last Synced")}</Label>
<Label>{t("sync.mode.lastSynced")}</Label>
<div className="flex gap-2 items-center">
<Badge variant="outline">
{formatLastSync(profile.last_sync)}
@@ -319,7 +302,7 @@ export function ProfileSyncDialog({
</Button>
{hasConfig && isSyncEnabled(profile) && (
<LoadingButton onClick={handleSyncNow} isLoading={isSyncing}>
{t("sync.mode.syncNow", "Sync Now")}
{t("sync.mode.syncNow")}
</LoadingButton>
)}
</DialogFooter>
+312 -322
View File
@@ -392,7 +392,7 @@ export function ProxyManagementDialog({
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col">
<DialogContent className="max-w-[min(95vw,1600px)] max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle>{t("proxies.management.title")}</DialogTitle>
<DialogDescription>
@@ -411,7 +411,7 @@ export function ProxyManagementDialog({
</TabsTrigger>
</TabsList>
<TabsContent value="proxies">
<TabsContent value="proxies" className="mt-4">
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex gap-2">
@@ -460,196 +460,188 @@ export function ProxyManagementDialog({
{t("proxies.management.noneCreated")}
</div>
) : (
<div className="border rounded-md">
<ScrollArea className="h-[240px]">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("common.labels.name")}</TableHead>
<TableHead className="w-20">
{t("proxies.management.usage")}
</TableHead>
<TableHead className="w-24">
{t("proxies.management.syncCol")}
</TableHead>
<TableHead className="w-24">
{t("common.labels.actions")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{storedProxies.map((proxy) => {
const syncDot = getSyncStatusDot(
proxy,
proxySyncStatus[proxy.id],
t,
proxySyncErrors[proxy.id],
);
return (
<TableRow key={proxy.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<div
className={`w-2 h-2 rounded-full shrink-0 ${syncDot.color} ${
syncDot.animate
? "animate-pulse"
: ""
}`}
/>
</TooltipTrigger>
<TooltipContent>
<p>{syncDot.tooltip}</p>
</TooltipContent>
</Tooltip>
{proxy.name}
</div>
</TableCell>
<TableCell>
<Badge variant="secondary">
{proxyUsage[proxy.id] ?? 0}
</Badge>
</TableCell>
<TableCell>
<div className="border rounded-md max-h-[240px] overflow-auto">
<Table className="min-w-max">
<TableHeader>
<TableRow>
<TableHead>{t("common.labels.name")}</TableHead>
<TableHead className="whitespace-nowrap w-px">
{t("proxies.management.usage")}
</TableHead>
<TableHead className="whitespace-nowrap w-px">
{t("proxies.management.syncCol")}
</TableHead>
<TableHead className="whitespace-nowrap w-px">
{t("common.labels.actions")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{storedProxies.map((proxy) => {
const syncDot = getSyncStatusDot(
proxy,
proxySyncStatus[proxy.id],
t,
proxySyncErrors[proxy.id],
);
return (
<TableRow key={proxy.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Checkbox
checked={proxy.sync_enabled}
onCheckedChange={() =>
void handleToggleSync(proxy)
}
disabled={
isTogglingSync[proxy.id] ||
proxyInUse[proxy.id]
}
/>
</div>
<div
className={`w-2 h-2 rounded-full shrink-0 ${syncDot.color} ${
syncDot.animate
? "animate-pulse"
: ""
}`}
/>
</TooltipTrigger>
<TooltipContent>
{proxyInUse[proxy.id] ? (
<p>
{t(
"proxies.management.syncCannotDisable",
)}
</p>
) : (
<p>
{proxy.sync_enabled
? t(
"proxies.management.disableSync",
)
: t(
"proxies.management.enableSync",
)}
</p>
)}
<p>{syncDot.tooltip}</p>
</TooltipContent>
</Tooltip>
</TableCell>
<TableCell>
<div className="flex gap-1">
<ProxyCheckButton
proxy={proxy}
profileId={proxy.id}
checkingProfileId={checkingProxyId}
cachedResult={
proxyCheckResults[proxy.id]
}
setCheckingProfileId={
setCheckingProxyId
}
onCheckComplete={(result) => {
setProxyCheckResults((prev) => ({
...prev,
[proxy.id]: result,
}));
}}
onCheckFailed={(result) => {
setProxyCheckResults((prev) => ({
...prev,
[proxy.id]: result,
}));
}}
/>
<Tooltip>
<TooltipTrigger asChild>
{proxy.name}
</div>
</TableCell>
<TableCell>
<Badge variant="secondary">
{proxyUsage[proxy.id] ?? 0}
</Badge>
</TableCell>
<TableCell>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Checkbox
checked={proxy.sync_enabled}
onCheckedChange={() =>
void handleToggleSync(proxy)
}
disabled={
isTogglingSync[proxy.id] ||
proxyInUse[proxy.id]
}
/>
</div>
</TooltipTrigger>
<TooltipContent>
{proxyInUse[proxy.id] ? (
<p>
{t(
"proxies.management.syncCannotDisable",
)}
</p>
) : (
<p>
{proxy.sync_enabled
? t(
"proxies.management.disableSync",
)
: t(
"proxies.management.enableSync",
)}
</p>
)}
</TooltipContent>
</Tooltip>
</TableCell>
<TableCell>
<div className="flex gap-1">
<ProxyCheckButton
proxy={proxy}
profileId={proxy.id}
checkingProfileId={checkingProxyId}
cachedResult={proxyCheckResults[proxy.id]}
setCheckingProfileId={setCheckingProxyId}
onCheckComplete={(result) => {
setProxyCheckResults((prev) => ({
...prev,
[proxy.id]: result,
}));
}}
onCheckFailed={(result) => {
setProxyCheckResults((prev) => ({
...prev,
[proxy.id]: result,
}));
}}
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => {
handleEditProxy(proxy);
}}
>
<LuPencil className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{t("proxies.management.editProxy")}
</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="ghost"
size="sm"
onClick={() => {
handleEditProxy(proxy);
handleDeleteProxy(proxy);
}}
disabled={
(proxyUsage[proxy.id] ?? 0) > 0
}
>
<LuPencil className="w-4 h-4" />
<LuTrash2 className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
</span>
</TooltipTrigger>
<TooltipContent>
{(proxyUsage[proxy.id] ?? 0) > 0 ? (
<p>
{t("proxies.management.editProxy")}
{(proxyUsage[proxy.id] ?? 0) === 1
? t(
"proxies.management.cannotDelete_one",
{
count: proxyUsage[proxy.id],
},
)
: t(
"proxies.management.cannotDelete_other",
{
count: proxyUsage[proxy.id],
},
)}
</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="ghost"
size="sm"
onClick={() => {
handleDeleteProxy(proxy);
}}
disabled={
(proxyUsage[proxy.id] ?? 0) > 0
}
>
<LuTrash2 className="w-4 h-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{(proxyUsage[proxy.id] ?? 0) > 0 ? (
<p>
{(proxyUsage[proxy.id] ?? 0) === 1
? t(
"proxies.management.cannotDelete_one",
{
count:
proxyUsage[proxy.id],
},
)
: t(
"proxies.management.cannotDelete_other",
{
count:
proxyUsage[proxy.id],
},
)}
</p>
) : (
<p>
{t(
"proxies.management.deleteProxy",
)}
</p>
)}
</TooltipContent>
</Tooltip>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</ScrollArea>
) : (
<p>
{t(
"proxies.management.deleteProxy",
)}
</p>
)}
</TooltipContent>
</Tooltip>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</div>
</TabsContent>
<TabsContent value="vpns">
<TabsContent value="vpns" className="mt-4">
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex gap-2">
@@ -684,169 +676,167 @@ export function ProxyManagementDialog({
{t("vpns.management.noneCreated")}
</div>
) : (
<div className="border rounded-md">
<ScrollArea className="h-[240px]">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("common.labels.name")}</TableHead>
<TableHead className="w-16">
{t("common.labels.type")}
</TableHead>
<TableHead className="w-20">
{t("proxies.management.usage")}
</TableHead>
<TableHead className="w-24">
{t("proxies.management.syncCol")}
</TableHead>
<TableHead className="w-24">
{t("common.labels.actions")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{vpnConfigs.map((vpn) => {
const syncDot = getSyncStatusDot(
vpn,
vpnSyncStatus[vpn.id],
t,
vpnSyncErrors[vpn.id],
);
return (
<TableRow key={vpn.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<div
className={`w-2 h-2 rounded-full shrink-0 ${syncDot.color} ${
syncDot.animate
? "animate-pulse"
: ""
}`}
/>
</TooltipTrigger>
<TooltipContent>
<p>{syncDot.tooltip}</p>
</TooltipContent>
</Tooltip>
{vpn.name}
</div>
</TableCell>
<TableCell>
<Badge variant="outline">WG</Badge>
</TableCell>
<TableCell>
<Badge variant="secondary">
{vpnUsage[vpn.id] ?? 0}
</Badge>
</TableCell>
<TableCell>
<div className="border rounded-md max-h-[240px] overflow-auto">
<Table className="min-w-max">
<TableHeader>
<TableRow>
<TableHead>{t("common.labels.name")}</TableHead>
<TableHead className="whitespace-nowrap w-px">
{t("common.labels.type")}
</TableHead>
<TableHead className="whitespace-nowrap w-px">
{t("proxies.management.usage")}
</TableHead>
<TableHead className="whitespace-nowrap w-px">
{t("proxies.management.syncCol")}
</TableHead>
<TableHead className="whitespace-nowrap w-px">
{t("common.labels.actions")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{vpnConfigs.map((vpn) => {
const syncDot = getSyncStatusDot(
vpn,
vpnSyncStatus[vpn.id],
t,
vpnSyncErrors[vpn.id],
);
return (
<TableRow key={vpn.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Checkbox
checked={vpn.sync_enabled}
onCheckedChange={() =>
void handleToggleVpnSync(vpn)
}
disabled={
isTogglingVpnSync[vpn.id] ||
vpnInUse[vpn.id]
}
/>
</div>
<div
className={`w-2 h-2 rounded-full shrink-0 ${syncDot.color} ${
syncDot.animate
? "animate-pulse"
: ""
}`}
/>
</TooltipTrigger>
<TooltipContent>
{vpnInUse[vpn.id] ? (
<p>
{t(
"vpns.management.syncCannotDisable",
)}
</p>
) : (
<p>
{vpn.sync_enabled
? t(
"proxies.management.disableSync",
)
: t(
"proxies.management.enableSync",
)}
</p>
)}
<p>{syncDot.tooltip}</p>
</TooltipContent>
</Tooltip>
</TableCell>
<TableCell>
<div className="flex gap-1">
<VpnCheckButton
vpnId={vpn.id}
vpnName={vpn.name}
checkingVpnId={checkingVpnId}
setCheckingVpnId={setCheckingVpnId}
/>
<Tooltip>
<TooltipTrigger asChild>
{vpn.name}
</div>
</TableCell>
<TableCell>
<Badge variant="outline">WG</Badge>
</TableCell>
<TableCell>
<Badge variant="secondary">
{vpnUsage[vpn.id] ?? 0}
</Badge>
</TableCell>
<TableCell>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<Checkbox
checked={vpn.sync_enabled}
onCheckedChange={() =>
void handleToggleVpnSync(vpn)
}
disabled={
isTogglingVpnSync[vpn.id] ||
vpnInUse[vpn.id]
}
/>
</div>
</TooltipTrigger>
<TooltipContent>
{vpnInUse[vpn.id] ? (
<p>
{t(
"vpns.management.syncCannotDisable",
)}
</p>
) : (
<p>
{vpn.sync_enabled
? t(
"proxies.management.disableSync",
)
: t(
"proxies.management.enableSync",
)}
</p>
)}
</TooltipContent>
</Tooltip>
</TableCell>
<TableCell>
<div className="flex gap-1">
<VpnCheckButton
vpnId={vpn.id}
vpnName={vpn.name}
checkingVpnId={checkingVpnId}
setCheckingVpnId={setCheckingVpnId}
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => {
handleEditVpn(vpn);
}}
>
<LuPencil className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("vpns.management.editVpn")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="ghost"
size="sm"
onClick={() => {
handleEditVpn(vpn);
handleDeleteVpn(vpn);
}}
disabled={
(vpnUsage[vpn.id] ?? 0) > 0
}
>
<LuPencil className="w-4 h-4" />
<LuTrash2 className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("vpns.management.editVpn")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="ghost"
size="sm"
onClick={() => {
handleDeleteVpn(vpn);
}}
disabled={
(vpnUsage[vpn.id] ?? 0) > 0
}
>
<LuTrash2 className="w-4 h-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{(vpnUsage[vpn.id] ?? 0) > 0 ? (
<p>
{(vpnUsage[vpn.id] ?? 0) === 1
? t(
"vpns.management.cannotDelete_one",
{ count: vpnUsage[vpn.id] },
)
: t(
"vpns.management.cannotDelete_other",
{ count: vpnUsage[vpn.id] },
)}
</p>
) : (
<p>
{t("vpns.management.deleteVpn")}
</p>
)}
</TooltipContent>
</Tooltip>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</ScrollArea>
</span>
</TooltipTrigger>
<TooltipContent>
{(vpnUsage[vpn.id] ?? 0) > 0 ? (
<p>
{(vpnUsage[vpn.id] ?? 0) === 1
? t(
"vpns.management.cannotDelete_one",
{ count: vpnUsage[vpn.id] },
)
: t(
"vpns.management.cannotDelete_other",
{ count: vpnUsage[vpn.id] },
)}
</p>
) : (
<p>
{t("vpns.management.deleteVpn")}
</p>
)}
</TooltipContent>
</Tooltip>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</div>
+29 -69
View File
@@ -811,7 +811,7 @@ export function SettingsDialog({
</div>
<p className="text-xs text-muted-foreground">
Choose your preferred language for the application interface.
{t("settings.language.description")}
</p>
</div>
@@ -820,10 +820,12 @@ export function SettingsDialog({
<div className="space-y-4">
<div className="flex justify-between items-center">
<Label className="text-base font-medium">
Default Browser
{t("settings.defaultBrowser.title")}
</Label>
<Badge variant={isDefaultBrowser ? "default" : "secondary"}>
{isDefaultBrowser ? "Active" : "Inactive"}
{isDefaultBrowser
? t("common.status.active")
: t("common.status.inactive")}
</Badge>
</div>
@@ -839,13 +841,12 @@ export function SettingsDialog({
className="w-full"
>
{isDefaultBrowser
? "Already Default Browser"
: "Set as Default Browser"}
? t("settings.defaultBrowser.alreadyDefault")
: t("settings.defaultBrowser.setAsDefault")}
</LoadingButton>
<p className="text-xs text-muted-foreground">
When set as default, Donut Browser will handle web links and
allow you to choose which profile to use.
{t("settings.defaultBrowser.description")}
</p>
</div>
)}
@@ -854,12 +855,12 @@ export function SettingsDialog({
{isMacOS && (
<div className="space-y-4">
<Label className="text-base font-medium">
System Permissions
{t("settings.permissions.title")}
</Label>
{isLoadingPermissions ? (
<div className="text-sm text-muted-foreground">
Loading permissions...
{t("settings.permissions.loading")}
</div>
) : (
<div className="space-y-3">
@@ -928,7 +929,7 @@ export function SettingsDialog({
className="w-full"
onClick={onIntegrationsOpen}
>
Open Integrations Settings
{t("integrations.openSettings")}
</RippleButton>
</div>
@@ -952,33 +953,24 @@ export function SettingsDialog({
{/* Sync Encryption Section */}
<div className="space-y-4">
<Label className="text-base font-medium">
{t("settings.encryption.title", "Sync Encryption")}
{t("settings.encryption.title")}
</Label>
<p className="text-xs text-muted-foreground">
{t(
"settings.encryption.description",
"Set a password to enable E2E encrypted sync. If you lose this password, encrypted profiles cannot be recovered.",
)}
{t("settings.encryption.description")}
</p>
{!canUseEncryption ? (
<p className="text-sm text-muted-foreground">
{t(
"settings.encryption.requiresProOrOwner",
"Profile encryption is available for Pro users and team owners.",
)}
{t("settings.encryption.requiresProOrOwner")}
</p>
) : hasE2ePassword ? (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Badge variant="default">
{t("settings.encryption.passwordSet", "Active")}
{t("settings.encryption.passwordSet")}
</Badge>
<span className="text-sm text-muted-foreground">
{t(
"settings.encryption.passwordSetDescription",
"E2E encryption password is set",
)}
{t("settings.encryption.passwordSetDescription")}
</span>
</div>
<div className="flex gap-2">
@@ -992,10 +984,7 @@ export function SettingsDialog({
setE2eError("");
}}
>
{t(
"settings.encryption.changePassword",
"Change Password",
)}
{t("settings.encryption.changePassword")}
</Button>
<Button
variant="destructive"
@@ -1004,21 +993,13 @@ export function SettingsDialog({
try {
await invoke("delete_e2e_password");
setHasE2ePassword(false);
showSuccessToast(
t(
"settings.encryption.removed",
"Encryption password removed",
),
);
showSuccessToast(t("settings.encryption.removed"));
} catch (error) {
showErrorToast(String(error));
}
}}
>
{t(
"settings.encryption.removePassword",
"Remove Password",
)}
{t("settings.encryption.removePassword")}
</Button>
</div>
</div>
@@ -1026,10 +1007,7 @@ export function SettingsDialog({
<div className="space-y-3">
<Input
type="password"
placeholder={t(
"settings.encryption.passwordPlaceholder",
"Password (min 8 characters)",
)}
placeholder={t("settings.encryption.passwordPlaceholder")}
value={e2ePassword}
onChange={(e) => {
setE2ePassword(e.target.value);
@@ -1038,10 +1016,7 @@ export function SettingsDialog({
/>
<Input
type="password"
placeholder={t(
"settings.encryption.confirmPlaceholder",
"Confirm password",
)}
placeholder={t("settings.encryption.confirmPlaceholder")}
value={e2ePasswordConfirm}
onChange={(e) => {
setE2ePasswordConfirm(e.target.value);
@@ -1057,21 +1032,11 @@ export function SettingsDialog({
isLoading={isSavingE2e}
onClick={async () => {
if (e2ePassword.length < 8) {
setE2eError(
t(
"settings.encryption.passwordTooShort",
"Password must be at least 8 characters",
),
);
setE2eError(t("settings.encryption.passwordTooShort"));
return;
}
if (e2ePassword !== e2ePasswordConfirm) {
setE2eError(
t(
"settings.encryption.passwordMismatch",
"Passwords do not match",
),
);
setE2eError(t("settings.encryption.passwordMismatch"));
return;
}
setIsSavingE2e(true);
@@ -1083,10 +1048,7 @@ export function SettingsDialog({
setE2ePassword("");
setE2ePasswordConfirm("");
showSuccessToast(
t(
"settings.encryption.passwordSaved",
"Encryption password set",
),
t("settings.encryption.passwordSaved"),
);
} catch (error) {
showErrorToast(String(error));
@@ -1095,7 +1057,7 @@ export function SettingsDialog({
}
}}
>
{t("settings.encryption.setPassword", "Set Password")}
{t("settings.encryption.setPassword")}
</LoadingButton>
</div>
)}
@@ -1172,13 +1134,11 @@ export function SettingsDialog({
variant="outline"
className="w-full"
>
Clear All Version Cache
{t("settings.advanced.clearCache")}
</LoadingButton>
<p className="text-xs text-muted-foreground">
Clear all cached browser version data and refresh all browser
versions from their sources. This will force a fresh download of
version information for all browsers.
{t("settings.advanced.clearCacheDescription")}
</p>
</div>
@@ -1194,7 +1154,7 @@ export function SettingsDialog({
<DialogFooter className="shrink-0">
<RippleButton variant="outline" onClick={handleClose}>
Cancel
{t("common.buttons.cancel")}
</RippleButton>
<LoadingButton
isLoading={isSaving}
@@ -1205,7 +1165,7 @@ export function SettingsDialog({
}}
disabled={isLoading || !hasChanges}
>
Save Settings
{t("common.buttons.saveSettings")}
</LoadingButton>
</DialogFooter>
</DialogContent>
+5 -1
View File
@@ -452,7 +452,11 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
setShowToken(!showToken);
}}
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
aria-label={showToken ? "Hide token" : "Show token"}
aria-label={
showToken
? t("common.aria.hideToken")
: t("common.aria.showToken")
}
>
{showToken ? (
<LuEyeOff className="w-4 h-4 text-muted-foreground hover:text-foreground" />
+6 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuCheck, LuCopy } from "react-icons/lu";
import { Button } from "@/components/ui/button";
import { showSuccessToast } from "@/lib/toast-utils";
@@ -26,6 +27,7 @@ export function CopyToClipboard({
className,
successMessage = "Copied to clipboard",
}: CopyToClipboardProps) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const copyToClipboard = useCallback(async () => {
@@ -47,9 +49,11 @@ export function CopyToClipboard({
size={size}
className={`relative ${className ?? ""}`}
onClick={copyToClipboard}
aria-label={copied ? "Copied" : "Copy to clipboard"}
aria-label={copied ? t("common.aria.copied") : t("common.aria.copy")}
>
<span className="sr-only">{copied ? "Copied" : "Copy"}</span>
<span className="sr-only">
{copied ? t("common.srOnly.copied") : t("common.srOnly.copy")}
</span>
<LuCopy
className={`h-4 w-4 transition-all duration-300 ${
copied ? "scale-0" : "scale-100"
+1 -1
View File
@@ -160,7 +160,7 @@ function DialogContent({
}}
transition={transition}
className={cn(
"bg-background fixed top-[50%] left-[50%] z-10000 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg",
"bg-background fixed top-[50%] left-[50%] z-10000 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg",
className,
)}
{...props}
+47 -14
View File
@@ -60,7 +60,8 @@
"optional": "Optional",
"required": "Required",
"unknownProfile": "Unknown",
"mode": "Mode"
"mode": "Mode",
"never": "Never"
},
"time": {
"days": "days",
@@ -72,7 +73,11 @@
"aria": {
"selectAll": "Select all",
"selectRow": "Select row",
"selectProfile": "Select profile"
"selectProfile": "Select profile",
"copy": "Copy to clipboard",
"copied": "Copied",
"showToken": "Show token",
"hideToken": "Hide token"
},
"keys": {
"escape": "Escape"
@@ -87,7 +92,11 @@
"title": "Command Palette",
"description": "Search for a command to run..."
},
"noResults": "No results found."
"noResults": "No results found.",
"srOnly": {
"copy": "Copy",
"copied": "Copied"
}
},
"settings": {
"title": "Settings",
@@ -196,7 +205,8 @@
"group": "Group",
"proxy": "Proxy / VPN",
"lastLaunch": "Last Launch",
"empty": "No profiles found."
"empty": "No profiles found.",
"notSelected": "Not Selected"
},
"actions": {
"launch": "Launch",
@@ -488,7 +498,8 @@
"deleteGroupAndProfiles": "Delete Group & Profiles",
"loadProfilesFailed": "Failed to load profiles",
"unknownGroup": "Unknown Group",
"profileGroupsAriaLabel": "Profile groups"
"profileGroupsAriaLabel": "Profile groups",
"loading": "Loading groups..."
},
"sync": {
"mode": {
@@ -631,7 +642,8 @@
"mcpAcceptTermsFirst": "(Accept Wayfern terms in Settings first)",
"mcpStarted": "MCP server started on port {{port}}",
"mcpStopped": "MCP server stopped",
"mcpToggleFailed": "Failed to toggle MCP server"
"mcpToggleFailed": "Failed to toggle MCP server",
"openSettings": "Open Integrations Settings"
},
"import": {
"title": "Import Profile",
@@ -711,6 +723,10 @@
"webrtc": "Block WebRTC",
"webgl": "Block WebGL"
}
},
"shared": {
"browserBehavior": "Browser Behavior",
"allowAddonsOpenTabs": "Allow browser addons to open new tabs automatically"
}
},
"cookies": {
@@ -875,7 +891,8 @@
"loadProxiesFailed": "Failed to load proxies: {{error}}",
"setupProxyListenersFailed": "Failed to setup proxy event listeners: {{error}}",
"loadVpnConfigsFailed": "Failed to load VPN configs: {{error}}",
"setupVpnListenersFailed": "Failed to setup VPN event listeners: {{error}}"
"setupVpnListenersFailed": "Failed to setup VPN event listeners: {{error}}",
"themeNotFound": "Tokyo Night theme not found"
},
"browser": {
"camoufox": "Camoufox",
@@ -1124,7 +1141,9 @@
"syncEnabled": "Sync enabled",
"syncDisabled": "Sync disabled",
"syncEnableTooltip": "Enable sync",
"syncDisableTooltip": "Disable sync"
"syncDisableTooltip": "Disable sync",
"loadGroupsFailed": "Failed to load extension groups",
"assignGroupFailed": "Failed to assign extension group"
},
"pro": {
"badge": "PRO",
@@ -1256,12 +1275,11 @@
"importedSuccess": "Successfully imported profile \"{{name}}\"",
"notInstalled": "{{browser}} is not installed. Please download {{browser}} first from the main window, then try importing again.",
"importFailed": "Failed to import profile: {{error}}",
"importedAsPrefix": "This profile will be imported as a",
"importedAsSuffix": "profile.",
"proxyOptional": "Proxy (Optional)",
"noProxy": "No proxy",
"nextButton": "Next",
"importButton": "Import"
"importButton": "Import",
"importedAs": "This profile will be imported as a {{browser}} profile."
},
"syncTooltips": {
"syncing": "Syncing...",
@@ -1503,7 +1521,12 @@
"syncTooltipNotSynced": "Not synced",
"noTags": "No tags",
"syncTooltipCloseToSync": "Close the profile to sync",
"syncTooltipDisabledWithLast": "Sync disabled, last sync {{time}}"
"syncTooltipDisabledWithLast": "Sync disabled, last sync {{time}}",
"addTagsPlaceholder": "Add tags",
"tagsHeader": "Tags",
"noteHeader": "Note",
"vpnsHeading": "VPNs",
"createByCountryHeading": "Create by country"
},
"releaseTypeSelector": {
"noReleaseTypes": "No release types available.",
@@ -1521,7 +1544,14 @@
"appUpdate": {
"toast": {
"updateFailed": "Failed to update Donut Browser",
"restartFailed": "Failed to restart"
"restartFailed": "Failed to restart",
"updateReady": "Update ready, restart to apply",
"manualDownloadRequired": "Manual download required",
"restartNow": "Restart Now",
"viewRelease": "View Release",
"later": "Later",
"uploading": "Uploading",
"downloading": "Downloading"
}
},
"browserDownload": {
@@ -1532,7 +1562,10 @@
"downloadFailed": "Failed to download {{browser}} {{version}}",
"calculating": "calculating...",
"extractionFailed": "{{browser}} {{version}}: extraction failed",
"extractionFailedDescription": "The corrupt file was deleted. It will be re-downloaded on next attempt."
"extractionFailedDescription": "The corrupt file was deleted. It will be re-downloaded on next attempt.",
"extracting": "Extracting browser files... Please do not close the app.",
"verifying": "Verifying browser files...",
"downloadingRolling": "Downloading rolling release build..."
}
},
"versionUpdater": {
+61 -28
View File
@@ -60,7 +60,8 @@
"optional": "Opcional",
"required": "Requerido",
"unknownProfile": "Desconocido",
"mode": "Modo"
"mode": "Modo",
"never": "Nunca"
},
"time": {
"days": "días",
@@ -72,7 +73,11 @@
"aria": {
"selectAll": "Seleccionar todo",
"selectRow": "Seleccionar fila",
"selectProfile": "Seleccionar perfil"
"selectProfile": "Seleccionar perfil",
"copy": "Copiar al portapapeles",
"copied": "Copiado",
"showToken": "Mostrar token",
"hideToken": "Ocultar token"
},
"keys": {
"escape": "Escape"
@@ -87,7 +92,11 @@
"title": "Paleta de comandos",
"description": "Busca un comando para ejecutar..."
},
"noResults": "No se encontraron resultados."
"noResults": "No se encontraron resultados.",
"srOnly": {
"copy": "Copiar",
"copied": "Copiado"
}
},
"settings": {
"title": "Configuración",
@@ -196,7 +205,8 @@
"group": "Grupo",
"proxy": "Proxy / VPN",
"lastLaunch": "Último Inicio",
"empty": "No se encontraron perfiles."
"empty": "No se encontraron perfiles.",
"notSelected": "No seleccionado"
},
"actions": {
"launch": "Iniciar",
@@ -488,7 +498,8 @@
"deleteGroupAndProfiles": "Eliminar Grupo y Perfiles",
"loadProfilesFailed": "Error al cargar los perfiles",
"unknownGroup": "Grupo desconocido",
"profileGroupsAriaLabel": "Grupos de perfiles"
"profileGroupsAriaLabel": "Grupos de perfiles",
"loading": "Cargando grupos..."
},
"sync": {
"mode": {
@@ -631,7 +642,8 @@
"mcpAcceptTermsFirst": "(Acepta primero los términos de Wayfern en Configuración)",
"mcpStarted": "Servidor MCP iniciado en puerto {{port}}",
"mcpStopped": "Servidor MCP detenido",
"mcpToggleFailed": "Error al alternar el servidor MCP"
"mcpToggleFailed": "Error al alternar el servidor MCP",
"openSettings": "Abrir configuración de integraciones"
},
"import": {
"title": "Importar Perfil",
@@ -711,6 +723,10 @@
"webrtc": "Bloquear WebRTC",
"webgl": "Bloquear WebGL"
}
},
"shared": {
"browserBehavior": "Comportamiento del navegador",
"allowAddonsOpenTabs": "Permitir que los complementos abran nuevas pestañas automáticamente"
}
},
"cookies": {
@@ -875,7 +891,8 @@
"loadProxiesFailed": "Error al cargar los proxies: {{error}}",
"setupProxyListenersFailed": "Error al configurar los listeners de eventos de proxies: {{error}}",
"loadVpnConfigsFailed": "Error al cargar las configuraciones de VPN: {{error}}",
"setupVpnListenersFailed": "Error al configurar los listeners de eventos de VPN: {{error}}"
"setupVpnListenersFailed": "Error al configurar los listeners de eventos de VPN: {{error}}",
"themeNotFound": "Tema Tokyo Night no encontrado"
},
"browser": {
"camoufox": "Camoufox",
@@ -901,15 +918,15 @@
"blockWebRTC": "Bloquear WebRTC",
"blockWebGL": "Bloquear WebGL",
"navigatorProperties": "Propiedades del navegador",
"userAgent": "User Agent",
"userAgent": "Agente de usuario",
"userAgentAndPlatform": "User Agent y plataforma",
"platform": "Plataforma",
"platformVersion": "Versión de plataforma",
"appVersion": "Versión de la aplicación",
"osCpu": "OS CPU",
"osCpu": "CPU del SO",
"hardwareConcurrency": "Concurrencia de hardware",
"maxTouchPoints": "Puntos táctiles máximos",
"doNotTrack": "Do Not Track",
"doNotTrack": "No rastrear",
"selectDntPlaceholder": "Seleccionar valor DNT",
"dntAllowed": "0 (rastreo permitido)",
"dntNotAllowed": "1 (rastreo no permitido)",
@@ -931,8 +948,8 @@
"outerHeight": "Alto exterior",
"innerWidth": "Ancho interior",
"innerHeight": "Alto interior",
"screenX": "Screen X",
"screenY": "Screen Y",
"screenX": "Pantalla X",
"screenY": "Pantalla Y",
"geolocation": "Geolocalización",
"timezoneAndGeolocation": "Zona horaria y geolocalización",
"timezoneGeolocationDescription": "Estos valores anulan las APIs de zona horaria y geolocalización del navegador.",
@@ -946,15 +963,15 @@
"region": "Región",
"script": "Script",
"webglProperties": "Propiedades de WebGL",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglVendor": "Proveedor WebGL",
"webglRenderer": "Renderizador WebGL",
"webglParameters": "Parámetros de WebGL",
"webglParametersJson": "Parámetros de WebGL (JSON)",
"webgl2Parameters": "Parámetros de WebGL2",
"webglShaderPrecisionFormats": "Formatos de precisión de WebGL Shader",
"webgl2ShaderPrecisionFormats": "Formatos de precisión de WebGL2 Shader",
"webglShaderPrecisionFormats": "Formatos de precisión de shader WebGL",
"webgl2ShaderPrecisionFormats": "Formatos de precisión de shader WebGL2",
"canvasFingerprint": "Canvas Fingerprint",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeed": "Semilla de ruido de Canvas",
"canvasNoiseSeedDescription": "Esta semilla se usa para generar una huella digital de Canvas consistente pero única. Cada perfil debe tener una semilla diferente.",
"fonts": "Fuentes",
"fontsJson": "Fuentes (JSON array)",
@@ -975,8 +992,8 @@
"maxChannelCount": "Número máximo de canales",
"vendorInfo": "Información del proveedor",
"vendor": "Proveedor",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"vendorSub": "Proveedor Sub",
"productSub": "Producto Sub",
"brand": "Marca",
"brandVersion": "Versión de marca",
"proFeature": "Esta es una función Pro",
@@ -1124,7 +1141,9 @@
"syncEnabled": "Sincronización habilitada",
"syncDisabled": "Sincronización deshabilitada",
"syncEnableTooltip": "Habilitar sincronización",
"syncDisableTooltip": "Deshabilitar sincronización"
"syncDisableTooltip": "Deshabilitar sincronización",
"loadGroupsFailed": "Error al cargar grupos de extensiones",
"assignGroupFailed": "Error al asignar grupo de extensiones"
},
"pro": {
"badge": "PRO",
@@ -1256,12 +1275,11 @@
"importedSuccess": "Perfil \"{{name}}\" importado correctamente",
"notInstalled": "{{browser}} no está instalado. Por favor descarga {{browser}} primero desde la ventana principal y luego intenta importar de nuevo.",
"importFailed": "Error al importar el perfil: {{error}}",
"importedAsPrefix": "Este perfil se importará como un perfil de",
"importedAsSuffix": ".",
"proxyOptional": "Proxy (Opcional)",
"noProxy": "Sin proxy",
"nextButton": "Siguiente",
"importButton": "Importar"
"importButton": "Importar",
"importedAs": "Este perfil se importará como un perfil de {{browser}}."
},
"syncTooltips": {
"syncing": "Sincronizando...",
@@ -1497,13 +1515,18 @@
"syncTooltipSyncing": "Sincronizando...",
"syncTooltipSyncedAt": "Sincronizado {{time}}",
"syncTooltipSynced": "Sincronizado",
"syncTooltipWaiting": "Esperando sincronización",
"syncTooltipWaiting": "Esperando para sincronizar",
"syncTooltipErrorWith": "Error de sincronización: {{error}}",
"syncTooltipError": "Error de sincronización",
"syncTooltipNotSynced": "Sin sincronizar",
"syncTooltipNotSynced": "No sincronizado",
"noTags": "Sin etiquetas",
"syncTooltipCloseToSync": "Cierra el perfil para sincronizar",
"syncTooltipDisabledWithLast": "Sincronización desactivada, última sincronización {{time}}"
"syncTooltipDisabledWithLast": "Sincronización desactivada, última sincronización {{time}}",
"addTagsPlaceholder": "Añadir etiquetas",
"tagsHeader": "Etiquetas",
"noteHeader": "Nota",
"vpnsHeading": "VPN",
"createByCountryHeading": "Crear por país"
},
"releaseTypeSelector": {
"noReleaseTypes": "No hay tipos de versión disponibles.",
@@ -1521,7 +1544,14 @@
"appUpdate": {
"toast": {
"updateFailed": "Error al actualizar Donut Browser",
"restartFailed": "Error al reiniciar"
"restartFailed": "Error al reiniciar",
"updateReady": "Actualización lista, reinicia para aplicar",
"manualDownloadRequired": "Descarga manual requerida",
"restartNow": "Reiniciar ahora",
"viewRelease": "Ver lanzamiento",
"later": "Más tarde",
"uploading": "Subiendo",
"downloading": "Descargando"
}
},
"browserDownload": {
@@ -1532,7 +1562,10 @@
"downloadFailed": "Error al descargar {{browser}} {{version}}",
"calculating": "calculando...",
"extractionFailed": "{{browser}} {{version}}: error de extracción",
"extractionFailedDescription": "El archivo dañado fue eliminado. Se volverá a descargar en el próximo intento."
"extractionFailedDescription": "El archivo dañado fue eliminado. Se volverá a descargar en el próximo intento.",
"extracting": "Extrayendo archivos del navegador... No cierre la aplicación.",
"verifying": "Verificando archivos del navegador...",
"downloadingRolling": "Descargando compilación rolling release..."
}
},
"versionUpdater": {
+60 -27
View File
@@ -60,7 +60,8 @@
"optional": "Optionnel",
"required": "Requis",
"unknownProfile": "Inconnu",
"mode": "Mode"
"mode": "Mode",
"never": "Jamais"
},
"time": {
"days": "jours",
@@ -72,7 +73,11 @@
"aria": {
"selectAll": "Tout sélectionner",
"selectRow": "Sélectionner la ligne",
"selectProfile": "Sélectionner le profil"
"selectProfile": "Sélectionner le profil",
"copy": "Copier dans le presse-papiers",
"copied": "Copié",
"showToken": "Afficher le jeton",
"hideToken": "Masquer le jeton"
},
"keys": {
"escape": "Échap"
@@ -87,7 +92,11 @@
"title": "Palette de commandes",
"description": "Rechercher une commande à exécuter..."
},
"noResults": "Aucun résultat trouvé."
"noResults": "Aucun résultat trouvé.",
"srOnly": {
"copy": "Copier",
"copied": "Copié"
}
},
"settings": {
"title": "Paramètres",
@@ -196,7 +205,8 @@
"group": "Groupe",
"proxy": "Proxy / VPN",
"lastLaunch": "Dernier lancement",
"empty": "Aucun profil trouvé."
"empty": "Aucun profil trouvé.",
"notSelected": "Non sélectionné"
},
"actions": {
"launch": "Lancer",
@@ -488,7 +498,8 @@
"deleteGroupAndProfiles": "Supprimer le Groupe et les Profils",
"loadProfilesFailed": "Échec du chargement des profils",
"unknownGroup": "Groupe inconnu",
"profileGroupsAriaLabel": "Groupes de profils"
"profileGroupsAriaLabel": "Groupes de profils",
"loading": "Chargement des groupes..."
},
"sync": {
"mode": {
@@ -631,7 +642,8 @@
"mcpAcceptTermsFirst": "(Acceptez d'abord les conditions Wayfern dans les Paramètres)",
"mcpStarted": "Serveur MCP démarré sur le port {{port}}",
"mcpStopped": "Serveur MCP arrêté",
"mcpToggleFailed": "Échec du basculement du serveur MCP"
"mcpToggleFailed": "Échec du basculement du serveur MCP",
"openSettings": "Ouvrir les paramètres d'intégrations"
},
"import": {
"title": "Importer un profil",
@@ -711,6 +723,10 @@
"webrtc": "Bloquer WebRTC",
"webgl": "Bloquer WebGL"
}
},
"shared": {
"browserBehavior": "Comportement du navigateur",
"allowAddonsOpenTabs": "Autoriser les modules complémentaires à ouvrir automatiquement de nouveaux onglets"
}
},
"cookies": {
@@ -875,7 +891,8 @@
"loadProxiesFailed": "Échec du chargement des proxies : {{error}}",
"setupProxyListenersFailed": "Échec de la configuration des écouteurs d’événements de proxies : {{error}}",
"loadVpnConfigsFailed": "Échec du chargement des configurations VPN : {{error}}",
"setupVpnListenersFailed": "Échec de la configuration des écouteurs d’événements VPN : {{error}}"
"setupVpnListenersFailed": "Échec de la configuration des écouteurs d’événements VPN : {{error}}",
"themeNotFound": "Thème Tokyo Night introuvable"
},
"browser": {
"camoufox": "Camoufox",
@@ -906,10 +923,10 @@
"platform": "Plateforme",
"platformVersion": "Version de la plateforme",
"appVersion": "Version de l'application",
"osCpu": "OS CPU",
"osCpu": "CPU OS",
"hardwareConcurrency": "Concurrence matérielle",
"maxTouchPoints": "Points tactiles maximum",
"doNotTrack": "Do Not Track",
"doNotTrack": "Ne pas suivre",
"selectDntPlaceholder": "Sélectionner la valeur DNT",
"dntAllowed": "0 (suivi autorisé)",
"dntNotAllowed": "1 (suivi non autorisé)",
@@ -931,8 +948,8 @@
"outerHeight": "Hauteur extérieure",
"innerWidth": "Largeur intérieure",
"innerHeight": "Hauteur intérieure",
"screenX": "Screen X",
"screenY": "Screen Y",
"screenX": "Écran X",
"screenY": "Écran Y",
"geolocation": "Géolocalisation",
"timezoneAndGeolocation": "Fuseau horaire et géolocalisation",
"timezoneGeolocationDescription": "Ces valeurs remplacent les APIs de fuseau horaire et de géolocalisation du navigateur.",
@@ -946,15 +963,15 @@
"region": "Région",
"script": "Script",
"webglProperties": "Propriétés WebGL",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglVendor": "Fournisseur WebGL",
"webglRenderer": "Moteur de rendu WebGL",
"webglParameters": "Paramètres WebGL",
"webglParametersJson": "Paramètres WebGL (JSON)",
"webgl2Parameters": "Paramètres WebGL2",
"webglShaderPrecisionFormats": "Formats de précision WebGL Shader",
"webgl2ShaderPrecisionFormats": "Formats de précision WebGL2 Shader",
"webglShaderPrecisionFormats": "Formats de précision shader WebGL",
"webgl2ShaderPrecisionFormats": "Formats de précision shader WebGL2",
"canvasFingerprint": "Canvas Fingerprint",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeed": "Graine de bruit Canvas",
"canvasNoiseSeedDescription": "Cette graine est utilisée pour générer une empreinte Canvas cohérente mais unique. Chaque profil doit avoir une graine différente.",
"fonts": "Polices",
"fontsJson": "Polices (JSON array)",
@@ -975,8 +992,8 @@
"maxChannelCount": "Nombre maximum de canaux",
"vendorInfo": "Informations du fournisseur",
"vendor": "Fournisseur",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"vendorSub": "Fournisseur Sub",
"productSub": "Produit Sub",
"brand": "Marque",
"brandVersion": "Version de la marque",
"proFeature": "Ceci est une fonctionnalité Pro",
@@ -1124,7 +1141,9 @@
"syncEnabled": "Synchronisation activée",
"syncDisabled": "Synchronisation désactivée",
"syncEnableTooltip": "Activer la synchronisation",
"syncDisableTooltip": "Désactiver la synchronisation"
"syncDisableTooltip": "Désactiver la synchronisation",
"loadGroupsFailed": "Échec du chargement des groupes d'extensions",
"assignGroupFailed": "Échec de l'attribution du groupe d'extensions"
},
"pro": {
"badge": "PRO",
@@ -1256,12 +1275,11 @@
"importedSuccess": "Profil « {{name}} » importé avec succès",
"notInstalled": "{{browser}} n'est pas installé. Veuillez télécharger {{browser}} depuis la fenêtre principale puis réessayer.",
"importFailed": "Échec de l'import du profil : {{error}}",
"importedAsPrefix": "Ce profil sera importé en tant que profil",
"importedAsSuffix": ".",
"proxyOptional": "Proxy (optionnel)",
"noProxy": "Aucun proxy",
"nextButton": "Suivant",
"importButton": "Importer"
"importButton": "Importer",
"importedAs": "Ce profil sera importé en tant que profil {{browser}}."
},
"syncTooltips": {
"syncing": "Synchronisation...",
@@ -1497,13 +1515,18 @@
"syncTooltipSyncing": "Synchronisation...",
"syncTooltipSyncedAt": "Synchronisé {{time}}",
"syncTooltipSynced": "Synchronisé",
"syncTooltipWaiting": "En attente de sync",
"syncTooltipWaiting": "En attente de synchronisation",
"syncTooltipErrorWith": "Erreur de sync : {{error}}",
"syncTooltipError": "Erreur de sync",
"syncTooltipError": "Erreur de synchronisation",
"syncTooltipNotSynced": "Non synchronisé",
"noTags": "Aucune étiquette",
"syncTooltipCloseToSync": "Fermez le profil pour synchroniser",
"syncTooltipDisabledWithLast": "Sync désactivée, dernière sync {{time}}"
"syncTooltipDisabledWithLast": "Sync désactivée, dernière sync {{time}}",
"addTagsPlaceholder": "Ajouter des étiquettes",
"tagsHeader": "Étiquettes",
"noteHeader": "Note",
"vpnsHeading": "VPN",
"createByCountryHeading": "Créer par pays"
},
"releaseTypeSelector": {
"noReleaseTypes": "Aucun type de version disponible.",
@@ -1521,7 +1544,14 @@
"appUpdate": {
"toast": {
"updateFailed": "Échec de la mise à jour de Donut Browser",
"restartFailed": "Échec du redémarrage"
"restartFailed": "Échec du redémarrage",
"updateReady": "Mise à jour prête, redémarrer pour appliquer",
"manualDownloadRequired": "Téléchargement manuel requis",
"restartNow": "Redémarrer maintenant",
"viewRelease": "Voir la version",
"later": "Plus tard",
"uploading": "Envoi",
"downloading": "Téléchargement"
}
},
"browserDownload": {
@@ -1532,7 +1562,10 @@
"downloadFailed": "Échec du téléchargement de {{browser}} {{version}}",
"calculating": "calcul en cours...",
"extractionFailed": "{{browser}} {{version}} : échec de lextraction",
"extractionFailedDescription": "Le fichier corrompu a été supprimé. Il sera retéléchargé lors de la prochaine tentative."
"extractionFailedDescription": "Le fichier corrompu a été supprimé. Il sera retéléchargé lors de la prochaine tentative.",
"extracting": "Extraction des fichiers du navigateur... Ne fermez pas l'application.",
"verifying": "Vérification des fichiers du navigateur...",
"downloadingRolling": "Téléchargement de la version rolling release..."
}
},
"versionUpdater": {
+58 -25
View File
@@ -60,7 +60,8 @@
"optional": "任意",
"required": "必須",
"unknownProfile": "不明",
"mode": "モード"
"mode": "モード",
"never": "一度もありません"
},
"time": {
"days": "日",
@@ -72,7 +73,11 @@
"aria": {
"selectAll": "すべて選択",
"selectRow": "行を選択",
"selectProfile": "プロファイルを選択"
"selectProfile": "プロファイルを選択",
"copy": "クリップボードにコピー",
"copied": "コピーしました",
"showToken": "トークンを表示",
"hideToken": "トークンを非表示"
},
"keys": {
"escape": "Esc"
@@ -87,7 +92,11 @@
"title": "コマンドパレット",
"description": "実行するコマンドを検索..."
},
"noResults": "結果が見つかりません。"
"noResults": "結果が見つかりません。",
"srOnly": {
"copy": "コピー",
"copied": "コピーしました"
}
},
"settings": {
"title": "設定",
@@ -196,7 +205,8 @@
"group": "グループ",
"proxy": "プロキシ / VPN",
"lastLaunch": "最終起動",
"empty": "プロファイルが見つかりません。"
"empty": "プロファイルが見つかりません。",
"notSelected": "未選択"
},
"actions": {
"launch": "起動",
@@ -488,7 +498,8 @@
"deleteGroupAndProfiles": "グループとプロファイルを削除",
"loadProfilesFailed": "プロファイルの読み込みに失敗しました",
"unknownGroup": "不明なグループ",
"profileGroupsAriaLabel": "プロファイルグループ"
"profileGroupsAriaLabel": "プロファイルグループ",
"loading": "グループを読み込み中..."
},
"sync": {
"mode": {
@@ -631,7 +642,8 @@
"mcpAcceptTermsFirst": "(設定で先に Wayfern の規約に同意してください)",
"mcpStarted": "MCP サーバーをポート {{port}} で起動しました",
"mcpStopped": "MCP サーバーを停止しました",
"mcpToggleFailed": "MCP サーバーの切り替えに失敗しました"
"mcpToggleFailed": "MCP サーバーの切り替えに失敗しました",
"openSettings": "統合設定を開く"
},
"import": {
"title": "プロファイルをインポート",
@@ -711,6 +723,10 @@
"webrtc": "WebRTCをブロック",
"webgl": "WebGLをブロック"
}
},
"shared": {
"browserBehavior": "ブラウザの動作",
"allowAddonsOpenTabs": "ブラウザアドオンが新しいタブを自動的に開くことを許可"
}
},
"cookies": {
@@ -875,7 +891,8 @@
"loadProxiesFailed": "プロキシの読み込みに失敗しました: {{error}}",
"setupProxyListenersFailed": "プロキシイベントリスナーの設定に失敗しました: {{error}}",
"loadVpnConfigsFailed": "VPN設定の読み込みに失敗しました: {{error}}",
"setupVpnListenersFailed": "VPNイベントリスナーの設定に失敗しました: {{error}}"
"setupVpnListenersFailed": "VPNイベントリスナーの設定に失敗しました: {{error}}",
"themeNotFound": "Tokyo Night テーマが見つかりません"
},
"browser": {
"camoufox": "Camoufox",
@@ -901,7 +918,7 @@
"blockWebRTC": "WebRTCをブロック",
"blockWebGL": "WebGLをブロック",
"navigatorProperties": "Navigatorプロパティ",
"userAgent": "User Agent",
"userAgent": "ユーザーエージェント",
"userAgentAndPlatform": "User Agent & Platform",
"platform": "Platform",
"platformVersion": "Platform Version",
@@ -909,7 +926,7 @@
"osCpu": "OS CPU",
"hardwareConcurrency": "Hardware Concurrency",
"maxTouchPoints": "最大タッチポイント数",
"doNotTrack": "Do Not Track",
"doNotTrack": "追跡しない",
"selectDntPlaceholder": "DNT値を選択",
"dntAllowed": "0(トラッキング許可)",
"dntNotAllowed": "1(トラッキング不許可)",
@@ -931,8 +948,8 @@
"outerHeight": "外側の高さ",
"innerWidth": "内側の幅",
"innerHeight": "内側の高さ",
"screenX": "Screen X",
"screenY": "Screen Y",
"screenX": "画面 X",
"screenY": "画面 Y",
"geolocation": "ジオロケーション",
"timezoneAndGeolocation": "タイムゾーンとジオロケーション",
"timezoneGeolocationDescription": "これらの値はブラウザのタイムゾーンとジオロケーションAPIを上書きします。",
@@ -946,15 +963,15 @@
"region": "地域",
"script": "スクリプト",
"webglProperties": "WebGLプロパティ",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglVendor": "WebGL ベンダー",
"webglRenderer": "WebGL レンダラー",
"webglParameters": "WebGLパラメータ",
"webglParametersJson": "WebGLパラメータ (JSON)",
"webgl2Parameters": "WebGL2パラメータ",
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
"webglShaderPrecisionFormats": "WebGL シェーダー精度フォーマット",
"webgl2ShaderPrecisionFormats": "WebGL2 シェーダー精度フォーマット",
"canvasFingerprint": "Canvas Fingerprint",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeed": "Canvas ノイズシード",
"canvasNoiseSeedDescription": "このシードは一貫性がありながらもユニークなCanvasフィンガープリントを生成するために使用されます。各プロファイルには異なるシードを設定してください。",
"fonts": "フォント",
"fontsJson": "フォント (JSON配列)",
@@ -975,8 +992,8 @@
"maxChannelCount": "最大チャンネル数",
"vendorInfo": "ベンダー情報",
"vendor": "ベンダー",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"vendorSub": "ベンダーサブ",
"productSub": "プロダクトサブ",
"brand": "ブランド",
"brandVersion": "ブランドバージョン",
"proFeature": "これはPro機能です",
@@ -1124,7 +1141,9 @@
"syncEnabled": "同期が有効",
"syncDisabled": "同期が無効",
"syncEnableTooltip": "同期を有効にする",
"syncDisableTooltip": "同期を無効にする"
"syncDisableTooltip": "同期を無効にする",
"loadGroupsFailed": "拡張機能グループの読み込みに失敗しました",
"assignGroupFailed": "拡張機能グループの割り当てに失敗しました"
},
"pro": {
"badge": "PRO",
@@ -1256,12 +1275,11 @@
"importedSuccess": "プロファイル「{{name}}」をインポートしました",
"notInstalled": "{{browser}} はインストールされていません。メインウィンドウから {{browser}} をダウンロードしてからもう一度インポートしてください。",
"importFailed": "プロファイルのインポートに失敗しました: {{error}}",
"importedAsPrefix": "このプロファイルは次のプロファイルとしてインポートされます:",
"importedAsSuffix": "",
"proxyOptional": "プロキシ (任意)",
"noProxy": "プロキシなし",
"nextButton": "次へ",
"importButton": "インポート"
"importButton": "インポート",
"importedAs": "このプロファイルは {{browser}} プロファイルとしてインポートされます。"
},
"syncTooltips": {
"syncing": "同期中...",
@@ -1503,7 +1521,12 @@
"syncTooltipNotSynced": "未同期",
"noTags": "タグなし",
"syncTooltipCloseToSync": "プロファイルを閉じて同期",
"syncTooltipDisabledWithLast": "同期無効、最終同期 {{time}}"
"syncTooltipDisabledWithLast": "同期無効、最終同期 {{time}}",
"addTagsPlaceholder": "タグを追加",
"tagsHeader": "タグ",
"noteHeader": "メモ",
"vpnsHeading": "VPN",
"createByCountryHeading": "国別に作成"
},
"releaseTypeSelector": {
"noReleaseTypes": "利用可能なリリースタイプがありません。",
@@ -1521,7 +1544,14 @@
"appUpdate": {
"toast": {
"updateFailed": "Donut Browser の更新に失敗しました",
"restartFailed": "再起動に失敗しました"
"restartFailed": "再起動に失敗しました",
"updateReady": "アップデートの準備完了。再起動して適用",
"manualDownloadRequired": "手動ダウンロードが必要です",
"restartNow": "今すぐ再起動",
"viewRelease": "リリースを見る",
"later": "後で",
"uploading": "アップロード中",
"downloading": "ダウンロード中"
}
},
"browserDownload": {
@@ -1532,7 +1562,10 @@
"downloadFailed": "{{browser}} {{version}} のダウンロードに失敗しました",
"calculating": "計算中...",
"extractionFailed": "{{browser}} {{version}}: 展開に失敗しました",
"extractionFailedDescription": "破損したファイルは削除されました。次回の試行時に再ダウンロードされます。"
"extractionFailedDescription": "破損したファイルは削除されました。次回の試行時に再ダウンロードされます。",
"extracting": "ブラウザファイルを展開中... アプリを閉じないでください。",
"verifying": "ブラウザファイルを検証中...",
"downloadingRolling": "ローリングリリースビルドをダウンロード中..."
}
},
"versionUpdater": {
+59 -26
View File
@@ -60,7 +60,8 @@
"optional": "Opcional",
"required": "Obrigatório",
"unknownProfile": "Desconhecido",
"mode": "Modo"
"mode": "Modo",
"never": "Nunca"
},
"time": {
"days": "dias",
@@ -72,7 +73,11 @@
"aria": {
"selectAll": "Selecionar tudo",
"selectRow": "Selecionar linha",
"selectProfile": "Selecionar perfil"
"selectProfile": "Selecionar perfil",
"copy": "Copiar para a área de transferência",
"copied": "Copiado",
"showToken": "Mostrar token",
"hideToken": "Ocultar token"
},
"keys": {
"escape": "Esc"
@@ -87,7 +92,11 @@
"title": "Paleta de comandos",
"description": "Pesquise um comando para executar..."
},
"noResults": "Nenhum resultado encontrado."
"noResults": "Nenhum resultado encontrado.",
"srOnly": {
"copy": "Copiar",
"copied": "Copiado"
}
},
"settings": {
"title": "Configurações",
@@ -196,7 +205,8 @@
"group": "Grupo",
"proxy": "Proxy / VPN",
"lastLaunch": "Último Início",
"empty": "Nenhum perfil encontrado."
"empty": "Nenhum perfil encontrado.",
"notSelected": "Não selecionado"
},
"actions": {
"launch": "Iniciar",
@@ -488,7 +498,8 @@
"deleteGroupAndProfiles": "Excluir Grupo e Perfis",
"loadProfilesFailed": "Falha ao carregar os perfis",
"unknownGroup": "Grupo desconhecido",
"profileGroupsAriaLabel": "Grupos de perfis"
"profileGroupsAriaLabel": "Grupos de perfis",
"loading": "Carregando grupos..."
},
"sync": {
"mode": {
@@ -631,7 +642,8 @@
"mcpAcceptTermsFirst": "(Aceite primeiro os termos da Wayfern nas Configurações)",
"mcpStarted": "Servidor MCP iniciado na porta {{port}}",
"mcpStopped": "Servidor MCP parado",
"mcpToggleFailed": "Falha ao alternar o servidor MCP"
"mcpToggleFailed": "Falha ao alternar o servidor MCP",
"openSettings": "Abrir configurações de integrações"
},
"import": {
"title": "Importar Perfil",
@@ -711,6 +723,10 @@
"webrtc": "Bloquear WebRTC",
"webgl": "Bloquear WebGL"
}
},
"shared": {
"browserBehavior": "Comportamento do navegador",
"allowAddonsOpenTabs": "Permitir que extensões abram novas abas automaticamente"
}
},
"cookies": {
@@ -875,7 +891,8 @@
"loadProxiesFailed": "Falha ao carregar os proxies: {{error}}",
"setupProxyListenersFailed": "Falha ao configurar os listeners de eventos de proxies: {{error}}",
"loadVpnConfigsFailed": "Falha ao carregar as configurações de VPN: {{error}}",
"setupVpnListenersFailed": "Falha ao configurar os listeners de eventos de VPN: {{error}}"
"setupVpnListenersFailed": "Falha ao configurar os listeners de eventos de VPN: {{error}}",
"themeNotFound": "Tema Tokyo Night não encontrado"
},
"browser": {
"camoufox": "Camoufox",
@@ -901,15 +918,15 @@
"blockWebRTC": "Bloquear WebRTC",
"blockWebGL": "Bloquear WebGL",
"navigatorProperties": "Propriedades do Navigator",
"userAgent": "User Agent",
"userAgent": "Agente do usuário",
"userAgentAndPlatform": "User Agent & Platform",
"platform": "Platform",
"platformVersion": "Platform Version",
"appVersion": "App Version",
"osCpu": "OS CPU",
"osCpu": "CPU do SO",
"hardwareConcurrency": "Hardware Concurrency",
"maxTouchPoints": "Pontos de Toque Máximos",
"doNotTrack": "Do Not Track",
"doNotTrack": "Não rastrear",
"selectDntPlaceholder": "Selecionar valor DNT",
"dntAllowed": "0 (rastreamento permitido)",
"dntNotAllowed": "1 (rastreamento não permitido)",
@@ -931,8 +948,8 @@
"outerHeight": "Altura Externa",
"innerWidth": "Largura Interna",
"innerHeight": "Altura Interna",
"screenX": "Screen X",
"screenY": "Screen Y",
"screenX": "Tela X",
"screenY": "Tela Y",
"geolocation": "Geolocalização",
"timezoneAndGeolocation": "Fuso Horário e Geolocalização",
"timezoneGeolocationDescription": "Estes valores substituem as APIs de fuso horário e geolocalização do navegador.",
@@ -946,15 +963,15 @@
"region": "Região",
"script": "Script",
"webglProperties": "Propriedades WebGL",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglVendor": "Fornecedor WebGL",
"webglRenderer": "Renderizador WebGL",
"webglParameters": "Parâmetros WebGL",
"webglParametersJson": "Parâmetros WebGL (JSON)",
"webgl2Parameters": "Parâmetros WebGL2",
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
"webglShaderPrecisionFormats": "Formatos de precisão de shader WebGL",
"webgl2ShaderPrecisionFormats": "Formatos de precisão de shader WebGL2",
"canvasFingerprint": "Canvas Fingerprint",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeed": "Semente de ruído Canvas",
"canvasNoiseSeedDescription": "Este seed é usado para gerar uma impressão digital Canvas consistente, mas única. Cada perfil deve ter um seed diferente.",
"fonts": "Fontes",
"fontsJson": "Fontes (JSON array)",
@@ -975,8 +992,8 @@
"maxChannelCount": "Contagem Máxima de Canais",
"vendorInfo": "Informações do Fabricante",
"vendor": "Fabricante",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"vendorSub": "Fornecedor Sub",
"productSub": "Produto Sub",
"brand": "Marca",
"brandVersion": "Versão da Marca",
"proFeature": "Este é um recurso Pro",
@@ -1124,7 +1141,9 @@
"syncEnabled": "Sincronização ativada",
"syncDisabled": "Sincronização desativada",
"syncEnableTooltip": "Ativar sincronização",
"syncDisableTooltip": "Desativar sincronização"
"syncDisableTooltip": "Desativar sincronização",
"loadGroupsFailed": "Falha ao carregar grupos de extensões",
"assignGroupFailed": "Falha ao atribuir grupo de extensões"
},
"pro": {
"badge": "PRO",
@@ -1256,12 +1275,11 @@
"importedSuccess": "Perfil \"{{name}}\" importado com sucesso",
"notInstalled": "{{browser}} não está instalado. Baixe {{browser}} primeiro pela janela principal e tente importar novamente.",
"importFailed": "Falha ao importar perfil: {{error}}",
"importedAsPrefix": "Este perfil será importado como um perfil",
"importedAsSuffix": ".",
"proxyOptional": "Proxy (Opcional)",
"noProxy": "Sem proxy",
"nextButton": "Próximo",
"importButton": "Importar"
"importButton": "Importar",
"importedAs": "Este perfil será importado como um perfil {{browser}}."
},
"syncTooltips": {
"syncing": "Sincronizando...",
@@ -1503,7 +1521,12 @@
"syncTooltipNotSynced": "Não sincronizado",
"noTags": "Sem tags",
"syncTooltipCloseToSync": "Feche o perfil para sincronizar",
"syncTooltipDisabledWithLast": "Sincronização desativada, última sincronização {{time}}"
"syncTooltipDisabledWithLast": "Sincronização desativada, última sincronização {{time}}",
"addTagsPlaceholder": "Adicionar etiquetas",
"tagsHeader": "Etiquetas",
"noteHeader": "Nota",
"vpnsHeading": "VPNs",
"createByCountryHeading": "Criar por país"
},
"releaseTypeSelector": {
"noReleaseTypes": "Nenhum tipo de versão disponível.",
@@ -1521,7 +1544,14 @@
"appUpdate": {
"toast": {
"updateFailed": "Falha ao atualizar o Donut Browser",
"restartFailed": "Falha ao reiniciar"
"restartFailed": "Falha ao reiniciar",
"updateReady": "Atualização pronta, reinicie para aplicar",
"manualDownloadRequired": "Download manual necessário",
"restartNow": "Reiniciar agora",
"viewRelease": "Ver lançamento",
"later": "Mais tarde",
"uploading": "Enviando",
"downloading": "Baixando"
}
},
"browserDownload": {
@@ -1532,7 +1562,10 @@
"downloadFailed": "Falha ao baixar {{browser}} {{version}}",
"calculating": "calculando...",
"extractionFailed": "{{browser}} {{version}}: falha na extração",
"extractionFailedDescription": "O arquivo corrompido foi excluído. Será baixado novamente na próxima tentativa."
"extractionFailedDescription": "O arquivo corrompido foi excluído. Será baixado novamente na próxima tentativa.",
"extracting": "Extraindo arquivos do navegador... Não feche o aplicativo.",
"verifying": "Verificando arquivos do navegador...",
"downloadingRolling": "Baixando build rolling release..."
}
},
"versionUpdater": {
+58 -25
View File
@@ -60,7 +60,8 @@
"optional": "Необязательно",
"required": "Обязательно",
"unknownProfile": "Неизвестный",
"mode": "Режим"
"mode": "Режим",
"never": "Никогда"
},
"time": {
"days": "дней",
@@ -72,7 +73,11 @@
"aria": {
"selectAll": "Выбрать все",
"selectRow": "Выбрать строку",
"selectProfile": "Выбрать профиль"
"selectProfile": "Выбрать профиль",
"copy": "Скопировать в буфер обмена",
"copied": "Скопировано",
"showToken": "Показать токен",
"hideToken": "Скрыть токен"
},
"keys": {
"escape": "Esc"
@@ -87,7 +92,11 @@
"title": "Палитра команд",
"description": "Найдите команду для выполнения..."
},
"noResults": "Результаты не найдены."
"noResults": "Результаты не найдены.",
"srOnly": {
"copy": "Скопировать",
"copied": "Скопировано"
}
},
"settings": {
"title": "Настройки",
@@ -196,7 +205,8 @@
"group": "Группа",
"proxy": "Прокси / VPN",
"lastLaunch": "Последний запуск",
"empty": "Профили не найдены."
"empty": "Профили не найдены.",
"notSelected": "Не выбрано"
},
"actions": {
"launch": "Запустить",
@@ -488,7 +498,8 @@
"deleteGroupAndProfiles": "Удалить группу и профили",
"loadProfilesFailed": "Не удалось загрузить профили",
"unknownGroup": "Неизвестная группа",
"profileGroupsAriaLabel": "Группы профилей"
"profileGroupsAriaLabel": "Группы профилей",
"loading": "Загрузка групп..."
},
"sync": {
"mode": {
@@ -631,7 +642,8 @@
"mcpAcceptTermsFirst": "(Сначала примите условия Wayfern в Настройках)",
"mcpStarted": "MCP сервер запущен на порту {{port}}",
"mcpStopped": "MCP сервер остановлен",
"mcpToggleFailed": "Не удалось переключить MCP сервер"
"mcpToggleFailed": "Не удалось переключить MCP сервер",
"openSettings": "Открыть настройки интеграций"
},
"import": {
"title": "Импорт профиля",
@@ -711,6 +723,10 @@
"webrtc": "Блокировать WebRTC",
"webgl": "Блокировать WebGL"
}
},
"shared": {
"browserBehavior": "Поведение браузера",
"allowAddonsOpenTabs": "Разрешить расширениям браузера автоматически открывать новые вкладки"
}
},
"cookies": {
@@ -875,7 +891,8 @@
"loadProxiesFailed": "Не удалось загрузить прокси: {{error}}",
"setupProxyListenersFailed": "Не удалось настроить слушатели событий прокси: {{error}}",
"loadVpnConfigsFailed": "Не удалось загрузить конфигурации VPN: {{error}}",
"setupVpnListenersFailed": "Не удалось настроить слушатели событий VPN: {{error}}"
"setupVpnListenersFailed": "Не удалось настроить слушатели событий VPN: {{error}}",
"themeNotFound": "Тема Tokyo Night не найдена"
},
"browser": {
"camoufox": "Camoufox",
@@ -906,10 +923,10 @@
"platform": "Платформа",
"platformVersion": "Версия платформы",
"appVersion": "Версия приложения",
"osCpu": "OS CPU",
"osCpu": "ЦП ОС",
"hardwareConcurrency": "Количество потоков процессора",
"maxTouchPoints": "Максимальное количество точек касания",
"doNotTrack": "Do Not Track",
"doNotTrack": "Не отслеживать",
"selectDntPlaceholder": "Выберите значение DNT",
"dntAllowed": "0 (отслеживание разрешено)",
"dntNotAllowed": "1 (отслеживание не разрешено)",
@@ -931,8 +948,8 @@
"outerHeight": "Внешняя высота",
"innerWidth": "Внутренняя ширина",
"innerHeight": "Внутренняя высота",
"screenX": "Screen X",
"screenY": "Screen Y",
"screenX": "Экран X",
"screenY": "Экран Y",
"geolocation": "Геолокация",
"timezoneAndGeolocation": "Часовой пояс и геолокация",
"timezoneGeolocationDescription": "Эти значения переопределяют API часового пояса и геолокации браузера.",
@@ -946,15 +963,15 @@
"region": "Регион",
"script": "Скрипт",
"webglProperties": "Свойства WebGL",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglVendor": "Производитель WebGL",
"webglRenderer": "Рендерер WebGL",
"webglParameters": "Параметры WebGL",
"webglParametersJson": "Параметры WebGL (JSON)",
"webgl2Parameters": "Параметры WebGL2",
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
"webglShaderPrecisionFormats": "Форматы точности шейдера WebGL",
"webgl2ShaderPrecisionFormats": "Форматы точности шейдера WebGL2",
"canvasFingerprint": "Отпечаток Canvas",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeed": "Сид шума Canvas",
"canvasNoiseSeedDescription": "Это зерно используется для генерации постоянного, но уникального отпечатка Canvas. У каждого профиля должно быть своё зерно.",
"fonts": "Шрифты",
"fontsJson": "Шрифты (JSON-массив)",
@@ -975,8 +992,8 @@
"maxChannelCount": "Максимальное количество каналов",
"vendorInfo": "Информация о производителе",
"vendor": "Производитель",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"vendorSub": "Подверсия производителя",
"productSub": "Подверсия продукта",
"brand": "Бренд",
"brandVersion": "Версия бренда",
"proFeature": "Это функция Pro",
@@ -1124,7 +1141,9 @@
"syncEnabled": "Синхронизация включена",
"syncDisabled": "Синхронизация отключена",
"syncEnableTooltip": "Включить синхронизацию",
"syncDisableTooltip": "Отключить синхронизацию"
"syncDisableTooltip": "Отключить синхронизацию",
"loadGroupsFailed": "Не удалось загрузить группы расширений",
"assignGroupFailed": "Не удалось назначить группу расширений"
},
"pro": {
"badge": "PRO",
@@ -1256,12 +1275,11 @@
"importedSuccess": "Профиль «{{name}}» успешно импортирован",
"notInstalled": "{{browser}} не установлен. Сначала загрузите {{browser}} из главного окна, затем попробуйте импортировать снова.",
"importFailed": "Не удалось импортировать профиль: {{error}}",
"importedAsPrefix": "Этот профиль будет импортирован как профиль",
"importedAsSuffix": ".",
"proxyOptional": "Прокси (необязательно)",
"noProxy": "Без прокси",
"nextButton": "Далее",
"importButton": "Импорт"
"importButton": "Импорт",
"importedAs": "Этот профиль будет импортирован как профиль {{browser}}."
},
"syncTooltips": {
"syncing": "Синхронизация...",
@@ -1503,7 +1521,12 @@
"syncTooltipNotSynced": "Не синхронизировано",
"noTags": "Нет тегов",
"syncTooltipCloseToSync": "Закройте профиль для синхронизации",
"syncTooltipDisabledWithLast": "Синхронизация отключена, последняя синхронизация {{time}}"
"syncTooltipDisabledWithLast": "Синхронизация отключена, последняя синхронизация {{time}}",
"addTagsPlaceholder": "Добавить теги",
"tagsHeader": "Теги",
"noteHeader": "Заметка",
"vpnsHeading": "VPN",
"createByCountryHeading": "Создать по стране"
},
"releaseTypeSelector": {
"noReleaseTypes": "Нет доступных типов выпусков.",
@@ -1521,7 +1544,14 @@
"appUpdate": {
"toast": {
"updateFailed": "Не удалось обновить Donut Browser",
"restartFailed": "Не удалось перезапустить"
"restartFailed": "Не удалось перезапустить",
"updateReady": "Обновление готово, перезапустите для применения",
"manualDownloadRequired": "Требуется ручная загрузка",
"restartNow": "Перезапустить сейчас",
"viewRelease": "Посмотреть релиз",
"later": "Позже",
"uploading": "Загрузка",
"downloading": "Скачивание"
}
},
"browserDownload": {
@@ -1532,7 +1562,10 @@
"downloadFailed": "Не удалось загрузить {{browser}} {{version}}",
"calculating": "вычисление...",
"extractionFailed": "{{browser}} {{version}}: ошибка распаковки",
"extractionFailedDescription": "Повреждённый файл удалён. Он будет повторно загружен при следующей попытке."
"extractionFailedDescription": "Повреждённый файл удалён. Он будет повторно загружен при следующей попытке.",
"extracting": "Распаковка файлов браузера... Не закрывайте приложение.",
"verifying": "Проверка файлов браузера...",
"downloadingRolling": "Загрузка rolling release сборки..."
}
},
"versionUpdater": {
+59 -26
View File
@@ -60,7 +60,8 @@
"optional": "可选",
"required": "必填",
"unknownProfile": "未知",
"mode": "模式"
"mode": "模式",
"never": "从不"
},
"time": {
"days": "天",
@@ -72,7 +73,11 @@
"aria": {
"selectAll": "全选",
"selectRow": "选择行",
"selectProfile": "选择配置文件"
"selectProfile": "选择配置文件",
"copy": "复制到剪贴板",
"copied": "已复制",
"showToken": "显示令牌",
"hideToken": "隐藏令牌"
},
"keys": {
"escape": "Esc"
@@ -87,7 +92,11 @@
"title": "命令面板",
"description": "搜索要执行的命令..."
},
"noResults": "未找到结果。"
"noResults": "未找到结果。",
"srOnly": {
"copy": "复制",
"copied": "已复制"
}
},
"settings": {
"title": "设置",
@@ -196,7 +205,8 @@
"group": "分组",
"proxy": "代理 / VPN",
"lastLaunch": "最后启动",
"empty": "未找到配置文件。"
"empty": "未找到配置文件。",
"notSelected": "未选择"
},
"actions": {
"launch": "启动",
@@ -488,7 +498,8 @@
"deleteGroupAndProfiles": "删除组和配置文件",
"loadProfilesFailed": "加载配置文件失败",
"unknownGroup": "未知分组",
"profileGroupsAriaLabel": "配置文件分组"
"profileGroupsAriaLabel": "配置文件分组",
"loading": "正在加载组..."
},
"sync": {
"mode": {
@@ -631,7 +642,8 @@
"mcpAcceptTermsFirst": "(请先在设置中接受 Wayfern 条款)",
"mcpStarted": "MCP 服务器已在端口 {{port}} 上启动",
"mcpStopped": "MCP 服务器已停止",
"mcpToggleFailed": "切换 MCP 服务器失败"
"mcpToggleFailed": "切换 MCP 服务器失败",
"openSettings": "打开集成设置"
},
"import": {
"title": "导入配置文件",
@@ -711,6 +723,10 @@
"webrtc": "阻止 WebRTC",
"webgl": "阻止 WebGL"
}
},
"shared": {
"browserBehavior": "浏览器行为",
"allowAddonsOpenTabs": "允许浏览器附加组件自动打开新标签页"
}
},
"cookies": {
@@ -875,7 +891,8 @@
"loadProxiesFailed": "加载代理失败: {{error}}",
"setupProxyListenersFailed": "设置代理事件监听器失败: {{error}}",
"loadVpnConfigsFailed": "加载 VPN 配置失败: {{error}}",
"setupVpnListenersFailed": "设置 VPN 事件监听器失败: {{error}}"
"setupVpnListenersFailed": "设置 VPN 事件监听器失败: {{error}}",
"themeNotFound": "未找到 Tokyo Night 主题"
},
"browser": {
"camoufox": "Camoufox",
@@ -901,15 +918,15 @@
"blockWebRTC": "阻止 WebRTC",
"blockWebGL": "阻止 WebGL",
"navigatorProperties": "Navigator 属性",
"userAgent": "User Agent",
"userAgent": "用户代理",
"userAgentAndPlatform": "User Agent 和平台",
"platform": "平台",
"platformVersion": "平台版本",
"appVersion": "应用版本",
"osCpu": "OS CPU",
"osCpu": "操作系统 CPU",
"hardwareConcurrency": "硬件并发数",
"maxTouchPoints": "最大触摸点数",
"doNotTrack": "Do Not Track",
"doNotTrack": "请勿跟踪",
"selectDntPlaceholder": "选择 DNT 值",
"dntAllowed": "0(允许跟踪)",
"dntNotAllowed": "1(不允许跟踪)",
@@ -931,8 +948,8 @@
"outerHeight": "外部高度",
"innerWidth": "内部宽度",
"innerHeight": "内部高度",
"screenX": "Screen X",
"screenY": "Screen Y",
"screenX": "屏幕 X",
"screenY": "屏幕 Y",
"geolocation": "地理位置",
"timezoneAndGeolocation": "时区和地理位置",
"timezoneGeolocationDescription": "这些值会覆盖浏览器的时区和地理位置 API。",
@@ -946,15 +963,15 @@
"region": "地区",
"script": "脚本",
"webglProperties": "WebGL 属性",
"webglVendor": "WebGL Vendor",
"webglRenderer": "WebGL Renderer",
"webglVendor": "WebGL 供应商",
"webglRenderer": "WebGL 渲染器",
"webglParameters": "WebGL 参数",
"webglParametersJson": "WebGL 参数 (JSON)",
"webgl2Parameters": "WebGL2 参数",
"webglShaderPrecisionFormats": "WebGL Shader Precision Formats",
"webgl2ShaderPrecisionFormats": "WebGL2 Shader Precision Formats",
"webglShaderPrecisionFormats": "WebGL 着色器精度格式",
"webgl2ShaderPrecisionFormats": "WebGL2 着色器精度格式",
"canvasFingerprint": "Canvas 指纹",
"canvasNoiseSeed": "Canvas Noise Seed",
"canvasNoiseSeed": "Canvas 噪声种子",
"canvasNoiseSeedDescription": "此种子用于生成一致但唯一的 Canvas 指纹。每个配置文件应使用不同的种子。",
"fonts": "字体",
"fontsJson": "字体 (JSON 数组)",
@@ -975,8 +992,8 @@
"maxChannelCount": "最大通道数",
"vendorInfo": "供应商信息",
"vendor": "供应商",
"vendorSub": "Vendor Sub",
"productSub": "Product Sub",
"vendorSub": "供应商子版本",
"productSub": "产品子版本",
"brand": "品牌",
"brandVersion": "品牌版本",
"proFeature": "这是 Pro 功能",
@@ -1124,7 +1141,9 @@
"syncEnabled": "同步已启用",
"syncDisabled": "同步已禁用",
"syncEnableTooltip": "启用同步",
"syncDisableTooltip": "禁用同步"
"syncDisableTooltip": "禁用同步",
"loadGroupsFailed": "加载扩展组失败",
"assignGroupFailed": "分配扩展组失败"
},
"pro": {
"badge": "PRO",
@@ -1256,12 +1275,11 @@
"importedSuccess": "已成功导入配置文件「{{name}}」",
"notInstalled": "{{browser}} 未安装。请先从主窗口下载 {{browser}},然后再尝试导入。",
"importFailed": "导入配置文件失败: {{error}}",
"importedAsPrefix": "此配置文件将作为以下配置文件导入:",
"importedAsSuffix": "",
"proxyOptional": "代理 (可选)",
"noProxy": "无代理",
"nextButton": "下一步",
"importButton": "导入"
"importButton": "导入",
"importedAs": "此配置文件将作为 {{browser}} 配置文件导入。"
},
"syncTooltips": {
"syncing": "同步中...",
@@ -1503,7 +1521,12 @@
"syncTooltipNotSynced": "未同步",
"noTags": "无标签",
"syncTooltipCloseToSync": "关闭配置文件以进行同步",
"syncTooltipDisabledWithLast": "同步已禁用,上次同步 {{time}}"
"syncTooltipDisabledWithLast": "同步已禁用,上次同步 {{time}}",
"addTagsPlaceholder": "添加标签",
"tagsHeader": "标签",
"noteHeader": "备注",
"vpnsHeading": "VPN",
"createByCountryHeading": "按国家创建"
},
"releaseTypeSelector": {
"noReleaseTypes": "没有可用的发布类型。",
@@ -1521,7 +1544,14 @@
"appUpdate": {
"toast": {
"updateFailed": "更新 Donut Browser 失败",
"restartFailed": "重启失败"
"restartFailed": "重启失败",
"updateReady": "更新就绪,请重启以应用",
"manualDownloadRequired": "需要手动下载",
"restartNow": "立即重启",
"viewRelease": "查看版本",
"later": "稍后",
"uploading": "上传中",
"downloading": "下载中"
}
},
"browserDownload": {
@@ -1532,7 +1562,10 @@
"downloadFailed": "下载 {{browser}} {{version}} 失败",
"calculating": "计算中...",
"extractionFailed": "{{browser}} {{version}}: 解压失败",
"extractionFailedDescription": "损坏的文件已删除。下次尝试时将重新下载。"
"extractionFailedDescription": "损坏的文件已删除。下次尝试时将重新下载。",
"extracting": "正在提取浏览器文件...请不要关闭应用。",
"verifying": "正在验证浏览器文件...",
"downloadingRolling": "正在下载滚动发布版本..."
}
},
"versionUpdater": {