diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1bebf61b1..6c9e6ebe6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -72,8 +72,8 @@ User scripts are compiled with sandbox context isolation: ## Technology Stack - **React 18** - Component framework with automatic runtime -- **Arco Design** - UI component library (`@arco-design/web-react`) -- **UnoCSS** - Atomic CSS framework for styling +- **shadcn/ui** - UI component library built on Radix UI +- **Tailwind CSS** - Utility-first CSS framework for styling - **Rspack** - Fast bundler (Webpack alternative) with SWC - **TypeScript** - Type-safe development @@ -95,7 +95,7 @@ pnpm run coverage # Generate coverage reports ``` **Testing Patterns:** -- Uses Vitest with jsdom environment +- Uses Vitest with jsdom/happy-dom environment - Chrome extension APIs mocked via `@Packages/chrome-extension-mock` - Message system testing with `MockMessage` classes - Sandbox testing validates script isolation diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3569ba69d..53ed25593 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -5,7 +5,7 @@ on: branches: - main - release/* - - dev + - develop/* paths-ignore: - ".github/**" - ".gitignore" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b63f4104a..043641895 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -5,7 +5,6 @@ on: branches: - main - release/* - - dev - develop/* pull_request: diff --git a/.husky/pre-commit b/.husky/pre-commit index 46c8005ca..511d606d3 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -6,7 +6,23 @@ if [ "$SKIP_PRE_COMMIT" = "1" ]; then exit 0 fi -pnpm run lint || exit 1 +lint_files=$(mktemp) +git diff --cached --name-only -z --diff-filter=ACMR -- "*.js" "*.jsx" "*.ts" "*.tsx" "*.mjs" "*.cjs" > "$lint_files" + +if [ -s "$lint_files" ]; then + pnpm run typecheck || { + rm -f "$lint_files" + exit 1 + } + xargs -0 pnpm exec eslint < "$lint_files" || { + rm -f "$lint_files" + exit 1 + } +else + echo "No staged JS/TS files, skipping lint checks" +fi + +rm -f "$lint_files" # 在 main 或 release/* 分支上提交时额外跑测试 branch=$(git rev-parse --abbrev-ref HEAD) diff --git a/AGENTS.md b/AGENTS.md index 441c321e1..aa04a6bfb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,8 +14,8 @@ This file provides guidance to AI coding agents (Claude Code, etc.) when working > **Before any translation/localization work, read [`docs/translation/README.md`](docs/translation/README.md)** — > the single source of truth for translation. Whenever you add or change localized content -> (`src/locales//translation.json`, per-language docs, UI copy, or test snapshots), you must first read -> that guide and follow the matching `docs/translation/terminology-.md` if it exists. +> (`src/locales//*.json` namespace files, per-language docs, UI copy, or test snapshots), you must first +> read that guide and follow the matching `docs/translation/terminology-.md` if it exists. > **Before adding, editing, reorganizing, or reviewing any contributor doc (this file or `docs/*`), read > [`docs/DOC-MAINTENANCE.md`](docs/DOC-MAINTENANCE.md)** — keep the doc set organized (links resolve, index @@ -27,7 +27,12 @@ This file provides guidance to AI coding agents (Claude Code, etc.) when working ## Project Overview -ScriptCat — Manifest V3 browser extension that runs Tampermonkey-compatible user scripts. TypeScript + React 18 + Rspack. Package manager is **pnpm** (preinstall enforces). +ScriptCat — Manifest V3 browser extension that runs Tampermonkey-compatible user scripts. TypeScript + React 19 + Rspack. Package manager is **pnpm** (preinstall enforces). + +> **UI stack.** The presentation layer (`src/pages/`) is built with **shadcn/ui + Tailwind CSS v4** on +> **React 19** (migrated from Arco Design + UnoCSS). The concrete UI/theme rules live in +> [`docs/DEVELOP.md`](docs/DEVELOP.md); the design system (color tokens, components, layout/motion/state +> patterns, new-page recipe) lives in [`docs/DESIGN.md`](docs/DESIGN.md) — read it before building any page. ## Engineering Principles diff --git a/components.json b/components.json new file mode 100644 index 000000000..a5ab072b4 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@App/pages/components", + "utils": "@App/pkg/utils/cn", + "ui": "@App/pages/components/ui", + "lib": "@App/pkg", + "hooks": "@App/pages/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3a4fdce8f..4c63bf2c3 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -481,7 +481,7 @@ Output goes to `dist/ext/src/[name].js` (cleaned each build). Notable behavior: test-only — defined in `vitest.config.ts` / `tsconfig.json`, not in the Rspack build). - **Dev vs prod** via `NODE_ENV`: dev enables watch + inline source maps (skipped when `NO_MAP=true`, needed for incognito); prod minifies with SWC + Lightning CSS and drops debug. -- **Code splitting** pulls big libs into named `lib_*` chunks (react, monaco, arco, dnd-kit, eslint, message), +- **Code splitting** pulls big libs into named `lib_*` chunks (react, monaco, radix-ui, dnd-kit, eslint, message), but **never splits** `service_worker`, `content`, `inject`, `scripting`, or the workers — MV3 requires those to be single self-contained files. - **`CopyRspackPlugin`** copies [`src/manifest.json`](../src/manifest.json) — its `transform` rewrites the beta @@ -565,7 +565,7 @@ premature abstraction. ## 11. Testing the Internals -- **Unit (Vitest + jsdom).** Co-locate `*.test.ts` next to source. `chrome.*` is mocked via +- **Unit (Vitest + jsdom/happy-dom).** Co-locate `*.test.ts` next to source. `chrome.*` is mocked via [`@Packages/chrome-extension-mock`](../packages/chrome-extension-mock) (`tests/vitest.setup.ts`); message-bus behavior uses `MockMessage`. Run one file: `pnpm test -- --run path/to/file.test.ts`. - **TDD first.** Write the failing test before the implementation. When a test fails, fix the code — don't edit diff --git a/docs/CONTRIBUTING_RU.md b/docs/CONTRIBUTING_RU.md index 1db40f4bc..fb5fcefc9 100644 --- a/docs/CONTRIBUTING_RU.md +++ b/docs/CONTRIBUTING_RU.md @@ -69,9 +69,9 @@ pnpm start Файлы перевода ScriptCat размещены на GitHub. Мы приветствуем вклад через Pull Request. -- Файлы перевода находятся в каталоге [`src/locales`](https://github.com/scriptscat/scriptcat/tree/main/src/locales); для каждого языка есть отдельный файл `<язык>/translation.json` -- **Улучшение существующего перевода**: отредактируйте соответствующий `translation.json` напрямую -- **Добавление нового языка**: создайте каталог в `src/locales/` (например, `fr-FR`), скопируйте `en-US/translation.json` в качестве шаблона и переведите строки, затем зарегистрируйте локаль в `src/locales/locales.ts` +- Файлы перевода находятся в каталоге [`src/locales`](https://github.com/scriptscat/scriptcat/tree/main/src/locales); для каждого языка есть каталог `<язык>/`, разделённый по пространствам имён на несколько файлов `*.json` (например, `common.json`, `popup.json`, `script.json`) +- **Улучшение существующего перевода**: отредактируйте нужный файл `*.json` соответствующего пространства имён напрямую +- **Добавление нового языка**: создайте каталог в `src/locales/` (например, `fr-FR`), скопируйте файлы `*.json` каждого пространства имён и `index.ts` из `en-US/` в качестве шаблона и переведите строки, затем зарегистрируйте локаль в `src/locales/locales.ts` - По завершении создайте Pull Request в ветку `main` ## Участие в разработке @@ -86,8 +86,8 @@ pnpm run lint Для разработки страниц ScriptCat используются следующие технологии: - [React](https://reactjs.org/) -- UI-фреймворк [arco](https://arco.design) -- CSS-фреймворк [unocss](https://unocss.dev/interactive/) +- UI-фреймворк [shadcn/ui](https://ui.shadcn.com/) (на основе [Radix UI](https://www.radix-ui.com/)) +- CSS-фреймворк [Tailwind CSS](https://tailwindcss.com/) - Инструмент сборки RsPack [rspack](https://rspack.dev/) Если вы хотите запустить ScriptCat локально, вы можете использовать следующую команду: diff --git a/docs/CONTRIBUTING_ZH.md b/docs/CONTRIBUTING_ZH.md index 3eb0af5fc..201ec8efd 100644 --- a/docs/CONTRIBUTING_ZH.md +++ b/docs/CONTRIBUTING_ZH.md @@ -71,9 +71,9 @@ pnpm start ScriptCat 的翻译文件托管在 GitHub 上,欢迎通过 Pull Request 贡献翻译。 -- 翻译文件位于 [`src/locales`](https://github.com/scriptscat/scriptcat/tree/main/src/locales),每种语言对应一个 `<语言代码>/translation.json` -- **改进已有翻译**:直接编辑对应语言的 `translation.json` -- **新增语言**:在 `src/locales/` 下新建对应语言代码目录(如 `fr-FR`),复制 `en-US/translation.json` 作为模板进行翻译,并在 `src/locales/locales.ts` 中注册 +- 翻译文件位于 [`src/locales`](https://github.com/scriptscat/scriptcat/tree/main/src/locales),每种语言对应一个 `<语言代码>/` 目录,按命名空间拆分为多个 `*.json` 文件(如 `common.json`、`popup.json`、`script.json`) +- **改进已有翻译**:直接编辑对应语言目录下相应命名空间的 `*.json` 文件 +- **新增语言**:在 `src/locales/` 下新建对应语言代码目录(如 `fr-FR`),复制 `en-US/` 下的各命名空间 `*.json` 与 `index.ts` 作为模板进行翻译,并在 `src/locales/locales.ts` 中注册 - 完成后向 `main` 分支提交 Pull Request 即可 ## 参与开发 @@ -88,8 +88,8 @@ pnpm run lint ScriptCat 的页面开发使用了以下技术: - [React](https://reactjs.org/) -- UI 框架 [arco](https://arco.design) -- CSS 框架 [unocss](https://unocss.dev/interactive/) +- UI 框架 [shadcn/ui](https://ui.shadcn.com/)(基于 [Radix UI](https://www.radix-ui.com/)) +- CSS 框架 [Tailwind CSS](https://tailwindcss.com/) - RsPack 打包工具 [rspack](https://rspack.dev/) 如果你想在本地运行 ScriptCat,可以使用以下命令: diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 000000000..c85dc6697 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,476 @@ +# ScriptCat Design System + +> **A reuse-oriented design reference.** It consolidates the visual language that lives in `src/index.css` and the shadcn component layer into one place you can copy from: **color tokens (full light/dark values), the theming mechanism, the component palette, layout & responsive patterns, motion, state patterns, and an end-to-end new-page recipe.** Read this before building any new page, dialog, or block so it stays visually and behaviorally consistent with the rest of the app. + +> **Stack in one line:** React 19 + shadcn/ui (Radix primitives, `new-york` style) + Tailwind CSS v4 + React Router. Colors and motion are defined in the `@theme inline` block of `src/index.css`. **There is no `tailwind.config.js`** (Tailwind v4); PostCSS runs through `@tailwindcss/postcss` (`postcss.config.mjs`); **class names have no prefix** (`bg-background`, not `tw-bg-background`). + +--- + +## 0. What this doc owns + +| Owned here | Owned elsewhere | +| --- | --- | +| Color-token values, semantics, usage | The hard rules that mandate them (no hard-coded colors, hover via pseudo-classes, `cn()` / CVA / `lucide`) → [`DEVELOP.md` § UI](./DEVELOP.md) | +| Theming mechanism, `dark:` usage | Commands, structure, coding style, testing, i18n, commit/PR → [`DEVELOP.md`](./DEVELOP.md) | +| Component palette, variants, selection guidance | Process model, message passing, service layers, internals → [`ARCHITECTURE.md`](./ARCHITECTURE.md) | +| Layout shell, responsive patterns, motion, state patterns, page recipe | — | + +This doc restates the `DEVELOP.md` hard rules only where needed, then links back — it does not duplicate them. + +--- + +## 1. Core Constraints (non-negotiable) + +Every UI change must satisfy all of these. They are the bar for "friendly, consistent UI/UX" in this codebase. + +- **Use tokens, not literal colors — one value, one place.** Never write a hex (`#1296db`), an `rgb()`, or a palette class (`text-blue-500`). Always use a semantic token — `bg-background`, `text-foreground`, `border-border`, `bg-primary`, `text-muted-foreground`, … (§3). All color values live in exactly one place — the token definitions in `src/index.css` — so the palette stays unified and a single edit re-skins everything. One semantic concept maps to **one** token: before adding a color, check §3 for an existing token and reuse it; don't introduce a near-duplicate (a second slightly-different gray or blue). Only add a new token when the concept is genuinely new — with both light and dark values — and document it in §3. +- **Both themes, always.** Light and dark are first-class. Because every color comes from a token that has a `:root` and a `.dark` value, using tokens makes a component theme-correct for free. Verify on real light *and* dark before considering anything done (§4). +- **Design for mobile too.** The UI is responsive around a single `768px` breakpoint (`useIsMobile`). Mobile is **a different shell, not a shrunk desktop** — side nav becomes bottom tabs + drawer, tables become cards, rows stack, details/code collapse, actions move into a sticky bar (§7). A feature isn't finished until it works on a narrow viewport. +- **No inline `style={{}}` for what Tailwind can express.** Compose utility classes via `cn()` (`clsx` + `tailwind-merge`); build variants with `class-variance-authority` (CVA). Inline styles only for genuinely dynamic values (e.g. a computed width). +- **Hover/focus are CSS, not state.** Express interactive visuals with pseudo-classes (`hover:bg-primary/90`, `focus-visible:ring-ring/50`). React state is for data/logic, not styling. +- **Reuse components before building new ones.** Default to the shadcn primitives in `src/pages/components/ui/` (§6); icons come from `lucide-react` only — don't hand-roll a control that already exists. Beyond primitives, search the existing pages for a composed block (card row, identity header, permission card, state screen…) that already does what you need and reuse it. When the same block appears in two or more places, extract one shared component instead of copy-pasting — keep one implementation per concept so behavior and styling stay consistent and a fix lands everywhere at once. +- **Keep motion restrained.** Enter/leave in `150–250ms`, `ease-out`; reuse the existing `@utility` animations rather than inlining `@keyframes`; prefer `transition-colors` over `transition-all` (§8). +- **No silent operations.** Every async flow surfaces loading / empty / error / success (and progress for long-running work). The user must always know whether their action worked (§9). +- **Don't introduce new colors or fonts ad hoc.** New color → add a token in `src/index.css` (with both light and dark values) and document it here. New font → add a `--font-*` token; don't reference an unconfigured family. + +--- + +## 2. Design Principles + +The "why" behind the constraints — apply these when shaping a screen. + +1. **Trust-first, clear hierarchy.** Let the most important information win the visual weight. For decision screens (install / permission / import), order content as **identity → permissions → code**, with code demoted to a height-capped scroll region. +2. **System state is always visible.** No silent work. Each async flow shows **progress (a top indeterminate bar) → process (skeletons / per-row status) → result (toast or result screen)**. +3. **Color is semantic, never decorative.** Blue = interactive / primary; green = safe / enabled / success; amber = caution / sensitive; red = danger / error / blocked. Color carries meaning. +4. **Mobile is a different shell.** Don't scale the desktop down — re-shell it: bottom tabs + drawer instead of a side rail, cards instead of tables, vertical stacks, collapsed detail. +5. **Consistent shell.** Major pages share one skeleton: **sticky TopBar + single scroll container + sticky ActionBar**. Swap the content, not the frame. +6. **High cohesion, low coupling.** Each UI unit has a single purpose, a clear interface, and is understandable and testable on its own. A file growing large is usually a signal to split it. + +--- + +## 3. Color Tokens (full light / dark values) + +**Single source:** [`src/index.css`](../src/index.css). `:root` defines light, `.dark` overrides for dark, and `@theme inline` exposes every `--token` as a Tailwind color (`--color-*`), so `bg-` / `text-` / `border-` all work **and switch with the theme automatically**. + +**Usage:** +- Background `bg-`, text `text-`, border `border-`, focus ring `ring-ring`. +- Opacity modifiers compose directly: `bg-primary/90` (hover), `ring-destructive/20`, `bg-input/30`. +- **Never hard-code a color value** — see Constraint 1 and [`DEVELOP.md` § UI](./DEVELOP.md). For dark-only tweaks use the `dark:` variant. + +### 3.1 Base surfaces & text + +| Token / class | Light | Dark | Use | +| --- | --- | --- | --- | +| `background` | `#fafafa` | `#1e1e1e` | Page background | +| `foreground` | `#1a1a1a` | `#e5e5e5` | Primary text | +| `card` | `#ffffff` | `#151515` | Card / surface | +| `card-foreground` | `#1a1a1a` | `#e5e5e5` | Text on cards | +| `popover` | `#ffffff` | `#151515` | Floating layers (dropdown/tooltip/toast) surface | +| `popover-foreground` | `#1a1a1a` | `#e5e5e5` | Text in floating layers | +| `fg-secondary` | `#666666` | `#b5b5b5` | Secondary text (slightly stronger than `muted-foreground`) | + +### 3.2 Brand primary (blue) + +| Token / class | Light | Dark | Use | +| --- | --- | --- | --- | +| `primary` | `#1296db` | `#3aacef` | Primary actions, active state, emphasis (dark is brightened for contrast) | +| `primary-foreground` | `#ffffff` | `#ffffff` | Text on the brand color | +| `primary-hover` | `#0a7db8` | `#1296db` | Brand hover (or use `bg-primary/90`) | +| `primary-light` | `#d6ecfa` | `#1e3040` | Soft brand wash — icon backgrounds, chip fills | + +### 3.3 Secondary / muted / accent backgrounds + +> Per the shadcn convention, `secondary` / `muted` / `accent` share the **same gray value** here — different semantics, one fill color. + +| Token / class | Light | Dark | Use | +| --- | --- | --- | --- | +| `secondary` | `#f0f0f0` | `#2a2a2a` | Secondary buttons / fills | +| `secondary-foreground` | `#1a1a1a` | `#e5e5e5` | Text on secondary | +| `muted` | `#f0f0f0` | `#2a2a2a` | Muted background (group fills, placeholders) | +| `muted-foreground` | `#888888` | `#8a8a8a` | De-emphasized / descriptive text | +| `accent` | `#f0f0f0` | `#2a2a2a` | Hover / selected background (menu items, etc.) | +| `accent-foreground` | `#1a1a1a` | `#e5e5e5` | Text on accent | + +### 3.4 Borders, inputs, ring, switch + +| Token / class | Light | Dark | Use | +| --- | --- | --- | --- | +| `border` | `#e5e5e5` | `#2a2a2a` | Global borders (the `@layer base` reset gives every element `border-border`) | +| `input` | `#e5e5e5` | `#2a2a2a` | Form control borders | +| `ring` | `#1296db` | `#3aacef` | Focus ring (`focus-visible:ring-ring/50`) | +| `switch-off` | `#d0d0d0` | `#3a3a3a` | Switch off-state track | +| `thumb` | `#ffffff` | `#eeeeee` | Switch/Checkbox thumb (stays light even in dark) | + +### 3.5 Status colors + +| Token / class | Light | Dark | Use | +| --- | --- | --- | --- | +| `destructive` | `#e7000b` | `#ff6669e6` | Dangerous / delete / error actions | +| `destructive-foreground` | `#ffffff` | `#ffffff` | Text on destructive | +| `success` | `#34c759` | `#34c759` | Success / enabled / running (solid) | +| `success-bg` / `success-fg` | `#e8f9ec` / `#0c8833` | `#1e3520` / `#6fdd8a` | Success **badge** (soft bg, deep fg) | +| `warning` | `#ff9500` | `#ff9500` | Caution / sensitive (solid) | +| `warning-bg` / `warning-fg` | `#fff4e6` / `#c46c00` | `#352c1e` / `#ffb84d` | Warning **badge** (soft bg, deep fg) | + +> Use the solid status colors (`success`/`warning`) for icons and dots; use the `*-bg` / `*-fg` pairs for badges (see the `Badge` `success` / `warning` variants). + +**Skill / purple accent.** Skills carry a purple brand identity across the install flow and the Agent management pages. Use the `skill` family — never raw `violet-*` palette classes. + +| Token / class | Light | Dark | Use | +| --- | --- | --- | --- | +| `skill` | `#9333ea` | `#a855f7` | Skill accent (solid) — install button, section icons (dark is brightened for contrast) | +| `skill-foreground` | `#ffffff` | `#ffffff` | Text on the solid skill color | +| `skill-bg` / `skill-fg` | `#f3e8ff` / `#7e22ce` | `#2a1e3a` / `#c084fc` | Skill **badge / chip** (soft bg, deep fg) — also the `CapabilityTag` `violet` tone | + +### 3.6 Stored-value type badges (string / number / boolean / object) + +For the storage table's "type" column — soft bg, deep fg; in dark the bg darkens and the fg brightens: + +| Type | bg (Light → Dark) | fg (Light → Dark) | +| --- | --- | --- | +| `type-string` (green) | `#e4f7ea` → `#1e3520` | `#2ba24e` → `#4ade80` | +| `type-number` (blue) | `#d6ecfa` → `#1e3040` | `#1296db` → `#3aacef` | +| `type-boolean` (amber) | `#fceedb` → `#352c1e` | `#c2710c` → `#fb923c` | +| `type-object` (purple) | `#f3e8ff` → `#2a1e3a` | `#9333ea` → `#c084fc` | + +### 3.7 Sidebar + +| Token / class | Light | Dark | Use | +| --- | --- | --- | --- | +| `sidebar` | `#ffffff` | `#1a1a1a` | Sidebar background | +| `sidebar-foreground` | `#1a1a1a` | `#e5e5e5` | Sidebar text | +| `sidebar-primary` | `#1296db` | `#3aacef` | Sidebar emphasis | +| `sidebar-accent` | `#edf5fc` | `#2a2a30` | Sidebar selected background | +| `sidebar-border` | `#e5e5e5` | `#2a2a2a` | Sidebar border | +| `sidebar-ring` | `#1296db` | `#3aacef` | Sidebar focus ring | + +(Also `sidebar-primary-foreground` / `sidebar-accent-foreground`, equal to `#ffffff` / the primary text color.) + +### 3.8 Scrollbar + +| Token | Light | Dark | +| --- | --- | --- | +| `--scrollbar-thumb` | `rgba(0,0,0,.18)` | `rgba(255,255,255,.16)` | +| `--scrollbar-thumb-hover` | `rgba(0,0,0,.32)` | `rgba(255,255,255,.30)` | + +Add the `.scrollbar-custom` class to any scroll container to get a thin, rounded, semi-transparent, theme-aware scrollbar (covers both the Firefox `scrollbar-*` properties and the WebKit pseudo-elements). + +--- + +## 4. Theming + +**Mechanism:** the theme switches by adding/removing `.dark` on `document.documentElement` (`@custom-variant dark (&:is(.dark *))` is what makes the `dark:` variant work). Every token is defined under both `:root` and `.dark`, so toggling the class re-skins the whole app — no per-component color changes needed. + +**Provider:** [`src/pages/components/theme-provider.tsx`](../src/pages/components/theme-provider.tsx) + +```tsx +import { useTheme } from "@App/pages/components/theme-provider"; + +const { theme, resolvedTheme, setTheme } = useTheme(); +// theme: "light" | "dark" | "auto" (user choice, persisted to localStorage key "lightMode") +// resolvedTheme: "light" | "dark" (in "auto", resolved live from prefers-color-scheme) +setTheme("auto"); // "auto" follows the system theme and updates on change +``` + +**Flash prevention:** [`src/pages/common.ts`](../src/pages/common.ts) reads `lightMode` and sets `.dark` *before* React mounts, so non-auto users don't see a wrong-theme frame on refresh. New page entry points should reuse the existing `main.tsx` pattern rather than rolling their own theme logic. + +**Correct usage (do / don't):** + +```tsx +// ✅ Tokens — adapt to light/dark automatically +
+ + +// ✅ dark: variant only for a dark-specific tweak +
+ +// ❌ Hard-coded colors — break in dark and violate the DEVELOP.md rule +
+``` + +**Every UI change must hold up in both themes.** Verify on real light and dark — don't ship after checking only one. + +--- + +## 5. Typography & Radius + +### Fonts + +| Token | Value | Use | +| --- | --- | --- | +| `font-mono` (`--font-mono`) | `"Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace` | Code, version numbers, `@match`/permission rules, stored values — anything monospaced (`font-mono`) | + +> **There is no dedicated sans token.** `src/index.css` defines only `--font-mono`; body text uses Tailwind's default system sans stack. If you need a specific sans family, add a `--font-sans` token first (see Constraint 9) and update this section — don't reference an unconfigured font. + +### Radius + +`--radius: 0.5rem` (8px) is the base; four steps are derived via `calc` in `@theme inline`: + +| class | Value | Typical use | +| --- | --- | --- | +| `rounded-sm` | 4px | Small tags, compact controls | +| `rounded-md` | 6px | Buttons, inputs (`Button` defaults to `rounded-md`) | +| `rounded-lg` | 8px | Cards, panels | +| `rounded-xl` | 12px | Large cards, dialogs, emphasized containers | + +### Spacing & width rhythm + +- **Desktop centered content width:** ~`864px` for narrow decision pages, ~`1120px` for wide list pages, ~`1280px` as a general cap. +- **Sticky bars:** TopBar ≈ `52px`, ActionBar ≈ `68px`. +- **Block spacing:** start sections at `gap-4` (16px); card padding `p-6`/`p-7`. +- **Mobile:** single column, `100vw`, narrower horizontal padding (e.g. `px-4` vs desktop `px-8`). + +--- + +## 6. Component palette & usage + +The shadcn primitives live in [`src/pages/components/ui/`](../src/pages/components/ui/) — `new-york` style, CSS variables enabled, no class prefix (`components.json`). Icons are always `lucide-react`; class merging is always `cn()` ([`src/pkg/utils/cn.ts`](../src/pkg/utils/cn.ts)); variants are always CVA — these are the [`DEVELOP.md` § UI](./DEVELOP.md) hard rules, not repeated here. This section is "what exists and how to choose." + +### 6.1 Primitives + +| File | Use | +| --- | --- | +| `button.tsx` | Buttons (variants/sizes below) | +| `badge.tsx` | Status / label badges | +| `card.tsx` | Card container | +| `alert.tsx` | Inline alert banner | +| `alert-dialog.tsx` | Blocking confirmation dialog (dangerous actions) | +| `dialog.tsx` | General modal dialog | +| `sheet.tsx` | Drawer (left/right/top/bottom; mobile nav, side panels) | +| `popover.tsx` / `popconfirm.tsx` | Floating layer / lightweight inline confirm (custom wrapper) | +| `dropdown-menu.tsx` | Dropdown menu | +| `tooltip.tsx` | Hover tooltip | +| `tabs.tsx` | Tabs | +| `accordion.tsx` / `collapsible.tsx` | Accordion / collapsible region (common for mobile collapsed detail) | +| `select.tsx` | Select | +| `input.tsx` / `textarea.tsx` | Text input | +| `checkbox.tsx` / `radio-group.tsx` / `switch.tsx` | Checkbox / radio / toggle | +| `label.tsx` | Form label | +| `progress.tsx` | Progress bar | +| `avatar.tsx` | Avatar | +| `separator.tsx` | Divider | +| `scroll-area.tsx` | Controlled scroll region | +| `sonner.tsx` | Global toast container | +| `use-hover-menu.ts` | Helper hook for hover-triggered menus | + +> No form library (react-hook-form / zod) is used; forms are plain `useState` + controlled components. Keep new forms on this pattern — don't pull in a library unprompted. + +### 6.2 Button variants / sizes + +Source: [`button.tsx`](../src/pages/components/ui/button.tsx). + +- **variant:** `default` (brand solid), `destructive`, `outline`, `secondary`, `ghost`, `link` +- **size:** `default`, `xs`, `sm`, `lg`, `icon`, `icon-xs`, `icon-sm`, `icon-lg` + +```tsx +import { Button } from "@App/pages/components/ui/button"; +import { Plus } from "lucide-react"; + + {/* primary action */} + {/* secondary action */} + {/* dangerous action */} + {/* icon button; svg auto-sizes to size-4 */} +``` + +### 6.3 Badge variants + +Source: [`badge.tsx`](../src/pages/components/ui/badge.tsx). Variants: `default`, `secondary`, `destructive`, `outline`, `success`, `warning`. + +```tsx +import { Badge } from "@App/pages/components/ui/badge"; + +Enabled {/* success-bg / success-fg */} +Sensitive {/* warning-bg / warning-fg */} +Parse failed +``` + +### 6.4 Toast (sonner) + +The container [`sonner.tsx`](../src/pages/components/ui/sonner.tsx) is already theme-aware, **bottom-right**, with `richColors`; mount it once per page entry. Trigger from anywhere: + +```tsx +import { toast } from "sonner"; + +toast.success("Script installed"); +toast.error("Update failed: network error"); +``` + +### 6.5 Selection guidance + +- **Confirmation:** dangerous / irreversible → `AlertDialog`; lightweight inline confirm (e.g. row delete) → `popconfirm`. +- **Transient panels:** mobile nav / side detail → `Sheet`; small anchored layer → `Popover` / `DropdownMenu`. +- **Feedback:** transient → `toast`; persistent / in-page → see §9 state patterns. + +--- + +## 7. Layout & responsive + +### Shell + +Major pages share one structure: **sticky TopBar (no scroll) + single scroll container (`.scrollbar-custom`) + sticky ActionBar (no scroll)**. Only the middle layer scrolls; head and foot stay put. + +### Single mobile breakpoint + +[`src/pages/components/use-is-mobile.ts`](../src/pages/components/use-is-mobile.ts) is the **only** breakpoint source: `MOBILE_BREAKPOINT = 768`; a viewport `< 768px` is mobile. + +```tsx +import { useIsMobile } from "@App/pages/components/use-is-mobile"; + +function Page() { + const isMobile = useIsMobile(); + return isMobile ? : ; +} +``` + +### Desktop ↔ mobile transforms + +| Desktop (≥768px) | Mobile (<768px) | +| --- | --- | +| Left nav rail | Bottom tab bar + drawer (`Sheet`) — tabs for high-frequency, drawer for everything else | +| Multi-column table | Single-column cards | +| Side-by-side panels | Vertical stack; detail/code collapsed by default | +| Inline dropdowns | Drawer / Accordion overlays | +| Categories in a left rail | Categories in a top horizontal-scroll chip bar | + +The bottom bar is [`BottomTabBar.tsx`](../src/pages/options/layout/BottomTabBar.tsx). **Mobile re-shells, it doesn't shrink** — see Principle 4. + +### Scroll-spy (long settings pages) + +Long pages (settings / tools) use scroll-spy: scrolling the content highlights the current category, and clicking a category smooth-scrolls to its section. See [`SettingsLayout.tsx`](../src/pages/options/layout/SettingsLayout.tsx) + [`useScrollSpy.ts`](../src/pages/options/hooks/useScrollSpy.ts). On desktop the categories sit in a left rail; on mobile they become a top horizontal chip bar (the active chip `scrollIntoView`s to center). + +--- + +## 8. Motion + +**Sources:** [`src/index.css`](../src/index.css) (custom keyframes/utilities) + `tw-animate-css` (the `@import` provides `animate-in/out`, `fade-*`, `zoom-*`, `slide-*`, `accordion-*`, …) + Radix `data-state`. **No Framer Motion** — all motion is CSS. + +### How to add motion that stays friendly + +- **Fast and light:** enter/leave in `150–250ms`, `ease-out`; the built-in collapse/progress animations use `200ms ease-out`. +- **Hover/focus via CSS pseudo-classes, not React state** (`hover:bg-primary/90`, `focus-visible:ring-ring/50`) — a `DEVELOP.md` rule. +- **Enter/leave via Radix `data-state`** — don't hand-roll show/hide with `setTimeout`. +- **Prefer `transition-colors` over `transition-all`:** animate only what should move, avoiding layout thrash and wasted work. +- **Reuse existing utilities;** don't inline `@keyframes` in a component. New animation → add an `@utility` in `src/index.css` so it's globally reusable. +- **Large looping animations** (e.g. the indeterminate bar) should animate `transform` (already `translateX`) for performance. + +### Available animations + +| utility / pattern | Source | Use | +| --- | --- | --- | +| `animate-collapsible-down` / `-up` | `index.css` | Radix Collapsible expand/collapse (uses `--radix-collapsible-content-height`) | +| `animate-expand-bar` / `animate-collapse-bar` | `index.css` | Height expand/collapse of bars/rows (incl. border and opacity) | +| `animate-indeterminate-bar` | `index.css` | Indeterminate progress bar (`translateX` loop, `1.1s`) | +| `data-[state=open]:animate-in data-[state=closed]:animate-out` + `fade-*` / `zoom-95` / `slide-*` | `tw-animate-css` | Dialog / Dropdown / Sheet enter/leave (Radix state driven) | +| `animate-spin` | Tailwind built-in | Spinner rotation — the `Loader2` / `RefreshCw` icons used for inline, button, and full-page loading (`animate-spin`, usually `text-primary`) | +| `animate-pulse` | Tailwind built-in | Skeleton placeholder pulse | +| `transition-colors` / `transition-transform` / `duration-200` | Tailwind | hover/focus color transitions, icon rotation | + +```tsx +// Floating layer enter/leave (Radix data-state + tw-animate-css) +
+ +// Indeterminate progress bar +
+
+
+``` + +--- + +## 9. State patterns + +Every async flow covers the states below, presented consistently: + +| State | Standard presentation | +| --- | --- | +| **Loading** | A skeleton that preserves the layout, a centered spinner, or a thin top indeterminate bar — pick by *where* the wait happens (see **Loading patterns** below) | +| **Empty** | Centered `muted` icon (e.g. `lucide` `PackageOpen`/`Inbox`) + title + explanation + primary CTA | +| **Error** | Centered red icon + an "X failed" title + a monospace (`font-mono`) box with the raw error + retry/close | +| **Success** | Centered green icon + title + summary stats + next-step CTA; for transient feedback use `toast.success` | +| **In-progress** | Top progress bar + per-row status icons (✓ green done / ○ brand in-progress / ⏱ `muted` pending / ✗ `muted` skipped) + readable copy ("Importing… 2/5, keep this page open") | + +### Loading patterns + +A loading state is not one thing — and a centered spinner is the *last* resort, not the default. The guiding rule is **keep the page's shape stable**: show a placeholder where the content will land instead of collapsing the layout to a spinner and snapping it back when data arrives. Match the indicator to where the wait happens: + +| Where the wait is | Indicator | Reference in code | +| --- | --- | --- | +| **First load of a whole page / screen** (no shape yet) | Centered `Loader2` (`size-12 animate-spin text-primary`) + title/desc; pair with a determinate bar (`transition-[width]`) when bytes/percent are known, else an indeterminate fill | `InstallLoading` ([`install/components/InstallStates.tsx`](../src/pages/install/components/InstallStates.tsx)) | +| **Reloading content that already has a shape** (table / list) | A **skeleton** that keeps the real header + placeholder rows (`animate-pulse rounded bg-muted`) — not a centered spinner — so the layout doesn't collapse and reflow | `SkeletonTable` / `SkeletonBar` ([`batchupdate/components.tsx`](../src/pages/batchupdate/components.tsx)) | +| **Background refresh / check while content stays visible** | A thin top `animate-indeterminate-bar` (`h-0.5`, `role="progressbar"` + `aria-label`) pinned under the TopBar, not scrolling with content | `TopProgressBar` ([`batchupdate/components.tsx`](../src/pages/batchupdate/components.tsx)) | +| **A single action** (button, connection test, fetch) | Disable the control and show an inline `Loader2 size-4 animate-spin`; if the action already has an icon, spin that icon instead (`RefreshCw className={cn(checking && "animate-spin")}`) | `McpFormDialog` test button, `ScriptList` / `AgentSkills` refresh | + +Practical rules: + +- **Never freeze and never wait silently.** A region that is loading must show a skeleton, spinner, or bar — never a blank or stale frame with no signal (Constraint 7). +- **Don't fake determinism.** Use the determinate progress bar only when the percent/bytes are actually known; otherwise use an indeterminate fill or a skeleton. +- **One indicator per wait.** Don't stack a full-page spinner over content that is already skeletoned, or two bars for one fetch. +- **The spinner is always `Loader2` + `animate-spin`** (`text-primary` when it should read as active), sized to context — `size-3.5`/`size-4` inline, `size-12` full-page (§8). + +The rule: **no silent operations** — after any action the user can see success / failure / in-progress. + +--- + +## 10. New-page / block recipe + +When building a new page or dialog, run this checklist to stay consistent: + +- [ ] **Entry** reuses the existing `main.tsx` pattern — mount `ThemeProvider`, `Toaster` (and `TooltipProvider` if needed); don't roll your own theme logic. +- [ ] **Shell:** sticky TopBar + `.scrollbar-custom` scroll container + sticky ActionBar (§7). +- [ ] **Responsive:** branch on `useIsMobile()`; re-shell on mobile (bottom bar/drawer, cards, collapse) rather than scaling down (Constraint 3, §7). +- [ ] **Color** entirely from tokens (`bg-card` / `text-foreground` / `border-border` / `bg-primary` …), no literals, verified on both themes (Constraint 1–2, §3–4). +- [ ] **Components** reuse first — search existing pages for a composed block before building; use `src/pages/components/ui/` primitives; extract a shared component when a block repeats; variants via CVA, classes via `cn()`, icons via `lucide-react` (Constraint 6, §6). +- [ ] **Hierarchy** orders the most important info first; decision pages go identity → permissions → code (Principle 1). +- [ ] **State:** loading / empty / error / success / in-progress all covered, never silent (§9). +- [ ] **Motion** restrained (`150–250ms`, `ease-out`), hover/focus via pseudo-classes, enter/leave via `data-state`, reuse existing utilities (§8). +- [ ] **Copy** defaults to English + i18n (see [`DEVELOP.md`](./DEVELOP.md) and [`translation/README.md`](./translation/README.md)). + +Page skeleton (tokens + existing primitives + the shell pattern): + +```tsx +import { useIsMobile } from "@App/pages/components/use-is-mobile"; +import { Button } from "@App/pages/components/ui/button"; + +export default function ExamplePage() { + const isMobile = useIsMobile(); + return ( +
+ {/* sticky TopBar */} +
+

Title

+
+ + {/* single scroll container */} +
+
+
+
+
+ + {/* sticky ActionBar */} +
+ + +
+
+ ); +} +``` + +--- + +## 11. Sources & verification + +**Implementation source of truth (read/edit these when changing the design):** + +- Color / motion / scrollbar tokens → [`src/index.css`](../src/index.css) +- Theming → [`src/pages/components/theme-provider.tsx`](../src/pages/components/theme-provider.tsx) + [`src/pages/common.ts`](../src/pages/common.ts) +- Component primitives → [`src/pages/components/ui/`](../src/pages/components/ui/); shadcn config → [`components.json`](../components.json) +- `cn()` → [`src/pkg/utils/cn.ts`](../src/pkg/utils/cn.ts); breakpoint → [`src/pages/components/use-is-mobile.ts`](../src/pages/components/use-is-mobile.ts) + +**Related docs:** UI hard rules and commit flow → [`DEVELOP.md`](./DEVELOP.md); internals → [`ARCHITECTURE.md`](./ARCHITECTURE.md); doc maintenance and fact-checking → [`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md). + +> When editing this doc, follow [`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md): token values, component names, and variant names track the current branch's `src/` code (if you can't `git grep` it, don't claim it); enumerate counts and lists rather than trusting memory. diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md index 08a25acf4..9fe17432c 100644 --- a/docs/DEVELOP.md +++ b/docs/DEVELOP.md @@ -26,7 +26,8 @@ pnpm run lint # tsc --noEmit + eslint pnpm run lint-fix # tsc --noEmit + eslint --fix (also applies Prettier via eslint-plugin-prettier) ``` -No standalone `format` script — formatting is part of `lint-fix`. Husky pre-commit runs `pnpm run lint-fix` and re-stages the files it touched. +No standalone `format` script — formatting is part of `lint-fix`. Husky pre-commit runs `pnpm run typecheck` plus +ESLint for staged JS/TS files, and also runs `pnpm run test:ci` when committing on `main` or `release/*`. After `pnpm run dev`, load `dist/ext` as an unpacked extension. The browser hot-reloads page changes, but edits to `manifest.json`, `service_worker`, `offscreen`, or `sandbox` require reloading the extension. @@ -51,22 +52,29 @@ Use strict TypeScript, React JSX runtime, 2-space indentation, semicolons, doubl ## UI -React 18 + Arco Design + UnoCSS + React Router. Pages in `src/pages/`. +React 19 + shadcn/ui (Radix UI primitives, "new-york" style) + Tailwind CSS v4 + React Router. Pages in +`src/pages/`; shared primitives in `src/pages/components/ui/` (config in `components.json`). -- Use `tw-` UnoCSS utilities; avoid inline `style={{}}`. +- Compose styles with Tailwind utility classes joined via `cn()` (`src/pkg/utils/cn.ts` — clsx + tailwind-merge); + avoid inline `style={{}}`. Build component variants with `class-variance-authority`; icons from `lucide-react`. - **Hover/focus visuals → CSS pseudo-classes (`hover:`, `focus:`)**, not React state. State is for data/logic. -- **Theme** (light/dark/auto, persisted as `lightMode`, state in `src/pages/store/AppContext.tsx`) — every UI change must work in both themes: - - Arco vars (`var(--color-fill-1)`, `var(--color-text-1)`, `var(--color-border-2)`) — auto-adapt. - - UnoCSS `dark:tw-*` (configured `dark: "class"`). - - `body[arco-theme="dark"]` selector for custom CSS overrides. +- **Theme** (light/dark/auto) — managed by `src/pages/components/theme-provider.tsx` and applied as the `.dark` + class on `document.documentElement` (`src/pages/common.ts` sets the initial class before React mounts to avoid a + flash). Every UI change must work in both themes: + - Use the design-system CSS variables defined in `src/index.css` (`bg-background`, `text-foreground`, + `border-border`, `bg-primary`, `text-muted-foreground`, …) — they auto-adapt per theme. + - Use Tailwind's `dark:` variant for dark-only overrides (`@custom-variant dark` in `src/index.css`). - No hard-coded colors. +- **Design system** — the full color-token reference (light/dark values), component palette, layout & + responsive patterns, motion guidance, state patterns, and a new-page recipe live in + [`DESIGN.md`](./DESIGN.md). Read it before building a new page, dialog, or block. ## Testing > The **TDD/BDD-first principle** (write failing tests before implementation; fix code not tests) lives in > [`AGENTS.md`](../AGENTS.md) → *Engineering Principles*. This section is the mechanics. -Vitest + jsdom, 500ms timeout, isolation disabled. Chrome APIs mocked via `@Packages/chrome-extension-mock` (`tests/vitest.setup.ts`). `MockMessage` available for message-system tests. +Vitest + jsdom/happy-dom, 850ms timeout. Chrome APIs mocked via `@Packages/chrome-extension-mock` (`tests/vitest.setup.ts`). `MockMessage` available for message-system tests. - Write failing tests **before** implementation; co-locate `*.test.ts`/`*.test.tsx` next to source (or place in `tests`). - BDD-style Chinese `describe`/`it` titles. Use `describe.concurrent()` / `it.concurrent()` where independent. @@ -74,13 +82,29 @@ Vitest + jsdom, 500ms timeout, isolation disabled. Chrome APIs mocked via `@Pack - 避免冗余测试 — 如果调用方测试已充分覆盖,可省略被调函数的独立单测。 - Playwright tests are `*.spec.ts` files in `e2e`; they run with one worker and retain failure artifacts. Run targeted tests while iterating, then run `pnpm run lint` plus the relevant full suite before a PR. +### Vitest Performance Hygiene + +- Keep `tests/vitest.setup.ts` lightweight. Shared setup should only install global browser/chrome mocks; heavier + feature helpers belong in opt-in test utilities. +- For files that use one fixed UI language, prefer `initTestLanguage()` from `tests/initTestLanguage.ts` in + `beforeAll` over repeated `initLanguage()` calls inside every test. Tests that intentionally switch languages + should keep explicit language setup. +- Prefer shared DOM helpers such as `mockMatchMedia()` from `tests/mockMatchMedia.ts` over copying local browser + stubs into every page test. +- To spot setup/import regressions without running the full suite, run one small file and read Vitest's timing + breakdown, for example: + +```bash +pnpm exec vitest run --test-timeout=850 --no-coverage --reporter=verbose src/pkg/utils/url-utils.test.ts +``` + > To **verify a change works end-to-end without growing the suite** — drive the real built extension with a > throwaway scratch script — see [`VERIFICATION.md`](./VERIFICATION.md). That is lightweight verification, not > the committed test suite owned by this section. ## i18n -i18next, 7 locales (`src/locales/`: en-US, zh-CN, zh-TW, ja-JP, de-DE, vi-VN, ru-RU); extension strings in `src/assets/_locales/`. ESLint `react/jsx-no-literals: warn` enforces translation. For localization, edit `src/locales//translation.json`; new locales must also be registered in `src/locales/locales.ts`. +i18next, 7 locales (`src/locales/`: en-US, zh-CN, zh-TW, ja-JP, de-DE, vi-VN, ru-RU); extension strings in `src/assets/_locales/`. ESLint `react/jsx-no-literals: warn` enforces translation. Each locale is split by namespace into multiple `*.json` files (`common.json`, `popup.json`, `script.json`, …), re-exported via the locale's `index.ts` and merged in `src/locales/locales.ts`. `defaultNS` is `common`; keys in any other namespace need the `ns:` prefix (e.g. `t("script:tags")`). For localization, edit the relevant namespace `*.json` under `src/locales//`; new locales must also be registered in `src/locales/locales.ts`. **Before translating, read [`docs/translation/README.md`](translation/README.md)** — the translation/localization guide (terminology rules + per-locale `terminology-.md` specs). diff --git a/docs/DOC-MAINTENANCE.md b/docs/DOC-MAINTENANCE.md index c1f434e15..5ecef8255 100644 --- a/docs/DOC-MAINTENANCE.md +++ b/docs/DOC-MAINTENANCE.md @@ -12,13 +12,13 @@ The contributor docs describe a living codebase, so two failure modes recur: examples caught in review: docs named the offscreen GM API `OffscreenGMApi` (no such class — it is `GMApi`), and claimed "8 locales" when there were 7. - **Branch leakage** — work that only lives on a feature branch gets documented as if it ships on `main`. - Example: the Agent Subsystem only lives on its feature branch and is not committed to `main`; describing it - in `main`'s quick-map misleads readers into expecting code that is not there. + Example: a subsystem committed only on a feature branch (not on `main`) gets written into `main`'s + quick-map, misleading readers into expecting code that is not there. **Rule of thumb: if you can't `git grep` it in the committed code on this branch, don't claim it.** Verify with git-aware commands (`git grep`, `git ls-files`, `git ls-tree`) — never a plain `rg`/`ls`, which also match **untracked** files in your working tree, so feature-branch code sitting in your checkout but not committed to -`main` will masquerade as shipped (this is exactly how the Agent Subsystem above sneaks into a `main` doc). +`main` will masquerade as shipped (this is exactly how a feature-branch-only subsystem sneaks into a `main` doc). Aspirational / feature-branch content belongs in that branch's docs, or is clearly marked as planned. ## Doc set & responsibilities (don't duplicate — cross-link) @@ -27,6 +27,7 @@ Aspirational / feature-branch content belongs in that branch's docs, or is clear | --- | --- | | [`../AGENTS.md`](../AGENTS.md) | Engineering principles + architecture quick-map. Single source of truth; `CLAUDE.md` only `@import`s it. | | [`DEVELOP.md`](./DEVELOP.md) | The concrete "how": commands, structure, style, testing, i18n, commit/PR. | +| [`DESIGN.md`](./DESIGN.md) | The design system: light/dark color tokens, theme mechanism, shadcn component palette, layout & responsive patterns, motion, state patterns, new-page recipe. | | [`VERIFICATION.md`](./VERIFICATION.md) | Lightweight end-to-end functional verification — throwaway scratch scripts driving the real built extension. | | [`ARCHITECTURE.md`](./ARCHITECTURE.md) | Deep internals: process model, message passing, service/data layers, GM API, execution, build. | | [`translation/README.md`](./translation/README.md) | Translation / localization single source of truth. | @@ -97,7 +98,7 @@ git ls-files eslint-rules/; git grep -l "require-last-error-check" -- eslint.con Link integrity — confirm every relative markdown link in the core docs resolves: ```bash -for doc in AGENTS.md docs/README.md docs/DEVELOP.md docs/VERIFICATION.md docs/ARCHITECTURE.md docs/DOC-MAINTENANCE.md docs/translation/README.md; do +for doc in AGENTS.md docs/README.md docs/DEVELOP.md docs/DESIGN.md docs/VERIFICATION.md docs/ARCHITECTURE.md docs/DOC-MAINTENANCE.md docs/translation/README.md; do grep -oE '\]\(([^)]+)\)' "$doc" | sed -E 's/^\]\(|\)$//g' | grep -vE '^https?:|^#' | while read -r link; do target="$(dirname "$doc")/${link%%#*}" [ -e "$target" ] && echo "ok $doc → $link" || echo "BROKEN $doc → $link" diff --git a/docs/README.md b/docs/README.md index 653ea9e25..b155b4f26 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ | --- | --- | | [`../AGENTS.md`](../AGENTS.md) | 工程原则、架构速览、AI/贡献者约定的单一信息源(`CLAUDE.md` 仅导入它)。 | | [`DEVELOP.md`](./DEVELOP.md) | 开发规范:命令、目录结构、编码风格、UI/主题、测试机制、i18n、提交/PR 流程。**写代码前先读。** | +| [`DESIGN.md`](./DESIGN.md) | 设计系统参考:深/浅色令牌完整值、主题机制、shadcn 组件清单与选型、布局与响应式范式、动效、状态模式、新建页面配方。**做页面/对话框/区块前先读。** | | [`VERIFICATION.md`](./VERIFICATION.md) | 功能验证指南:用一次性 scratch 脚本驱动真实扩展做端到端验证(不跑全量 E2E、不加永久用例)。**验证改动是否真正跑通时读。** | | [`ARCHITECTURE.md`](./ARCHITECTURE.md) | 内部原理深入:多进程模型、消息传递、服务/数据层、GM API、脚本执行、构建管线。 | | [`DOC-MAINTENANCE.md`](./DOC-MAINTENANCE.md) | 文档维护与事实核对指南:组织规则、逐条核对清单、一键校验脚本。**改/审文档前先读。** | diff --git a/docs/translation/README.md b/docs/translation/README.md index 9627d208f..0b8788bea 100644 --- a/docs/translation/README.md +++ b/docs/translation/README.md @@ -1,7 +1,7 @@ # 翻译与本地化指南 / Translation & Localization Guide > **开始任何翻译之前,先读本文件。** -> 凡是新增或修改本地化内容(`src/locales//translation.json`、对应语言的文档、界面文案、测试快照),都必须先阅读本指南,并遵循对应语言的术语规范文件。 +> 凡是新增或修改本地化内容(`src/locales//` 下按命名空间拆分的 `*.json` 文件、对应语言的文档、界面文案、测试快照),都必须先阅读本指南,并遵循对应语言的术语规范文件。 本目录是 ScriptCat 翻译 / 本地化的单一信息源: @@ -43,9 +43,9 @@ ## 翻译工作流 / Workflow -- 翻译文件位于 `src/locales//translation.json`,按页面划分,最终由 `src/locales/locales.ts` 合并导出。 -- **改进已有翻译**:直接编辑对应语言的 `translation.json`。 -- **新增语言**:在 `src/locales/` 下新建语言代码目录(如 `fr-FR`),复制 `en-US/translation.json` 作为模板翻译,并在 `src/locales/locales.ts` 中注册;如需术语规范,在本目录新增 `terminology-fr-FR.md`。 +- 翻译文件位于 `src/locales//`,按命名空间(页面)拆分为多个 `*.json` 文件(如 `common.json`、`popup.json`、`script.json`),最终由 `src/locales/locales.ts` 合并导出。 +- **改进已有翻译**:直接编辑对应语言目录下相应命名空间的 `*.json` 文件。`defaultNS` 为 `common`,其它命名空间的 key 在代码中需带 `ns:` 前缀(如 `t("script:tags")`)。 +- **新增语言**:在 `src/locales/` 下新建语言代码目录(如 `fr-FR`),复制 `en-US/` 下的各命名空间 `*.json` 与 `index.ts` 作为模板翻译,并在 `src/locales/locales.ts` 中注册;如需术语规范,在本目录新增 `terminology-fr-FR.md`。 - **关键字冲突**:同一页面中关键字相同但翻译不同时,使用 `page.key` 的方式区分。 - 为满足部分扩展市场要求,`chrome.i18n` 语言文件位于 `src/assets/_locales`。 - i18n 方案的实现细节见 [`src/locales/README.md`](../../src/locales/README.md)。 @@ -55,7 +55,7 @@ 将 React 文件中的中文提取为 i18next key 时使用: ```md -你是一个翻译专家,使用 react-i18next 做为翻译框架,我需要你帮助我翻译这个 React 文件中的中文,首先你需要提取文件中的中文部分,生成一个合适的 key,使用蛇形命名,添加到 src/locales/zh-CN/translation.json 文件中,然后使用 `useTranslation` 替换原有中文,如果有参数你可以使用 i18next 的格式,不需要处理其他语言,不要做多余的事情 +你是一个翻译专家,使用 react-i18next 做为翻译框架,我需要你帮助我翻译这个 React 文件中的中文,首先你需要提取文件中的中文部分,生成一个合适的 key,使用蛇形命名,添加到 src/locales/zh-CN/ 下对应命名空间的 json 文件中(如 common.json、script.json),注意非 common 命名空间的 key 在代码中需带 `ns:` 前缀,然后使用 `useTranslation` 替换原有中文,如果有参数你可以使用 i18next 的格式,不需要处理其他语言,不要做多余的事情 ``` ## 完成前检查清单 / Checklist diff --git a/docs/translation/terminology-de-DE.md b/docs/translation/terminology-de-DE.md index 89fd243d5..a3405e30e 100644 --- a/docs/translation/terminology-de-DE.md +++ b/docs/translation/terminology-de-DE.md @@ -2,7 +2,7 @@ Dieses Dokument definiert die Terminologie für die deutsche (`de-DE`) Benutzeroberfläche und Dokumentation von ScriptCat. Es dient dazu, Produktkonzepte eindeutig zu benennen, UI-Texte natürlich zu formulieren und technische Bezeichner bei künftigen Übersetzungen unverändert zu erhalten. -Geprüfte Verwendungsquelle: `src/locales/de-DE/translation.json` +Geprüfte Verwendungsquelle: `src/locales/de-DE/*.json` ## Grundsätze diff --git a/docs/translation/terminology-en-US.md b/docs/translation/terminology-en-US.md index 43e16f915..fc2226f69 100644 --- a/docs/translation/terminology-en-US.md +++ b/docs/translation/terminology-en-US.md @@ -2,7 +2,7 @@ This document defines terminology for ScriptCat's US English (`en-US`) interface and documentation. It is intended to keep product concepts identifiable, make UI actions read naturally in English, and prevent later translations from copying unclear source wording. -Usage sources reviewed: `src/locales/en-US/translation.json`, `README.md` +Usage sources reviewed: `src/locales/en-US/*.json`, `README.md` ## Principles @@ -81,7 +81,7 @@ Usage sources reviewed: `src/locales/en-US/translation.json`, `README.md` ## E. Copy Review Targets -The entries below identify defects or inconsistencies already present in `translation.json`. Creating this guideline does not itself change runtime strings; these should be corrected in a scoped English copy pass with UI checks. +The entries below identify defects or inconsistencies already present in `*.json`. Creating this guideline does not itself change runtime strings; these should be corrected in a scoped English copy pass with UI checks. | Target | Current wording or issue | Preferred direction | Current example keys | | --- | --- | --- | --- | diff --git a/docs/translation/terminology-ja-JP.md b/docs/translation/terminology-ja-JP.md index 8a28d1981..6e84b41e2 100644 --- a/docs/translation/terminology-ja-JP.md +++ b/docs/translation/terminology-ja-JP.md @@ -2,7 +2,7 @@ 本書は、ScriptCat の日本語(`ja-JP`)UI およびドキュメントで使用する用語と UI 文言の基準です。日本語の文言を翻訳または修正する際は、日本語として自然で、製品 UI として一貫した表現を使用します。 -用例の参照元:`src/locales/ja-JP/translation.json`、`docs/README_ja.md` +用例の参照元:`src/locales/ja-JP/*.json`、`docs/README_ja.md` ## 外来語表記の基準 diff --git a/docs/translation/terminology-ru-RU.md b/docs/translation/terminology-ru-RU.md index 20564b52a..ffdbea935 100644 --- a/docs/translation/terminology-ru-RU.md +++ b/docs/translation/terminology-ru-RU.md @@ -2,7 +2,7 @@ Этот документ задает терминологию для русского (`ru-RU`) интерфейса и документации ScriptCat. Его цель - сохранять различия между возможностями продукта, использовать естественные для интерфейса формулировки и не повреждать технические идентификаторы при дальнейшей локализации. -Проверенные источники употребления: `src/locales/ru-RU/translation.json`, `docs/README_RU.md` +Проверенные источники употребления: `src/locales/ru-RU/*.json`, `docs/README_RU.md` ## Основные принципы @@ -76,7 +76,7 @@ ## E. Темы для последующей проверки -Эти пункты описывают имеющиеся расхождения. Создание руководства само по себе не меняет `translation.json`; исправления должны выполняться отдельной проверяемой правкой. +Эти пункты описывают имеющиеся расхождения. Создание руководства само по себе не меняет `*.json`; исправления должны выполняться отдельной проверяемой правкой. | Тема | Текущее состояние | Предпочтительное направление | Примеры ключей | | --- | --- | --- | --- | diff --git a/docs/translation/terminology-vi-VN.md b/docs/translation/terminology-vi-VN.md index 82c8955f3..f14245f9d 100644 --- a/docs/translation/terminology-vi-VN.md +++ b/docs/translation/terminology-vi-VN.md @@ -2,7 +2,7 @@ Tài liệu này quy định thuật ngữ dùng cho giao diện và tài liệu tiếng Việt (`vi-VN`) của ScriptCat. Mục đích là giữ rõ các khái niệm sản phẩm, dùng câu chữ tự nhiên trong giao diện và bảo toàn các định danh kỹ thuật khi tiếp tục bản địa hóa. -Nguồn sử dụng đã kiểm tra: `src/locales/vi-VN/translation.json` +Nguồn sử dụng đã kiểm tra: `src/locales/vi-VN/*.json` ## Nguyên tắc @@ -76,7 +76,7 @@ Nguồn sử dụng đã kiểm tra: `src/locales/vi-VN/translation.json` ## E. Điểm cần rà soát sau -Các mục dưới đây ghi nhận vấn đề đã tồn tại. Việc tạo hướng dẫn không tự thay đổi `translation.json`; mọi sửa đổi nên là một đợt chỉnh riêng có kiểm tra giao diện. +Các mục dưới đây ghi nhận vấn đề đã tồn tại. Việc tạo hướng dẫn không tự thay đổi `*.json`; mọi sửa đổi nên là một đợt chỉnh riêng có kiểm tra giao diện. | Chủ đề | Hiện trạng | Hướng ưu tiên | Key ví dụ hiện tại | | --- | --- | --- | --- | diff --git a/docs/translation/terminology-zh-CN.md b/docs/translation/terminology-zh-CN.md index ed3a261e5..96a0e30df 100644 --- a/docs/translation/terminology-zh-CN.md +++ b/docs/translation/terminology-zh-CN.md @@ -2,7 +2,7 @@ 本文档是 ScriptCat 简体中文(`zh-CN`)界面与文档的用语依据。翻译或修改简体中文时,目标是让中国大陆用户自然理解,同时保留 ScriptCat 已建立的脚本类型、功能名称和开发者术语。 -用例参考来源:`src/locales/zh-CN/translation.json`、`docs/README_zh-CN.md` +用例参考来源:`src/locales/zh-CN/*.json`、`docs/README_zh-CN.md` 作者确认参考:[PR #1421 讨论](https://github.com/scriptscat/scriptcat/pull/1421)。其中 CodFrm 明确确认了 `同步删除`、`同步脚本删除`、`查询`,并建议将存储权限标题写为 `脚本正在尝试访问存储空间`。 @@ -85,7 +85,7 @@ ## E. 后续审查项目 -以下内容记录当前文案中需要确认或统一的地方。本规范的建立本身不要求同时修改 `translation.json`。 +以下内容记录当前文案中需要确认或统一的地方。本规范的建立本身不要求同时修改 `*.json`。 | 对象 | 当前情况 | 建议方向 | 当前用例 key | | --- | --- | --- | --- | diff --git a/docs/translation/terminology-zh-TW.md b/docs/translation/terminology-zh-TW.md index 2e87ff526..ca6136942 100644 --- a/docs/translation/terminology-zh-TW.md +++ b/docs/translation/terminology-zh-TW.md @@ -2,7 +2,7 @@ 本文件是 ScriptCat 繁體中文(`zh-TW`)介面與文件的用語依據。翻譯或修改繁體中文時,目標是讓台灣使用者自然理解,並維持台灣軟體產品介面常見的語氣。 -盤點來源:`src/locales/zh-TW/translation.json`、`docs/README_zh-TW.md` +盤點來源:`src/locales/zh-TW/*.json`、`docs/README_zh-TW.md` 產品互動參考:[PR #1421 討論](https://github.com/scriptscat/scriptcat/pull/1421)。CodFrm 對相同同步刪除設定確認應以簡短標籤呈現,詳細行為由旁側說明交代;此項產品決策在 `zh-TW` 對應為 `同步刪除` / `同步腳本刪除`,不代表其他 `zh-CN` 詞彙偏好應套用至繁中。 diff --git a/e2e/agent-chat.spec.ts b/e2e/agent-chat.spec.ts index d8a44c4b1..e834d7bfe 100644 --- a/e2e/agent-chat.spec.ts +++ b/e2e/agent-chat.spec.ts @@ -1,17 +1,18 @@ import { test, expect } from "./agent-fixtures"; import { openAgentChatPage } from "./utils"; -test.describe("Agent Chat", () => { - test("should show new chat button and model selector", async ({ context, extensionId }) => { +// new-ui Agent 会话页(shadcn):新建会话按钮 data-testid="conv-new"(会话列表)/ +// "header-new"(顶栏);模型选择器 data-testid="agent-model-select"(Radix Select)。 +// agent-fixtures 预置了 "Mock LLM" 模型,故选择器可用。 +test.describe("Agent 会话", () => { + test("应显示新建会话按钮和模型选择器", async ({ context, extensionId }) => { const page = await openAgentChatPage(context, extensionId); - // 新建会话按钮应可见 - const newChatBtn = page.locator("button", { hasText: /new|新建/i }).first(); - await expect(newChatBtn).toBeVisible({ timeout: 10000 }); + // 新建会话按钮(会话列表侧栏常驻) + await expect(page.getByTestId("conv-new")).toBeVisible({ timeout: 10_000 }); - // 模型选择器应可见且包含预配置的 Mock LLM 模型 - const modelSelect = page.locator(".arco-select"); - await expect(modelSelect.first()).toBeVisible({ timeout: 5000 }); + // 模型选择器可见 + await expect(page.getByTestId("agent-model-select")).toBeVisible({ timeout: 10_000 }); await page.close(); }); diff --git a/e2e/agent-navigation.spec.ts b/e2e/agent-navigation.spec.ts index f8c63b5d2..81866487f 100644 --- a/e2e/agent-navigation.spec.ts +++ b/e2e/agent-navigation.spec.ts @@ -1,65 +1,48 @@ import { test, expect } from "./fixtures"; import { openAgentChatPage, openAgentProviderPage, openOptionsPage } from "./utils"; -test.describe("Agent Navigation", () => { - test("should navigate to agent chat page via sidebar", async ({ context, extensionId }) => { +// new-ui 侧边栏 Agent 子菜单(shadcn):折叠态切换按钮 data-testid="nav-agent", +// 展开后子项为 NavLink(a[href="#/agent/..."]),容器 data-testid="sidebar-agent-submenu"。 +test.describe("Agent 导航", () => { + test("应通过侧边栏展开 Agent 菜单并进入会话页", async ({ context, extensionId }) => { const page = await openOptionsPage(context, extensionId); - // Agent SubMenu title 的 onClick 直接导航到 /agent/chat - // Sider.tsx 中 SubMenu title 的 span onClick 设置 hash - const agentMenuTitle = page.locator(".arco-menu-inline-header span", { hasText: /agent/i }).first(); - await expect(agentMenuTitle).toBeVisible({ timeout: 10_000 }); - await agentMenuTitle.click(); + await page.getByTestId("nav-agent").click(); + const submenu = page.getByTestId("sidebar-agent-submenu"); + await expect(submenu).toBeVisible({ timeout: 10_000 }); - // 验证 URL hash 包含 /agent - await expect(page).toHaveURL(/#\/agent/); + await submenu.locator('a[href="#/agent/chat"]').click(); + await expect(page).toHaveURL(/#\/agent\/chat/); await page.close(); }); - test("should navigate to agent sub-pages via sidebar", async ({ context, extensionId }) => { + test("应通过侧边栏进入 Agent 模型服务页", async ({ context, extensionId }) => { const page = await openOptionsPage(context, extensionId); - // 先展开 Agent 子菜单 - const agentMenuHeader = page.locator(".arco-menu-inline-header").filter({ hasText: /agent/i }).first(); - await expect(agentMenuHeader).toBeVisible({ timeout: 10_000 }); - await agentMenuHeader.click(); - - // 展开后点击 Provider 子菜单项(使用 key 属性匹配) - const providerItem = page - .locator('[class*="arco-menu-item"]') - .filter({ hasText: /model service|provider|模型/i }) - .first(); - await expect(providerItem).toBeVisible({ timeout: 10_000 }); - await providerItem.click(); + await page.getByTestId("nav-agent").click(); + const submenu = page.getByTestId("sidebar-agent-submenu"); + await expect(submenu).toBeVisible({ timeout: 10_000 }); + await submenu.locator('a[href="#/agent/provider"]').click(); await expect(page).toHaveURL(/#\/agent\/provider/); await page.close(); }); - test("should load agent chat page directly", async ({ context, extensionId }) => { + test("应能直接加载 Agent 会话页", async ({ context, extensionId }) => { const page = await openAgentChatPage(context, extensionId); - - // 聊天页面应包含新建会话按钮 - const newChatBtn = page.locator("button", { hasText: /new|新建/i }).first(); - await expect(newChatBtn).toBeVisible({ timeout: 10000 }); + await expect(page.getByTestId("conv-new")).toBeVisible({ timeout: 10_000 }); await page.close(); }); - test("should load agent provider page directly", async ({ context, extensionId }) => { + test("应能直接加载 Agent 模型服务页", async ({ context, extensionId }) => { const page = await openAgentProviderPage(context, extensionId); - - // Provider 页面应包含添加模型按钮 - const addBtn = page.locator("button", { hasText: /add|添加/i }).first(); - await expect(addBtn).toBeVisible({ timeout: 10000 }); + await expect(page.getByTestId("model-add")).toBeVisible({ timeout: 10_000 }); await page.close(); }); - test("should show empty state on provider page when no models configured", async ({ context, extensionId }) => { + test("未配置模型时模型服务页应显示空状态", async ({ context, extensionId }) => { const page = await openAgentProviderPage(context, extensionId); - - // 没有配置模型时应显示空状态 - const emptyState = page.locator(".arco-empty"); - await expect(emptyState).toBeVisible({ timeout: 10000 }); + await expect(page.getByTestId("empty-state")).toBeVisible({ timeout: 10_000 }); await page.close(); }); }); diff --git a/e2e/agent-provider.spec.ts b/e2e/agent-provider.spec.ts index b73d84437..9b22fa967 100644 --- a/e2e/agent-provider.spec.ts +++ b/e2e/agent-provider.spec.ts @@ -1,84 +1,51 @@ import { test, expect } from "./fixtures"; import { openAgentProviderPage } from "./utils"; -test.describe("Agent Provider Management", () => { - test("should show empty state when no models configured", async ({ context, extensionId }) => { +// new-ui Agent 模型服务页(shadcn):空状态 data-testid="empty-state",添加按钮 +// "model-add",对话框为 Radix Dialog(role=dialog),表单字段 model-name/model-provider/ +// model-base-url/model-api-key/model-id/model-test,Provider 为 Radix Select(role=option)。 +test.describe("Agent 模型服务管理", () => { + test("未配置模型时应显示空状态", async ({ context, extensionId }) => { const page = await openAgentProviderPage(context, extensionId); - - await expect(page.locator(".arco-empty")).toBeVisible({ timeout: 10000 }); + await expect(page.getByTestId("empty-state")).toBeVisible({ timeout: 10_000 }); await page.close(); }); - test("should open add model dialog with correct form fields", async ({ context, extensionId }) => { + test("应打开添加模型对话框且含正确表单字段", async ({ context, extensionId }) => { const page = await openAgentProviderPage(context, extensionId); - - // 点击添加按钮 - const addBtn = page.locator("button.arco-btn-primary").first(); - await expect(addBtn).toBeVisible({ timeout: 10_000 }); - await addBtn.evaluate((element) => (element as HTMLElement).click()); - - // 验证弹窗出现 - const modal = page.locator(".arco-modal"); - await expect(modal).toBeVisible({ timeout: 5000 }); - - // 验证表单包含名称输入框 - const nameInput = modal.locator('input[placeholder*="GPT"]').first(); - await expect(nameInput).toBeVisible(); - - // 验证 Provider 选择器(默认 OpenAI) - const providerSelect = modal.locator(".arco-select").first(); - await expect(providerSelect).toBeVisible(); - await expect(providerSelect.locator("text=OpenAI")).toBeVisible(); - - // 验证 API Base URL 输入框 - const baseUrlInput = modal.locator('input[placeholder*="openai"]').first(); - await expect(baseUrlInput).toBeVisible(); - - // 验证 API Key 输入框 - await expect(modal.locator("input[type='password']").first()).toBeVisible(); - - // 验证模型 Select - await expect(modal.locator(".arco-select").last()).toBeVisible(); - - // 验证测试连接按钮 - const testBtn = modal.locator("button", { hasText: /test|测试/i }).first(); - await expect(testBtn).toBeVisible(); - - // 取消关闭弹窗 - await modal - .locator("button", { hasText: /cancel|取消/i }) - .first() - .evaluate((element) => (element as HTMLElement).click()); - await expect(modal).not.toBeVisible(); - + await page.getByTestId("model-add").click(); + + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5_000 }); + + await expect(dialog.getByTestId("model-name")).toBeVisible(); + await expect(dialog.getByTestId("model-provider")).toBeVisible(); + // 默认 provider 为 openai,API 地址 placeholder 含 openai + await expect(dialog.getByTestId("model-base-url")).toHaveAttribute("placeholder", /openai/i); + await expect(dialog.getByTestId("model-api-key")).toHaveAttribute("type", "password"); + await expect(dialog.getByTestId("model-id")).toBeVisible(); + await expect(dialog.getByTestId("model-test")).toBeVisible(); + + // 取消关闭对话框 + await dialog.getByRole("button", { name: /cancel|取消/i }).click(); + await expect(dialog).not.toBeVisible(); await page.close(); }); - test("should switch provider between OpenAI and Anthropic", async ({ context, extensionId }) => { + test("应能在 OpenAI 与 Anthropic 间切换 Provider", async ({ context, extensionId }) => { const page = await openAgentProviderPage(context, extensionId); + await page.getByTestId("model-add").click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 5_000 }); - // 打开添加弹窗 - const addBtn = page.locator("button.arco-btn-primary").first(); - await expect(addBtn).toBeVisible({ timeout: 10_000 }); - await addBtn.evaluate((element) => (element as HTMLElement).click()); - const modal = page.locator(".arco-modal"); - await expect(modal).toBeVisible({ timeout: 5000 }); - - // 默认是 OpenAI,placeholder 应包含 openai.com - const baseUrlInput = modal.locator('input[placeholder*="openai"]'); - await expect(baseUrlInput.first()).toBeVisible(); - - // 切换到 Anthropic - const providerSelect = modal.locator(".arco-select").first(); - await providerSelect.evaluate((element) => (element as HTMLElement).click()); - const anthropicOption = page.locator(".arco-select-option", { hasText: "Anthropic" }); - await expect(anthropicOption).toBeVisible({ timeout: 10_000 }); - await anthropicOption.click(); + const baseUrl = dialog.getByTestId("model-base-url"); + await expect(baseUrl).toHaveAttribute("placeholder", /openai/i); - // placeholder 应变为 anthropic.com - const anthropicInput = modal.locator('input[placeholder*="anthropic"]'); - await expect(anthropicInput.first()).toBeVisible(); + // 打开 Provider Select 并选择 Anthropic(Radix Select 选项渲染在 portal) + await dialog.getByTestId("model-provider").click(); + await page.getByRole("option", { name: /anthropic/i }).click(); + await expect(baseUrl).toHaveAttribute("placeholder", /anthropic/i); await page.close(); }); }); diff --git a/e2e/background-script.spec.ts b/e2e/background-script.spec.ts index f3f4a7e93..13f6a7551 100644 --- a/e2e/background-script.spec.ts +++ b/e2e/background-script.spec.ts @@ -86,7 +86,7 @@ async function waitForBackgroundComplete(page: Page): Promise { - const scriptSwitch = page.locator(".arco-switch").first(); + const scriptSwitch = page.getByRole("switch").first(); await expect(scriptSwitch).toBeVisible({ timeout: 10_000 }); if ((await scriptSwitch.getAttribute("aria-checked")) === String(enabled)) return; diff --git a/e2e/gm-api.spec.ts b/e2e/gm-api.spec.ts index 54a96b971..56e16b974 100644 --- a/e2e/gm-api.spec.ts +++ b/e2e/gm-api.spec.ts @@ -4,7 +4,7 @@ import os from "os"; import { createServer } from "http"; import type { AddressInfo } from "net"; import { test as base, expect, chromium, type BrowserContext } from "@playwright/test"; -import { installScriptByCode } from "./utils"; +import { autoApprovePermissions, installScriptByCode } from "./utils"; const MOCK_CONNECT_HOST = "127.0.0.1"; const CSP_TARGET_HOST = "content-security-policy.test"; @@ -204,28 +204,6 @@ function patchGMApiTestCode(code: string, mockOrigin: string): string { .replace(/httpbun\.com\/get/g, `${mockHost}/get`); } -function autoApprovePermissions(context: BrowserContext): void { - context.on("page", async (page) => { - const url = page.url(); - if (!url.includes("confirm.html")) return; - - try { - await page.waitForLoadState("domcontentloaded"); - const successButtons = page.locator("button.arco-btn-status-success"); - await successButtons.first().waitFor({ timeout: 5_000 }); - const count = await successButtons.count(); - if (count >= 3) { - await successButtons.nth(2).click(); - } else { - await successButtons.last().click(); - } - console.log("[autoApprove] Permission approved on confirm page"); - } catch (e) { - console.log("[autoApprove] Failed to approve:", e); - } - }); -} - async function runTestScript( context: BrowserContext, extensionId: string, diff --git a/e2e/install.spec.ts b/e2e/install.spec.ts index c9118c259..57753d1b9 100644 --- a/e2e/install.spec.ts +++ b/e2e/install.spec.ts @@ -1,23 +1,54 @@ +import type { BrowserContext } from "@playwright/test"; import { test, expect } from "./fixtures"; -import { openInstallPage } from "./utils"; -test.describe("Install Page", () => { - // Use a well-known public userscript URL for testing - const testScriptUrl = - "https://raw.githubusercontent.com/nicedayzhu/userscripts/refs/heads/master/hello-world.user.js"; +// new-ui 安装页(shadcn,data-testid 丰富):?url= 触发页面级 fetch 拉取脚本, +// 用 page.route 拦截避免真实网络抖动;脚本元信息渲染后 content-area 可见、 +// 主操作按钮(install-primary)可用、脚本名出现在 h1。 +const SCRIPT_URL = "https://e2e.test/install-test.user.js"; +const SCRIPT_NAME = "E2E Install Test"; +const SCRIPT_BODY = `// ==UserScript== +// @name ${SCRIPT_NAME} +// @namespace https://e2e.test +// @version 1.0.0 +// @description install page e2e +// @author E2E +// @match https://example.com/* +// ==/UserScript== - test("should open install page with URL parameter", async ({ context, extensionId }) => { - const page = await openInstallPage(context, extensionId, testScriptUrl); +console.log("install e2e"); +`; - // The page should load without errors - await expect(page).toHaveTitle(/Install.*ScriptCat|ScriptCat/i); +async function openMockedInstallPage(context: BrowserContext, extensionId: string) { + const page = await context.newPage(); + // 必须在 goto 之前注册路由,安装页挂载即发起 fetch + await page.route("**/install-test.user.js", (route) => + route.fulfill({ + status: 200, + headers: { "Content-Type": "application/javascript; charset=utf-8" }, + body: SCRIPT_BODY, + }) + ); + // 安装页按原始子串读取 url=(location.search.slice),不做 decode,故此处不能 encodeURIComponent + await page.goto(`chrome-extension://${extensionId}/src/install.html?url=${SCRIPT_URL}`, { + waitUntil: "domcontentloaded", }); + return page; +} - test("should display script metadata when loading a script", async ({ context, extensionId }) => { - const page = await openInstallPage(context, extensionId, testScriptUrl); +test.describe("Install 安装页", () => { + test("应通过 URL 参数打开安装页且标题为 ScriptCat", async ({ context, extensionId }) => { + const page = await openMockedInstallPage(context, extensionId); + await expect(page).toHaveTitle(/ScriptCat/i); + }); + + test("加载脚本后应展示脚本元信息并可安装", async ({ context, extensionId }) => { + const page = await openMockedInstallPage(context, extensionId); - // Check that the page has loaded content (not just blank) - const body = page.locator("body"); - await expect(body).not.toHaveText("", { timeout: 10_000 }); + // 脚本名渲染(元信息加载完成的可见信号) + await expect(page.getByText(SCRIPT_NAME).first()).toBeVisible({ timeout: 15_000 }); + // 内容区已填充 + await expect(page.getByTestId("content-area")).toBeVisible({ timeout: 10_000 }); + // 主操作按钮可用(非 disabled) + await expect(page.getByTestId("install-primary")).toBeEnabled({ timeout: 10_000 }); }); }); diff --git a/e2e/options-pages-smoke.spec.ts b/e2e/options-pages-smoke.spec.ts new file mode 100644 index 000000000..4d0a9dac1 --- /dev/null +++ b/e2e/options-pages-smoke.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from "./fixtures"; +import { openOptionsPage } from "./utils"; + +// 高价值回归网:逐一访问每个 options 路由,断言「无未捕获异常 / 无 doThrow 报错」。 +// 这能廉价地兜住「页面挂载即抛错」一类缺陷(例如 getDefaultModelId 在全新安装无默认模型 +// 时用 doThrow 抛错导致模型服务/会话页卡死的回归)。 +const ROUTES = [ + "/", + "/subscribe", + "/logs", + "/tools", + "/settings", + "/agent/chat", + "/agent/provider", + "/agent/skills", + "/agent/mcp", + "/agent/tasks", + "/agent/opfs", + "/agent/settings", +]; + +test.describe("Options 各页加载冒烟", () => { + test("每个路由挂载后均无未捕获异常", async ({ context, extensionId }) => { + const page = await openOptionsPage(context, extensionId); + + const errorsByRoute: Record = {}; + let current = "/"; + page.on("pageerror", (e) => { + (errorsByRoute[current] ??= []).push(`pageerror: ${e.message}`); + }); + + for (const route of ROUTES) { + current = route; + await page.goto(`chrome-extension://${extensionId}/src/options.html#${route}`); + await page.waitForLoadState("domcontentloaded"); + // 给页面挂载副作用(数据加载/消息往返)一点时间触发可能的异常 + await page.waitForTimeout(1500); + } + + expect(errorsByRoute, JSON.stringify(errorsByRoute, null, 2)).toEqual({}); + await page.close(); + }); +}); diff --git a/e2e/options.spec.ts b/e2e/options.spec.ts index efab438a5..8c3160d61 100644 --- a/e2e/options.spec.ts +++ b/e2e/options.spec.ts @@ -1,91 +1,60 @@ import { test, expect } from "./fixtures"; import { openOptionsPage } from "./utils"; -test.describe("Options Page", () => { - test("should load and display ScriptCat title and logo", async ({ context, extensionId }) => { +// new-ui 选项页(shadcn):侧边栏为 React Router NavLink(HashRouter → a[href="#/..."]), +// 主题切换为循环按钮(data-testid="theme-toggle"),新建脚本为 Radix 下拉 +// (data-testid="create-script"),脚本列表空状态 data-testid="script-list-empty"。 +test.describe("Options 选项页", () => { + test("应加载并显示 ScriptCat 标题和 Logo", async ({ context, extensionId }) => { const page = await openOptionsPage(context, extensionId); - - // Check logo is visible - const logo = page.locator('img[alt="ScriptCat"]'); - await expect(logo).toBeVisible(); - - // Check title text - await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible(); + await expect(page.locator('img[alt="ScriptCat"]')).toBeVisible(); + await expect(page.getByText("ScriptCat", { exact: true }).first()).toBeVisible(); }); - test("should navigate via sidebar menu items", async ({ context, extensionId }) => { + test("应通过侧边栏导航切换路由", async ({ context, extensionId }) => { const page = await openOptionsPage(context, extensionId); + const side = page.locator("aside"); + await expect(side).toBeVisible(); - // Wait for the sidebar menu to be visible (use first() since there are two menus) - await expect(page.locator(".arco-menu").first()).toBeVisible(); + await side.locator('a[href="#/subscribe"]').click(); + await expect(page).toHaveURL(/#\/subscribe/); - // Click "Subscribe" / "订阅" menu item and verify route change - await page - .locator(".arco-menu-item") - .filter({ hasText: /subscribe|订阅/i }) - .first() - .click(); - await expect(page).toHaveURL(/.*#\/subscribe/); + await side.locator('a[href="#/logs"]').click(); + await expect(page).toHaveURL(/#\/logs/); - // Click "Logs" / "日志" menu item - await page - .locator(".arco-menu-item") - .filter({ hasText: /log|日志/i }) - .first() - .click(); - await expect(page).toHaveURL(/.*#\/logger/); + await side.locator('a[href="#/tools"]').click(); + await expect(page).toHaveURL(/#\/tools/); - // Click "Tools" / "工具" menu item (use .menu-tools class to avoid hitting "CATool" submenu) - await page.locator(".menu-tools .arco-menu-item").click(); - await expect(page).toHaveURL(/.*#\/tools/); + await side.locator('a[href="#/settings"]').click(); + await expect(page).toHaveURL(/#\/settings/); - // Click "Settings" / "设置" menu item (use .menu-setting to avoid matching agent_settings in submenu) - await page.locator(".menu-setting .arco-menu-item").click(); - await expect(page).toHaveURL(/.*#\/setting/); - - // Navigate back to script list (home) - click the first menu item - await page - .locator(".arco-menu-item") - .filter({ hasText: /installed.*script|已安装脚本/i }) - .first() - .click(); - await expect(page).toHaveURL(/.*#\//); + // 返回首页(脚本列表) + await side.locator('a[href="#/"]').first().click(); + await expect(page).toHaveURL(/options\.html#\/$/); }); - test("should show theme switch dropdown with light/dark/auto options", async ({ context, extensionId }) => { + test("主题切换按钮应在亮/暗/自动间循环", async ({ context, extensionId }) => { const page = await openOptionsPage(context, extensionId); + const themeBtn = page.getByTestId("theme-toggle"); + await expect(themeBtn).toBeVisible(); - // Find the theme toggle button in the action-tools area (icon-only button) - const actionTools = page.locator(".action-tools"); - const themeButton = actionTools.locator(".arco-btn-icon-only").first(); - await themeButton.click(); - - // Verify dropdown with theme options appears - use role="menuitem" - const menuItems = page.locator('[role="menuitem"]'); - await expect(menuItems.first()).toBeVisible({ timeout: 10_000 }); - const count = await menuItems.count(); - expect(count).toBeGreaterThanOrEqual(3); + // 循环切换会更换图标(Sun/Moon/Monitor),断言图标 class 变化 + const before = await themeBtn.locator("svg").getAttribute("class"); + await themeBtn.click(); + await expect.poll(() => themeBtn.locator("svg").getAttribute("class"), { timeout: 5_000 }).not.toBe(before); }); - test("should show create script dropdown menu", async ({ context, extensionId }) => { + test("新建脚本按钮应展开下拉菜单", async ({ context, extensionId }) => { const page = await openOptionsPage(context, extensionId); + await page.getByTestId("create-script").click(); - // The create script button is the first text button in action-tools - const createBtn = page.locator(".action-tools .arco-btn-text").first(); - await createBtn.click(); - - // Verify dropdown menu appears - use role="menuitem" const menuItems = page.locator('[role="menuitem"]'); await expect(menuItems.first()).toBeVisible({ timeout: 10_000 }); - const count = await menuItems.count(); - expect(count).toBeGreaterThanOrEqual(3); + expect(await menuItems.count()).toBeGreaterThanOrEqual(3); }); - test("should show empty state when script list is empty", async ({ context, extensionId }) => { + test("脚本列表为空时应显示空状态", async ({ context, extensionId }) => { const page = await openOptionsPage(context, extensionId); - - // The empty state component from arco-design should be visible - const emptyState = page.locator(".arco-empty"); - await expect(emptyState).toBeVisible({ timeout: 10_000 }); + await expect(page.getByTestId("script-list-empty")).toBeVisible({ timeout: 10_000 }); }); }); diff --git a/e2e/popup.spec.ts b/e2e/popup.spec.ts index 88671c038..409e3cd32 100644 --- a/e2e/popup.spec.ts +++ b/e2e/popup.spec.ts @@ -1,53 +1,34 @@ import { test, expect } from "./fixtures"; import { openPopupPage } from "./utils"; -test.describe("Popup Page", () => { - test("should load and display ScriptCat title", async ({ context, extensionId }) => { +// new-ui popup(shadcn):标题 h1、全局 Radix Switch、Radix Accordion 分组、 +// 图标按钮(aria-label 设置/更多菜单)、Radix DropdownMenu(role=menuitem)。 +test.describe("Popup 页面", () => { + test("应加载并显示 ScriptCat 标题", async ({ context, extensionId }) => { const page = await openPopupPage(context, extensionId); - - // The popup should show "ScriptCat" title in the card header await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 10_000 }); }); - test("should show global script enable/disable switch", async ({ context, extensionId }) => { + test("应显示全局脚本启用/禁用开关", async ({ context, extensionId }) => { const page = await openPopupPage(context, extensionId); - - // The switch for enabling/disabling scripts should be present - const globalSwitch = page.locator(".arco-switch").first(); - await expect(globalSwitch).toBeVisible({ timeout: 10_000 }); + // 顶部全局开关为 Radix Switch(role=switch) + await expect(page.getByRole("switch").first()).toBeVisible({ timeout: 10_000 }); }); - test("should render Collapse sections for scripts", async ({ context, extensionId }) => { + test("应渲染脚本分组折叠区", async ({ context, extensionId }) => { const page = await openPopupPage(context, extensionId); - - // Wait for the collapse component to render - const collapse = page.locator(".arco-collapse"); - await expect(collapse).toBeVisible({ timeout: 10_000 }); - - // Should have at least one collapse item (current page scripts) - const collapseItems = page.locator(".arco-collapse-item"); - const count = await collapseItems.count(); - expect(count).toBeGreaterThanOrEqual(1); + // 「当前页运行脚本」分组标题(0 脚本时分组仍渲染,仅内容为空提示) + await expect(page.getByText(/current page|当前页/i).first()).toBeVisible({ timeout: 10_000 }); }); - test("should have settings button that works", async ({ context, extensionId }) => { + test("点击设置按钮应打开选项页", async ({ context, extensionId }) => { const page = await openPopupPage(context, extensionId); - - // Wait for the popup to fully load await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 10_000 }); - // Find the settings button - it's an icon-only button in the header - // The order is: Switch, Settings, Notification, MoreMenu - const iconButtons = page.locator(".arco-btn-icon-only"); - // Settings is the first icon-only button - const settingsBtn = iconButtons.first(); - await expect(settingsBtn).toBeVisible(); - const existingPages = new Set(context.pages()); - await settingsBtn.click(); + await page.getByLabel("设置").click(); - // Startup guide pages may open around the same time; assert on the - // settings page specifically instead of whichever page event arrives first. + // 首次引导页等可能同时打开,断言专门匹配 options 页而非最先到达的 page await expect .poll( () => @@ -60,22 +41,14 @@ test.describe("Popup Page", () => { .toMatch(/options\.html/); }); - test("should show more menu dropdown with items", async ({ context, extensionId }) => { + test("更多菜单应展开并含多个菜单项", async ({ context, extensionId }) => { const page = await openPopupPage(context, extensionId); - - // Wait for popup to load await expect(page.getByText("ScriptCat", { exact: true })).toBeVisible({ timeout: 10_000 }); - // The more menu button is the last icon-only button - const iconButtons = page.locator(".arco-btn-icon-only"); - const count = await iconButtons.count(); - const moreBtn = iconButtons.nth(count - 1); - await moreBtn.click(); + await page.getByLabel("更多菜单").click(); - // The dropdown menu items use role="menuitem" const menuItems = page.locator('[role="menuitem"]'); await expect(menuItems.first()).toBeVisible({ timeout: 10_000 }); - const itemCount = await menuItems.count(); - expect(itemCount).toBeGreaterThanOrEqual(3); + expect(await menuItems.count()).toBeGreaterThanOrEqual(3); }); }); diff --git a/e2e/script-editor.spec.ts b/e2e/script-editor.spec.ts index 5ae310579..722baa86a 100644 --- a/e2e/script-editor.spec.ts +++ b/e2e/script-editor.spec.ts @@ -1,54 +1,37 @@ import { test, expect } from "./fixtures"; import { openEditorPage, openOptionsPage, saveCurrentEditor } from "./utils"; -test.describe("Script Editor", () => { - test("should load editor page with Monaco editor", async ({ context, extensionId }) => { +// new-ui 脚本编辑器:路由 #/script/editor 加载空白模板(normal.tpl,含 ==UserScript==); +// Monaco 选择器(.monaco-editor/.view-lines) 为框架级不变;保存成功为 sonner toast。 +test.describe("Script 编辑器", () => { + test("应加载编辑器页并渲染 Monaco", async ({ context, extensionId }) => { const page = await openEditorPage(context, extensionId); - - // Wait for Monaco editor to render - const monacoEditor = page.locator(".monaco-editor"); - await expect(monacoEditor).toBeVisible({ timeout: 10_000 }); + await expect(page.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 }); }); - test("should load new user script template", async ({ context, extensionId }) => { + test("应载入新建脚本模板", async ({ context, extensionId }) => { const page = await openEditorPage(context, extensionId); - - // Wait for Monaco editor - const monacoEditor = page.locator(".monaco-editor"); - await expect(monacoEditor).toBeVisible({ timeout: 10_000 }); - - // The editor should contain a UserScript header with default template content - const editorContent = page.locator(".view-lines"); - await expect(editorContent).toContainText("==UserScript==", { timeout: 10_000 }); + await expect(page.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 }); + await expect(page.locator(".view-lines")).toContainText("==UserScript==", { timeout: 10_000 }); }); - test("should save script successfully", async ({ context, extensionId }) => { + test("应能成功保存脚本", async ({ context, extensionId }) => { const page = await openEditorPage(context, extensionId); - - // Wait for Monaco editor to fully load - const monacoEditor = page.locator(".monaco-editor"); - await expect(monacoEditor).toBeVisible({ timeout: 10_000 }); + await expect(page.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 }); await expect(page.locator(".view-lines")).toContainText("==UserScript==", { timeout: 10_000 }); await saveCurrentEditor(context, extensionId, page); }); - test("should show newly created script in the list after saving", async ({ context, extensionId }) => { - // First create a script via the editor + test("保存后脚本应出现在列表中", async ({ context, extensionId }) => { const editorPage = await openEditorPage(context, extensionId); - await expect(editorPage.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 }); - await expect(editorPage.locator(".view-lines")).toContainText("==UserScript==", { - timeout: 10_000, - }); + await expect(editorPage.locator(".view-lines")).toContainText("==UserScript==", { timeout: 10_000 }); await saveCurrentEditor(context, extensionId, editorPage); - // Now open the options page to check the script list const listPage = await openOptionsPage(context, extensionId); - - // The script list should now contain at least one script entry (no empty state) - const emptyState = listPage.locator(".arco-empty"); - await expect(emptyState).toHaveCount(0); + // 保存后列表非空(无空状态) + await expect(listPage.getByTestId("script-list-empty")).toHaveCount(0, { timeout: 10_000 }); }); }); diff --git a/e2e/script-management.spec.ts b/e2e/script-management.spec.ts index eeee06f5f..5988fbbe8 100644 --- a/e2e/script-management.spec.ts +++ b/e2e/script-management.spec.ts @@ -2,96 +2,67 @@ import { test, expect } from "./fixtures"; import type { BrowserContext, Page } from "@playwright/test"; import { openEditorPage, openOptionsPage, saveCurrentEditor } from "./utils"; -/** - * Helper: create a script via the editor, then open the options page. - */ +// new-ui 脚本列表(shadcn 表格视图,桌面默认):空状态 data-testid="script-list-empty", +// 每行启用开关为 Radix Switch(role=switch),删除为行内 Trash2 图标 + Popconfirm +// (确认按钮 data-testid="popconfirm-confirm"),搜索框 data-testid="script-search"。 + +/** 通过编辑器创建一个脚本,再打开脚本列表 */ async function createScriptAndGoToList(context: BrowserContext, extensionId: string): Promise { const editorPage = await openEditorPage(context, extensionId); - - // Wait for Monaco editor await expect(editorPage.locator(".monaco-editor")).toBeVisible({ timeout: 10_000 }); - await expect(editorPage.locator(".view-lines")).toContainText("==UserScript==", { - timeout: 10_000, - }); - + await expect(editorPage.locator(".view-lines")).toContainText("==UserScript==", { timeout: 10_000 }); await saveCurrentEditor(context, extensionId, editorPage); + await editorPage.close(); - // Open the options page (script list) - const page = await openOptionsPage(context, extensionId); - - return page; + return openOptionsPage(context, extensionId); } -test.describe("Script Management", () => { - test("should create a script and see it in the list", async ({ context, extensionId }) => { +test.describe("脚本管理", () => { + test("创建脚本后应出现在列表中", async ({ context, extensionId }) => { const page = await createScriptAndGoToList(context, extensionId); - - // The script list should have at least one entry (no empty state) - const emptyState = page.locator(".arco-empty"); - await expect(emptyState).toHaveCount(0); + // 列表非空(无空状态) + await expect(page.getByTestId("script-list-empty")).toHaveCount(0, { timeout: 10_000 }); + await expect(page.getByRole("switch").first()).toBeVisible({ timeout: 10_000 }); }); - test("should toggle enable/disable on a script", async ({ context, extensionId }) => { + test("应能切换脚本的启用/禁用", async ({ context, extensionId }) => { const page = await createScriptAndGoToList(context, extensionId); - // Find the switch/toggle in the script list - const scriptSwitch = page.locator(".arco-switch").first(); + const scriptSwitch = page.getByRole("switch").first(); await expect(scriptSwitch).toBeVisible({ timeout: 10_000 }); - // Get initial state const initialChecked = await scriptSwitch.getAttribute("aria-checked"); - - // Click to toggle. Use DOM click because the current layout can intercept - // Playwright's pointer event even when the switch itself is visible. - await scriptSwitch.evaluate((element) => (element as HTMLElement).click()); + await scriptSwitch.click(); await expect(scriptSwitch).not.toHaveAttribute("aria-checked", initialChecked || "", { timeout: 10_000 }); - - const newChecked = await scriptSwitch.getAttribute("aria-checked"); - expect(newChecked).not.toBe(initialChecked); }); - test("should delete a script", async ({ context, extensionId }) => { + test("应能删除脚本", async ({ context, extensionId }) => { const page = await createScriptAndGoToList(context, extensionId); - // Right-click on a script row to get context menu - const scriptRow = page.locator(".arco-table-row, .arco-card-body .arco-list-item, [class*='script']").first(); - if (await scriptRow.isVisible()) { - await scriptRow.click({ button: "right" }); - - // Look for delete option in context menu - const deleteOption = page.getByText(/delete|删除/i).first(); - if (await deleteOption.isVisible({ timeout: 10_000 }).catch(() => false)) { - await deleteOption.click(); + // 行内删除按钮(Trash2 图标,lucide class 语言无关) + const deleteBtn = page.locator("button:has(svg.lucide-trash-2)").first(); + await expect(deleteBtn).toBeVisible({ timeout: 10_000 }); + await deleteBtn.click(); - // Confirm deletion if a modal appears - const confirmBtn = page.locator(".arco-modal .arco-btn-primary"); - if (await confirmBtn.isVisible({ timeout: 10_000 }).catch(() => false)) { - await confirmBtn.click(); - } + // Popconfirm 确认 + await page.getByTestId("popconfirm-confirm").click(); - // After deletion, the list should be empty again - const emptyState = page.locator(".arco-empty"); - await expect(emptyState).toBeVisible({ timeout: 10_000 }); - } - } + // 删除后回到空状态 + await expect(page.getByTestId("script-list-empty")).toBeVisible({ timeout: 10_000 }); }); - test("should search/filter scripts", async ({ context, extensionId }) => { + test("应能搜索/过滤脚本", async ({ context, extensionId }) => { const page = await createScriptAndGoToList(context, extensionId); - // Look for a search input - const searchInput = page.locator('input[type="text"], .arco-input').first(); - if (await searchInput.isVisible({ timeout: 10_000 }).catch(() => false)) { - // Type a search query that won't match - await searchInput.fill("nonexistent_script_xyz"); + const search = page.getByTestId("script-search"); + await expect(search).toBeVisible({ timeout: 10_000 }); - // The list should show empty or no results - const emptyState = page.locator(".arco-empty"); - await expect(emptyState).toBeVisible({ timeout: 10_000 }); + // 不匹配的关键字 → 空状态 + await search.fill("nonexistent_script_xyz"); + await expect(page.getByTestId("script-list-empty")).toBeVisible({ timeout: 10_000 }); - // Clear search and scripts should reappear - await searchInput.clear(); - await expect(emptyState).toHaveCount(0, { timeout: 10_000 }); - } + // 清空 → 脚本重新出现 + await search.fill(""); + await expect(page.getByTestId("script-list-empty")).toHaveCount(0, { timeout: 10_000 }); }); }); diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts index 8aae86d65..de4295a1d 100644 --- a/e2e/settings.spec.ts +++ b/e2e/settings.spec.ts @@ -1,36 +1,34 @@ import { test, expect } from "./fixtures"; import { openOptionsPage } from "./utils"; -test.describe("Settings Page", () => { - test("should render the settings page", async ({ context, extensionId }) => { +// new-ui 设置页(shadcn,scroll-spy 一次性渲染全部分区):路由 #/settings, +// 滚动容器 data-testid="setting-page";交互控件为 Radix Select(role=combobox)/ +// Switch(role=switch)/Checkbox(role=checkbox) 及原生 input。 +test.describe("Settings 设置页", () => { + test("应渲染设置页内容容器", async ({ context, extensionId }) => { const page = await openOptionsPage(context, extensionId); - - // Navigate to settings via hash route - await page.goto(`chrome-extension://${extensionId}/src/options.html#/setting`); + await page.goto(`chrome-extension://${extensionId}/src/options.html#/settings`); await page.waitForLoadState("domcontentloaded"); - // The settings page should have visible content (cards, selects, inputs, etc.) - const content = page.locator(".arco-layout-content"); - await expect(content).toBeVisible({ timeout: 10_000 }); + await expect(page.getByTestId("setting-page")).toBeVisible({ timeout: 10_000 }); }); - test("should have visible and interactive settings items", async ({ context, extensionId }) => { + test("应包含可见且可交互的设置项", async ({ context, extensionId }) => { const page = await openOptionsPage(context, extensionId); - - // Navigate to settings - await page.goto(`chrome-extension://${extensionId}/src/options.html#/setting`); + await page.goto(`chrome-extension://${extensionId}/src/options.html#/settings`); await page.waitForLoadState("domcontentloaded"); - // Check that at least one Select component or Input is visible - const selects = page.locator(".arco-select"); - const inputs = page.locator(".arco-input"); - const checkboxes = page.locator(".arco-checkbox"); + const combobox = page.getByRole("combobox"); + const switches = page.getByRole("switch"); + const checkboxes = page.getByRole("checkbox"); + const inputs = page.locator("input"); - // Settings page should have at least some interactive elements await expect - .poll(async () => (await selects.count()) + (await inputs.count()) + (await checkboxes.count()), { - timeout: 10_000, - }) + .poll( + async () => + (await combobox.count()) + (await switches.count()) + (await checkboxes.count()) + (await inputs.count()), + { timeout: 10_000 } + ) .toBeGreaterThan(0); }); }); diff --git a/e2e/user-config-yaml.spec.ts b/e2e/user-config-yaml.spec.ts index e40cc758b..71c951367 100644 --- a/e2e/user-config-yaml.spec.ts +++ b/e2e/user-config-yaml.spec.ts @@ -84,9 +84,10 @@ test.describe("UserConfig YAML prototype pollution (#1494)", () => { const page = await context.newPage(); await page.goto(`chrome-extension://${extensionId}/src/options.html#/?userConfig=${info!.uuid}`); await page.waitForLoadState("domcontentloaded"); - const modal = page.locator(".modal-config"); - await expect(modal).toBeVisible({ timeout: 10_000 }); - await expect(modal).toContainText("配置A标题"); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible({ timeout: 10_000 }); + await expect(dialog).toContainText("UC Valid E2E"); + await expect(dialog).toContainText("配置A标题"); await page.close(); }); diff --git a/e2e/utils.ts b/e2e/utils.ts index 1cd393d7f..b0390fc02 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -11,8 +11,9 @@ export function patchScriptCode(code: string): string { /** * Auto-approve permission confirm dialogs opened by the extension. - * Listens for new pages matching confirm.html and clicks the - * "permanent allow all" button (type=4, allow=true). + * Listens for new pages matching confirm.html (new-ui / shadcn) and grants the request: + * site-access variant → click "request permission"; otherwise pick "permanent" duration + * then click "allow". Selectors are data-testid based, so they are language-agnostic. */ export function autoApprovePermissions(context: BrowserContext): void { context.on("page", async (page) => { @@ -21,13 +22,20 @@ export function autoApprovePermissions(context: BrowserContext): void { try { await page.waitForLoadState("domcontentloaded"); - const successButtons = page.locator("button.arco-btn-status-success"); - await successButtons.first().waitFor({ timeout: 5_000 }); - const count = await successButtons.count(); - if (count >= 3) { - await successButtons.nth(2).click(); + const request = page.getByTestId("confirm-request"); + const allow = page.getByTestId("confirm-allow"); + await allow.or(request).first().waitFor({ timeout: 5_000 }); + if (await request.count()) { + await request.first().click(); } else { - await successButtons.last().click(); + // 尽量永久授权,避免同一测试内重复弹窗 + const permanent = page.getByTestId("confirm-duration-permanent"); + if (await permanent.count()) + await permanent + .first() + .click() + .catch(() => {}); + await allow.first().click(); } console.log("[autoApprove] Permission approved on confirm page"); } catch (e) { @@ -129,9 +137,10 @@ async function focusMonacoEditor(page: Page): Promise { async function waitForSavedScriptInList(context: BrowserContext, extensionId: string): Promise { const listPage = await openOptionsPage(context, extensionId); try { - await listPage.locator("#script-list").waitFor({ timeout: 15_000 }); + // new-ui 列表页加载完成的稳定信号(桌面工具栏 view-toggle / 移动搜索栏) await listPage - .locator("#script-list .arco-table-row, #script-list .script-list-card") + .getByTestId("view-toggle") + .or(listPage.getByTestId("mobile-search")) .first() .waitFor({ state: "visible", timeout: 30_000 }); } finally { @@ -143,13 +152,14 @@ export async function saveCurrentEditor(context: BrowserContext, extensionId: st await focusMonacoEditor(page); await page.keyboard.press("ControlOrMeta+s"); - const messageAppeared = await page - .locator(".arco-message") + // new-ui 保存成功为 sonner toast + const toastAppeared = await page + .locator("[data-sonner-toast]") .first() .waitFor({ timeout: 10_000 }) .then(() => true) .catch(() => false); - if (messageAppeared) return; + if (toastAppeared) return; await waitForSavedScriptInList(context, extensionId); } diff --git a/e2e/vscode-connect.spec.ts b/e2e/vscode-connect.spec.ts index db3585895..7f17a6876 100644 --- a/e2e/vscode-connect.spec.ts +++ b/e2e/vscode-connect.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from "./fixtures"; import { openOptionsPage } from "./utils"; -import type { Page } from "@playwright/test"; +import type { BrowserContext, Page } from "@playwright/test"; import { WebSocketServer, type WebSocket } from "ws"; // ──────────────────────────────────────────────── @@ -8,17 +8,16 @@ import { WebSocketServer, type WebSocket } from "ws"; // ──────────────────────────────────────────────── /** 打开 Tools 页面 */ -async function openToolsPage(context: Parameters[0], extensionId: string): Promise { +async function openToolsPage(context: BrowserContext, extensionId: string): Promise { const page = await openOptionsPage(context, extensionId); await page.goto(`chrome-extension://${extensionId}/src/options.html#/tools`); await page.waitForLoadState("domcontentloaded"); return page; } -/** 获取「开发工具」卡片区域的定位器 */ +/** 「开发工具」卡片(new-ui SettingCard,data-spy-id="dev-tools") */ function getDevCard(page: Page) { - // 开发工具 / Development Tool 卡片是页面上第二个 Card - return page.locator(".arco-card").nth(1); + return page.locator('[data-spy-id="dev-tools"]'); } /** 启动一个临时 WebSocket 服务器,返回 URL 和清理函数 */ @@ -26,9 +25,7 @@ function createMockWSServer(): Promise<{ url: string; connections: WebSocket[]; close: () => Promise; - /** 向所有已连接客户端发送消息 */ broadcast: (data: unknown) => void; - /** 等待收到指定 action 的消息 */ waitForAction: (action: string, timeout?: number) => Promise; }> { return new Promise((resolve, reject) => { @@ -37,7 +34,7 @@ function createMockWSServer(): Promise<{ const wss = new WebSocketServer({ host: "127.0.0.1", port: 0 }, () => { const addr = wss.address(); - if (typeof addr === "string") { + if (!addr || typeof addr === "string") { reject(new Error("Unexpected address type")); return; } @@ -48,9 +45,7 @@ function createMockWSServer(): Promise<{ ws.on("message", (raw) => { try { const msg = JSON.parse(raw.toString()); - for (const listener of messageListeners) { - listener(msg); - } + for (const listener of messageListeners) listener(msg); } catch { // 忽略非 JSON 消息 } @@ -68,17 +63,15 @@ function createMockWSServer(): Promise<{ broadcast: (data: unknown) => { const payload = JSON.stringify(data); for (const ws of connections) { - if (ws.readyState === ws.OPEN) { - ws.send(payload); - } + if (ws.readyState === ws.OPEN) ws.send(payload); } }, waitForAction: (action: string, timeout = 10_000) => - new Promise((resolve, reject) => { + new Promise((resolveAction, rejectAction) => { const timer = setTimeout(() => { const idx = messageListeners.indexOf(handler); if (idx >= 0) messageListeners.splice(idx, 1); - reject(new Error(`Timeout waiting for action: ${action}`)); + rejectAction(new Error(`Timeout waiting for action: ${action}`)); }, timeout); const handler = (msg: any) => { @@ -86,7 +79,7 @@ function createMockWSServer(): Promise<{ clearTimeout(timer); const idx = messageListeners.indexOf(handler); if (idx >= 0) messageListeners.splice(idx, 1); - resolve(msg); + resolveAction(msg); } }; messageListeners.push(handler); @@ -106,60 +99,47 @@ test.describe("VSCode 连接", () => { const card = getDevCard(page); // 卡片标题 - await expect(card.getByText(/development tool|开发工具/i)).toBeVisible(); + await expect(card.getByText(/development tool|开发工具/i).first()).toBeVisible(); - // VSCode URL 输入框 - const urlInput = card.locator(".arco-input"); + // VSCode URL 输入框(默认值含 ws://) + const urlInput = card.getByLabel("vscode_url_input"); await expect(urlInput).toBeVisible(); - // 默认值应包含 ws:// - const value = await urlInput.inputValue(); - expect(value).toMatch(/^ws:\/\//); + expect(await urlInput.inputValue()).toMatch(/^ws:\/\//); // 自动连接复选框 - const checkbox = card.locator(".arco-checkbox"); - await expect(checkbox).toBeVisible(); + await expect(card.getByLabel("vscode_reconnect")).toBeVisible(); await expect(card.getByText(/auto connect vscode|自动连接\s*vscode/i)).toBeVisible(); // 连接按钮 - const connectBtn = card.locator(".arco-btn-primary"); - await expect(connectBtn).toBeVisible(); - await expect(connectBtn.getByText(/connect|连接/i)).toBeVisible(); + await expect(card.getByLabel("vscode_connect")).toBeVisible(); }); test("应能修改 VSCode URL 和切换自动连接", async ({ context, extensionId }) => { const page = await openToolsPage(context, extensionId); const card = getDevCard(page); - // 修改 URL - const urlInput = card.locator(".arco-input"); - await urlInput.clear(); + const urlInput = card.getByLabel("vscode_url_input"); await urlInput.fill("ws://localhost:9999"); await expect(urlInput).toHaveValue("ws://localhost:9999"); - // 切换自动连接复选框 - const checkbox = card.locator(".arco-checkbox input"); - const initialChecked = await checkbox.isChecked(); - await card.locator(".arco-checkbox").evaluate((element) => (element as HTMLElement).click()); - const newChecked = await checkbox.isChecked(); - expect(newChecked).toBe(!initialChecked); + const checkbox = card.getByLabel("vscode_reconnect"); + const initialChecked = await checkbox.getAttribute("aria-checked"); + await checkbox.click(); + await expect(checkbox).not.toHaveAttribute("aria-checked", initialChecked || ""); }); - test("点击连接按钮应发送连接命令", async ({ context, extensionId }) => { + test("点击连接按钮应发送连接命令并提示成功", async ({ context, extensionId }) => { const page = await openToolsPage(context, extensionId); const card = getDevCard(page); - // 连接按钮存在且可点击 - const connectBtn = card.locator(".arco-btn-primary"); - await connectBtn.evaluate((element) => (element as HTMLElement).click()); + // connectVSCode 为消息传递操作,消息投递成功即 resolve, + // 即使没有 WebSocket 服务器运行也应显示「连接成功」toast。 + await card.getByLabel("vscode_connect").click(); - // connectVSCode 是消息传递操作,消息投递成功即 resolve, - // 所以即使没有 WebSocket 服务器运行,也应显示「连接成功」提示 - const successMsg = page.locator(".arco-message").getByText(/connection successful|连接成功/i); - await expect(successMsg).toBeVisible({ timeout: 10_000 }); + await expect(page.getByText(/connection success|连接成功/i).first()).toBeVisible({ timeout: 10_000 }); }); test("应能通过 WebSocket 连接并接收脚本同步", async ({ context, extensionId }) => { - // 启动 Mock WebSocket 服务器 const server = await createMockWSServer(); try { @@ -167,29 +147,20 @@ test.describe("VSCode 连接", () => { const card = getDevCard(page); // 设置 URL 为 Mock 服务器地址 - const urlInput = card.locator(".arco-input"); - await urlInput.clear(); + const urlInput = card.getByLabel("vscode_url_input"); await urlInput.fill(server.url); - // 在点击连接之前就开始监听 hello 消息,避免竞态 + // 点击连接前先监听 hello 握手,避免竞态 const helloPromise = server.waitForAction("hello", 30_000); - // 点击连接 - const connectBtn = card.locator(".arco-btn-primary"); - await expect(connectBtn).toBeVisible({ timeout: 10_000 }); - await connectBtn.evaluate((element) => (element as HTMLElement).click()); + await card.getByLabel("vscode_connect").click(); + await expect(page.getByText(/connection success|连接成功/i).first()).toBeVisible({ timeout: 10_000 }); - // 等待「连接成功」消息 - const successMsg = page.locator(".arco-message").getByText(/connection successful|连接成功/i); - await expect(successMsg).toBeVisible({ timeout: 10_000 }); - - // 等待收到 hello 握手消息 + // 收到 hello 握手 await helloPromise; - - // 验证客户端已连接 expect(server.connections.length).toBeGreaterThanOrEqual(1); - // 发送 onchange 消息,模拟 VSCode 推送脚本 + // 推送 onchange,模拟 VSCode 同步脚本 const testScript = `// ==UserScript== // @name VSCode E2E Test Script // @namespace https://e2e.test/vscode @@ -201,19 +172,14 @@ test.describe("VSCode 连接", () => { console.log("VSCode synced script"); `; - server.broadcast({ action: "onchange", - data: { - script: testScript, - uri: "file:///e2e-test/vscode-sync-test.user.js", - }, + data: { script: testScript, uri: "file:///e2e-test/vscode-sync-test.user.js" }, }); - // 验证脚本已安装:导航到脚本列表,检查脚本是否出现 + // 脚本应安装并出现在列表 const listPage = await openOptionsPage(context, extensionId); - const scriptItem = listPage.getByText("VSCode E2E Test Script"); - await expect(scriptItem).toBeVisible({ timeout: 15_000 }); + await expect(listPage.getByText("VSCode E2E Test Script")).toBeVisible({ timeout: 15_000 }); await listPage.close(); } finally { await server.close(); diff --git a/package.json b/package.json index bbcae64a2..09206971f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scriptcat", - "version": "1.4.0-beta.4", + "version": "1.5.0-beta", "description": "脚本猫,一个可以执行用户脚本的浏览器扩展,万物皆可脚本化,让你的浏览器可以做更多的事情!", "author": "CodFrm", "license": "GPLv3", @@ -8,8 +8,8 @@ "scripts": { "preinstall": "pnpm dlx only-allow pnpm", "prepare": "husky", - "test": "vitest --test-timeout=500 --no-coverage --reporter=verbose", - "test:ci": "vitest run --test-timeout=500 --no-coverage --reporter=default --reporter.summary=false", + "test": "vitest --test-timeout=850 --no-coverage --reporter=verbose", + "test:ci": "vitest run --test-timeout=850 --no-coverage --reporter=default --reporter.summary=false", "coverage": "vitest run --coverage", "coverage:ci": "vitest run --coverage --silent --reporter=default --reporter.default.summary=false", "typecheck": "tsc --noEmit", @@ -27,12 +27,34 @@ "test:e2e:ui": "pnpm exec playwright test --ui" }, "dependencies": { - "@arco-design/web-react": "^2.66.7", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@testing-library/dom": "^10.4.1", "chardet": "^2.1.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "cron": "^4.4.0", "crypto-js": "^4.2.0", "dayjs": "^1.11.13", @@ -41,20 +63,22 @@ "eslint-linter-browserify": "9.26.0", "eventemitter3": "^5.0.1", "fast-xml-parser": "^5.5.8", + "happy-dom": "^20.10.2", "highlight.js": "^11.11.1", - "i18next": "^23.16.4", + "i18next": "^26.0.3", + "lucide-react": "^1.7.0", "monaco-editor": "^0.52.2", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-dropzone": "^14.3.8", - "react-i18next": "^15.6.0", - "react-icons": "^5.5.0", - "react-joyride": "^2.9.3", + "radix-ui": "^1.4.3", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-i18next": "^17.0.2", "react-markdown": "^9.1.0", - "react-router-dom": "^7.13.0", + "react-router-dom": "^7.14.0", "rehype-highlight": "^7.0.2", "remark-gfm": "^4.0.1", + "sonner": "^2.0.7", "string-similarity-js": "^2.1.4", + "tailwind-merge": "^3.5.0", "uuid": "^11.1.0", "web-jszipp": "^1.0.8", "webdav": "^5.9.0", @@ -67,19 +91,19 @@ "@rspack/cli": "^1.7.11", "@rspack/core": "^1.7.11", "@swc/helpers": "^0.5.17", + "@tailwindcss/postcss": "^4.2.4", "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.0", - "@types/chrome": "^0.1.27", + "@testing-library/react": "^16.3.2", + "@types/chrome": "^0.1.39", "@types/crypto-js": "^4.2.2", "@types/node": "^22.12.0", - "@types/react": "^18.3.1", - "@types/react-dom": "^18.3.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", "@types/serviceworker": "^0.0.120", - "@unocss/postcss": "66.5.4", + "@types/ws": "^8.18.1", "@vitest/coverage-v8": "^4.0.18", "acorn": "^8.16.0", - "autoprefixer": "^10.4.21", "cross-env": "^10.1.0", "crx": "^5.0.1", "eslint": "^9.39.2", @@ -91,19 +115,21 @@ "globals": "^16.5.0", "husky": "^9.1.7", "iconv-lite": "^0.7.2", - "jsdom": "^26.1.0", + "jsdom": "^29.1.1", "magic-string": "^0.30.21", "mock-xmlhttprequest": "^8.4.1", "postcss": "^8.5.6", "postcss-loader": "^8.2.0", "prettier": "^3.6.2", "semver": "^7.7.1", + "tailwindcss": "^4.2.4", "ts-node": "^10.9.2", + "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", "typescript-eslint": "^8.46.2", "uglify-js": "^3.19.3", - "unocss": "66.5.4", - "vitest": "^4.0.18" + "vitest": "^4.1.8", + "ws": "^8.21.0" }, "packageManager": "pnpm@10.34.1", "sideEffects": [ diff --git a/packages/filesystem/auth.test.ts b/packages/filesystem/auth.test.ts index bf18a4d5d..637a2b2d6 100644 --- a/packages/filesystem/auth.test.ts +++ b/packages/filesystem/auth.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AuthVerify } from "./auth"; import { LocalStorageDAO } from "@App/app/repo/localStorage"; diff --git a/packages/filesystem/baidu/baidu.test.ts b/packages/filesystem/baidu/baidu.test.ts index 033aec0fa..4f3eac36d 100644 --- a/packages/filesystem/baidu/baidu.test.ts +++ b/packages/filesystem/baidu/baidu.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it, vi, afterEach } from "vitest"; import BaiduFileSystem from "./baidu"; diff --git a/packages/filesystem/dropbox/dropbox.test.ts b/packages/filesystem/dropbox/dropbox.test.ts index 6bf549d41..87e7b5290 100644 --- a/packages/filesystem/dropbox/dropbox.test.ts +++ b/packages/filesystem/dropbox/dropbox.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { beforeEach, describe, expect, it, vi } from "vitest"; import DropboxFileSystem from "./dropbox"; diff --git a/packages/filesystem/googledrive/googledrive.test.ts b/packages/filesystem/googledrive/googledrive.test.ts index 320f2cb55..66cfa24be 100644 --- a/packages/filesystem/googledrive/googledrive.test.ts +++ b/packages/filesystem/googledrive/googledrive.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { LocalStorageDAO } from "@App/app/repo/localStorage"; import { FileSystemError, isAuthError, isConflictError, isNotFoundError, isRateLimitError } from "../error"; diff --git a/packages/filesystem/limiter.test.ts b/packages/filesystem/limiter.test.ts index f76ed56f4..b9342a564 100644 --- a/packages/filesystem/limiter.test.ts +++ b/packages/filesystem/limiter.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { afterEach, describe, expect, it, vi } from "vitest"; import type FileSystem from "./filesystem"; import type { FileInfo, FileReader, FileWriter } from "./filesystem"; diff --git a/packages/filesystem/onedrive/onedrive.test.ts b/packages/filesystem/onedrive/onedrive.test.ts index a7400cad1..2d76fc8ba 100644 --- a/packages/filesystem/onedrive/onedrive.test.ts +++ b/packages/filesystem/onedrive/onedrive.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import OneDriveFileSystem from "./onedrive"; import { LocalStorageDAO } from "@App/app/repo/localStorage"; diff --git a/packages/filesystem/s3/client.test.ts b/packages/filesystem/s3/client.test.ts index 29bc234a8..f7b7207ec 100644 --- a/packages/filesystem/s3/client.test.ts +++ b/packages/filesystem/s3/client.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { S3Client, S3Error } from "./client"; import type { S3ClientConfig } from "./client"; diff --git a/packages/filesystem/s3/s3.test.ts b/packages/filesystem/s3/s3.test.ts index 8e52a51c7..43ee2343f 100644 --- a/packages/filesystem/s3/s3.test.ts +++ b/packages/filesystem/s3/s3.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach } from "vitest"; import S3FileSystem from "./s3"; import { S3Client, S3Error } from "./client"; diff --git a/packages/filesystem/utils.test.ts b/packages/filesystem/utils.test.ts index 725425cfb..f232e94fa 100644 --- a/packages/filesystem/utils.test.ts +++ b/packages/filesystem/utils.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it } from "vitest"; import { joinPath } from "./utils"; diff --git a/packages/filesystem/webdav/webdav.test.ts b/packages/filesystem/webdav/webdav.test.ts index 4d91a5edb..856e413e1 100644 --- a/packages/filesystem/webdav/webdav.test.ts +++ b/packages/filesystem/webdav/webdav.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach } from "vitest"; import type { WebDAVClient } from "webdav"; import { getPatcher } from "webdav"; diff --git a/packages/message/message_queue.test.ts b/packages/message/message_queue.test.ts index 67669fb55..71f044880 100644 --- a/packages/message/message_queue.test.ts +++ b/packages/message/message_queue.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { MessageQueue, MessageQueueGroup, type IMessageQueue } from "./message_queue"; diff --git a/packages/message/message_queue_group.test.ts b/packages/message/message_queue_group.test.ts index 004c9a419..94af26011 100644 --- a/packages/message/message_queue_group.test.ts +++ b/packages/message/message_queue_group.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { MessageQueue, MessageQueueGroup, type IMessageQueue } from "./message_queue"; diff --git a/packages/message/server.test.ts b/packages/message/server.test.ts index b58037491..fb33ccea2 100644 --- a/packages/message/server.test.ts +++ b/packages/message/server.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { describe, expect, it, beforeEach, vi, afterEach } from "vitest"; import { GetSenderType, SenderConnect, SenderRuntime, Server, type IGetSender } from "./server"; import { CustomEventMessage } from "./custom_event_message"; diff --git a/packages/message/window_message.test.ts b/packages/message/window_message.test.ts index 358e7f33f..17687c7fd 100644 --- a/packages/message/window_message.test.ts +++ b/packages/message/window_message.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; import { ServiceWorkerMessageSend, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b6fdd518..d71c7418a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,24 +8,90 @@ importers: .: dependencies: - '@arco-design/web-react': - specifier: ^2.66.7 - version: 2.66.7(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@dnd-kit/core': specifier: ^6.3.1 - version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@dnd-kit/modifiers': specifier: ^9.0.0 - version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@dnd-kit/sortable': specifier: ^10.0.0 - version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@dnd-kit/utilities': specifier: ^3.2.2 - version: 3.2.2(react@18.3.1) + version: 3.2.2(react@19.2.7) + '@radix-ui/react-accordion': + specifier: ^1.2.12 + version: 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.15 + version: 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-avatar': + specifier: ^1.1.11 + version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-label': + specifier: ^2.1.8 + version: 2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-progress': + specifier: ^1.1.8 + version: 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-radio-group': + specifier: ^1.3.8 + version: 1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-separator': + specifier: ^1.1.8 + version: 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle': + specifier: ^1.1.10 + version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle-group': + specifier: ^1.1.11 + version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.1 chardet: specifier: ^2.1.1 version: 2.1.1 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 cron: specifier: ^4.4.0 version: 4.4.0 @@ -50,48 +116,54 @@ importers: fast-xml-parser: specifier: ^5.5.8 version: 5.5.8 + happy-dom: + specifier: ^20.10.2 + version: 20.10.2 highlight.js: specifier: ^11.11.1 version: 11.11.1 i18next: - specifier: ^23.16.4 - version: 23.16.4 + specifier: ^26.0.3 + version: 26.3.1(typescript@5.9.3) + lucide-react: + specifier: ^1.7.0 + version: 1.17.0(react@19.2.7) monaco-editor: specifier: ^0.52.2 version: 0.52.2 + radix-ui: + specifier: ^1.4.3 + version: 1.5.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: ^19.2.4 + version: 19.2.7 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) - react-dropzone: - specifier: ^14.3.8 - version: 14.3.8(react@18.3.1) + specifier: ^19.2.4 + version: 19.2.7(react@19.2.7) react-i18next: - specifier: ^15.6.0 - version: 15.6.0(i18next@23.16.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - react-icons: - specifier: ^5.5.0 - version: 5.5.0(react@18.3.1) - react-joyride: - specifier: ^2.9.3 - version: 2.9.3(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^17.0.2 + version: 17.0.8(i18next@26.3.1(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3) react-markdown: specifier: ^9.1.0 - version: 9.1.0(@types/react@18.3.23)(react@18.3.1) + version: 9.1.0(@types/react@19.2.17)(react@19.2.7) react-router-dom: - specifier: ^7.13.0 - version: 7.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.14.0 + version: 7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) rehype-highlight: specifier: ^7.0.2 version: 7.0.2 remark-gfm: specifier: ^4.0.1 version: 4.0.1 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) string-similarity-js: specifier: ^2.1.4 version: 2.1.4 + tailwind-merge: + specifier: ^3.5.0 + version: 3.6.0 uuid: specifier: ^11.1.0 version: 11.1.0 @@ -123,15 +195,18 @@ importers: '@swc/helpers': specifier: ^0.5.17 version: 0.5.17 + '@tailwindcss/postcss': + specifier: ^4.2.4 + version: 4.3.0 '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 '@testing-library/react': - specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@types/chrome': - specifier: ^0.1.27 - version: 0.1.27 + specifier: ^0.1.39 + version: 0.1.43 '@types/crypto-js': specifier: ^4.2.2 version: 4.2.2 @@ -139,29 +214,26 @@ importers: specifier: ^22.12.0 version: 22.16.0 '@types/react': - specifier: ^18.3.1 - version: 18.3.23 + specifier: ^19.2.14 + version: 19.2.17 '@types/react-dom': - specifier: ^18.3.1 - version: 18.3.7(@types/react@18.3.23) + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) '@types/semver': specifier: ^7.7.1 version: 7.7.1 '@types/serviceworker': specifier: ^0.0.120 version: 0.0.120 - '@unocss/postcss': - specifier: 66.5.4 - version: 66.5.4(postcss@8.5.6) + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@22.16.0)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.43.1)(yaml@2.8.3)) + version: 4.0.18(vitest@4.1.8) acorn: specifier: ^8.16.0 version: 8.16.0 - autoprefixer: - specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.6) cross-env: specifier: ^10.1.0 version: 10.1.0 @@ -196,8 +268,8 @@ importers: specifier: ^0.7.2 version: 0.7.2 jsdom: - specifier: ^26.1.0 - version: 26.1.0 + specifier: ^29.1.1 + version: 29.1.1 magic-string: specifier: ^0.30.21 version: 0.30.21 @@ -216,9 +288,15 @@ importers: semver: specifier: ^7.7.1 version: 7.7.2 + tailwindcss: + specifier: ^4.2.4 + version: 4.3.0 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.16.0)(typescript@5.9.3) + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -228,71 +306,49 @@ importers: uglify-js: specifier: ^3.19.3 version: 3.19.3 - unocss: - specifier: 66.5.4 - version: 66.5.4(postcss@8.5.6)(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3)) vitest: - specifier: ^4.0.18 - version: 4.0.18(@types/node@22.16.0)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.43.1)(yaml@2.8.3) + specifier: ^4.1.8 + version: 4.1.8(@types/node@22.16.0)(@vitest/coverage-v8@4.0.18)(happy-dom@20.10.2)(jsdom@29.1.1)(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(yaml@2.8.3)) + ws: + specifier: ^8.21.0 + version: 8.21.0 packages: '@adobe/css-tools@4.4.3': resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} - '@antfu/install-pkg@1.1.0': - resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} - '@antfu/utils@9.3.0': - resolution: {integrity: sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - '@arco-design/color@0.4.0': - resolution: {integrity: sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g==} + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - '@arco-design/web-react@2.66.7': - resolution: {integrity: sha512-heZoNjsdD2tXFAv0SmVsMsMFVcwhOVUsjqfRZZtW7WHAWIx1vygbVJceww61DgBwFXPtYPUlyu2SRmPiVz2xlg==} - peerDependencies: - react: '>=16' - react-dom: '>=16' + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - '@asamuzakjp/css-color@3.2.0': - resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.5': - resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/parser@7.27.7': - resolution: {integrity: sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.28.0': - resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.28.5': - resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.29.0': resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} @@ -302,20 +358,8 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.27.7': - resolution: {integrity: sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.28.0': - resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} '@babel/types@7.29.0': @@ -326,6 +370,10 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@buttercup/fetch@0.2.1': resolution: {integrity: sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==} @@ -333,33 +381,41 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@csstools/color-helpers@5.0.2': - resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} - engines: {node: '>=18'} + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} - '@csstools/css-calc@2.1.4': - resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} - engines: {node: '>=18'} + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@3.0.10': - resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} - engines: {node: '>=18'} + '@csstools/css-color-parser@4.1.3': + resolution: {integrity: sha512-DOgvIPkikIOixQRlD4YF31VN6fLLUTdrzhfRbis8vm0kMTgIbEPX0Ip/YX9fOeV9iywAS4sUUbTclpan7yYP8Q==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-parser-algorithms@3.0.5': - resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} - engines: {node: '>=18'} + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-tokenizer@3.0.4': - resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} - engines: {node: '>=18'} + '@csstools/css-syntax-patches-for-csstree@1.1.5': + resolution: {integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} @@ -614,11 +670,29 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@gilbarbara/deep-equal@0.1.2': - resolution: {integrity: sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' - '@gilbarbara/deep-equal@0.3.1': - resolution: {integrity: sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==} + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -640,12 +714,6 @@ packages: resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} - '@iconify/types@2.0.0': - resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - - '@iconify/utils@3.0.2': - resolution: {integrity: sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==} - '@jridgewell/gen-mapping@0.3.12': resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} @@ -662,9 +730,6 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.29': - resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -839,126 +904,813 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} - '@quansync/fs@0.1.5': - resolution: {integrity: sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==} + '@radix-ui/number@1.1.2': + resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} - cpu: [arm] - os: [android] + '@radix-ui/primitive@1.1.4': + resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} - cpu: [arm64] - os: [android] + '@radix-ui/react-accessible-icon@1.1.9': + resolution: {integrity: sha512-5W9KzJz/3DeYbGJHbZv8Q6AkxMOKUmALfc+PRg9dWwJZMk6zD37Sz8sZrF7UD6CBkiJvn7dNeRzn5G7XiCMyig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} - cpu: [arm64] - os: [darwin] + '@radix-ui/react-accordion@1.2.13': + resolution: {integrity: sha512-xITxBB2p5m5tAe7M0F95kb4uAh7jSIKGlExMEm93HlW+XxZHV2eXFbPWLktd4JhRiwcnXNbO7iekcrbZy6ZCvA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} - cpu: [x64] - os: [darwin] + '@radix-ui/react-alert-dialog@1.1.16': + resolution: {integrity: sha512-vPaIgo0mxYlvcFaM9jB2Uot9TjGXMuAPEvrc6BOLeV+I5U8s1dkIoouYaa6lmSfc5SPMo5x5djOTOTvaigdGMQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} - cpu: [arm64] - os: [freebsd] + '@radix-ui/react-arrow@1.1.9': + resolution: {integrity: sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} - cpu: [x64] - os: [freebsd] + '@radix-ui/react-aspect-ratio@1.1.9': + resolution: {integrity: sha512-Xy+Dpxt/5n9rVTdPrNFmf8GwG1NlT1pzCF/z1MgOGZMLZWdWl+km+ZRWGQAPEhbkzSwYEsfYmTca8NhUtVxqnw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} - cpu: [arm] - os: [linux] - libc: [glibc] + '@radix-ui/react-avatar@1.1.12': + resolution: {integrity: sha512-NQCQyWC7QrDPhjMn8hUqFeU0lUrprIgm1AyMgLbzuQJibNnatdc3SSMo3/UGFu/eUkJUU1cEcKCnyhXTQzq6tA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} - cpu: [arm] - os: [linux] - libc: [musl] + '@radix-ui/react-checkbox@1.3.4': + resolution: {integrity: sha512-m3JmIOAX5ZzZ6VPjxEU2dbTOhoHi0nT5riwcDwe8idocsWf4a5DXJLDtZ6LfJwMBx7W+A2b7kp2TgPEKtaiF6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] + '@radix-ui/react-collapsible@1.1.13': + resolution: {integrity: sha512-F0s8+p2XNpfc3k02zBfB0jPWbkHVG162+p7BdUMyJ2308QMqZ+oaclX+FAzKFovgL5OqRU+Rvy6f/vbdlJVaqA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} - cpu: [arm64] - os: [linux] - libc: [musl] + '@radix-ui/react-collection@1.1.9': + resolution: {integrity: sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} - cpu: [loong64] - os: [linux] - libc: [glibc] + '@radix-ui/react-compose-refs@1.1.3': + resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} - cpu: [loong64] - os: [linux] - libc: [musl] + '@radix-ui/react-context-menu@2.3.0': + resolution: {integrity: sha512-d7CouXhAW+CGmFOqmB+IEvd3E9GcaqfgvfjCc3hfulp2pkaUCEVEGa0SN5nNWYA+IvQ6g1Pt+S5dpNn1AoY9hg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} - cpu: [ppc64] - os: [linux] - libc: [glibc] + '@radix-ui/react-context@1.1.4': + resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} - cpu: [ppc64] - os: [linux] - libc: [musl] + '@radix-ui/react-dialog@1.1.16': + resolution: {integrity: sha512-l9ok83YBclEZhbjgzt76Hw733e6cvRKPNgO6GJ/IETlufXG9p+fRu2wlvpImQvR6xdJ8h7J8J2DBvsPEiEsKMw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] + '@radix-ui/react-direction@1.1.2': + resolution: {integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} - cpu: [riscv64] - os: [linux] - libc: [musl] + '@radix-ui/react-dismissable-layer@1.1.12': + resolution: {integrity: sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] + '@radix-ui/react-dropdown-menu@2.1.17': + resolution: {integrity: sha512-S6b3Jm57sY5EdDyOMLkacbB0qMnKhy1RCKZCt795ZkmtUOAvojYIZ5p7dXHIh5Cyr3jCLLI5/g64V3FKLudZmw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} - cpu: [x64] - os: [linux] - libc: [glibc] + '@radix-ui/react-focus-guards@1.1.4': + resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} - cpu: [x64] - os: [linux] - libc: [musl] + '@radix-ui/react-focus-scope@1.1.9': + resolution: {integrity: sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} - cpu: [x64] - os: [openbsd] + '@radix-ui/react-form@0.1.9': + resolution: {integrity: sha512-eTPyThIKDacJ3mJDvYwf/PSmsEYlOyA2Qcb+aGyWwYv+P5w57VPUkMVA2XJ9z0Du2KBY1HoHQzhPV9iYL/r4hg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} - cpu: [arm64] - os: [openharmony] + '@radix-ui/react-hover-card@1.1.16': + resolution: {integrity: sha512-hAileDBtd6CX7nlZOarOnISQ6PP4q0e16BX51ulzdZ+7IzjL0sDTVpFdmSYrIjw6zVNsfQBao5gG6AWr3qwfvA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.2': + resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.9': + resolution: {integrity: sha512-rDoTeMbCwRVcnmo7NGT9IlPo1yXmEI+xc1URP3oeewwZEV4mdTp1dYUhYbQdo4D1q2SjKVvv4N1gNY77QAQtjA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.17': + resolution: {integrity: sha512-fmbNnFyf+JYCN0DhhWnEdUTDnZD1mXaPQWivdsPIb8oOSbARfD3LIQJbLCG8a8QLCwoMxiJ7GVPIFcC8Dw8v2Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menubar@1.1.17': + resolution: {integrity: sha512-AKtZ4O782yO7qwIyq73WpulYt1IHhQ0htDb6wNcxzxnSDCcSWMVBiU9ycpcA90XzQO4IVIxIErtak6Kg/Vt0rQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.15': + resolution: {integrity: sha512-/fS8hKCcRt4DwCGa5QIB3juRXmfYSOk4a2AEe/BDIyy7Hm+eje2Y13oUx5zejl+wFt1owrM7E8NWlbaEl5EGpg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-one-time-password-field@0.1.9': + resolution: {integrity: sha512-fvCzA9hm7yN5xxTPJIi4VhSmH5gv+76ILsxguBK3cm3icD5BR4vW7POQmu8Zio0yh91uuouG/Kang40IbMkaSQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-password-toggle-field@0.1.4': + resolution: {integrity: sha512-qoDSkObZ9faJlsjlwyBH6ia7kq9vaJ2QwWTowT3nQpzPvUTAKesmWuGJYpd91HIoJqS+5ZPXy5uFPp+HlwdaAg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.16': + resolution: {integrity: sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.3.0': + resolution: {integrity: sha512-9PB589e1aWZbrlFUHdz6WiPCL+xLZHQFX7oibqG/6Q0SwOkxDyQX9W/cyPa+sAPPKuC8cpLCpRczE5a/1DiwVQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.11': + resolution: {integrity: sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.6': + resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.5': + resolution: {integrity: sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.9': + resolution: {integrity: sha512-+EOkvg1Zn1vI1+fRDfRSAiJ7BWfcDAo5ASMmbqrcLZ4s4USk2FGkoHgeb2X+CkUgo2zJMiyObwf1k44CrRWsyw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.4.0': + resolution: {integrity: sha512-eHdV5bLx9sH+tBnbDjkIBdvQEH/c6MEtQYhTbxkaDK9qsIFFLtmJYEQFVdwhnruWotLfQmIuWEL/J+L3utE8rQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.12': + resolution: {integrity: sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.11': + resolution: {integrity: sha512-DS39ziOgea75U/TrXKU2/oKp0be2jrDHnzFLvahg/0iNAT1Zq16e4Uw0WXwyXvsK+mG3BRyMb7A3NRZMDuEXtQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.3.0': + resolution: {integrity: sha512-mENc7WpJvJcW8hlMpzfFcHcEhTvYS5JMBmi9HVC1Q00uhBwML086MHYUV8QQdQv6lcu0Wg8dzd1RB8AFADcG/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.9': + resolution: {integrity: sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.4.0': + resolution: {integrity: sha512-RHcPlLOThRJM51DSIC33ZnpDEBYhyEFroVWkd2P54PGGjkmAt14RboYUU9E1MFst666zFHM0tGtWvMjSOtU1pw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.5': + resolution: {integrity: sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.3.0': + resolution: {integrity: sha512-GP1EZwhoZO/GGnhM1P5/2Vpm8iN8EnngyU0oezn2l78kN8tj25pyrvjIaT7azBhK615KSt+P2w39y57YV5jVkA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.14': + resolution: {integrity: sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.16': + resolution: {integrity: sha512-WUymDDiN2DpoGudRN1aW4wF5O3BNQjZZO/5nngPoNiEVqjyOzirvZZNO0R6dC1ifucSINVaSv8JX1aq47VGgiA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.12': + resolution: {integrity: sha512-TEgECgJaWGAHJJZGzNNEYTNBdIXqX7LchANycpyP7DkfjmuiSN7ISt1k/ZRGVJgVJonsgP4vwaiKMn5utrcwWQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.11': + resolution: {integrity: sha512-FikrKJemoBGZQ6uRID0HJqSPBP6D7OppdD2OhLl0ZYLlAyPXI7MezoYGmumwNkrAoRm35xXkb4C8JPfJZZzcaw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.12': + resolution: {integrity: sha512-4wHtJVdIgqMmEwUvxA0BYg/2JMRbt0L3+8UD8Ml/nhKkfXtiZcM8u/S15gQ5xj9YEd/0qlrm5bE805LsjQ+J8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.9': + resolution: {integrity: sha512-u6F9MmTtBSLkiXNVDrtB/yPCZarM9smNswC24YYLV/M+bth6J3Gs3vlJezEoFwKZvPvxhCpUYdUnOsNG/0XOlA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.2': + resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.3': + resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.3': + resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.2': + resolution: {integrity: sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.1': + resolution: {integrity: sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.2': + resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.2': + resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.2': + resolution: {integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.2': + resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.5': + resolution: {integrity: sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.2': + resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} + + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + cpu: [arm64] + os: [openharmony] '@rollup/rollup-win32-arm64-msvc@4.60.1': resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} @@ -1066,16 +1818,108 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@testing-library/dom@10.4.0': - resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.3.0': + resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} '@testing-library/jest-dom@6.9.1': resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/react@16.3.0': - resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 @@ -1116,8 +1960,8 @@ packages: '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} - '@types/chrome@0.1.27': - resolution: {integrity: sha512-pkkCb0Ft8X+Igi751POzT+YqchSxUCtB6s4Gs6ttgSj8qzJga/qlJMgSW1mKxuQTW4i0sTqQbqVtzXDS5AU+4A==} + '@types/chrome@0.1.43': + resolution: {integrity: sha512-ukH/HhmR6ht+UTX3PLUWJxgJ/RQcK2Foj4lBzsF24SIWsXgqhGuXqjd8FFuwioPP7d/JUKLM4g8GZxw3F4HTcA==} '@types/connect-history-api-fallback@1.5.4': resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} @@ -1191,22 +2035,19 @@ packages: '@types/node@22.16.2': resolution: {integrity: sha512-Cdqa/eJTvt4fC4wmq1Mcc0CPUjp/Qy2FGqLza3z3pKymsI969TcZ54diNJv8UYUgeWxyb8FSbCkhdR6WqmUFhA==} - '@types/prop-types@15.7.15': - resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@18.3.7': - resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^18.0.0 + '@types/react': ^19.2.0 - '@types/react@18.3.23': - resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} @@ -1241,6 +2082,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -1303,94 +2147,8 @@ packages: resolution: {integrity: sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - - '@unocss/astro@66.5.4': - resolution: {integrity: sha512-6KsilC1SiTBmEJRMuPl+Mg8KDWB1+DaVoirGZR7BAEtMf2NzrfQcR4+O/3DHtzb38pfb0K1aHCfWwCozHxLlfA==} - peerDependencies: - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 - peerDependenciesMeta: - vite: - optional: true - - '@unocss/cli@66.5.4': - resolution: {integrity: sha512-GltHfmJ27O5VHB/ABkkUWwWT72xGBVwFyhTnhIOut4EPkIurKDnfY5MZFEl2PLlIFnYqIJxeTHMHONVg7pySMg==} - engines: {node: '>=14'} - hasBin: true - - '@unocss/config@66.5.4': - resolution: {integrity: sha512-TYwkUw9nZlLTBGCndsyrcHCJ7M+sEsJiK77Edggmd6B3urjkVc1cDjKF3k3tjg4ghQteGsc2akhzn1a4TouybQ==} - engines: {node: '>=14'} - - '@unocss/core@66.5.4': - resolution: {integrity: sha512-UDS2CRgyQCEFH+5kStDyJd7OFtgkIUZYn5Ahr5z7v3jc/pEfeOJ0mxsNAr+FgMS/xb17vy4sVFvx3jj/FwMZ1A==} - - '@unocss/extractor-arbitrary-variants@66.5.4': - resolution: {integrity: sha512-JsgITF11Z2WdXzF8eO2/qkcFIff/dEEc9C2eKYOSUv5pe+RMZxXHoAw4x+D4n0UrGAbHpoUVaJ8E7kG0ayTbGw==} - - '@unocss/inspector@66.5.4': - resolution: {integrity: sha512-eBf1HAxwNz1YItNiiP/YQaIE9LWeErt4Ofdm7Imj0WW464ZZ/TqXhu0ZREcCgyQDy2Lghuyq6Q3d9orm2Cby/w==} - - '@unocss/postcss@66.5.4': - resolution: {integrity: sha512-EpizX20MR8wTWlCI9+E6pI8Xj5S1kk2jRVQuQAxjjXcOEFoLZNTZmyULSlaIpg9vEtT5kuxQgBN6VmvyMgeD7g==} - engines: {node: '>=14'} - peerDependencies: - postcss: ^8.4.21 - - '@unocss/preset-attributify@66.5.4': - resolution: {integrity: sha512-6NmRUKpXp4qc+ZwWI+ImBIUzcdPRKWISfYu65hi8Sads453jzLJko78WOCVTLlgnmkquUJ98akNW5twG9p2mDw==} - - '@unocss/preset-icons@66.5.4': - resolution: {integrity: sha512-wPR2j191mw89hstQflQvsACzu+3QkueV9lHH8as94By9PKfswWnVZ1nPqJBc3Yl5v8fHEAR1YY4i7egN7TEjIA==} - - '@unocss/preset-mini@66.5.4': - resolution: {integrity: sha512-KaBGsw3+Pi5ZTsp5u0OrUUUXFVltHin02cYhv3A4b9392Kej5R3y7zIf1VjiQ3ZXR4KZWfv0CQj0LBqIqAJ5WA==} - - '@unocss/preset-tagify@66.5.4': - resolution: {integrity: sha512-ck71rFCQZEwYDzwf99sjCBuzpT0PnkzwdqWwnR34pN71Lf3ePN16hV6pEo7pWuIiatAGFzXh0AHzJrIQ/61Cxw==} - - '@unocss/preset-typography@66.5.4': - resolution: {integrity: sha512-RG0Bw4lGvwAJxHw8I/0Hx2/r4t8L6fnkWyEP4Iehie3kEGJ+S7Ku9sF6kkIeyOqjwL6Hp68rlnTGAycnYofoEg==} - - '@unocss/preset-uno@66.5.4': - resolution: {integrity: sha512-CEBtkNbbd1lYbCJw+s7HDeOtPeCEkvf+NDi/IrVkkBhOCcYRtYC+VDxjBgh4zjlmgZIQifkU2l7PPfGjd4IMNA==} - - '@unocss/preset-web-fonts@66.5.4': - resolution: {integrity: sha512-FQ/P/a1fSmGkkjWn/FNmErwK5YtsuX2VrkHsEa9DTP372td8Oea3hkK40UUYj3zRUivA71PmjVwhbBf+35nAiA==} - - '@unocss/preset-wind3@66.5.4': - resolution: {integrity: sha512-cqQGg9E2476YVpnX3sgO/jEoA4cKCA5rEl2NgemoAJpKAgdM68JPB+Tve4LlSLssxRQZ7ZYNO6hOfW8R2gVVuw==} - - '@unocss/preset-wind4@66.5.4': - resolution: {integrity: sha512-S5ZysCSTfl/h93jDnXIss214jqYfq+W6xZH50Krc1QTWy5teAOVCFTluRJEB70JTDOdUMwcTtmqFklSHIU5I7g==} - - '@unocss/preset-wind@66.5.4': - resolution: {integrity: sha512-2TWP2QrJwGFr21iwVsPKVzDa2JWjh1EUt1+OtAk5JQfGmmTeyw8EF3pIAcaM06WVi/m4em55RKIPNoW/Kr61yg==} - - '@unocss/reset@66.5.4': - resolution: {integrity: sha512-RF/Xscv4mOEDUltUpdKYZEBgIiE7nAVkipJ8gZWEwKfmUFz0TRcwlf0igjqjgGn11tuix0mJyk5Uwis9LioX4Q==} - - '@unocss/rule-utils@66.5.4': - resolution: {integrity: sha512-LFzLuXQfZKI/qJBrsqkaVKlw0ECU8Xw7m+MaKIKyFH/hqggzkvNG0PyWU2HnPNzz1dIiVBn+Epfpz8pzi+MLFA==} - engines: {node: '>=14'} - - '@unocss/transformer-attributify-jsx@66.5.4': - resolution: {integrity: sha512-VHzrxiWKBVsUZhFU0vG7e6MjSiQG/rR/PidPgi8KaMGAWi+CA6faRBfHaUQbfix2bLBhtDOdaZUDp9AsIgu1ZQ==} - - '@unocss/transformer-compile-class@66.5.4': - resolution: {integrity: sha512-H+IX9C8PHFMCJfYutIRjzkpgIiW/AFC7PzP/EQwM+p9VoC8LH+Wd8Csl/Uyi+uoQ+a78q4nQ2lDYZr42eanmgg==} - - '@unocss/transformer-directives@66.5.4': - resolution: {integrity: sha512-BqM4fRqCC5wIRkEB14SN476/KWKlEhHhrHECL9kOjbZYmIcxBDCYKf/iuZGvtK9IZJtE0JhrSGAdYfyp0Td7fQ==} - - '@unocss/transformer-variant-group@66.5.4': - resolution: {integrity: sha512-n/A74083b8zZtl05A3M0Lo9oWepFKuvRZKPXh+y4bJM2oSZl1Clzm1I+qmgvO/DKoa4rjgj4a/CRaIxeDpoO9A==} - - '@unocss/vite@66.5.4': - resolution: {integrity: sha512-dmSJ3h7/kMbFOIrAyycg2W1VVN+59WCnH5s0dJTGRla2kUTAFB0iWJWdIa/W6IsvFlicMtsoLYJmTnQ/kBQm7A==} - peerDependencies: - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} '@vitest/coverage-v8@4.0.18': resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} @@ -1401,14 +2159,14 @@ packages: '@vitest/browser': optional: true - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true @@ -1418,18 +2176,24 @@ packages: '@vitest/pretty-format@4.0.18': resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} + + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1448,14 +2212,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agent-base@7.1.1: - resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} - engines: {node: '>= 14'} - - agent-base@7.1.3: - resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} - engines: {node: '>= 14'} - ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1510,6 +2266,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -1557,27 +2317,10 @@ packages: async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} - attr-accept@2.2.5: - resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} - engines: {node: '>=4'} - - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - b-tween@0.3.3: - resolution: {integrity: sha512-oEHegcRpA7fAuc9KC4nktucuZn2aS8htymCPcP3qkEGPqiBH+GfqtqoG2l7LxHngg6O0HFM7hOeOYExl1Oz4ZA==} - - b-validate@1.5.3: - resolution: {integrity: sha512-iCvCkGFskbaYtfQ0a3GmcQCHl/Sv1GufXFGuUQ+FE+WJa7A/espLOuFIn09B944V8/ImPj71T4+rTASxO2PAuA==} - bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -1593,6 +2336,9 @@ packages: batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1617,17 +2363,16 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.25.1: - resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -1642,10 +2387,6 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1666,9 +2407,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001727: - resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1702,25 +2440,20 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@3.2.1: - resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} - colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1746,26 +2479,13 @@ packages: resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} engines: {node: '>= 0.8.0'} - compute-scroll-into-view@1.0.20: - resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - confbox@0.2.2: - resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} - connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -1774,6 +2494,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.0.7: resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} @@ -1831,27 +2554,23 @@ packages: crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - css-tree@3.1.0: - resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - cssstyle@4.6.0: - resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} - engines: {node: '>=18'} - - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} - data-urls@5.0.0: - resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} - engines: {node: '>=18'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} @@ -1888,22 +2607,15 @@ packages: supports-color: optional: true - decimal.js@10.5.0: - resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} - deep-diff@1.0.2: - resolution: {integrity: sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - default-browser-id@5.0.1: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} @@ -1924,9 +2636,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - defu@6.1.6: - resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==} - depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -1939,13 +2648,14 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - destr@2.0.3: - resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} - destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -1976,9 +2686,6 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dom-helpers@5.2.1: - resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} - dompurify@3.3.3: resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} @@ -1992,9 +2699,6 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.180: - resolution: {integrity: sha512-ED+GEyEh3kYMwt2faNmgMB0b8O5qtATGgR4RmRsIp4T6p7B8vdMbIedYndnvZfsaXvSzegtpfqRMDNCjjiSduA==} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -2002,14 +2706,22 @@ packages: end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} + enhanced-resolve@5.23.0: + resolution: {integrity: sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==} + engines: {node: '>=10.13.0'} entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2033,8 +2745,8 @@ packages: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} @@ -2060,10 +2772,6 @@ packages: engines: {node: '>=18'} hasBin: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -2178,17 +2886,14 @@ packages: resolution: {integrity: sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ==} engines: {node: '>=18'} - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} express@4.22.1: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} - exsolve@1.0.7: - resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -2242,10 +2947,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - file-selector@2.1.2: - resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} - engines: {node: '>= 12'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2265,10 +2966,6 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - focus-lock@1.3.5: - resolution: {integrity: sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ==} - engines: {node: '>=10'} - follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -2289,9 +2986,6 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -2330,6 +3024,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -2356,18 +3054,10 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@15.15.0: - resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} - engines: {node: '>=18'} - globals@16.5.0: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} @@ -2393,6 +3083,10 @@ packages: handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + happy-dom@20.10.2: + resolution: {integrity: sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ==} + engines: {node: '>=20.0.0'} + has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -2441,9 +3135,9 @@ packages: hpack.js@2.1.6: resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} - html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2468,10 +3162,6 @@ packages: http-parser-js@0.5.10: resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - http-proxy-middleware@2.0.9: resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} engines: {node: '>=12.0.0'} @@ -2485,10 +3175,6 @@ packages: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -2498,17 +3184,18 @@ packages: resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} engines: {node: '>=10.18'} - i18next@23.16.4: - resolution: {integrity: sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg==} + i18next@26.3.1: + resolution: {integrity: sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==} + peerDependencies: + typescript: ^5 || ^6 + peerDependenciesMeta: + typescript: + optional: true iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -2571,9 +3258,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-async-function@2.0.0: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} engines: {node: '>= 0.4'} @@ -2641,12 +3325,6 @@ packages: engines: {node: '>=14.16'} hasBin: true - is-lite@0.8.2: - resolution: {integrity: sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==} - - is-lite@1.2.1: - resolution: {integrity: sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw==} - is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -2753,20 +3431,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdom@26.1.0: - resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} - engines: {node: '>=18'} + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: canvas: optional: true - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -2789,9 +3462,6 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - kolorist@1.8.0: - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - launch-editor@2.12.0: resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==} @@ -2806,13 +3476,83 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - local-pkg@1.1.1: - resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} - engines: {node: '>=14'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2848,8 +3588,14 @@ packages: lowlight@3.3.0: resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + + lucide-react@1.17.0: + resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 luxon@3.7.2: resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} @@ -2927,8 +3673,8 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - mdn-data@2.12.2: - resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} @@ -3073,9 +3819,6 @@ packages: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} - mlly@1.7.4: - resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} - mock-xmlhttprequest@8.4.1: resolution: {integrity: sha512-2ORxRN+h40+3/Ylw9LKOtYGfQIoX6grGQlmbvMKqaeZ5/l7oeMvqdJxyG/ax3Poy7VbqMTADI6BwTmO7u10Wrw==} engines: {node: '>=16.0.0'} @@ -3102,6 +3845,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -3121,9 +3869,6 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead - node-fetch-native@1.6.4: - resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} - node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -3132,9 +3877,6 @@ packages: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - node-rsa@1.1.1: resolution: {integrity: sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==} @@ -3142,16 +3884,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - number-precision@1.6.0: - resolution: {integrity: sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ==} - - nwsapi@2.2.20: - resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3186,9 +3918,6 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - ofetch@1.4.1: - resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3228,9 +3957,6 @@ packages: resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} engines: {node: '>=16.17'} - package-manager-detector@1.3.0: - resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3242,8 +3968,8 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse5@7.2.1: - resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} @@ -3281,9 +4007,6 @@ packages: resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} hasBin: true - perfect-debounce@1.0.0: - resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3295,12 +4018,6 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - pkg-types@2.2.0: - resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} - playwright-core@1.58.2: resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} engines: {node: '>=18'} @@ -3311,10 +4028,6 @@ packages: engines: {node: '>=18'} hasBin: true - popper.js@1.16.1: - resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==} - deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1 - possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -3332,8 +4045,9 @@ packages: webpack: optional: true - postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} @@ -3362,8 +4076,8 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} @@ -3380,18 +4094,25 @@ packages: resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} engines: {node: '>=0.6'} - quansync@0.2.10: - resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} - - quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + radix-ui@1.5.0: + resolution: {integrity: sha512-Nzh2HNpClgB31FBHRqt2xG8XNUfVfQRpf34hACC5PNrXTd5JdXdqOXwLs3BL+D8CNYiNQiJiT8QGr5Q4vq+00w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -3400,45 +4121,19 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} - react-clientside-effect@1.2.6: - resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} - peerDependencies: - react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: - react: ^18.3.1 - - react-dropzone@14.3.8: - resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==} - engines: {node: '>= 10.13'} - peerDependencies: - react: '>= 16.8 || 18.0.0' - - react-floater@0.7.9: - resolution: {integrity: sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==} - peerDependencies: - react: 15 - 18 - react-dom: 15 - 18 - - react-focus-lock@2.13.2: - resolution: {integrity: sha512-T/7bsofxYqnod2xadvuwjGKHOoL5GH7/EIPI5UyEvaU/c2CcphvGI371opFtuY/SYdbMsNiuF4HsHQ50nA/TKQ==} - peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - '@types/react': - optional: true + react: ^19.2.7 - react-i18next@15.6.0: - resolution: {integrity: sha512-W135dB0rDfiFmbMipC17nOhGdttO5mzH8BivY+2ybsQBbXvxWIwl3cmeH3T9d+YPBSJu/ouyJKFJTtkK7rJofw==} + react-i18next@17.0.8: + resolution: {integrity: sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==} peerDependencies: - i18next: '>= 23.2.3' + i18next: '>= 26.2.0' react: '>= 16.8.0' react-dom: '*' react-native: '*' - typescript: ^5 + typescript: ^5 || ^6 peerDependenciesMeta: react-dom: optional: true @@ -3447,47 +4142,47 @@ packages: typescript: optional: true - react-icons@5.5.0: - resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} - peerDependencies: - react: '*' - - react-innertext@1.1.5: - resolution: {integrity: sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==} - peerDependencies: - '@types/react': '>=0.0.0 <=99' - react: '>=0.0.0 <=99' - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - - react-joyride@2.9.3: - resolution: {integrity: sha512-1+Mg34XK5zaqJ63eeBhqdbk7dlGCFp36FXwsEvgpjqrtyywX2C6h9vr3jgxP0bGHCw8Ilsp/nRDzNVq6HJ3rNw==} - peerDependencies: - react: 15 - 18 - react-dom: 15 - 18 - react-markdown@9.1.0: resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} peerDependencies: '@types/react': '>=18' react: '>=18' - react-router-dom@7.13.0: - resolution: {integrity: sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==} + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router-dom@7.17.0: + resolution: {integrity: sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.13.0: - resolution: {integrity: sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==} + react-router@7.17.0: + resolution: {integrity: sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -3496,14 +4191,18 @@ packages: react-dom: optional: true - react-transition-group@4.4.5: - resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} peerDependencies: - react: '>=16.6.0' - react-dom: '>=16.6.0' + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} readable-stream@2.3.8: @@ -3551,9 +4250,6 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - resize-observer-polyfill@1.5.1: - resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3578,9 +4274,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rrweb-cssom@0.8.0: - resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} - run-applescript@7.1.0: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} @@ -3613,22 +4306,13 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} schema-utils@4.3.2: resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} engines: {node: '>= 10.13.0'} - scroll-into-view-if-needed@2.2.31: - resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} - - scroll@3.0.1: - resolution: {integrity: sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==} - - scrollparent@2.1.0: - resolution: {integrity: sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==} - select-hose@2.0.0: resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} @@ -3675,9 +4359,6 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shallowequal@1.1.0: - resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3709,20 +4390,19 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} - sirv@3.0.2: - resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} - engines: {node: '>=18'} - sockjs@0.3.24: resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3758,6 +4438,9 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + string-similarity-js@2.1.4: resolution: {integrity: sha512-uApODZNjCHGYROzDSAdCmAHf60L/pMDHnP/yk6TAbvGg7JSPZlSto/ceCI7hZEqzc53/juU2aOJFkM2yUVTMTA==} @@ -3821,6 +4504,16 @@ packages: resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} engines: {node: ^14.18.0 || >=16.0.0} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -3842,9 +4535,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.1: - resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -3857,11 +4547,15 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} - tldts-core@6.1.60: - resolution: {integrity: sha512-XHjoxak8SFQnHnmYHb3PcnW5TZ+9ErLZemZei3azuIRhQLw4IExsVbL3VZJdHcLeNaXq6NqawgpDPpjBOg4B5g==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.2: + resolution: {integrity: sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==} - tldts@6.1.60: - resolution: {integrity: sha512-TYVHm7G9NCnhgqOsFalbX6MG1Po5F4efF+tLfoeiOGQq48Oqgwcgz8upY2R1BHWa4aDrj28RYx0dkYJ63qCFMg==} + tldts@7.4.2: + resolution: {integrity: sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==} hasBin: true to-regex-range@5.0.1: @@ -3876,19 +4570,13 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@5.1.2: - resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} - - tree-changes@0.11.2: - resolution: {integrity: sha512-4gXlUthrl+RabZw6lLvcCDl6KfJOCmrC16BC5CRdut1EAH509Omgg0BfKLY+ViRlzrvYOTWR0FMS2SQTwzumrw==} - - tree-changes@0.9.3: - resolution: {integrity: sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} tree-dump@1.1.0: resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} @@ -3928,14 +4616,13 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@4.31.0: - resolution: {integrity: sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==} - engines: {node: '>=16'} - type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -3968,9 +4655,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.5.4: - resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} - uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -3980,12 +4664,13 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - unconfig@7.3.3: - resolution: {integrity: sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.27.2: + resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} + engines: {node: '>=20.18.1'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -4007,32 +4692,10 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - unocss@66.5.4: - resolution: {integrity: sha512-yNajR8ADgvOzLhDkMKAXVE/SHM4sDrtVhhCnhBjiUMOR0LHIYO7cqunJJudbccrsfJbRTn/odSTBGu9f2IaXOg==} - engines: {node: '>=14'} - peerDependencies: - '@unocss/webpack': 66.5.4 - vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 - peerDependenciesMeta: - '@unocss/webpack': - optional: true - vite: - optional: true - unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin-utils@0.3.1: - resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} - engines: {node: '>=20.19.0'} - - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4043,26 +4706,31 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - use-callback-ref@1.3.2: - resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} peerDependencies: - '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true - use-sidecar@1.1.2: - resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} peerDependencies: - '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4131,20 +4799,23 @@ packages: yaml: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -4158,6 +4829,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -4169,9 +4844,6 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} - vue-flow-layout@0.2.0: - resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==} - w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -4191,9 +4863,9 @@ packages: resolution: {integrity: sha512-OMJ6wtK1WvCO++aOLoQgE96S8KT4e5aaClWHmHXfFU369r4eyELN569B7EqT4OOUb99mmO58GkyuiCv/Ag6J0Q==} engines: {node: '>=14'} - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} webpack-bundle-analyzer@4.10.2: resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==} @@ -4230,17 +4902,17 @@ packages: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} @@ -4287,8 +4959,8 @@ packages: utf-8-validate: optional: true - ws@8.18.1: - resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4334,45 +5006,27 @@ snapshots: '@adobe/css-tools@4.4.3': {} - '@antfu/install-pkg@1.1.0': - dependencies: - package-manager-detector: 1.3.0 - tinyexec: 1.0.1 - - '@antfu/utils@9.3.0': {} + '@alloc/quick-lru@5.2.0': {} - '@arco-design/color@0.4.0': + '@asamuzakjp/css-color@5.1.11': dependencies: - color: 3.2.1 + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - '@arco-design/web-react@2.66.7(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@asamuzakjp/dom-selector@7.1.1': dependencies: - '@arco-design/color': 0.4.0 - '@babel/runtime': 7.27.6 - b-tween: 0.3.3 - b-validate: 1.5.3 - compute-scroll-into-view: 1.0.20 - dayjs: 1.11.13 - lodash: 4.18.1 - number-precision: 1.6.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-focus-lock: 2.13.2(@types/react@18.3.23)(react@18.3.1) - react-is: 18.3.1 - react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - resize-observer-polyfill: 1.5.1 - scroll-into-view-if-needed: 2.2.31 - shallowequal: 1.1.0 - transitivePeerDependencies: - - '@types/react' + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 - '@asamuzakjp/css-color@3.2.0': - dependencies: - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - lru-cache: 10.4.3 + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} '@babel/code-frame@7.27.1': dependencies: @@ -4380,65 +5034,17 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/generator@7.28.5': - dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 - jsesc: 3.1.0 - '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/parser@7.27.7': - dependencies: - '@babel/types': 7.28.0 - - '@babel/parser@7.28.0': - dependencies: - '@babel/types': 7.28.0 - - '@babel/parser@7.28.5': - dependencies: - '@babel/types': 7.28.5 - - '@babel/parser@7.29.0': - dependencies: - '@babel/types': 7.29.0 - - '@babel/runtime@7.27.6': {} - - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 - - '@babel/traverse@7.27.7': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.5 - '@babel/parser': 7.28.0 - '@babel/template': 7.27.2 - '@babel/types': 7.28.0 - debug: 4.4.1 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color + '@babel/helper-validator-identifier@7.28.5': {} - '@babel/types@7.28.0': + '@babel/parser@7.29.0': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/types': 7.29.0 - '@babel/types@7.28.5': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/runtime@7.27.6': {} + + '@babel/runtime@7.29.7': {} '@babel/types@7.29.0': dependencies: @@ -4447,6 +5053,10 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@buttercup/fetch@0.2.1': optionalDependencies: node-fetch: 3.3.2 @@ -4455,58 +5065,62 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@csstools/color-helpers@5.0.2': {} + '@csstools/color-helpers@6.0.2': {} - '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-color-parser@4.1.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/color-helpers': 5.0.2 - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 - '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-tokenizer@4.0.0': {} '@discoveryjs/json-ext@0.5.7': {} - '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + '@dnd-kit/accessibility@3.1.1(react@19.2.7)': dependencies: - react: 18.3.1 + react: 19.2.7 tslib: 2.8.1 - '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - '@dnd-kit/accessibility': 3.1.1(react@18.3.1) - '@dnd-kit/utilities': 3.2.2(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + '@dnd-kit/accessibility': 3.1.1(react@19.2.7) + '@dnd-kit/utilities': 3.2.2(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) tslib: 2.8.0 - '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: - '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@dnd-kit/utilities': 3.2.2(react@18.3.1) - react: 18.3.1 + '@dnd-kit/core': 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@dnd-kit/utilities': 3.2.2(react@19.2.7) + react: 19.2.7 tslib: 2.8.1 - '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: - '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@dnd-kit/utilities': 3.2.2(react@18.3.1) - react: 18.3.1 + '@dnd-kit/core': 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@dnd-kit/utilities': 3.2.2(react@19.2.7) + react: 19.2.7 tslib: 2.8.0 - '@dnd-kit/utilities@3.2.2(react@18.3.1)': + '@dnd-kit/utilities@3.2.2(react@19.2.7)': dependencies: - react: 18.3.1 + react: 19.2.7 tslib: 2.8.0 '@emnapi/core@1.7.0': @@ -4662,9 +5276,24 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 - '@gilbarbara/deep-equal@0.1.2': {} + '@exodus/bytes@1.15.1': {} + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) - '@gilbarbara/deep-equal@0.3.1': {} + '@floating-ui/utils@0.2.11': {} '@humanfs/core@0.19.1': {} @@ -4679,30 +5308,15 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} - '@iconify/types@2.0.0': {} - - '@iconify/utils@3.0.2': - dependencies: - '@antfu/install-pkg': 1.1.0 - '@antfu/utils': 9.3.0 - '@iconify/types': 2.0.0 - debug: 4.4.1 - globals: 15.15.0 - kolorist: 1.8.0 - local-pkg: 1.1.1 - mlly: 1.7.4 - transitivePeerDependencies: - - supports-color - '@jridgewell/gen-mapping@0.3.12': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': dependencies: '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} @@ -4714,11 +5328,6 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.29': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -4910,9 +5519,750 @@ snapshots: '@polka/url@1.0.0-next.28': {} - '@quansync/fs@0.1.5': + '@radix-ui/number@1.1.2': {} + + '@radix-ui/primitive@1.1.4': {} + + '@radix-ui/react-accessible-icon@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-accordion@1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collapsible': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-alert-dialog@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dialog': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-arrow@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-aspect-ratio@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-avatar@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-checkbox@1.3.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-collapsible@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-collection@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-context-menu@2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-dialog@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-dismissable-layer@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-escape-keydown': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-dropdown-menu@2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-focus-scope@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-form@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-label': 2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-hover-card@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-label@2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-menu@2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-menubar@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-navigation-menu@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-one-time-password-field@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-password-toggle-field@0.1.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-popover@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-popper@1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/rect': 1.1.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-portal@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-primitive@2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-progress@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-radio-group@1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-roving-focus@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-scroll-area@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-select@2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-separator@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-slider@1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-slot@1.2.5(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-switch@1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-tabs@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-toast@1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-toggle-group@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-toggle@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-toolbar@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-separator': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle-group': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-tooltip@1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-escape-keydown@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-is-hydrated@0.1.1(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-previous@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-rect@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/rect': 1.1.2 + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - quansync: 0.2.11 + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-visually-hidden@1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/rect@1.1.2': {} '@rollup/rollup-android-arm-eabi@4.60.1': optional: true @@ -5066,7 +6416,7 @@ snapshots: http-proxy-middleware: 2.0.9(@types/express@4.17.25) p-retry: 6.2.1 webpack-dev-server: 5.2.2(tslib@2.8.1) - ws: 8.18.1 + ws: 8.21.0 transitivePeerDependencies: - '@types/express' - bufferutil @@ -5085,15 +6435,84 @@ snapshots: dependencies: tslib: 2.8.1 - '@testing-library/dom@10.4.0': + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.23.0 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/postcss@4.3.0': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + postcss: 8.5.15 + tailwindcss: 4.3.0 + + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.29.7 '@types/aria-query': 5.0.4 aria-query: 5.3.0 - chalk: 4.1.2 dom-accessibility-api: 0.5.16 lz-string: 1.5.0 + picocolors: 1.1.1 pretty-format: 27.5.1 '@testing-library/jest-dom@6.9.1': @@ -5105,15 +6524,15 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@babel/runtime': 7.27.6 - '@testing-library/dom': 10.4.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + '@testing-library/dom': 10.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) optionalDependencies: - '@types/react': 18.3.23 - '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) '@tsconfig/node10@1.0.11': {} @@ -5143,7 +6562,7 @@ snapshots: dependencies: '@types/deep-eql': 4.0.2 - '@types/chrome@0.1.27': + '@types/chrome@0.1.43': dependencies: '@types/filesystem': 0.0.36 '@types/har-format': 1.2.16 @@ -5233,20 +6652,17 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/prop-types@15.7.15': {} - '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} - '@types/react-dom@18.3.7(@types/react@18.3.23)': + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: - '@types/react': 18.3.23 + '@types/react': 19.2.17 - '@types/react@18.3.23': + '@types/react@19.2.17': dependencies: - '@types/prop-types': 15.7.15 - csstype: 3.1.3 + csstype: 3.2.3 '@types/retry@0.12.2': {} @@ -5284,6 +6700,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/whatwg-mimetype@3.0.2': {} + '@types/ws@8.18.1': dependencies: '@types/node': 22.16.2 @@ -5381,158 +6799,9 @@ snapshots: '@typescript-eslint/types': 8.46.3 eslint-visitor-keys: 4.2.1 - '@ungap/structured-clone@1.3.0': {} - - '@unocss/astro@66.5.4(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3))': - dependencies: - '@unocss/core': 66.5.4 - '@unocss/reset': 66.5.4 - '@unocss/vite': 66.5.4(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3)) - optionalDependencies: - vite: 7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3) - - '@unocss/cli@66.5.4': - dependencies: - '@jridgewell/remapping': 2.3.5 - '@unocss/config': 66.5.4 - '@unocss/core': 66.5.4 - '@unocss/preset-uno': 66.5.4 - cac: 6.7.14 - chokidar: 3.6.0 - colorette: 2.0.20 - consola: 3.4.2 - magic-string: 0.30.21 - pathe: 2.0.3 - perfect-debounce: 1.0.0 - tinyglobby: 0.2.15 - unplugin-utils: 0.3.1 - - '@unocss/config@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - unconfig: 7.3.3 - - '@unocss/core@66.5.4': {} - - '@unocss/extractor-arbitrary-variants@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - - '@unocss/inspector@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - '@unocss/rule-utils': 66.5.4 - colorette: 2.0.20 - gzip-size: 6.0.0 - sirv: 3.0.2 - vue-flow-layout: 0.2.0 - - '@unocss/postcss@66.5.4(postcss@8.5.6)': - dependencies: - '@unocss/config': 66.5.4 - '@unocss/core': 66.5.4 - '@unocss/rule-utils': 66.5.4 - css-tree: 3.1.0 - postcss: 8.5.6 - tinyglobby: 0.2.15 - - '@unocss/preset-attributify@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - - '@unocss/preset-icons@66.5.4': - dependencies: - '@iconify/utils': 3.0.2 - '@unocss/core': 66.5.4 - ofetch: 1.4.1 - transitivePeerDependencies: - - supports-color - - '@unocss/preset-mini@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - '@unocss/extractor-arbitrary-variants': 66.5.4 - '@unocss/rule-utils': 66.5.4 - - '@unocss/preset-tagify@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - - '@unocss/preset-typography@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - '@unocss/rule-utils': 66.5.4 - - '@unocss/preset-uno@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - '@unocss/preset-wind3': 66.5.4 - - '@unocss/preset-web-fonts@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - ofetch: 1.4.1 - - '@unocss/preset-wind3@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - '@unocss/preset-mini': 66.5.4 - '@unocss/rule-utils': 66.5.4 - - '@unocss/preset-wind4@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - '@unocss/extractor-arbitrary-variants': 66.5.4 - '@unocss/rule-utils': 66.5.4 - - '@unocss/preset-wind@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - '@unocss/preset-wind3': 66.5.4 - - '@unocss/reset@66.5.4': {} - - '@unocss/rule-utils@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - magic-string: 0.30.21 - - '@unocss/transformer-attributify-jsx@66.5.4': - dependencies: - '@babel/parser': 7.27.7 - '@babel/traverse': 7.27.7 - '@unocss/core': 66.5.4 - transitivePeerDependencies: - - supports-color - - '@unocss/transformer-compile-class@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - - '@unocss/transformer-directives@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - '@unocss/rule-utils': 66.5.4 - css-tree: 3.1.0 - - '@unocss/transformer-variant-group@66.5.4': - dependencies: - '@unocss/core': 66.5.4 - - '@unocss/vite@66.5.4(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3))': - dependencies: - '@jridgewell/remapping': 2.3.5 - '@unocss/config': 66.5.4 - '@unocss/core': 66.5.4 - '@unocss/inspector': 66.5.4 - chokidar: 3.6.0 - magic-string: 0.30.21 - pathe: 2.0.3 - tinyglobby: 0.2.15 - unplugin-utils: 0.3.1 - vite: 7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3) + '@ungap/structured-clone@1.3.1': {} - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@22.16.0)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.43.1)(yaml@2.8.3))': + '@vitest/coverage-v8@4.0.18(vitest@4.1.8)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -5544,47 +6813,58 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@22.16.0)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.43.1)(yaml@2.8.3) + vitest: 4.1.8(@types/node@22.16.0)(@vitest/coverage-v8@4.0.18)(happy-dom@20.10.2)(jsdom@29.1.1)(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(yaml@2.8.3)) - '@vitest/expect@4.0.18': + '@vitest/expect@4.1.8': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.2 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3))': + '@vitest/mocker@4.1.8(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3) + vite: 7.3.2(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(yaml@2.8.3) '@vitest/pretty-format@4.0.18': dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@4.0.18': + '@vitest/pretty-format@4.1.8': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.8': dependencies: - '@vitest/utils': 4.0.18 + '@vitest/utils': 4.1.8 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.8': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.8': {} '@vitest/utils@4.0.18': dependencies: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + '@vitest/utils@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -5600,14 +6880,6 @@ snapshots: acorn@8.16.0: {} - agent-base@7.1.1: - dependencies: - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - - agent-base@7.1.3: {} - ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -5673,6 +6945,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -5750,26 +7026,10 @@ snapshots: dependencies: lodash: 4.18.1 - attr-accept@2.2.5: {} - - autoprefixer@10.4.21(postcss@8.5.6): - dependencies: - browserslist: 4.25.1 - caniuse-lite: 1.0.30001727 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.1.1 - postcss: 8.5.6 - postcss-value-parser: 4.2.0 - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.0.0 - b-tween@0.3.3: {} - - b-validate@1.5.3: {} - bail@2.0.2: {} balanced-match@1.0.2: {} @@ -5780,6 +7040,10 @@ snapshots: batch@0.6.1: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} bl@4.1.0: @@ -5823,18 +7087,15 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.25.1: - dependencies: - caniuse-lite: 1.0.30001727 - electron-to-chromium: 1.5.180 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.1) - buffer-crc32@0.2.13: {} buffer-from@1.1.2: optional: true + buffer-image-size@0.6.4: + dependencies: + '@types/node': 22.16.2 + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -5848,8 +7109,6 @@ snapshots: bytes@3.1.2: {} - cac@6.7.14: {} - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -5874,8 +7133,6 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001727: {} - ccount@2.0.1: {} chai@6.2.2: {} @@ -5909,28 +7166,18 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - color-convert@1.9.3: + class-variance-authority@0.7.1: dependencies: - color-name: 1.1.3 + clsx: 2.1.1 + + clsx@2.1.1: {} color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - - color@3.2.1: - dependencies: - color-convert: 1.9.3 - color-string: 1.9.1 - colorette@2.0.20: {} comma-separated-tokens@2.0.3: {} @@ -5962,24 +7209,18 @@ snapshots: transitivePeerDependencies: - supports-color - compute-scroll-into-view@1.0.20: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} - - confbox@0.2.2: {} - connect-history-api-fallback@2.0.0: {} - consola@3.4.2: {} - content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.0.7: {} cookie@0.7.2: {} @@ -6035,26 +7276,23 @@ snapshots: crypto-js@4.2.0: {} - css-tree@3.1.0: + css-tree@3.2.1: dependencies: - mdn-data: 2.12.2 + mdn-data: 2.27.1 source-map-js: 1.2.1 css.escape@1.5.1: {} - cssstyle@4.6.0: - dependencies: - '@asamuzakjp/css-color': 3.2.0 - rrweb-cssom: 0.8.0 - - csstype@3.1.3: {} + csstype@3.2.3: {} data-uri-to-buffer@4.0.1: {} - data-urls@5.0.0: + data-urls@7.0.0: dependencies: - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' data-view-buffer@1.0.2: dependencies: @@ -6086,18 +7324,14 @@ snapshots: dependencies: ms: 2.1.3 - decimal.js@10.5.0: {} + decimal.js@10.6.0: {} decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 - deep-diff@1.0.2: {} - deep-is@0.1.4: {} - deepmerge@4.3.1: {} - default-browser-id@5.0.1: {} default-browser@5.5.0: @@ -6119,18 +7353,16 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - defu@6.1.6: {} - depd@1.1.2: {} depd@2.0.0: {} dequal@2.0.3: {} - destr@2.0.3: {} - destroy@1.2.0: {} + detect-libc@2.1.2: {} + detect-node-es@1.1.0: {} detect-node@2.1.0: {} @@ -6155,11 +7387,6 @@ snapshots: dom-accessibility-api@0.6.3: {} - dom-helpers@5.2.1: - dependencies: - '@babel/runtime': 7.27.6 - csstype: 3.1.3 - dompurify@3.3.3: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -6174,18 +7401,23 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.180: {} - encodeurl@2.0.0: {} end-of-stream@1.4.4: dependencies: once: 1.4.0 - entities@4.5.0: {} + enhanced-resolve@5.23.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 entities@6.0.1: {} + entities@7.0.1: {} + + entities@8.0.0: {} + env-paths@2.2.1: {} error-ex@1.3.2: @@ -6269,7 +7501,7 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 - es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} es-object-atoms@1.0.0: dependencies: @@ -6325,8 +7557,6 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 - escalade@3.2.0: {} - escape-html@1.0.3: {} escape-string-regexp@4.0.0: {} @@ -6462,7 +7692,7 @@ snapshots: exit-hook@4.0.0: {} - expect-type@1.2.2: {} + expect-type@1.3.0: {} express@4.22.1: dependencies: @@ -6500,8 +7730,6 @@ snapshots: transitivePeerDependencies: - supports-color - exsolve@1.0.7: {} - extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -6553,10 +7781,6 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-selector@2.1.2: - dependencies: - tslib: 2.8.1 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -6585,10 +7809,6 @@ snapshots: flatted@3.4.2: {} - focus-lock@1.3.5: - dependencies: - tslib: 2.8.1 - follow-redirects@1.15.11: {} for-each@0.3.3: @@ -6601,8 +7821,6 @@ snapshots: forwarded@0.2.0: {} - fraction.js@4.3.7: {} - fresh@0.5.2: {} fs-constants@1.0.0: {} @@ -6654,6 +7872,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -6686,12 +7906,8 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - globals@11.12.0: {} - globals@14.0.0: {} - globals@15.15.0: {} - globals@16.5.0: {} globalthis@1.0.4: @@ -6711,6 +7927,19 @@ snapshots: handle-thing@2.0.1: {} + happy-dom@20.10.2: + dependencies: + '@types/node': 22.16.2 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + buffer-image-size: 0.6.4 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + has-bigints@1.0.2: {} has-flag@4.0.0: {} @@ -6749,7 +7978,7 @@ snapshots: mdast-util-mdx-expression: 2.0.1 mdast-util-mdx-jsx: 3.2.0 mdast-util-mdxjs-esm: 2.0.1 - property-information: 7.1.0 + property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 unist-util-position: 5.0.0 @@ -6779,9 +8008,11 @@ snapshots: readable-stream: 2.3.8 wbuf: 1.7.3 - html-encoding-sniffer@4.0.0: + html-encoding-sniffer@6.0.0: dependencies: - whatwg-encoding: 3.1.1 + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' html-escaper@2.0.2: {} @@ -6811,13 +8042,6 @@ snapshots: http-parser-js@0.5.10: {} - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.1 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - http-proxy-middleware@2.0.9(@types/express@4.17.25): dependencies: '@types/http-proxy': 1.17.17 @@ -6838,29 +8062,18 @@ snapshots: transitivePeerDependencies: - debug - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.3 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - husky@9.1.7: {} hyperdyperid@1.2.0: {} - i18next@23.16.4: - dependencies: - '@babel/runtime': 7.27.6 + i18next@26.3.1(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -6914,8 +8127,6 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: {} - is-async-function@2.0.0: dependencies: has-tostringtag: 1.0.2 @@ -6976,10 +8187,6 @@ snapshots: dependencies: is-docker: 3.0.0 - is-lite@0.8.2: {} - - is-lite@1.2.1: {} - is-map@2.0.3: {} is-network-error@1.3.0: {} @@ -7078,34 +8285,31 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@26.1.0: - dependencies: - cssstyle: 4.6.0 - data-urls: 5.0.0 - decimal.js: 10.5.0 - html-encoding-sniffer: 4.0.0 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.20 - parse5: 7.2.1 - rrweb-cssom: 0.8.0 + lru-cache: 11.5.1 + parse5: 8.0.1 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 5.1.2 + tough-cookie: 6.0.1 + undici: 7.27.2 w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - ws: 8.18.1 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - jsesc@3.1.0: {} + - '@noble/hashes' json-buffer@3.0.1: {} @@ -7128,8 +8332,6 @@ snapshots: dependencies: json-buffer: 3.0.1 - kolorist@1.8.0: {} - launch-editor@2.12.0: dependencies: picocolors: 1.1.1 @@ -7146,13 +8348,56 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lines-and-columns@1.2.4: {} + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true - local-pkg@1.1.1: + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: dependencies: - mlly: 1.7.4 - pkg-types: 2.2.0 - quansync: 0.2.10 + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lines-and-columns@1.2.4: {} locate-path@6.0.0: dependencies: @@ -7184,7 +8429,11 @@ snapshots: devlop: 1.1.0 highlight.js: 11.11.1 - lru-cache@10.4.3: {} + lru-cache@11.5.1: {} + + lucide-react@1.17.0(react@19.2.7): + dependencies: + react: 19.2.7 luxon@3.7.2: {} @@ -7345,7 +8594,7 @@ snapshots: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.0 + '@ungap/structured-clone': 1.3.1 devlop: 1.1.0 micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 @@ -7369,7 +8618,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - mdn-data@2.12.2: {} + mdn-data@2.27.1: {} media-typer@0.3.0: {} @@ -7618,13 +8867,6 @@ snapshots: dependencies: brace-expansion: 2.0.3 - mlly@1.7.4: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.5.4 - mock-xmlhttprequest@8.4.1: {} monaco-editor@0.52.2: {} @@ -7642,6 +8884,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@3.3.12: {} + natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -7652,8 +8896,6 @@ snapshots: node-domexception@1.0.0: {} - node-fetch-native@1.6.4: {} - node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -7662,20 +8904,12 @@ snapshots: node-forge@1.4.0: {} - node-releases@2.0.19: {} - node-rsa@1.1.1: dependencies: asn1: 0.2.6 normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - - number-precision@1.6.0: {} - - nwsapi@2.2.20: {} - object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -7716,12 +8950,6 @@ snapshots: obug@2.1.1: {} - ofetch@1.4.1: - dependencies: - destr: 2.0.3 - node-fetch-native: 1.6.4 - ufo: 1.5.4 - on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -7770,8 +8998,6 @@ snapshots: is-network-error: 1.3.0 retry: 0.13.1 - package-manager-detector@1.3.0: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -7793,9 +9019,9 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse5@7.2.1: + parse5@8.0.1: dependencies: - entities: 4.5.0 + entities: 8.0.0 parseurl@1.3.3: {} @@ -7820,26 +9046,12 @@ snapshots: ieee754: 1.2.1 resolve-protobuf-schema: 2.1.0 - perfect-debounce@1.0.0: {} - picocolors@1.1.1: {} picomatch@2.3.2: {} picomatch@4.0.4: {} - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.7.4 - pathe: 2.0.3 - - pkg-types@2.2.0: - dependencies: - confbox: 0.2.2 - exsolve: 1.0.7 - pathe: 2.0.3 - playwright-core@1.58.2: {} playwright@1.58.2: @@ -7848,8 +9060,6 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - popper.js@1.16.1: {} - possible-typed-array-names@1.0.0: {} postcss-loader@8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.17))(postcss@8.5.6)(typescript@5.9.3): @@ -7863,7 +9073,11 @@ snapshots: transitivePeerDependencies: - typescript - postcss-value-parser@4.2.0: {} + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 postcss@8.5.6: dependencies: @@ -7893,7 +9107,7 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - property-information@7.1.0: {} + property-information@7.2.0: {} protocol-buffers-schema@3.6.0: {} @@ -7908,14 +9122,73 @@ snapshots: dependencies: side-channel: 1.1.0 - quansync@0.2.10: {} - - quansync@0.2.11: {} - querystringify@2.2.0: {} queue-microtask@1.2.3: {} + radix-ui@1.5.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-accessible-icon': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-accordion': 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-alert-dialog': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-aspect-ratio': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-avatar': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-checkbox': 1.3.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collapsible': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context-menu': 2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dialog': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dropdown-menu': 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-form': 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-hover-card': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-label': 2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-menu': 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-menubar': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-navigation-menu': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-one-time-password-field': 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-password-toggle-field': 0.1.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popover': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-progress': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-radio-group': 1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-scroll-area': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-select': 2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-separator': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slider': 1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-switch': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-tabs': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toast': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle-group': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toolbar': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-tooltip': 1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-escape-keydown': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + range-parser@1.2.1: {} raw-body@2.5.3: @@ -7925,99 +9198,36 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - react-clientside-effect@1.2.6(react@18.3.1): - dependencies: - '@babel/runtime': 7.27.6 - react: 18.3.1 - - react-dom@18.3.1(react@18.3.1): - dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 - - react-dropzone@14.3.8(react@18.3.1): - dependencies: - attr-accept: 2.2.5 - file-selector: 2.1.2 - prop-types: 15.8.1 - react: 18.3.1 - - react-floater@0.7.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-dom@19.2.7(react@19.2.7): dependencies: - deepmerge: 4.3.1 - is-lite: 0.8.2 - popper.js: 1.16.1 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tree-changes: 0.9.3 - - react-focus-lock@2.13.2(@types/react@18.3.23)(react@18.3.1): - dependencies: - '@babel/runtime': 7.27.6 - focus-lock: 1.3.5 - prop-types: 15.8.1 - react: 18.3.1 - react-clientside-effect: 1.2.6(react@18.3.1) - use-callback-ref: 1.3.2(@types/react@18.3.23)(react@18.3.1) - use-sidecar: 1.1.2(@types/react@18.3.23)(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.23 + react: 19.2.7 + scheduler: 0.27.0 - react-i18next@15.6.0(i18next@23.16.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3): + react-i18next@17.0.8(i18next@26.3.1(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3): dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.29.7 html-parse-stringify: 3.0.1 - i18next: 23.16.4 - react: 18.3.1 + i18next: 26.3.1(typescript@5.9.3) + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) optionalDependencies: - react-dom: 18.3.1(react@18.3.1) + react-dom: 19.2.7(react@19.2.7) typescript: 5.9.3 - react-icons@5.5.0(react@18.3.1): - dependencies: - react: 18.3.1 - - react-innertext@1.1.5(@types/react@18.3.23)(react@18.3.1): - dependencies: - '@types/react': 18.3.23 - react: 18.3.1 - react-is@16.13.1: {} react-is@17.0.2: {} - react-is@18.3.1: {} - - react-joyride@2.9.3(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@gilbarbara/deep-equal': 0.3.1 - deep-diff: 1.0.2 - deepmerge: 4.3.1 - is-lite: 1.2.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-floater: 0.7.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-innertext: 1.1.5(@types/react@18.3.23)(react@18.3.1) - react-is: 16.13.1 - scroll: 3.0.1 - scrollparent: 2.1.0 - tree-changes: 0.11.2 - type-fest: 4.31.0 - transitivePeerDependencies: - - '@types/react' - - react-markdown@9.1.0(@types/react@18.3.23)(react@18.3.1): + react-markdown@9.1.0(@types/react@19.2.17)(react@19.2.7): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 18.3.23 + '@types/react': 19.2.17 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.1 - react: 18.3.1 + react: 19.2.7 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -8026,32 +9236,48 @@ snapshots: transitivePeerDependencies: - supports-color - react-router-dom@7.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): + dependencies: + react: 19.2.7 + react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7): + dependencies: + react: 19.2.7 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.7) + react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) + use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + react-router-dom@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-router: 7.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-router: 7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react-router@7.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: cookie: 1.0.2 - react: 18.3.1 + react: 19.2.7 set-cookie-parser: 2.7.1 optionalDependencies: - react-dom: 18.3.1(react@18.3.1) + react-dom: 19.2.7(react@19.2.7) - react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7): dependencies: - '@babel/runtime': 7.27.6 - dom-helpers: 5.2.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + get-nonce: 1.0.1 + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 - react@18.3.1: - dependencies: - loose-envify: 1.4.0 + react@19.2.7: {} readable-stream@2.3.8: dependencies: @@ -8142,8 +9368,6 @@ snapshots: requires-port@1.0.0: {} - resize-observer-polyfill@1.5.1: {} - resolve-from@4.0.0: {} resolve-protobuf-schema@2.1.0: @@ -8191,8 +9415,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 - rrweb-cssom@0.8.0: {} - run-applescript@7.1.0: {} run-parallel@1.2.0: @@ -8228,9 +9450,7 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 + scheduler@0.27.0: {} schema-utils@4.3.2: dependencies: @@ -8239,14 +9459,6 @@ snapshots: ajv-formats: 2.1.1(ajv@8.18.0) ajv-keywords: 5.1.0(ajv@8.18.0) - scroll-into-view-if-needed@2.2.31: - dependencies: - compute-scroll-into-view: 1.0.20 - - scroll@3.0.1: {} - - scrollparent@2.1.0: {} - select-hose@2.0.0: {} selfsigned@2.4.1: @@ -8323,8 +9535,6 @@ snapshots: setprototypeof@1.2.0: {} - shallowequal@1.1.0: {} - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -8363,28 +9573,23 @@ snapshots: siginfo@2.0.0: {} - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.28 mrmime: 2.0.0 totalist: 3.0.1 - sirv@3.0.2: - dependencies: - '@polka/url': 1.0.0-next.28 - mrmime: 2.0.0 - totalist: 3.0.1 - sockjs@0.3.24: dependencies: faye-websocket: 0.11.4 uuid: 8.3.2 websocket-driver: 0.7.4 + sonner@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -8427,6 +9632,8 @@ snapshots: std-env@3.10.0: {} + std-env@4.1.0: {} + string-similarity-js@2.1.4: {} string.prototype.matchall@4.0.12: @@ -8514,6 +9721,12 @@ snapshots: dependencies: '@pkgr/core': 0.2.7 + tailwind-merge@3.6.0: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -8538,8 +9751,6 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.0.1: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -8549,11 +9760,13 @@ snapshots: tinyrainbow@3.0.3: {} - tldts-core@6.1.60: {} + tinyrainbow@3.1.0: {} - tldts@6.1.60: + tldts-core@7.4.2: {} + + tldts@7.4.2: dependencies: - tldts-core: 6.1.60 + tldts-core: 7.4.2 to-regex-range@5.0.1: dependencies: @@ -8563,24 +9776,14 @@ snapshots: totalist@3.0.1: {} - tough-cookie@5.1.2: + tough-cookie@6.0.1: dependencies: - tldts: 6.1.60 + tldts: 7.4.2 - tr46@5.1.1: + tr46@6.0.0: dependencies: punycode: 2.3.1 - tree-changes@0.11.2: - dependencies: - '@gilbarbara/deep-equal': 0.3.1 - is-lite: 1.2.1 - - tree-changes@0.9.3: - dependencies: - '@gilbarbara/deep-equal': 0.1.2 - is-lite: 0.8.2 - tree-dump@1.1.0(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -8615,12 +9818,12 @@ snapshots: tslib@2.8.1: {} + tw-animate-css@1.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - type-fest@4.31.0: {} - type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -8672,8 +9875,6 @@ snapshots: typescript@5.9.3: {} - ufo@1.5.4: {} - uglify-js@3.19.3: {} unbox-primitive@1.1.0: @@ -8683,15 +9884,10 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - unconfig@7.3.3: - dependencies: - '@quansync/fs': 0.1.5 - defu: 6.1.6 - jiti: 2.6.1 - quansync: 0.2.11 - undici-types@6.21.0: {} + undici@7.27.2: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -8730,46 +9926,8 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - unocss@66.5.4(postcss@8.5.6)(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3)): - dependencies: - '@unocss/astro': 66.5.4(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3)) - '@unocss/cli': 66.5.4 - '@unocss/core': 66.5.4 - '@unocss/postcss': 66.5.4(postcss@8.5.6) - '@unocss/preset-attributify': 66.5.4 - '@unocss/preset-icons': 66.5.4 - '@unocss/preset-mini': 66.5.4 - '@unocss/preset-tagify': 66.5.4 - '@unocss/preset-typography': 66.5.4 - '@unocss/preset-uno': 66.5.4 - '@unocss/preset-web-fonts': 66.5.4 - '@unocss/preset-wind': 66.5.4 - '@unocss/preset-wind3': 66.5.4 - '@unocss/preset-wind4': 66.5.4 - '@unocss/transformer-attributify-jsx': 66.5.4 - '@unocss/transformer-compile-class': 66.5.4 - '@unocss/transformer-directives': 66.5.4 - '@unocss/transformer-variant-group': 66.5.4 - '@unocss/vite': 66.5.4(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3)) - optionalDependencies: - vite: 7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3) - transitivePeerDependencies: - - postcss - - supports-color - unpipe@1.0.0: {} - unplugin-utils@0.3.1: - dependencies: - pathe: 2.0.3 - picomatch: 4.0.4 - - update-browserslist-db@1.1.3(browserslist@4.25.1): - dependencies: - browserslist: 4.25.1 - escalade: 3.2.0 - picocolors: 1.1.1 - uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -8781,20 +9939,24 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - use-callback-ref@1.3.2(@types/react@18.3.23)(react@18.3.1): + use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): dependencies: - react: 18.3.1 + react: 19.2.7 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 19.2.17 - use-sidecar@1.1.2(@types/react@18.3.23)(react@18.3.1): + use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): dependencies: detect-node-es: 1.1.0 - react: 18.3.1 + react: 19.2.7 tslib: 2.8.1 optionalDependencies: - '@types/react': 18.3.23 + '@types/react': 19.2.17 + + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 util-deprecate@1.0.2: {} @@ -8818,63 +9980,54 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3): + vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(yaml@2.8.3): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.6 + postcss: 8.5.15 rollup: 4.60.1 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.16.0 fsevents: 2.3.3 jiti: 2.6.1 + lightningcss: 1.32.0 terser: 5.43.1 yaml: 2.8.3 - vitest@4.0.18(@types/node@22.16.0)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.43.1)(yaml@2.8.3): - dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 - expect-type: 1.2.2 + vitest@4.1.8(@types/node@22.16.0)(@vitest/coverage-v8@4.0.18)(happy-dom@20.10.2)(jsdom@29.1.1)(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@7.3.2(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 3.10.0 + std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.0.2 tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(yaml@2.8.3) + tinyrainbow: 3.1.0 + vite: 7.3.2(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.16.0 - jsdom: 26.1.0 + '@vitest/coverage-v8': 4.0.18(vitest@4.1.8) + happy-dom: 20.10.2 + jsdom: 29.1.1 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml void-elements@3.1.0: {} - vue-flow-layout@0.2.0: {} - w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -8904,7 +10057,7 @@ snapshots: url-join: 5.0.0 url-parse: 1.5.10 - webidl-conversions@7.0.0: {} + webidl-conversions@8.0.1: {} webpack-bundle-analyzer@4.10.2: dependencies: @@ -8964,7 +10117,7 @@ snapshots: sockjs: 0.3.24 spdy: 4.0.2 webpack-dev-middleware: 7.4.5(tslib@2.8.1) - ws: 8.18.1 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - debug @@ -8980,16 +10133,17 @@ snapshots: websocket-extensions@0.1.4: {} - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 + whatwg-mimetype@3.0.0: {} - whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} - whatwg-url@14.2.0: + whatwg-url@16.0.1: dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' which-boxed-primitive@1.1.1: dependencies: @@ -9046,7 +10200,7 @@ snapshots: ws@7.5.10: {} - ws@8.18.1: {} + ws@8.21.0: {} wsl-utils@0.1.0: dependencies: diff --git a/postcss.config.mjs b/postcss.config.mjs index ec8c02982..c2ddf7482 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,6 +1,5 @@ -import UnoCSS from "@unocss/postcss"; -import autoprefixer from "autoprefixer"; - export default { - plugins: [UnoCSS(), autoprefixer()], + plugins: { + "@tailwindcss/postcss": {}, + }, }; diff --git a/rspack.config.ts b/rspack.config.ts index e670fe47e..bd1c775cf 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -58,11 +58,11 @@ export default { inject: `${src}/inject.ts`, common: `${src}/pages/common.ts`, popup: `${src}/pages/popup/main.tsx`, - install: `${src}/pages/install/main.tsx`, - batchupdate: `${src}/pages/batchupdate/main.tsx`, + options: `${src}/pages/options/main.tsx`, confirm: `${src}/pages/confirm/main.tsx`, + batchupdate: `${src}/pages/batchupdate/main.tsx`, + install: `${src}/pages/install/main.tsx`, import: `${src}/pages/import/main.tsx`, - options: `${src}/pages/options/main.tsx`, "editor.worker": "monaco-editor/esm/vs/editor/editor.worker.js", "ts.worker": "monaco-editor/esm/vs/language/typescript/ts.worker.js", "linter.worker": `${src}/linter.worker.ts`, @@ -171,55 +171,52 @@ export default { ], }), new rspack.HtmlRspackPlugin({ - filename: `${dist}/ext/src/install.html`, - template: `${src}/pages/template.html`, + filename: `${dist}/ext/src/popup.html`, + template: `${src}/pages/popup.html`, inject: "head", - title: "Install - ScriptCat", + title: "ScriptCat", minify: true, - chunks: ["install"], + chunks: ["popup"], }), new rspack.HtmlRspackPlugin({ - filename: `${dist}/ext/src/batchupdate.html`, - template: `${src}/pages/template.html`, + filename: `${dist}/ext/src/options.html`, + template: `${src}/pages/options.html`, inject: "head", - title: "BatchUpdate - ScriptCat", + title: "ScriptCat", minify: true, - chunks: ["batchupdate"], + chunks: ["options"], }), new rspack.HtmlRspackPlugin({ filename: `${dist}/ext/src/confirm.html`, - template: `${src}/pages/template.html`, + template: `${src}/pages/confirm.html`, inject: "head", - title: "Confirm - ScriptCat", + title: "ScriptCat", minify: true, chunks: ["confirm"], }), new rspack.HtmlRspackPlugin({ - filename: `${dist}/ext/src/import.html`, - template: `${src}/pages/template.html`, + filename: `${dist}/ext/src/batchupdate.html`, + template: `${src}/pages/batchupdate.html`, inject: "head", - title: "Import - ScriptCat", + title: "ScriptCat", minify: true, - chunks: ["import"], + chunks: ["batchupdate"], }), new rspack.HtmlRspackPlugin({ - templateParameters: { - isReactTools: isReactTools ? "true" : "false", - }, - filename: `${dist}/ext/src/options.html`, - template: `${src}/pages/options.html`, + filename: `${dist}/ext/src/install.html`, + template: `${src}/pages/install.html`, inject: "head", - title: "Home - ScriptCat", + title: "ScriptCat", minify: true, - chunks: ["options"], + chunks: ["install"], }), new rspack.HtmlRspackPlugin({ - filename: `${dist}/ext/src/popup.html`, - template: `${src}/pages/popup.html`, + filename: `${dist}/ext/src/import.html`, + template: `${src}/pages/import.html`, inject: "head", - title: "Home - ScriptCat", + title: "ScriptCat", minify: true, - chunks: ["popup"], + chunks: ["import"], }), new rspack.HtmlRspackPlugin({ filename: `${dist}/ext/src/offscreen.html`, @@ -321,31 +318,13 @@ export default { } if (module.type !== "css" && tag === "monaco-editor") return "lib_monaco"; switch (tag) { - case "react-icons": - if (p.includes("/react-icons/tb")) return undefined; - // eslint-disable-next-line no-fallthrough - case "react-dropzone": case "react-dom": - case "react-i18next": - case "react-router-dom": - case "react-joyride": case "react": return `lib_${tag}`; } - if (tag.startsWith("dnd-kit")) return "lib_dnd-kit"; - if (tag.startsWith("popper")) return "lib_react-joyride"; if (tag.startsWith("react-")) return "lib_react"; if (tag.startsWith("eslint")) return "lib_eslint"; if (tag.startsWith("i18n")) return "lib_i18n"; - if ( - tag.startsWith("arco-design") || - tag === "resize-observer-polyfill" || - tag === "b-validate" || - tag === "lodash" || - tag === "focus-lock" - ) { - return "lib_arco_design"; - } if (tag) { // cron, dayjs, yaml, jszip, prettier, ... if (tag === "luxon") return "lib_cron"; diff --git a/src/app/cache.test.ts b/src/app/cache.test.ts index c45c1cdaa..06a9aed55 100644 --- a/src/app/cache.test.ts +++ b/src/app/cache.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, beforeEach, vi } from "vitest"; import { cacheInstance } from "./cache"; diff --git a/src/app/cache.ts b/src/app/cache.ts index 4a5b8fe79..23ed13925 100644 --- a/src/app/cache.ts +++ b/src/app/cache.ts @@ -19,7 +19,7 @@ class ExtCache implements CacheStorage { console.error("chrome.runtime.lastError in chrome.storage.session.get:", lastError); // 无视storage API错误,继续执行 } - resolve(value[key]); + resolve(value[key] as T | undefined); }); }); } diff --git a/src/app/repo/agent_chat.test.ts b/src/app/repo/agent_chat.test.ts index f3b1e2c6e..319a09626 100644 --- a/src/app/repo/agent_chat.test.ts +++ b/src/app/repo/agent_chat.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach } from "vitest"; import { AgentChatRepo } from "./agent_chat"; diff --git a/src/app/repo/agent_task.test.ts b/src/app/repo/agent_task.test.ts index 068cac8ae..a1b0d5201 100644 --- a/src/app/repo/agent_task.test.ts +++ b/src/app/repo/agent_task.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it, beforeEach } from "vitest"; import { AgentTaskRepo, AgentTaskRunRepo } from "./agent_task"; import type { AgentTask, AgentTaskRun } from "@App/app/service/agent/core/types"; diff --git a/src/app/repo/metadata.test.ts b/src/app/repo/metadata.test.ts index 0f2268d17..432fa80bb 100644 --- a/src/app/repo/metadata.test.ts +++ b/src/app/repo/metadata.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it } from "vitest"; import { parseTags } from "./metadata"; import type { SCMetadata } from "./metadata"; diff --git a/src/app/repo/repo.test.ts b/src/app/repo/repo.test.ts index 2a6d14cbd..77a9775a9 100644 --- a/src/app/repo/repo.test.ts +++ b/src/app/repo/repo.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, beforeEach } from "vitest"; import { Repo } from "./repo"; diff --git a/src/app/repo/repo.ts b/src/app/repo/repo.ts index 9a5a157c0..ccf32f114 100644 --- a/src/app/repo/repo.ts +++ b/src/app/repo/repo.ts @@ -220,7 +220,7 @@ export abstract class Repo { console.error("chrome.runtime.lastError in chrome.storage.local.get:", lastError); // 无视storage API错误,继续执行 } - resolve(keys.map((key) => result[key])); + resolve(keys.map((key) => result[key] as T | undefined)); }); }); } diff --git a/src/app/repo/scripts.test.ts b/src/app/repo/scripts.test.ts index 54f040456..50bed0f94 100644 --- a/src/app/repo/scripts.test.ts +++ b/src/app/repo/scripts.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, beforeEach } from "vitest"; import { ScriptDAO, diff --git a/src/app/service/agent/core/agent.test.ts b/src/app/service/agent/core/agent.test.ts index bdbe8c10b..f4b9c6eca 100644 --- a/src/app/service/agent/core/agent.test.ts +++ b/src/app/service/agent/core/agent.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { SSEParser } from "./sse_parser"; import { buildOpenAIRequest, parseOpenAIStream } from "./providers/openai"; diff --git a/src/app/service/agent/core/compact_prompt.test.ts b/src/app/service/agent/core/compact_prompt.test.ts index a971016a9..612d3d861 100644 --- a/src/app/service/agent/core/compact_prompt.test.ts +++ b/src/app/service/agent/core/compact_prompt.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it } from "vitest"; import { extractSummary, buildCompactUserPrompt, COMPACT_SYSTEM_PROMPT } from "./compact_prompt"; diff --git a/src/app/service/agent/core/content_utils.test.ts b/src/app/service/agent/core/content_utils.test.ts index 2dd6cce39..6ff505861 100644 --- a/src/app/service/agent/core/content_utils.test.ts +++ b/src/app/service/agent/core/content_utils.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect } from "vitest"; import { getTextContent, normalizeContent, isContentBlocks } from "./content_utils"; import type { ContentBlock } from "./types"; diff --git a/src/app/service/agent/core/mcp_client.test.ts b/src/app/service/agent/core/mcp_client.test.ts index 7f888258d..efdebaad7 100644 --- a/src/app/service/agent/core/mcp_client.test.ts +++ b/src/app/service/agent/core/mcp_client.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach } from "vitest"; import { MCPClient } from "./mcp_client"; import type { MCPServerConfig } from "./types"; diff --git a/src/app/service/agent/core/mcp_tool_executor.test.ts b/src/app/service/agent/core/mcp_tool_executor.test.ts index d01415e63..6c44a1bb9 100644 --- a/src/app/service/agent/core/mcp_tool_executor.test.ts +++ b/src/app/service/agent/core/mcp_tool_executor.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi } from "vitest"; import { MCPToolExecutor } from "./mcp_tool_executor"; import type { MCPClient } from "./mcp_client"; diff --git a/src/app/service/agent/core/model_context.test.ts b/src/app/service/agent/core/model_context.test.ts index 39d0e8085..632f3d8a8 100644 --- a/src/app/service/agent/core/model_context.test.ts +++ b/src/app/service/agent/core/model_context.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it } from "vitest"; import { getContextWindow, inferContextWindow, DEFAULT_CONTEXT_WINDOW } from "./model_context"; diff --git a/src/app/service/agent/core/providers/anthropic.test.ts b/src/app/service/agent/core/providers/anthropic.test.ts index 36697e7b9..021577813 100644 --- a/src/app/service/agent/core/providers/anthropic.test.ts +++ b/src/app/service/agent/core/providers/anthropic.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect } from "vitest"; import { buildAnthropicRequest, parseAnthropicStream } from "./anthropic"; import type { AgentModelConfig } from "../types"; diff --git a/src/app/service/agent/core/providers/openai.test.ts b/src/app/service/agent/core/providers/openai.test.ts index b145895fc..c07402b8c 100644 --- a/src/app/service/agent/core/providers/openai.test.ts +++ b/src/app/service/agent/core/providers/openai.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect } from "vitest"; import { buildOpenAIRequest, parseOpenAIStream } from "./openai"; import type { AgentModelConfig } from "../types"; diff --git a/src/app/service/agent/core/providers/registry.test.ts b/src/app/service/agent/core/providers/registry.test.ts index c8fd59eec..69cb30d78 100644 --- a/src/app/service/agent/core/providers/registry.test.ts +++ b/src/app/service/agent/core/providers/registry.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, beforeEach } from "vitest"; import { ProviderRegistry } from "./registry"; import type { LLMProvider } from "./types"; diff --git a/src/app/service/agent/core/session_tool_registry.test.ts b/src/app/service/agent/core/session_tool_registry.test.ts index f821086e0..c707dce5d 100644 --- a/src/app/service/agent/core/session_tool_registry.test.ts +++ b/src/app/service/agent/core/session_tool_registry.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi } from "vitest"; import { ToolRegistry } from "./tool_registry"; import type { ToolExecutor } from "./tool_registry"; diff --git a/src/app/service/agent/core/skill_script_executor.test.ts b/src/app/service/agent/core/skill_script_executor.test.ts index 29e9ba737..3efb94c60 100644 --- a/src/app/service/agent/core/skill_script_executor.test.ts +++ b/src/app/service/agent/core/skill_script_executor.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, afterEach } from "vitest"; import { SkillScriptExecutor, diff --git a/src/app/service/agent/core/sse_parser.test.ts b/src/app/service/agent/core/sse_parser.test.ts index 6e8ddc845..28fad1c1b 100644 --- a/src/app/service/agent/core/sse_parser.test.ts +++ b/src/app/service/agent/core/sse_parser.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect } from "vitest"; import { SSEParser } from "./sse_parser"; diff --git a/src/app/service/agent/core/sub_agent_types.test.ts b/src/app/service/agent/core/sub_agent_types.test.ts index 90aa80bc0..09dafef54 100644 --- a/src/app/service/agent/core/sub_agent_types.test.ts +++ b/src/app/service/agent/core/sub_agent_types.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect } from "vitest"; import { resolveSubAgentType, getExcludeToolsForType, SUB_AGENT_TYPES } from "./sub_agent_types"; diff --git a/src/app/service/agent/core/system_prompt.test.ts b/src/app/service/agent/core/system_prompt.test.ts index 5e398412f..3e08f0d47 100644 --- a/src/app/service/agent/core/system_prompt.test.ts +++ b/src/app/service/agent/core/system_prompt.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it } from "vitest"; import { buildSystemPrompt, buildSubAgentSystemPrompt, _BUILTIN_SYSTEM_PROMPT_FOR_TEST } from "./system_prompt"; diff --git a/src/app/service/agent/core/task_scheduler.test.ts b/src/app/service/agent/core/task_scheduler.test.ts index a67c400ee..65ab1b740 100644 --- a/src/app/service/agent/core/task_scheduler.test.ts +++ b/src/app/service/agent/core/task_scheduler.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it, beforeEach, vi } from "vitest"; import { AgentTaskScheduler } from "./task_scheduler"; import { AgentTaskRepo, AgentTaskRunRepo } from "@App/app/repo/agent_task"; diff --git a/src/app/service/agent/core/tool_call_guard.test.ts b/src/app/service/agent/core/tool_call_guard.test.ts index 6c443c189..40f2bb643 100644 --- a/src/app/service/agent/core/tool_call_guard.test.ts +++ b/src/app/service/agent/core/tool_call_guard.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect } from "vitest"; import { detectToolCallIssues, type ToolCallRecord } from "./tool_call_guard"; diff --git a/src/app/service/agent/core/tool_registry.test.ts b/src/app/service/agent/core/tool_registry.test.ts index d9d5992c9..cfc2a9dac 100644 --- a/src/app/service/agent/core/tool_registry.test.ts +++ b/src/app/service/agent/core/tool_registry.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi } from "vitest"; import { ToolRegistry } from "./tool_registry"; import type { ToolExecutor } from "./tool_registry"; diff --git a/src/app/service/agent/core/tools/ask_user.test.ts b/src/app/service/agent/core/tools/ask_user.test.ts index bb0c1fd70..e78fa6579 100644 --- a/src/app/service/agent/core/tools/ask_user.test.ts +++ b/src/app/service/agent/core/tools/ask_user.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi } from "vitest"; import { createAskUserTool } from "./ask_user"; import type { ChatStreamEvent } from "@App/app/service/agent/core/types"; diff --git a/src/app/service/agent/core/tools/execute_script.test.ts b/src/app/service/agent/core/tools/execute_script.test.ts index 49a4fcd75..131963ddb 100644 --- a/src/app/service/agent/core/tools/execute_script.test.ts +++ b/src/app/service/agent/core/tools/execute_script.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi } from "vitest"; import { createExecuteScriptTool, type ExecuteScriptDeps } from "./execute_script"; diff --git a/src/app/service/agent/core/tools/opfs_tools.test.ts b/src/app/service/agent/core/tools/opfs_tools.test.ts index 0b1da86fb..ca0a746df 100644 --- a/src/app/service/agent/core/tools/opfs_tools.test.ts +++ b/src/app/service/agent/core/tools/opfs_tools.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, beforeEach, vi } from "vitest"; import { createOPFSTools, sanitizePath, setCreateBlobUrlFn, guessMimeType } from "./opfs_tools"; import { isText } from "@App/pkg/utils/istextorbinary"; diff --git a/src/app/service/agent/core/tools/search_config.ts b/src/app/service/agent/core/tools/search_config.ts index c35b03f73..be07a3ab3 100644 --- a/src/app/service/agent/core/tools/search_config.ts +++ b/src/app/service/agent/core/tools/search_config.ts @@ -14,7 +14,7 @@ export class SearchConfigRepo { async getConfig(): Promise { try { const result = await chrome.storage.local.get(STORAGE_KEY); - return result[STORAGE_KEY] || DEFAULT_CONFIG; + return (result[STORAGE_KEY] as SearchEngineConfig) || DEFAULT_CONFIG; } catch { return DEFAULT_CONFIG; } diff --git a/src/app/service/agent/core/tools/sub_agent.test.ts b/src/app/service/agent/core/tools/sub_agent.test.ts index 81edc42cb..b9b6e11c6 100644 --- a/src/app/service/agent/core/tools/sub_agent.test.ts +++ b/src/app/service/agent/core/tools/sub_agent.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi } from "vitest"; import { createSubAgentTool } from "./sub_agent"; diff --git a/src/app/service/agent/core/tools/tab_tools.test.ts b/src/app/service/agent/core/tools/tab_tools.test.ts index 52b309cb1..4a3ff36b9 100644 --- a/src/app/service/agent/core/tools/tab_tools.test.ts +++ b/src/app/service/agent/core/tools/tab_tools.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach } from "vitest"; import { createTabTools } from "./tab_tools"; diff --git a/src/app/service/agent/core/tools/task_tools.test.ts b/src/app/service/agent/core/tools/task_tools.test.ts index ac18fa40b..2fb7fc506 100644 --- a/src/app/service/agent/core/tools/task_tools.test.ts +++ b/src/app/service/agent/core/tools/task_tools.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi } from "vitest"; import { createTaskTools, type Task } from "./task_tools"; diff --git a/src/app/service/agent/core/tools/web_fetch.test.ts b/src/app/service/agent/core/tools/web_fetch.test.ts index 5a46a492f..331472528 100644 --- a/src/app/service/agent/core/tools/web_fetch.test.ts +++ b/src/app/service/agent/core/tools/web_fetch.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach } from "vitest"; import { WebFetchExecutor, stripHtmlTags } from "./web_fetch"; diff --git a/src/app/service/agent/core/tools/web_search.test.ts b/src/app/service/agent/core/tools/web_search.test.ts index 106491714..58d24e363 100644 --- a/src/app/service/agent/core/tools/web_search.test.ts +++ b/src/app/service/agent/core/tools/web_search.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach } from "vitest"; import { WebSearchExecutor } from "./web_search"; import type { SearchConfigRepo } from "./search_config"; diff --git a/src/app/service/agent/service_worker/autocompact.test.ts b/src/app/service/agent/service_worker/autocompact.test.ts index 9544fac08..f7b7ca46a 100644 --- a/src/app/service/agent/service_worker/autocompact.test.ts +++ b/src/app/service/agent/service_worker/autocompact.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { createTestService, makeSSEResponse } from "./test-helpers"; diff --git a/src/app/service/agent/service_worker/background.test.ts b/src/app/service/agent/service_worker/background.test.ts index 82db11843..119b67e2f 100644 --- a/src/app/service/agent/service_worker/background.test.ts +++ b/src/app/service/agent/service_worker/background.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { createTestService, makeTextResponse, createRunningConversation } from "./test-helpers"; diff --git a/src/app/service/agent/service_worker/chat.test.ts b/src/app/service/agent/service_worker/chat.test.ts index c9ce19b38..338152e3e 100644 --- a/src/app/service/agent/service_worker/chat.test.ts +++ b/src/app/service/agent/service_worker/chat.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { createTestService, makeSkillRecord, makeSkillScriptRecord, makeTextResponse } from "./test-helpers"; diff --git a/src/app/service/agent/service_worker/dom.test.ts b/src/app/service/agent/service_worker/dom.test.ts index a0cf6829d..45daaed12 100644 --- a/src/app/service/agent/service_worker/dom.test.ts +++ b/src/app/service/agent/service_worker/dom.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { AgentDomService } from "./dom"; import { assertDomUrlAllowed, SENSITIVE_HOST_PATTERNS } from "./dom_policy"; diff --git a/src/app/service/agent/service_worker/dom_cdp.test.ts b/src/app/service/agent/service_worker/dom_cdp.test.ts index 0170767ee..423795607 100644 --- a/src/app/service/agent/service_worker/dom_cdp.test.ts +++ b/src/app/service/agent/service_worker/dom_cdp.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach, afterAll } from "vitest"; // mock chrome.debugger 和 chrome.tabs diff --git a/src/app/service/agent/service_worker/llm.test.ts b/src/app/service/agent/service_worker/llm.test.ts index 46120a6d8..0a211a4f8 100644 --- a/src/app/service/agent/service_worker/llm.test.ts +++ b/src/app/service/agent/service_worker/llm.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { createTestService, makeSSEResponse, makeTextResponse } from "./test-helpers"; diff --git a/src/app/service/agent/service_worker/mcp.test.ts b/src/app/service/agent/service_worker/mcp.test.ts index ade68726d..7151ec6a2 100644 --- a/src/app/service/agent/service_worker/mcp.test.ts +++ b/src/app/service/agent/service_worker/mcp.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach } from "vitest"; import { MCPService } from "./mcp"; import { ToolRegistry } from "@App/app/service/agent/core/tool_registry"; diff --git a/src/app/service/agent/service_worker/opfs.test.ts b/src/app/service/agent/service_worker/opfs.test.ts index 9f2377f0b..21f267ef2 100644 --- a/src/app/service/agent/service_worker/opfs.test.ts +++ b/src/app/service/agent/service_worker/opfs.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi } from "vitest"; import { createTestService } from "./test-helpers"; diff --git a/src/app/service/agent/service_worker/retry.test.ts b/src/app/service/agent/service_worker/retry.test.ts index 7bec6361a..418a51711 100644 --- a/src/app/service/agent/service_worker/retry.test.ts +++ b/src/app/service/agent/service_worker/retry.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi } from "vitest"; import { isRetryableError, withRetry, classifyErrorCode } from "./agent"; diff --git a/src/app/service/agent/service_worker/skill-cleanup.test.ts b/src/app/service/agent/service_worker/skill-cleanup.test.ts index db5ad3707..bf801439f 100644 --- a/src/app/service/agent/service_worker/skill-cleanup.test.ts +++ b/src/app/service/agent/service_worker/skill-cleanup.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { AgentService } from "./agent"; import { createTestService, makeSkillRecord, makeSkillScriptRecord } from "./test-helpers"; diff --git a/src/app/service/agent/service_worker/skill.test.ts b/src/app/service/agent/service_worker/skill.test.ts index 6cf7e4db8..02352e853 100644 --- a/src/app/service/agent/service_worker/skill.test.ts +++ b/src/app/service/agent/service_worker/skill.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi } from "vitest"; import { createTestService, VALID_SKILLSCRIPT_CODE, makeSkillRecord, makeSkillScriptRecord } from "./test-helpers"; diff --git a/src/app/service/agent/service_worker/sub_agent_service.test.ts b/src/app/service/agent/service_worker/sub_agent_service.test.ts index b99a83469..d90081e75 100644 --- a/src/app/service/agent/service_worker/sub_agent_service.test.ts +++ b/src/app/service/agent/service_worker/sub_agent_service.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach } from "vitest"; import { SubAgentService } from "./sub_agent_service"; import type { SubAgentOrchestrator } from "./sub_agent_service"; diff --git a/src/app/service/content/create_context.test.ts b/src/app/service/content/create_context.test.ts index ca3deabdb..b69a76ff4 100644 --- a/src/app/service/content/create_context.test.ts +++ b/src/app/service/content/create_context.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { afterEach, describe, it, expect, vi } from "vitest"; import type { TScriptInfo } from "@App/app/repo/scripts"; import { encodeRValue } from "@App/pkg/utils/message_value"; diff --git a/src/app/service/content/exec_script.test.ts b/src/app/service/content/exec_script.test.ts index 3b82103e7..dfb1b7783 100644 --- a/src/app/service/content/exec_script.test.ts +++ b/src/app/service/content/exec_script.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment jsdom import ExecScript from "./exec_script"; import { compileScript, compileScriptCode } from "./utils"; import { ExtVersion } from "@App/app/const"; diff --git a/src/app/service/content/exec_warp.test.ts b/src/app/service/content/exec_warp.test.ts index 83dfffb00..1d88568bf 100644 --- a/src/app/service/content/exec_warp.test.ts +++ b/src/app/service/content/exec_warp.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { describe, expect, it } from "vitest"; import { BgExecScriptWarp } from "./exec_warp"; import type { ScriptLoadInfo } from "../service_worker/types"; diff --git a/src/app/service/content/gm_api/cat_agent.test.ts b/src/app/service/content/gm_api/cat_agent.test.ts index 47fce7225..a2e9cbac2 100644 --- a/src/app/service/content/gm_api/cat_agent.test.ts +++ b/src/app/service/content/gm_api/cat_agent.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it, vi } from "vitest"; import { ConversationInstance } from "./cat_agent"; import type { Conversation, StreamChunk } from "@App/app/service/agent/core/types"; diff --git a/src/app/service/content/gm_api/cat_agent_model.test.ts b/src/app/service/content/gm_api/cat_agent_model.test.ts index 614342ae4..17510d968 100644 --- a/src/app/service/content/gm_api/cat_agent_model.test.ts +++ b/src/app/service/content/gm_api/cat_agent_model.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it, vi } from "vitest"; import type { AgentModelSafeConfig, ModelApiRequest } from "@App/app/service/agent/core/types"; diff --git a/src/app/service/content/gm_api/cat_agent_opfs.test.ts b/src/app/service/content/gm_api/cat_agent_opfs.test.ts index d189ccf6e..419b96e29 100644 --- a/src/app/service/content/gm_api/cat_agent_opfs.test.ts +++ b/src/app/service/content/gm_api/cat_agent_opfs.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it, vi } from "vitest"; import type { OPFSApiRequest } from "@App/app/service/agent/core/types"; diff --git a/src/app/service/content/gm_api/gm_api.test.ts b/src/app/service/content/gm_api/gm_api.test.ts index 8a94e1b66..51c2f24b3 100644 --- a/src/app/service/content/gm_api/gm_api.test.ts +++ b/src/app/service/content/gm_api/gm_api.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { describe, expect, it, vi } from "vitest"; import ExecScript from "../exec_script"; import type { ScriptLoadInfo } from "@App/app/service/service_worker/types"; diff --git a/src/app/service/content/gm_api/navigation_handle.test.ts b/src/app/service/content/gm_api/navigation_handle.test.ts index 29d7dfbdd..4385e00e1 100644 --- a/src/app/service/content/gm_api/navigation_handle.test.ts +++ b/src/app/service/content/gm_api/navigation_handle.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { describe, it, expect, vi, beforeEach } from "vitest"; import { UrlChangeEvent, attachNavigateHandler, resetAttachedForTest } from "./navigation_handle"; diff --git a/src/app/service/content/listener_manager.test.ts b/src/app/service/content/listener_manager.test.ts index ac562974d..3365ab149 100644 --- a/src/app/service/content/listener_manager.test.ts +++ b/src/app/service/content/listener_manager.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node // listener_manager.test.ts // 根据需要调整导入路径,确保能正确导入 ListenerManager 类。 import { describe, it, expect, vi } from "vitest"; diff --git a/src/app/service/content/scripting.ts b/src/app/service/content/scripting.ts index a3c0c7aa9..4a08a0265 100644 --- a/src/app/service/content/scripting.ts +++ b/src/app/service/content/scripting.ts @@ -73,7 +73,7 @@ export default class ScriptingRuntime { deliveryStorage.onChanged.addListener((changes) => { const record = changes["valueUpdateDelivery"]; if (record?.newValue) { - const sendData = record.newValue.sendData as ValueUpdateDataEncoded; + const sendData = (record.newValue as { sendData: ValueUpdateDataEncoded }).sendData; const activeOn = this.activeStorageNames === null ? PageOrContent.PAGE_AND_CONTENT diff --git a/src/app/service/content/utils.test.ts b/src/app/service/content/utils.test.ts index ef46eefe3..f307d9184 100644 --- a/src/app/service/content/utils.test.ts +++ b/src/app/service/content/utils.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { compileScriptCode, diff --git a/src/app/service/extension/extension_env.test.ts b/src/app/service/extension/extension_env.test.ts index 150475902..58bb46aa5 100644 --- a/src/app/service/extension/extension_env.test.ts +++ b/src/app/service/extension/extension_env.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { afterEach, describe, expect, it, vi } from "vitest"; import { extensionEnv, getExtensionUserAgentData } from "./extension_env"; diff --git a/src/app/service/offscreen/html_extractor.test.ts b/src/app/service/offscreen/html_extractor.test.ts index eec41fe57..67804fec0 100644 --- a/src/app/service/offscreen/html_extractor.test.ts +++ b/src/app/service/offscreen/html_extractor.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { describe, it, expect } from "vitest"; import { HtmlExtractorService } from "./html_extractor"; diff --git a/src/app/service/offscreen/search_engines/baidu.test.ts b/src/app/service/offscreen/search_engines/baidu.test.ts index 663e15769..1b7a531d8 100644 --- a/src/app/service/offscreen/search_engines/baidu.test.ts +++ b/src/app/service/offscreen/search_engines/baidu.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { describe, it, expect } from "vitest"; import { baiduEngine } from "./baidu"; diff --git a/src/app/service/offscreen/search_engines/bing.test.ts b/src/app/service/offscreen/search_engines/bing.test.ts index d72a54a04..6bd63260f 100644 --- a/src/app/service/offscreen/search_engines/bing.test.ts +++ b/src/app/service/offscreen/search_engines/bing.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { describe, it, expect } from "vitest"; import { bingEngine } from "./bing"; diff --git a/src/app/service/offscreen/search_engines/duckduckgo.test.ts b/src/app/service/offscreen/search_engines/duckduckgo.test.ts index d4a24d8ea..6c556d633 100644 --- a/src/app/service/offscreen/search_engines/duckduckgo.test.ts +++ b/src/app/service/offscreen/search_engines/duckduckgo.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { describe, it, expect } from "vitest"; import { duckduckgoEngine } from "./duckduckgo"; diff --git a/src/app/service/offscreen/search_engines/registry.test.ts b/src/app/service/offscreen/search_engines/registry.test.ts index efb5091c4..1123b4433 100644 --- a/src/app/service/offscreen/search_engines/registry.test.ts +++ b/src/app/service/offscreen/search_engines/registry.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, beforeEach } from "vitest"; import { SearchEngineRegistry } from "./registry"; import type { SearchEngine } from "./types"; diff --git a/src/app/service/offscreen/vscode-connect.test.ts b/src/app/service/offscreen/vscode-connect.test.ts index d7d3feb22..129703fb0 100644 --- a/src/app/service/offscreen/vscode-connect.test.ts +++ b/src/app/service/offscreen/vscode-connect.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { initTestEnv } from "@Tests/utils"; import { VSCodeConnect, type VSCodeConnectParam } from "./vscode-connect"; import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; diff --git a/src/app/service/sandbox/runtime.test.ts b/src/app/service/sandbox/runtime.test.ts index 6e9c36a15..8cead7858 100644 --- a/src/app/service/sandbox/runtime.test.ts +++ b/src/app/service/sandbox/runtime.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { initTestEnv } from "@Tests/utils"; import type { Server } from "@Packages/message/server"; @@ -108,3 +109,32 @@ describe("Runtime.execScript run-in 过滤", () => { expect(BgExecScriptWarpCtor).toHaveBeenCalledTimes(1); }); }); + +describe("Runtime.executeSkillScript 执行环境", () => { + beforeEach(() => { + BgExecScriptWarpCtor.mockClear(); + mockExec.mockClear(); + mockStop.mockClear(); + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("执行 skill 脚本时向 BgExecScriptWarp 传入扩展环境", async () => { + const extensionEnv: TExtensionEnv = { inIncognitoContext: true }; + const runtime = setup(extensionEnv); + + await runtime.executeSkillScript({ + uuid: "skill-uuid", + name: "skill", + code: "return 1;", + grants: [], + args: {}, + }); + + expect(BgExecScriptWarpCtor).toHaveBeenCalledTimes(1); + expect(BgExecScriptWarpCtor.mock.calls[0][2]).toBe(extensionEnv); + }); +}); diff --git a/src/app/service/sandbox/runtime.ts b/src/app/service/sandbox/runtime.ts index e3c4ad93b..10991ae39 100644 --- a/src/app/service/sandbox/runtime.ts +++ b/src/app/service/sandbox/runtime.ts @@ -240,7 +240,7 @@ export class Runtime { crontabScript(script: ScriptLoadInfo) { // 执行定时脚本 运行表达式 if (!script.metadata.crontab) { - throw new Error(script.name + " - " + t("cron_invalid_expr")); + throw new Error(script.name + " - " + t("script:cron_invalid_expr")); } // 如果有nextruntime,则加入重试队列 this.joinRetryList(script); diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 9e7ac18f9..57a5dac63 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -1,6 +1,7 @@ import type { Script, ScriptCode, ScriptRunResource, TClientPageLoadInfo } from "@App/app/repo/scripts"; import { type Resource } from "@App/app/repo/resource"; import { type Subscribe } from "@App/app/repo/subscribe"; +import { type Logger } from "@App/app/repo/logger"; import { type Permission } from "@App/app/repo/permission"; import type { InstallSource, ScriptMenu, ScriptMenuItem, TBatchUpdateListAction } from "./types"; import { Client } from "@Packages/message/client"; @@ -304,6 +305,11 @@ export class SubscribeClient extends Client { super(msgSender, "serviceWorker/subscribe"); } + // 订阅数量通常不多,但与 getAllScripts 一致,直接从 serviceWorker 内存读取 + getAllSubscribe(): Promise { + return this.doThrow("getAllSubscribe"); + } + install(subscribe: Subscribe) { return this.do("install", { subscribe }); } @@ -321,6 +327,24 @@ export class SubscribeClient extends Client { } } +export class LogClient extends Client { + constructor(msgSender: MessageSend) { + super(msgSender, "serviceWorker/log"); + } + + getLogs(start: number, end: number): Promise { + return this.doThrow("getLogs", { start, end }); + } + + deleteLogs(ids: number[]): Promise { + return this.doThrow("deleteLogs", ids); + } + + clearLogs(): Promise { + return this.doThrow("clearLogs"); + } +} + export class SystemClient extends Client { constructor(msgSender: MessageSend) { super(msgSender, "serviceWorker/system"); @@ -424,8 +448,9 @@ export class AgentClient extends Client { return this.do("removeModel", id); } + // 默认模型(未设置时返回空字符串,不能用 doThrow,否则全新安装无默认模型时会抛错) getDefaultModelId(): Promise { - return this.doThrow("getDefaultModelId"); + return this.do("getDefaultModelId").then((id) => id || ""); } setDefaultModelId(id: string) { diff --git a/src/app/service/service_worker/download.test.ts b/src/app/service/service_worker/download.test.ts index d54328878..c0cc3e42d 100644 --- a/src/app/service/service_worker/download.test.ts +++ b/src/app/service/service_worker/download.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import chromeMock from "@Packages/chrome-extension-mock"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { detachDownloadCallback, startDownload, type DownloadCallback } from "./download"; diff --git a/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts b/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts index 57cef5bcf..66bda209c 100644 --- a/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts +++ b/src/app/service/service_worker/gm_api/dnr_id_controller.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { sleep } from "@App/pkg/utils/utils"; import { describe, expect, it } from "vitest"; import { diff --git a/src/app/service/service_worker/gm_api/gm_agent.ts b/src/app/service/service_worker/gm_api/gm_agent.ts index 717ae3b6c..d6b5bc59b 100644 --- a/src/app/service/service_worker/gm_api/gm_agent.ts +++ b/src/app/service/service_worker/gm_api/gm_agent.ts @@ -19,10 +19,10 @@ const agentConfirm: ApiParamConfirmFn = async (request: GMApiRequest, _sender: I metadata[i18next.t("script_name")] = i18nName(request.script); return { permission: "agent.conversation", - title: i18next.t("agent_permission_title"), + title: i18next.t("agent:permission_title"), metadata, - describe: i18next.t("agent_permission_describe"), - permissionContent: i18next.t("agent_permission_content"), + describe: i18next.t("agent:permission_describe"), + permissionContent: i18next.t("agent:permission_content"), } as ConfirmParam; }; diff --git a/src/app/service/service_worker/gm_api/gm_agent_dom.ts b/src/app/service/service_worker/gm_api/gm_agent_dom.ts index 16878bee8..c9e087265 100644 --- a/src/app/service/service_worker/gm_api/gm_agent_dom.ts +++ b/src/app/service/service_worker/gm_api/gm_agent_dom.ts @@ -19,10 +19,10 @@ const agentDomConfirm: ApiParamConfirmFn = async (request: GMApiRequest, _sender metadata[i18next.t("script_name")] = i18nName(request.script); return { permission: "agent.dom", - title: i18next.t("agent_dom_permission_title"), + title: i18next.t("agent:dom_permission_title"), metadata, - describe: i18next.t("agent_dom_permission_describe"), - permissionContent: i18next.t("agent_dom_permission_content"), + describe: i18next.t("agent:dom_permission_describe"), + permissionContent: i18next.t("agent:dom_permission_content"), } as ConfirmParam; }; diff --git a/src/app/service/service_worker/gm_api/gm_agent_model.ts b/src/app/service/service_worker/gm_api/gm_agent_model.ts index e9d81cc72..87eb9474b 100644 --- a/src/app/service/service_worker/gm_api/gm_agent_model.ts +++ b/src/app/service/service_worker/gm_api/gm_agent_model.ts @@ -21,10 +21,10 @@ const agentConfirm: ApiParamConfirmFn = async (request: GMApiRequest, _sender: I metadata[i18next.t("script_name")] = i18nName(request.script); return { permission: "agent.model", - title: i18next.t("agent_permission_title"), + title: i18next.t("agent:permission_title"), metadata, - describe: i18next.t("agent_permission_describe"), - permissionContent: i18next.t("agent_permission_content"), + describe: i18next.t("agent:permission_describe"), + permissionContent: i18next.t("agent:permission_content"), } as ConfirmParam; }; diff --git a/src/app/service/service_worker/gm_api/gm_agent_opfs.ts b/src/app/service/service_worker/gm_api/gm_agent_opfs.ts index aa3bcb454..70d993688 100644 --- a/src/app/service/service_worker/gm_api/gm_agent_opfs.ts +++ b/src/app/service/service_worker/gm_api/gm_agent_opfs.ts @@ -32,10 +32,10 @@ const agentConfirm: ApiParamConfirmFn = async (request: GMApiRequest, _sender: I metadata[i18next.t("script_name")] = i18nName(request.script); return { permission: "agent.opfs", - title: i18next.t("agent_permission_title"), + title: i18next.t("agent:permission_title"), metadata, - describe: i18next.t("agent_permission_describe"), - permissionContent: i18next.t("agent_permission_content"), + describe: i18next.t("agent:permission_describe"), + permissionContent: i18next.t("agent:permission_content"), persistentOnly: isWrite, } as ConfirmParam; }; diff --git a/src/app/service/service_worker/gm_api/gm_agent_skills.ts b/src/app/service/service_worker/gm_api/gm_agent_skills.ts index d8e13df48..47b961793 100644 --- a/src/app/service/service_worker/gm_api/gm_agent_skills.ts +++ b/src/app/service/service_worker/gm_api/gm_agent_skills.ts @@ -32,10 +32,10 @@ const agentConfirm: ApiParamConfirmFn = async (request: GMApiRequest, _sender: I metadata[i18next.t("script_name")] = i18nName(request.script); return { permission: "agent.skills", - title: i18next.t("agent_permission_title"), + title: i18next.t("agent:permission_title"), metadata, - describe: i18next.t("agent_permission_describe"), - permissionContent: i18next.t("agent_permission_content"), + describe: i18next.t("agent:permission_describe"), + permissionContent: i18next.t("agent:permission_content"), persistentOnly: isWrite, } as ConfirmParam; }; diff --git a/src/app/service/service_worker/gm_api/gm_agent_task.ts b/src/app/service/service_worker/gm_api/gm_agent_task.ts index 2dcd98898..7de5a2880 100644 --- a/src/app/service/service_worker/gm_api/gm_agent_task.ts +++ b/src/app/service/service_worker/gm_api/gm_agent_task.ts @@ -19,10 +19,10 @@ const agentTaskConfirm: ApiParamConfirmFn = async (request: GMApiRequest, _sende metadata[i18next.t("script_name")] = i18nName(request.script); return { permission: "agent.task", - title: i18next.t("agent_permission_title"), + title: i18next.t("agent:permission_title"), metadata, - describe: i18next.t("agent_permission_describe"), - permissionContent: i18next.t("agent_permission_content"), + describe: i18next.t("agent:permission_describe"), + permissionContent: i18next.t("agent:permission_content"), } as ConfirmParam; }; diff --git a/src/app/service/service_worker/gm_api/gm_api.test.ts b/src/app/service/service_worker/gm_api/gm_api.test.ts index 5bb7b6d4e..5cc411a4e 100644 --- a/src/app/service/service_worker/gm_api/gm_api.test.ts +++ b/src/app/service/service_worker/gm_api/gm_api.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect } from "vitest"; import { type IGetSender } from "@Packages/message/server"; import { type ExtMessageSender } from "@Packages/message/types"; diff --git a/src/app/service/service_worker/gm_api/gm_api.ts b/src/app/service/service_worker/gm_api/gm_api.ts index b7d7d0c9c..31535ff58 100644 --- a/src/app/service/service_worker/gm_api/gm_api.ts +++ b/src/app/service/service_worker/gm_api/gm_api.ts @@ -425,14 +425,14 @@ export default class GMApi { } const metadata: { [key: string]: string } = {}; metadata[i18next.t("script_name")] = i18nName(request.script); - metadata[i18next.t("request_domain")] = url.host; + metadata[i18next.t("permission:request_domain")] = url.host; return { permission: "cookie", permissionValue: url.host, - title: i18next.t("access_cookie_content")!, + title: i18next.t("permission:access_cookie_content")!, metadata, - describe: i18next.t("confirm_script_operation")!, - permissionContent: i18next.t("cookie_domain")!, + describe: i18next.t("permission:confirm_script_operation")!, + permissionContent: i18next.t("permission:cookie_domain")!, uuid: "", }; }, @@ -594,11 +594,11 @@ export default class GMApi { return { permission: "file_storage", permissionValue: dir, - title: i18next.t("script_operation_title"), + title: i18next.t("permission:script_operation_title"), metadata, - describe: i18next.t("script_operation_description", { dir }), + describe: i18next.t("permission:script_operation_description", { dir }), wildcard: false, - permissionContent: i18next.t("script_permission_content"), + permissionContent: i18next.t("permission:script_permission_content"), } as ConfirmParam; }, }) @@ -890,16 +890,16 @@ export default class GMApi { const confirmExtensionSiteAccess = (): ConfirmParam => { const metadata: { [key: string]: string } = {}; metadata[i18next.t("script_name")] = i18nName(request.script); - metadata[i18next.t("request_domain")] = url.hostname; - metadata[i18next.t("request_url")] = details.url; + metadata[i18next.t("permission:request_domain")] = url.hostname; + metadata[i18next.t("permission:request_url")] = details.url; throwErrorFn = null; // 确保 GC 可以释放 conn return { permission: "extension-site-access", permissionValue: originPattern, - title: i18next.t("extension_site_access_title"), + title: i18next.t("permission:extension_site_access_title"), metadata, - describe: i18next.t("extension_site_access_description"), - permissionContent: i18next.t("extension_site_access_content"), + describe: i18next.t("permission:extension_site_access_description"), + permissionContent: i18next.t("permission:extension_site_access_content"), extensionSiteAccessOrigins, } as ConfirmParam; }; @@ -930,8 +930,8 @@ export default class GMApi { } const metadata: { [key: string]: string } = {}; metadata[i18next.t("script_name")] = i18nName(request.script); - metadata[i18next.t("request_domain")] = url.hostname; - metadata[i18next.t("request_url")] = details.url; + metadata[i18next.t("permission:request_domain")] = url.hostname; + metadata[i18next.t("permission:request_url")] = details.url; throwErrorFn = null; // 确保 GC 可以释放 conn @@ -946,9 +946,9 @@ export default class GMApi { return { permission: "cors", permissionValue: url.hostname, - title: i18next.t("script_accessing_cross_origin_resource"), + title: i18next.t("permission:script_accessing_cross_origin_resource"), metadata, - describe: i18next.t("confirm_operation_description"), + describe: i18next.t("permission:confirm_operation_description"), wildcard: true, permissionContent: i18next.t("domain"), extensionSiteAccessOrigins, diff --git a/src/app/service/service_worker/gm_api/gm_api_subscribe.test.ts b/src/app/service/service_worker/gm_api/gm_api_subscribe.test.ts index 8b6cc351a..cb0fe57c5 100644 --- a/src/app/service/service_worker/gm_api/gm_api_subscribe.test.ts +++ b/src/app/service/service_worker/gm_api/gm_api_subscribe.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, beforeEach } from "vitest"; import { ScriptDAO, SCRIPT_TYPE_NORMAL, SCRIPT_STATUS_ENABLE, SCRIPT_RUN_STATUS_COMPLETE } from "@App/app/repo/scripts"; import type { Script } from "@App/app/repo/scripts"; diff --git a/src/app/service/service_worker/gm_api/mv3_utils.test.ts b/src/app/service/service_worker/gm_api/mv3_utils.test.ts index 976b7b12d..ef9825a6f 100644 --- a/src/app/service/service_worker/gm_api/mv3_utils.test.ts +++ b/src/app/service/service_worker/gm_api/mv3_utils.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, expect, it, vi } from "vitest"; import { ChromiumHeaderMarkerLinker, FirefoxWebRequestLinker, normalizeBackgroundRequestUrl } from "./mv3_utils"; import { scXhrRequests } from "./gm_xhr"; diff --git a/src/app/service/service_worker/index.ts b/src/app/service/service_worker/index.ts index e2f05b435..4494ff214 100644 --- a/src/app/service/service_worker/index.ts +++ b/src/app/service/service_worker/index.ts @@ -10,6 +10,7 @@ import { PopupService } from "./popup"; import { SystemConfig } from "@App/pkg/config/config"; import { SynchronizeService } from "./synchronize"; import { SubscribeService } from "./subscribe"; +import { LogService } from "./log"; import { ScriptDAO } from "@App/app/repo/scripts"; import { SystemService } from "./system"; import { type Logger, LoggerDAO } from "@App/app/repo/logger"; @@ -104,6 +105,8 @@ export default class ServiceWorkerManager { synchronize.init(); const subscribe = new SubscribeService(this.api.group("subscribe"), this.mq, script); subscribe.init(); + const log = new LogService(this.api.group("log")); + log.init(); const system = new SystemService( systemConfig, this.api.group("system"), @@ -266,9 +269,13 @@ export default class ServiceWorkerManager { const url = `${DocumentationSite}${localePath}/docs/change/${ExtVersion.includes("-") ? "beta-changelog/" : ""}#${ExtVersion}`; // 如果只是修复版本,只弹出通知不打开页面 // beta版本还是每次都打开更新页面 - InfoNotification(t("ext_update_notification"), t("ext_update_notification_desc", { version: ExtVersion }), { - url, - }); + InfoNotification( + t("popup:ext_update_notification"), + t("popup:ext_update_notification_desc", { version: ExtVersion }), + { + url, + } + ); if (ExtVersion.endsWith(".0")) { getCurrentTab() .then((tab) => { diff --git a/src/app/service/service_worker/log.test.ts b/src/app/service/service_worker/log.test.ts new file mode 100644 index 000000000..0bb395a3b --- /dev/null +++ b/src/app/service/service_worker/log.test.ts @@ -0,0 +1,53 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi } from "vitest"; +import type { Group } from "@Packages/message/server"; +import type { LoggerDAO } from "@App/app/repo/logger"; +import { LogService } from "./log"; + +function fakeDAO(overrides: Partial = {}) { + return { + queryLogs: vi.fn(() => Promise.resolve([])), + delete: vi.fn(() => Promise.resolve(1)), + clear: vi.fn(() => Promise.resolve()), + ...overrides, + } as unknown as LoggerDAO; +} + +const fakeGroup = () => ({ on: vi.fn() }) as unknown as Group; + +describe("日志服务 LogService", () => { + it("getLogs 按时间范围委托 DAO 查询", async () => { + const sample = [{ id: 1, level: "info", message: "x", label: {}, createtime: 5 }]; + const dao = fakeDAO({ queryLogs: vi.fn(() => Promise.resolve(sample)) as LoggerDAO["queryLogs"] }); + const svc = new LogService(fakeGroup(), dao); + const res = await svc.getLogs({ start: 10, end: 20 }); + expect(dao.queryLogs).toHaveBeenCalledWith(10, 20); + expect(res).toBe(sample); + }); + + it("deleteLogs 逐个按 id 删除", async () => { + const dao = fakeDAO(); + const svc = new LogService(fakeGroup(), dao); + await svc.deleteLogs([1, 2, 3]); + expect(dao.delete).toHaveBeenCalledTimes(3); + expect(dao.delete).toHaveBeenCalledWith(1); + expect(dao.delete).toHaveBeenCalledWith(3); + }); + + it("clearLogs 清空 DAO", async () => { + const dao = fakeDAO(); + const svc = new LogService(fakeGroup(), dao); + await svc.clearLogs(); + expect(dao.clear).toHaveBeenCalledTimes(1); + }); + + it("init 注册 getLogs/deleteLogs/clearLogs 三个处理器", () => { + const group = fakeGroup(); + new LogService(group, fakeDAO()).init(); + expect((group.on as ReturnType).mock.calls.map((c) => c[0])).toEqual([ + "getLogs", + "deleteLogs", + "clearLogs", + ]); + }); +}); diff --git a/src/app/service/service_worker/log.ts b/src/app/service/service_worker/log.ts new file mode 100644 index 000000000..c4a617aec --- /dev/null +++ b/src/app/service/service_worker/log.ts @@ -0,0 +1,28 @@ +import { type Group } from "@Packages/message/server"; +import { type Logger, LoggerDAO } from "@App/app/repo/logger"; + +// 日志查询服务:供页面通过消息读取/删除/清空本地日志(写入仍走 MessageWriter -> manager.logger) +export class LogService { + constructor( + private group: Group, + private logDAO: LoggerDAO = new LoggerDAO() + ) {} + + getLogs({ start, end }: { start: number; end: number }): Promise { + return this.logDAO.queryLogs(start, end); + } + + async deleteLogs(ids: number[]): Promise { + await Promise.all(ids.map((id) => this.logDAO.delete(id))); + } + + async clearLogs(): Promise { + await this.logDAO.clear(); + } + + init() { + this.group.on("getLogs", this.getLogs.bind(this)); + this.group.on("deleteLogs", this.deleteLogs.bind(this)); + this.group.on("clearLogs", this.clearLogs.bind(this)); + } +} diff --git a/src/app/service/service_worker/popup.ts b/src/app/service/service_worker/popup.ts index 9961ab7c0..4289ef929 100644 --- a/src/app/service/service_worker/popup.ts +++ b/src/app/service/service_worker/popup.ts @@ -5,7 +5,7 @@ import type { ScriptMenu, TPopupScript } from "./types"; import type { GetPopupDataReq, GetPopupDataRes, MenuClickParams } from "./client"; import { cacheInstance } from "@App/app/cache"; import type { ScriptDAO } from "@App/app/repo/scripts"; -import { scriptToMenu, type TPopupPageLoadInfo } from "./popup_scriptmenu"; +import { applyScriptDisplayInfo, scriptToMenu, type TPopupPageLoadInfo } from "./popup_scriptmenu"; import { SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, SCRIPT_RUN_STATUS_RUNNING } from "@App/app/repo/scripts"; import type { TDeleteScript, @@ -392,8 +392,20 @@ export class PopupService { const scriptMenu = [...scriptMenuMap.values()]; // 检查是否在黑名单中 const isBlacklist = this.runtime.isUrlBlacklist(url); + // 即时附加图标与本地化脚本名(仅写入响应,不回写 session 缓存,避免 icon64 等占用过大) + const [scriptListWithInfo, backScriptListWithInfo] = await Promise.all([ + this.attachScriptDisplayInfo(scriptMenu), + this.attachScriptDisplayInfo(backScriptList), + ]); // 后台脚本只显示开启或者运行中的脚本 - return { isBlacklist, scriptList: scriptMenu, backScriptList }; + return { isBlacklist, scriptList: scriptListWithInfo, backScriptList: backScriptListWithInfo }; + } + + /** 为 ScriptMenu 列表即时附加图标 URL 与本地化脚本名(返回浅拷贝,不修改缓存中的原对象) */ + private async attachScriptDisplayInfo(list: ScriptMenu[]): Promise { + if (!list.length) return list; + const scripts = await this.scriptDAO.gets(list.map((s) => s.uuid)); + return list.map((s, i) => (scripts[i] ? applyScriptDisplayInfo(s, scripts[i]!) : s)); } async getScriptMenu(tabId: number): Promise { diff --git a/src/app/service/service_worker/popup_scriptmenu.test.ts b/src/app/service/service_worker/popup_scriptmenu.test.ts new file mode 100644 index 000000000..c4e1495a2 --- /dev/null +++ b/src/app/service/service_worker/popup_scriptmenu.test.ts @@ -0,0 +1,63 @@ +// can be tested with vitest-environment node +import { describe, it, expect, beforeAll } from "vitest"; +import { initLanguage, changeLanguage } from "@App/locales/locales"; +import type { Script } from "@App/app/repo/scripts"; +import type { ScriptMenu } from "./types"; +import { applyScriptDisplayInfo } from "./popup_scriptmenu"; + +const makeMenu = (over: Partial = {}): ScriptMenu => ({ + uuid: "u1", + name: "Raw Name", + storageName: "Raw Name", + enable: true, + updatetime: 0, + hasUserConfig: false, + runNum: 0, + runNumByIframe: 0, + menus: [], + isEffective: null, + ...over, +}); + +// metadata 的 key 在解析时会被统一转小写(见 src/pkg/utils/script.ts), +// 因此本地化名键存为 name:zh-cn 而非 name:zh-CN。 +const makeScript = (metadata: Record, name = "Raw Name"): Script => + ({ name, metadata }) as unknown as Script; + +describe("applyScriptDisplayInfo Popup 展示信息补充", () => { + beforeAll(() => initLanguage("zh-CN")); + + it("应按当前语言本地化脚本名(@name:zh-CN)", () => { + changeLanguage("zh-CN"); + const out = applyScriptDisplayInfo(makeMenu(), makeScript({ "name:zh-cn": ["中文名"] })); + expect(out.name).toBe("中文名"); + }); + + it("仅有语言前缀匹配(@name:zh)时也应本地化", () => { + changeLanguage("zh-CN"); + const out = applyScriptDisplayInfo(makeMenu(), makeScript({ "name:zh": ["前缀中文名"] })); + expect(out.name).toBe("前缀中文名"); + }); + + it("无对应语言的 @name 时回退到原始脚本名", () => { + changeLanguage("en-US"); + const out = applyScriptDisplayInfo(makeMenu(), makeScript({ "name:zh-cn": ["中文名"] }, "Raw Name")); + expect(out.name).toBe("Raw Name"); + }); + + it("应附加脚本图标 URL", () => { + changeLanguage("zh-CN"); + const out = applyScriptDisplayInfo(makeMenu(), makeScript({ icon: ["https://example.com/i.png"] })); + expect(out.icon).toBe("https://example.com/i.png"); + }); + + it("无任何本地化/图标时不应丢失原始字段", () => { + changeLanguage("zh-CN"); + const menu = makeMenu({ uuid: "abc", enable: false }); + const out = applyScriptDisplayInfo(menu, makeScript({})); + expect(out.name).toBe("Raw Name"); + expect(out.uuid).toBe("abc"); + expect(out.enable).toBe(false); + expect(out.icon).toBeUndefined(); + }); +}); diff --git a/src/app/service/service_worker/popup_scriptmenu.ts b/src/app/service/service_worker/popup_scriptmenu.ts index 7076427c1..dafa06f3f 100644 --- a/src/app/service/service_worker/popup_scriptmenu.ts +++ b/src/app/service/service_worker/popup_scriptmenu.ts @@ -1,7 +1,8 @@ import { SCRIPT_RUN_STATUS_RUNNING, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL } from "@App/app/repo/scripts"; import type { ScriptMenu } from "@App/app/service/service_worker/types"; import type { Script } from "@App/app/repo/scripts"; -import { getStorageName } from "@App/pkg/utils/utils"; +import { getIcon, getStorageName } from "@App/pkg/utils/utils"; +import { i18nName } from "@App/locales/locales"; export type TPopupPageLoadInfo = { tabId: number; frameId?: number; scriptmenus: ScriptMenu[] }; @@ -24,3 +25,17 @@ export const scriptToMenu = (script: Script): ScriptMenu => { isEffective: null, }; }; + +/** + * 为 ScriptMenu 即时补充「响应专用」的展示信息:本地化脚本名(@name:<lang>)与图标 URL。 + * + * 这些信息依赖完整 metadata,而 ScriptMenu 出于 session 缓存体积考虑并不存 metadata, + * 因此在 getPopupData 返回前用完整 Script 即时计算并附加(返回浅拷贝,不回写缓存)。 + * 名称本地化逻辑与 options 列表(i18nName)保持一致。 + */ +export const applyScriptDisplayInfo = (menu: ScriptMenu, script: Script): ScriptMenu => { + const name = i18nName(script); + const icon = getIcon(script); + if (name === menu.name && !icon) return menu; + return icon ? { ...menu, name, icon } : { ...menu, name }; +}; diff --git a/src/app/service/service_worker/regular_updatecheck.test.ts b/src/app/service/service_worker/regular_updatecheck.test.ts new file mode 100644 index 000000000..8254c46d3 --- /dev/null +++ b/src/app/service/service_worker/regular_updatecheck.test.ts @@ -0,0 +1,67 @@ +// can be tested with vitest-environment node +import "@Packages/chrome-extension-mock"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { MessageQueue } from "@Packages/message/message_queue"; +import { SystemConfig } from "@App/pkg/config/config"; +import { watchRegularUpdateCheck } from "./regular_updatecheck"; + +// chrome.alarms 在 chrome-extension-mock 中没有实现,这里手动桩 +const alarmsCreate = vi.fn((_name: string, _info: unknown, cb?: () => void) => cb?.()); +const alarmsClear = vi.fn(); + +beforeEach(() => { + alarmsCreate.mockClear(); + alarmsClear.mockClear(); + // @ts-ignore - 测试环境注入 alarms 桩 + chrome.alarms = { create: alarmsCreate, clear: alarmsClear }; +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +// 等待 SystemConfig._set 内部的 storage.then() -> mq.publish 微任务完成 +const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); + +describe("定期更新检查闹钟随配置变化重新设定", () => { + it("当 check_script_update_cycle 变更时应重新设定 checkScriptUpdate 闹钟", async () => { + const mq = new MessageQueue(); + const systemConfig = new SystemConfig(mq); + // 初始周期:每天 + systemConfig.setCheckScriptUpdateCycle(86400); + await flush(); + + // 接线:监听配置变化并重新设定闹钟(生产代码会在 ScriptService.init() 中调用) + watchRegularUpdateCheck(systemConfig); + await flush(); + alarmsCreate.mockClear(); + + // 用户把周期改为每周 + systemConfig.setCheckScriptUpdateCycle(604800); + await flush(); + + // 闹钟应被重新创建,且周期取整后为一周(604800s -> 10080min -> 10080) + expect(alarmsCreate).toHaveBeenCalled(); + const [name, info] = alarmsCreate.mock.calls[alarmsCreate.mock.calls.length - 1]; + expect(name).toBe("checkScriptUpdate"); + expect((info as chrome.alarms.AlarmCreateInfo).periodInMinutes).toBe(10080); + }); + + it("当周期被改为 0(永不)时应清除 checkScriptUpdate 闹钟而非创建", async () => { + const mq = new MessageQueue(); + const systemConfig = new SystemConfig(mq); + systemConfig.setCheckScriptUpdateCycle(86400); + await flush(); + + watchRegularUpdateCheck(systemConfig); + await flush(); + alarmsCreate.mockClear(); + alarmsClear.mockClear(); + + systemConfig.setCheckScriptUpdateCycle(0); + await flush(); + + expect(alarmsClear).toHaveBeenCalledWith("checkScriptUpdate"); + expect(alarmsCreate).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/service/service_worker/regular_updatecheck.ts b/src/app/service/service_worker/regular_updatecheck.ts index 80686b8d0..be4104209 100644 --- a/src/app/service/service_worker/regular_updatecheck.ts +++ b/src/app/service/service_worker/regular_updatecheck.ts @@ -29,7 +29,7 @@ export const initRegularUpdateCheck = async (systemConfig: SystemConfig) => { return; } let when = 0; - const checkupdate_script_lasttime: number = result.checkupdate_script_lasttime || 0; + const checkupdate_script_lasttime = (result.checkupdate_script_lasttime as number) || 0; // 有 checkupdate_script_lasttime 而且是单数值(上次的定时更新检查有完成) if (checkupdate_script_lasttime && (checkupdate_script_lasttime & 1) === 1) { const updateCycleMs = updateCycleSecond * 1000; @@ -60,6 +60,13 @@ export const initRegularUpdateCheck = async (systemConfig: SystemConfig) => { allowRegularUpdateCheck = now + ALLOW_CHECK_DELAY_MS; // 可以触发alarm的更新程序了 }; +// 监听更新周期配置变更,变更后立即重新设定alarm(否则要等到SW重启才会生效) +export const watchRegularUpdateCheck = (systemConfig: SystemConfig) => { + return systemConfig.addListener("check_script_update_cycle", () => { + initRegularUpdateCheck(systemConfig); + }); +}; + const setCheckupdateScriptLasttime = async (t: number) => { try { // 试一下储存。储存不了也没所谓 @@ -88,7 +95,7 @@ export const onRegularUpdateCheckAlarm = async ( // 不需要检查更新。退出操作 return null; } - const checkupdate_script_lasttime: number = result.checkupdate_script_lasttime || 0; + const checkupdate_script_lasttime = (result.checkupdate_script_lasttime as number) || 0; const targetWhen = checkupdate_script_lasttime + updateCycleSecond * 1000; if (targetWhen - ALARM_TRIGGER_WINDOW_MS > now) return null; // 已检查过了(alarm触发了) const storeTime = Math.floor(now / 2) * 2; // 双数 diff --git a/src/app/service/service_worker/resource.test.ts b/src/app/service/service_worker/resource.test.ts index 4c142fef8..6517cc262 100644 --- a/src/app/service/service_worker/resource.test.ts +++ b/src/app/service/service_worker/resource.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment jsdom import { initTestEnv } from "@Tests/utils"; import { ResourceService } from "./resource"; import { vi, describe, it, expect, beforeEach } from "vitest"; diff --git a/src/app/service/service_worker/runtime.test.ts b/src/app/service/service_worker/runtime.test.ts index 0e29cc5bb..b4a9e0106 100644 --- a/src/app/service/service_worker/runtime.test.ts +++ b/src/app/service/service_worker/runtime.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { initTestEnv } from "@Tests/utils"; import { RuntimeService } from "./runtime"; import { vi, describe, it, expect, beforeEach, type MockedFunction } from "vitest"; diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 2d73ac25f..3700c74a7 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -750,7 +750,7 @@ export class RuntimeService { let jsCode = ""; if (withCode) { const code = compileInjectionCode(scriptRes, scriptRes.code, scriptMatchInfo.scriptUrlPatterns); - registerScript.js[0].code = jsCode = code; + registerScript.js![0].code = jsCode = code; } // 过滤掉matches为空的脚本 diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 3d116db44..cd037f69d 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -44,7 +44,7 @@ import { import { getSimilarityScore, ScriptUpdateCheck } from "./script_update_check"; import { LocalStorageDAO } from "@App/app/repo/localStorage"; import { CompiledResourceDAO } from "@App/app/repo/resource"; -import { initRegularUpdateCheck } from "./regular_updatecheck"; +import { initRegularUpdateCheck, watchRegularUpdateCheck } from "./regular_updatecheck"; import { parseSkillScriptMetadata } from "@App/pkg/utils/skill_script"; import { TempStorageDAO, TempStorageItemType } from "@App/app/repo/tempStorage"; @@ -252,14 +252,6 @@ export class ScriptService { isUrlFilterCaseSensitive: false, requestDomains: ["bitbucket.org"], // Chrome 101+ }, - // SkillScript (.skill.js) 安装检测 - { - regexFilter: "^([^?#]+?\\.skill\\.js)", - resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME], - requestMethods: ["get" as chrome.declarativeNetRequest.RequestMethod], - isUrlFilterCaseSensitive: false, - excludedRequestDomains: ["github.com", "gitlab.com", "gitea.com", "bitbucket.org"], - }, // Skill 包 (.cat.md) 安装检测 { regexFilter: "^([^?#]+?\\.cat\\.md)", @@ -1516,5 +1508,7 @@ export class ScriptService { this.group.on("checkScriptUpdate", this.checkScriptUpdate.bind(this)); initRegularUpdateCheck(this.systemConfig); + // 更新周期配置变更后立即重新设定alarm,无需等待SW重启 + watchRegularUpdateCheck(this.systemConfig); } } diff --git a/src/app/service/service_worker/subscribe.ts b/src/app/service/service_worker/subscribe.ts index 370d21136..0a3c1f1c2 100644 --- a/src/app/service/service_worker/subscribe.ts +++ b/src/app/service/service_worker/subscribe.ts @@ -191,8 +191,8 @@ export class SubscribeService { await this.subscribeDAO.update(subscribe.url, subscribe); InfoNotification( - i18n.t("notification.subscribe_update", { subscribeName: subscribe.name }), - i18n.t("notification.subscribe_update_desc", { + i18n.t("settings:notification.subscribe_update", { subscribeName: subscribe.name }), + i18n.t("settings:notification.subscribe_update_desc", { newScripts: addedScriptNames.join(","), deletedScripts: removedScriptNames.join(","), }) @@ -325,6 +325,11 @@ export class SubscribeService { return this.checkUpdate(url, "user"); } + // options 页读取全部订阅,直接从 serviceWorker 内存(DAO 缓存)取出,避免页面侧重复读取存储 + getAllSubscribe(): Promise { + return this.subscribeDAO.find(); + } + async enable(param: { url: string; enable: boolean }) { const logger = this.logger.with({ url: param.url, @@ -342,6 +347,7 @@ export class SubscribeService { } init() { + this.group.on("getAllSubscribe", this.getAllSubscribe.bind(this)); this.group.on("install", this.install.bind(this)); this.group.on("delete", this.delete.bind(this)); this.group.on("checkUpdate", this.requestCheckUpdate.bind(this)); diff --git a/src/app/service/service_worker/synchronize.test.ts b/src/app/service/service_worker/synchronize.test.ts index 4fc2ba742..9a1d2c231 100644 --- a/src/app/service/service_worker/synchronize.test.ts +++ b/src/app/service/service_worker/synchronize.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect, vi, beforeEach } from "vitest"; import { SynchronizeService } from "./synchronize"; import { initTestEnv } from "@Tests/utils"; diff --git a/src/app/service/service_worker/synchronize.ts b/src/app/service/service_worker/synchronize.ts index a9be09f77..1cb1c45d7 100644 --- a/src/app/service/service_worker/synchronize.ts +++ b/src/app/service/service_worker/synchronize.ts @@ -329,8 +329,8 @@ export class SynchronizeService { // 如果是token失效之类的错误,通知用户并关闭云同步 if (isWarpTokenError(e)) { InfoNotification( - `${t("sync_system_connect_failed")}, ${t("sync_system_closed")}`, - `${t("sync_system_closed_description")}\n${errorMsg(e)}` + `${t("settings:sync_system_connect_failed")}, ${t("settings:sync_system_closed")}`, + `${t("settings:sync_system_closed_description")}\n${errorMsg(e)}` ); this.systemConfig.setCloudSync({ ...config, @@ -429,8 +429,8 @@ export class SynchronizeService { // 删除脚本 await this.script.deleteScript(script.uuid, "sync"); InfoNotification( - i18n.t("notification.script_sync_delete"), - i18n.t("notification.script_sync_delete_desc", { + i18n.t("settings:notification.script_sync_delete"), + i18n.t("settings:notification.script_sync_delete_desc", { scriptName: i18nName(script), }) ); diff --git a/src/app/service/service_worker/types.ts b/src/app/service/service_worker/types.ts index 8b2c5d764..a672c8fbd 100644 --- a/src/app/service/service_worker/types.ts +++ b/src/app/service/service_worker/types.ts @@ -191,6 +191,7 @@ export type ScriptMenu = { enable: boolean; // 脚本是否启用 updatetime: number; // 脚本更新时间 hasUserConfig: boolean; // 是否有用户配置 + icon?: string; // 脚本图标 URL(由 getPopupData 即时附加在响应中,不写回 session 缓存) // 不需要完整 metadata。目前在 Popup 未使用 metadata。 // 有需要时请把 metadata 里需要的部份抽出 (例如 @match @include @exclude),避免 chrome.storage.session 储存量过大 // metadata: SCMetadata; // 脚本元数据 diff --git a/src/app/service/service_worker/utils.test.ts b/src/app/service/service_worker/utils.test.ts index 7794dfd87..d1cff8a4c 100644 --- a/src/app/service/service_worker/utils.test.ts +++ b/src/app/service/service_worker/utils.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect } from "vitest"; import { isBase64, diff --git a/src/app/service/service_worker/value.test.ts b/src/app/service/service_worker/value.test.ts index d6daae278..37ab4904d 100644 --- a/src/app/service/service_worker/value.test.ts +++ b/src/app/service/service_worker/value.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { initTestEnv } from "@Tests/utils"; import { ValueService } from "./value"; import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; diff --git a/src/index.css b/src/index.css index c083aa35d..c1e782b96 100644 --- a/src/index.css +++ b/src/index.css @@ -1,58 +1,353 @@ -@unocss preflights; -@unocss default; -@unocss; +@import "tailwindcss"; +@import "tw-animate-css"; -* { - scrollbar-color: inherit; +@custom-variant dark (&:is(.dark *)); + +/* + ScriptCat 设计系统色彩令牌 + 品牌色:蓝色 #1296DB(Light) / #3AACEF(Dark) + 基础色:温和灰阶(Light #FAFAFA/#FFFFFF, Dark #1E1E1E/#151515) + 对应 Pencil 设计稿变量 bg-primary/bg-card/border-light/brand-blue/fg-primary 等 +*/ +:root { + /* 基础背景与文字 */ + --background: #fafafa; + --foreground: #1a1a1a; + --card: #ffffff; + --card-foreground: #1a1a1a; + --popover: #ffffff; + --popover-foreground: #1a1a1a; + + /* 品牌主色 - 蓝色 */ + --primary: #1296db; + --primary-foreground: #ffffff; + --primary-hover: #0a7db8; + --primary-light: #d6ecfa; + + /* 次要/柔和/hover 背景 - 统一为同一灰色值(遵循 shadcn 规范) */ + --secondary: #f0f0f0; + --secondary-foreground: #1a1a1a; + --muted: #f0f0f0; + --muted-foreground: #888888; + --accent: #f0f0f0; + --accent-foreground: #1a1a1a; + + /* 文字层级 */ + --fg-secondary: #666666; + + /* 边框与表单输入边框 */ + --border: #e5e5e5; + --input: #e5e5e5; + --ring: #1296db; + + /* Switch 关闭态 track(对应设计稿 $border-default) */ + --switch-off: #d0d0d0; + + /* 滚动条(半透明,随明暗背景自适应) */ + --scrollbar-thumb: rgba(0, 0, 0, 0.18); + --scrollbar-thumb-hover: rgba(0, 0, 0, 0.32); + + /* 交互色 */ + --destructive: #e7000b; + --destructive-foreground: #ffffff; + + /* 状态色 - 运行中/周期执行 */ + --success: #34c759; + --success-foreground: #ffffff; + --success-bg: #e8f9ec; + --success-fg: #0c8833; + --warning: #ff9500; + --warning-foreground: #ffffff; + --warning-bg: #fff4e6; + --warning-fg: #c46c00; + + /* Skill / 紫色强调(Skill 安装、能力标签的紫色品牌识别色;solid 用于按钮/图标,bg/fg 用于软色 chip) */ + --skill: #9333ea; + --skill-foreground: #ffffff; + --skill-bg: #f3e8ff; + --skill-fg: #7e22ce; + + /* 储存值类型徽章色(设计稿储存表「类型」列:string 绿 / number 蓝 / boolean 琥珀 / object 紫) */ + --type-string-bg: #e4f7ea; + --type-string-fg: #2ba24e; + --type-number-bg: #d6ecfa; + --type-number-fg: #1296db; + --type-boolean-bg: #fceedb; + --type-boolean-fg: #c2710c; + --type-object-bg: #f3e8ff; + --type-object-fg: #9333ea; + + /* 切换按钮圆钮(Switch/Checkbox thumb) - 始终保持浅色 */ + --thumb: #ffffff; + + /* 侧边栏 */ + --sidebar: #ffffff; + --sidebar-foreground: #1a1a1a; + --sidebar-primary: #1296db; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #edf5fc; + --sidebar-accent-foreground: #1a1a1a; + --sidebar-border: #e5e5e5; + --sidebar-ring: #1296db; + + /* 圆角(对应设计稿 radius-sm 6 / md 8 / lg 12) */ + --radius: 0.5rem; +} + +.dark { + /* 基础背景与文字 - 温和深灰 */ + --background: #1e1e1e; + --foreground: #e5e5e5; + --card: #151515; + --card-foreground: #e5e5e5; + --popover: #151515; + --popover-foreground: #e5e5e5; + + /* 品牌主色 - 暗色蓝(稍亮以保证对比) */ + --primary: #3aacef; + --primary-foreground: #ffffff; + --primary-hover: #1296db; + --primary-light: #1e3040; + + /* 次要/柔和/hover 背景 - 统一为同一灰色值(遵循 shadcn 规范) */ + --secondary: #2a2a2a; + --secondary-foreground: #e5e5e5; + --muted: #2a2a2a; + --muted-foreground: #8a8a8a; + --accent: #2a2a2a; + --accent-foreground: #e5e5e5; + + /* 文字层级 */ + --fg-secondary: #b5b5b5; + + /* 边框与表单输入边框 */ + --border: #2a2a2a; + --input: #2a2a2a; + --ring: #3aacef; + + /* Switch 关闭态 track(对应设计稿 $dark-toggle-off) */ + --switch-off: #3a3a3a; + + /* 滚动条(半透明,随明暗背景自适应) */ + --scrollbar-thumb: rgba(255, 255, 255, 0.16); + --scrollbar-thumb-hover: rgba(255, 255, 255, 0.3); + + /* 交互色 */ + --destructive: #ff6669e6; + --destructive-foreground: #ffffff; + + /* 状态色 */ + --success: #34c759; + --success-foreground: #ffffff; + --success-bg: #1e3520; + --success-fg: #6fdd8a; + --warning: #ff9500; + --warning-foreground: #ffffff; + --warning-bg: #352c1e; + --warning-fg: #ffb84d; + + /* Skill / 紫色强调(暗色:solid 提亮以保证对比) */ + --skill: #a855f7; + --skill-foreground: #ffffff; + --skill-bg: #2a1e3a; + --skill-fg: #c084fc; + + /* 储存值类型徽章色(暗色) */ + --type-string-bg: #1e3520; + --type-string-fg: #4ade80; + --type-number-bg: #1e3040; + --type-number-fg: #3aacef; + --type-boolean-bg: #352c1e; + --type-boolean-fg: #fb923c; + --type-object-bg: #2a1e3a; + --type-object-fg: #c084fc; + + /* 切换按钮圆钮(Switch/Checkbox thumb) - 深色模式仍保持浅色 */ + --thumb: #eeeeee; + + /* 侧边栏 */ + --sidebar: #1a1a1a; + --sidebar-foreground: #e5e5e5; + --sidebar-primary: #3aacef; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #2a2a30; + --sidebar-accent-foreground: #e5e5e5; + --sidebar-border: #2a2a2a; + --sidebar-ring: #3aacef; } -html { - --body-background-color: #fff; - --body-text-color: #1d2129; +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-primary-hover: var(--primary-hover); + --color-primary-light: var(--primary-light); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-fg-secondary: var(--fg-secondary); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-switch-off: var(--switch-off); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-thumb: var(--thumb); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-success-bg: var(--success-bg); + --color-success-fg: var(--success-fg); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-warning-bg: var(--warning-bg); + --color-warning-fg: var(--warning-fg); + --color-skill: var(--skill); + --color-skill-foreground: var(--skill-foreground); + --color-skill-bg: var(--skill-bg); + --color-skill-fg: var(--skill-fg); + --color-type-orange: var(--warning); + --color-type-green: var(--success); + --color-type-string-bg: var(--type-string-bg); + --color-type-string-fg: var(--type-string-fg); + --color-type-number-bg: var(--type-number-bg); + --color-type-number-fg: var(--type-number-fg); + --color-type-boolean-bg: var(--type-boolean-bg); + --color-type-boolean-fg: var(--type-boolean-fg); + --color-type-object-bg: var(--type-object-bg); + --color-type-object-fg: var(--type-object-fg); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; } -html.dark { - --body-background-color: #232324; - --body-text-color: #ffffffe6; +/* Radix Collapsible 动画(基于 --radix-collapsible-content-height 变量) */ +@keyframes collapsible-down { + from { + height: 0; + } + to { + height: var(--radix-collapsible-content-height); + } } -html, body { /* lowest priority */ - background-color: var(--color-bg-2, var(--body-background-color)); - color: var(--color-text-1, var(--body-text-color)); +@keyframes collapsible-up { + from { + height: var(--radix-collapsible-content-height); + } + to { + height: 0; + } } -body { - scrollbar-color: var(--color-scrollbar-thumb) var(--color-scrollbar-track); +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } + button:not(:disabled), + [role="button"]:not(:disabled) { + cursor: pointer; + } + /* 统一屏蔽浏览器默认的焦点 outline(含键盘 :focus-visible 与 Radix 关闭弹层后程序化回焦), + 覆盖原生 button、role=button、以及渲染成 的导航/链接(NavLink/Link)。 + 需要焦点指示的组件(shadcn Button/Input 等)自带 box-shadow 的 ring,不依赖 outline,故不受影响。 */ + button:focus, + [role="button"]:focus, + a:focus { + outline: none; + } } -body[arco-theme='dark'] { - --un-default-border-color: var(--color-border-2); - --color-scrollbar-thumb: #6b6b6b; - --color-scrollbar-track: #2d2d2d; +@utility animate-collapsible-down { + animation: collapsible-down 200ms ease-out; } -body[arco-theme='light'] { - --color-scrollbar-thumb: #6b6b6b; - --color-scrollbar-track: #f0f0f0; +@utility animate-collapsible-up { + animation: collapsible-up 200ms ease-out; } -#root { - overscroll-behavior: none; - contain: content; - overflow: auto; +@keyframes expand-bar { + from { + height: 0; + border-bottom-width: 0; + opacity: 0; + } } -:root { - --sc-zero-1: 0; - --sc-zero-2: 0; - --sc-zero-3: 0; - --sc-zero-4: 0; -} - -/* 自定义 .sc-inset-0 避免打包成 inset: 0 使旧浏览器布局错位 */ -.sc-inset-0 { - top: var(--sc-zero-1); - left: var(--sc-zero-2); - right: var(--sc-zero-3); - bottom: var(--sc-zero-4); +@keyframes collapse-bar { + to { + height: 0; + border-bottom-width: 0; + opacity: 0; + } +} + +@utility animate-expand-bar { + animation: expand-bar 200ms ease-out; + overflow: hidden; +} + +@utility animate-collapse-bar { + animation: collapse-bar 200ms ease-out forwards; + overflow: hidden; +} + +@keyframes indeterminate { + from { + transform: translateX(-100%); + } + to { + transform: translateX(400%); + } +} + +@utility animate-indeterminate-bar { + animation: indeterminate 1.1s ease-in-out infinite; +} + +/* + 自定义滚动条:细、圆角、半透明轨道透明。 + 颜色取自 --scrollbar-thumb*,由 :root / .dark 提供,明暗主题自动切换。 + 同时覆盖 Firefox 标准属性(scrollbar-width/color)与 WebKit/Chromium 伪元素。 +*/ +.scrollbar-custom { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) transparent; +} +.scrollbar-custom::-webkit-scrollbar { + width: 8px; + height: 8px; +} +.scrollbar-custom::-webkit-scrollbar-track { + background: transparent; +} +.scrollbar-custom::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb); + border-radius: 9999px; + /* 透明边框 + padding-box 裁剪:让滑块两侧留白,视觉更细更精致 */ + border: 2px solid transparent; + background-clip: padding-box; +} +.scrollbar-custom::-webkit-scrollbar-thumb:hover { + background-color: var(--scrollbar-thumb-hover); } diff --git a/src/locales/README.md b/src/locales/README.md index 72ecc375a..8f7839594 100644 --- a/src/locales/README.md +++ b/src/locales/README.md @@ -23,14 +23,9 @@ for example: } ``` -## Help Us Translate +### Help Us Translate -ScriptCat's translation files are hosted on GitHub — contributions via Pull Request are welcome. - -- Translation files live in [`src/locales`](https://github.com/scriptscat/scriptcat/tree/main/src/locales); each language has its own `/translation.json` -- **Improve an existing translation**: edit the corresponding `translation.json` directly -- **Add a new language**: create a new directory under `src/locales/` (e.g. `fr-FR`), copy `en-US/translation.json` as a template and translate the strings, then register the locale in `src/locales/locales.ts` -- Open a Pull Request against the `main` branch when you are done +Translation files are located in `src/locales`, organized by namespace. If you're interested in helping translate, please submit a PR directly. # i18n 方案 @@ -53,11 +48,6 @@ i18n 使用[i8next](https://www.i18next.com/)实现,之所以不是用`chrome. } ``` -## 帮助我们翻译 - -ScriptCat 的翻译文件托管在 GitHub 上,欢迎通过 Pull Request 贡献翻译。 +### 帮助我们翻译 -- 翻译文件位于 [`src/locales`](https://github.com/scriptscat/scriptcat/tree/main/src/locales),每种语言对应一个 `<语言代码>/translation.json` -- **改进已有翻译**:直接编辑对应语言的 `translation.json` -- **新增语言**:在 `src/locales/` 下新建对应语言代码目录(如 `fr-FR`),复制 `en-US/translation.json` 作为模板进行翻译,并在 `src/locales/locales.ts` 中注册 -- 完成后向 `main` 分支提交 Pull Request 即可 +翻译文件位于 `src/locales` 目录下,按命名空间划分。如果您有兴趣帮助翻译,请直接提交 PR。 diff --git a/src/locales/arco.ts b/src/locales/arco.ts deleted file mode 100644 index a97fdb3de..000000000 --- a/src/locales/arco.ts +++ /dev/null @@ -1,32 +0,0 @@ -import enUS from "@arco-design/web-react/es/locale/en-US"; -import zhCN from "@arco-design/web-react/es/locale/zh-CN"; -import zhTW from "@arco-design/web-react/es/locale/zh-TW"; -import jaJP from "@arco-design/web-react/es/locale/ja-JP"; -import deDE from "@arco-design/web-react/es/locale/de-DE"; -import viVN from "@arco-design/web-react/es/locale/vi-VN"; -import ruRU from "@arco-design/web-react/es/locale/ru-RU"; -import type { Locale } from "@arco-design/web-react/es/locale/interface"; - -export function arcoLocale(lang: string): Locale { - switch (lang) { - case "en-US": - return enUS; - case "zh-CN": - return zhCN; - case "zh-TW": - return zhTW; - case "ja-JP": - return jaJP; - case "de-DE": - // @ts-ignore - return deDE; - case "vi-VN": - // @ts-ignore - return viVN; - case "ru-RU": - // @ts-ignore - return ruRU; - default: - return enUS; - } -} diff --git a/src/locales/de-DE/agent.json b/src/locales/de-DE/agent.json new file mode 100644 index 000000000..654eef813 --- /dev/null +++ b/src/locales/de-DE/agent.json @@ -0,0 +1,252 @@ +{ + "title": "AI Agent", + "docs": "Dokumentation", + "chat": "Chat", + "provider": "Model Service", + "mcp": "MCP", + "skills": "Skills", + "provider_title": "Model Service", + "provider_subtitle": "Manage AI model providers and connections", + "provider_select": "AI Provider", + "provider_api_base_url": "API Base URL", + "provider_api_key": "API Key", + "provider_model": "Default Model", + "provider_test_connection": "Test Connection", + "provider_test_success": "Connection Successful", + "provider_test_failed": "Connection Failed", + "provider_test_hint": "Der Test sendet eine minimale Anfrage, um die Konfiguration zu prüfen", + "provider_docs": "Dokumentation", + "provider_count_hint": "Das Standardmodell wird für neue Unterhaltungen verwendet", + "model_count": "{{count}} Modelle", + "model_fetch": "Modelle abrufen", + "model_fetch_failed": "Modellliste konnte nicht abgerufen werden", + "model_name": "Name", + "model_add": "Add Model", + "model_edit": "Edit", + "model_copy": "Copy", + "model_delete": "Delete", + "model_set_default": "Set as Default", + "model_default_label": "Default", + "model_delete_confirm": "Are you sure to delete this model?", + "model_max_tokens": "Max Output Tokens", + "model_context_window": "Kontextfenster", + "model_no_models": "No models configured", + "model_no_models_desc": "Add your first model to start using the AI Agent", + "model_vision_support": "Supports vision input", + "model_image_output": "Supports image output", + "model_capabilities": "Fähigkeiten", + "model_supports_vision": "Bildeingabe", + "model_supports_image_output": "Bildausgabe", + "coming_soon": "Coming soon...", + "skills_title": "Skills Management", + "skills_subtitle": "Manage agent skill packages (SKILL.md prompts, scripts, references)", + "skills_add": "Add Skill", + "skills_empty": "No skills installed", + "skills_empty_desc": "Upload a ZIP or import your first skill from a URL", + "skills_tools": "Tools", + "skills_references": "References", + "skills_detail": "Skill Details", + "skills_edit_prompt": "Prompt", + "skills_install": "Install Skill", + "skills_install_url": "Import from URL", + "skills_install_paste": "Paste SKILL.md", + "skills_uninstall": "Uninstall", + "skills_uninstall_confirm": "Are you sure to uninstall Skill \"{{name}}\"?", + "skills_save_success": "Saved successfully", + "skills_install_success": "Installed successfully", + "skills_fetch_failed": "Fetch failed", + "skills_add_script": "Add Script", + "skills_add_reference": "Add Reference", + "skills_install_zip": "Upload ZIP", + "skills_install_zip_hint": "Click to select a .zip file", + "skills_prompt": "Prompt", + "skills_installed_at": "Installed at", + "skills_refresh": "Refresh", + "skills_refresh_success": "Refreshed successfully", + "skills_tool_code": "Tool Code", + "skills_click_to_view_code": "Click tool name to view code", + "skills_config": "Konfiguration", + "skills_config_saved": "Konfiguration gespeichert", + "skills_check_updates": "Updates prüfen", + "skills_no_updates": "Alle Skills sind aktuell", + "skills_updates_available": "Skill-Update(s) verfügbar", + "skills_update": "Aktualisieren", + "skills_docs": "Dokumentation", + "skills_count": "{{count}} Skills installiert", + "skills_update_count": "{{count}} Updates verfügbar", + "skills_update_available": "Update {{version}}", + "skills_references_short": "Referenzen", + "skills_configurable": "Konfigurierbar", + "skills_open_config": "Konfiguration öffnen", + "skills_update_success": "Erfolgreich aktualisiert", + "skills_url_placeholder": "SKILL.cat.md URL eingeben", + "chat_new": "New Chat", + "chat_delete": "Delete Chat", + "chat_delete_confirm": "Delete this conversation?", + "chat_no_conversations": "No conversations", + "chat_search_placeholder": "Unterhaltungen suchen…", + "chat_search_no_results": "Keine passenden Unterhaltungen", + "chat_export": "Exportieren", + "chat_input_placeholder": "Type a message...", + "chat_send": "Send", + "chat_stop": "Stop", + "chat_thinking": "Thinking", + "chat_tool_call": "Tool Call", + "chat_tool_arguments": "Argumente", + "chat_tool_result": "Ergebnis", + "chat_error": "Error occurred", + "chat_no_model": "No model configured. Please add one in Model Service first.", + "chat_model_select": "Select Model", + "chat_rename": "Rename", + "chat_copy": "Copy", + "chat_copy_success": "Copied", + "chat_regenerate": "Regenerate", + "chat_streaming": "Generating...", + "chat_retrying": "Wiederholung läuft ({{attempt}}/{{max}})...", + "chat_attach_image": "Bild anhängen", + "chat_attach_file": "Datei anhängen", + "chat_welcome_hint": "Ask me anything about your scripts", + "chat_welcome_start": "Create a conversation to get started", + "chat_tokens": "tokens", + "chat_first_token": "TTFT", + "chat_tools_count": "{{count}} tools", + "chat_tools_enabled": "Tools enabled", + "chat_tools_disabled": "Tools disabled", + "chat_tools_enabled_tip": "Tools enabled — click to disable", + "chat_tools_disabled_tip": "Tools disabled — click to enable", + "chat_background_enabled_tip": "Hintergrundmodus AN — Konversation läuft nach dem Schließen der Seite weiter, klicken zum Deaktivieren", + "chat_background_disabled_tip": "Hintergrundmodus AUS — Konversation stoppt beim Schließen der Seite, klicken zum Aktivieren", + "chat_delete_round": "Delete", + "chat_copy_message": "Copy", + "chat_edit_message": "Bearbeiten", + "chat_save_and_send": "Speichern & Senden", + "chat_cancel_edit": "Abbrechen", + "chat_message_queued": "In Warteschlange", + "chat_cancel_message": "Senden abbrechen", + "permission_title": "Das Skript fordert die Verwendung des Agent-Gesprächs an", + "permission_describe": "Dieses Skript fordert Zugang zur Agent-Gesprächsfunktion an, was API-Token verbraucht. Erlauben Sie nur vertrauenswürdigen Skripten.", + "permission_content": "Agent-Gespräch", + "opfs": "OPFS", + "opfs_title": "OPFS Dateibrowser", + "opfs_empty": "Leeres Verzeichnis", + "opfs_name": "Name", + "opfs_size": "Größe", + "opfs_type": "Typ", + "opfs_modified": "Zuletzt geändert", + "opfs_delete_confirm": "Sind Sie sicher, dass Sie löschen möchten?", + "opfs_delete_success": "Gelöscht", + "opfs_file": "Datei", + "opfs_directory": "Verzeichnis", + "opfs_preview": "Vorschau", + "opfs_subtitle": "Origin Private File System · Privater Agent-Speicher", + "opfs_refresh": "Aktualisieren", + "opfs_upload": "Hochladen", + "opfs_actions": "Aktionen", + "opfs_item_count": "{{count}} Elemente", + "opfs_empty_desc": "Noch keine Dateien in diesem Verzeichnis", + "opfs_upload_success": "Hochgeladen", + "opfs_upload_failed": "Hochladen fehlgeschlagen", + "opfs_type_directory": "Ordner", + "opfs_type_image": "Bild", + "opfs_type_text": "Text", + "opfs_type_binary": "Binär", + "opfs_root": "Stammverzeichnis", + "dom_permission_title": "Das Skript fordert DOM-Zugriff an", + "dom_permission_describe": "Dieses Skript fordert die Fähigkeit an, das DOM der Webseite zu lesen und zu manipulieren (Klicken, Formulare ausfüllen, Navigation, Screenshot usw.). Erlauben Sie nur vertrauenswürdigen Skripten.", + "dom_permission_content": "Agent DOM-Operationen", + "mcp_title": "MCP-Server", + "mcp_subtitle": "Connect MCP servers to extend agent tools, resources and prompts", + "mcp_add_server": "Server hinzufügen", + "mcp_no_servers": "Keine MCP-Server konfiguriert", + "mcp_no_servers_desc": "Add your first MCP server to connect external tools and data", + "mcp_status_connected": "Connected", + "mcp_status_failed": "Connection failed", + "mcp_status_untested": "Not tested", + "mcp_has_key": "Geheimer Schlüssel", + "mcp_headers_count": "{{count}} Header", + "mcp_count_servers": "{{count}} Server", + "mcp_count_connected": "{{count}} verbunden", + "mcp_count_tools": "{{count}} Werkzeuge verfügbar", + "mcp_docs": "Dokumentation", + "mcp_test_connection": "Testen", + "mcp_name_url_required": "Name und URL sind erforderlich", + "mcp_optional": "optional", + "mcp_custom_headers": "Benutzerdefinierte Header", + "mcp_enabled": "Aktiviert", + "mcp_detail": "Details", + "mcp_tools": "Werkzeuge", + "mcp_resources": "Ressourcen", + "mcp_prompts": "Prompts", + "mcp_no_tools": "Keine Werkzeuge verfügbar", + "mcp_no_resources": "Keine Ressourcen verfügbar", + "mcp_no_prompts": "Keine Prompts verfügbar", + "mcp_loading": "Laden...", + "mcp_parameters": "Parameter", + "tasks": "Aufgaben", + "tasks_title": "Geplante Aufgaben", + "tasks_subtitle": "Agent-Aufgaben automatisch nach einem Cron-Zeitplan ausführen", + "settings": "Einstellungen", + "settings_title": "Agent-Einstellungen", + "settings_subtitle": "Modell-, Such- und allgemeine Einstellungen · Änderungen wirken sofort", + "settings_cat_model": "Modell", + "settings_cat_search": "Suche", + "model_settings": "Modelleinstellungen", + "summary_model": "Zusammenfassungsmodell", + "summary_model_desc": "Für Web-Zusammenfassungen. Wenn nicht eingestellt, wird das Standardmodell verwendet", + "summary_model_placeholder": "Standardmodell verwenden", + "search_settings": "Sucheinstellungen", + "search_engine": "Suchmaschine", + "search_engine_desc": "Die vom web_search-Tool verwendete Suchquelle.", + "search_engine_baidu": "Baidu", + "search_google_api_key": "Google API Key", + "search_google_api_key_desc": "Wird zum Aufruf der Custom Search JSON API verwendet.", + "search_google_cse_id": "Benutzerdefinierte Suchmaschinen-ID", + "search_google_cse_id_desc": "Der cx-Parameter der Programmable Search Engine.", + "settings_saved": "Einstellungen gespeichert", + "settings_save_failed": "Einstellungen konnten nicht gespeichert werden", + "search_engine_tip_bing": "Standard-Suchmaschine mit breiter globaler Abdeckung, keine zusätzliche Konfiguration erforderlich.", + "search_engine_tip_duckduckgo": "Datenschutzorientierte Suchmaschine, kein API-Schlüssel erforderlich.", + "search_engine_tip_baidu": "Optimiert für chinesische Inhalte, bessere Ergebnisse für chinesische Suchanfragen.", + "search_engine_tip_google": "Hochwertige Suchergebnisse, erfordert einen Google API Key und eine benutzerdefinierte Suchmaschinen-ID.", + "tasks_docs": "Dokumentation", + "tasks_count_total": "{{count}} Aufgaben", + "tasks_count_enabled": "{{count}} aktiviert", + "tasks_create": "Aufgabe erstellen", + "tasks_edit": "Aufgabe bearbeiten", + "tasks_mode": "Modus", + "tasks_mode_internal": "Interne Ausführung", + "tasks_mode_event": "Ereignisgesteuert", + "tasks_mode_internal_short": "Intern", + "tasks_mode_event_short": "Ereignis", + "tasks_cron": "Cron-Ausdruck", + "tasks_next_run": "Nächste Ausführung", + "tasks_last_status": "Letzter Status", + "tasks_run_now": "Jetzt ausführen", + "tasks_history": "Verlauf", + "tasks_prompt": "Prompt", + "tasks_max_iterations": "Maximale Iterationen", + "tasks_notify": "Bei Abschluss benachrichtigen", + "tasks_notify_desc": "Bei Abschluss der Aufgabe eine Browser-Benachrichtigung senden", + "tasks_no_tasks": "Keine geplanten Aufgaben", + "tasks_no_tasks_desc": "Erstellen Sie Ihre erste geplante Aufgabe, damit der Agent automatisch läuft", + "tasks_no_runs": "Noch kein Ausführungsverlauf", + "tasks_delete_confirm": "Möchten Sie diese Aufgabe wirklich löschen?", + "tasks_clear_runs": "Verlauf löschen", + "tasks_clear_runs_confirm": "Möchten Sie den Ausführungsverlauf wirklich löschen?", + "tasks_event_hint": "Bei Auslösung wird das Skript benachrichtigt, das diese Aufgabe erstellt hat", + "tasks_event_trigger": "Ereignisauslöser", + "tasks_name_cron_required": "Name und Cron-Ausdruck sind erforderlich", + "tasks_model_select": "Modell auswählen", + "tasks_skills": "Skills", + "tasks_skills_auto": "Alle automatisch laden", + "tasks_conversation_id": "Konversation fortsetzen – ID (optional)", + "tasks_run_status_success": "Erfolgreich", + "tasks_run_status_error": "Fehler", + "tasks_run_status_running": "Läuft", + "tasks_run_duration": "Dauer", + "tasks_run_usage": "Verbrauch", + "tasks_run_conversation": "Konversation anzeigen", + "tasks_run_time": "Zeit", + "tasks_run_status": "Status", + "tasks_never_run": "Nie ausgeführt" +} diff --git a/src/locales/de-DE/common.json b/src/locales/de-DE/common.json new file mode 100644 index 000000000..02957e136 --- /dev/null +++ b/src/locales/de-DE/common.json @@ -0,0 +1,131 @@ +{ + "user_guide": "Benutzerhandbuch", + "api_docs": "API-Dokumentation", + "development_guide": "Entwicklungshandbuch", + "script_gallery": "Skript-Galerie", + "community_forum": "Community-Forum", + "external_links": "Externe Links", + "system_follow": "System folgen", + "no_data": "Keine Daten", + "logs": "Protokolle", + "tools": "Werkzeuge", + "find": "Suchen", + "replace": "Ersetzen", + "settings": "Einstellungen", + "change_theme": "Change Theme", + "hide_main_sidebar": "Seitenleiste einklappen", + "show_main_sidebar": "Seitenleiste ausklappen", + "menu": "Menü", + "guide": "Einsteigeranleitung", + "helpcenter": "Hilfezentrum", + "save": "Speichern", + "file": "Datei", + "save_success": "Erfolgreich gespeichert", + "reset_success": "Erfolgreich zurückgesetzt", + "update": "Aktualisieren", + "check_update": "Nach Updates suchen", + "confirm_delete": "Löschen bestätigen", + "confirm_update": "Update bestätigen", + "delete_success": "Erfolgreich gelöscht", + "deleting": "Wird gelöscht", + "enable": "Aktivieren", + "script_list_enable_width": 120, + "script_list_last_updated_width": 120, + "script_list_apply_to_run_status_width": 140, + "subscribe_list_enable_width": 100, + "disable": "Deaktivieren", + "name": "Name", + "version": "Version", + "source": "Quelle", + "home": "Startseite", + "action": "Aktion", + "export": "Exportieren", + "delete": "Löschen", + "pin_to_top": "Klebrig", + "confirm": "Bestätigen", + "close": "Schließen", + "config": "Konfiguration", + "key": "Schlüssel", + "value": "Wert", + "add": "Hinzufügen", + "type": "Typ", + "size": "Größe", + "download": "Herunterladen", + "edit_value": "Wert bearbeiten", + "add_value": "Wert hinzufügen", + "update_success": "Änderung erfolgreich", + "add_success": "Hinzufügen erfolgreich", + "clear": "Bereinigen", + "type_string": "String", + "type_number": "Nummer", + "type_boolean": "Boolean", + "type_object": "Objekt", + "confirm_delete_resource": "Möchten Sie diese Ressource wirklich löschen? Beim nächsten Start wird diese Ressource neu geladen", + "confirm_clear_resource": "Möchten Sie wirklich diese Ressourcen bereinigen? Beim nächsten Start werden die Ressourcen neu geladen", + "yes": "Ja", + "no": "Nein", + "confirm_delete_permission": "Sind Sie sicher, dass Sie diese Berechtigung löschen möchten?", + "reset": "Zurücksetzen", + "run_once": "Einmal ausführen", + "stop": "Stoppen", + "edit": "Bearbeiten", + "copy": "Kopieren", + "exclude_on": "Wiederherstellen auf $0 zur Ausführung", + "exclude_off": "Ausschließen auf $0 zur Ausführung", + "get_confirm_error": "Bestätigungsinformationen abrufen fehlgeschlagen", + "confirm_error": "Bestätigung fehlgeschlagen", + "ignore": "Ignorieren", + "temporary_allow": "Diese {{permissionContent}} temporär erlauben", + "temporary_allow_all": "Alle {{permissionContent}} temporär erlauben", + "permanent_allow": "Diese {{permissionContent}} dauerhaft erlauben", + "permanent_allow_all": "Alle {{permissionContent}} dauerhaft erlauben", + "temporary_deny": "Diese {{permissionContent}} temporär verweigern", + "temporary_deny_all": "Alle {{permissionContent}} temporär verweigern", + "permanent_deny": "Diese {{permissionContent}} dauerhaft verweigern", + "permanent_deny_all": "Alle {{permissionContent}} dauerhaft verweigern", + "import": "Importieren", + "author": "Autor", + "description": "Beschreibung", + "operation": "Operation", + "error": "Fehler", + "unknown": "Unbekannt", + "add_new": "Neu hinzufügen", + "no_operation": "Keine Operation", + "enable_script": "Skript aktivieren", + "local_creation": "Lokale Erstellung", + "import_success": "Import erfolgreich", + "get_script": "Skript abrufen", + "report_issue": "Bug/Problem-Feedback", + "project_docs": "Projektdokumentation", + "community": "Community", + "domain": "Domain", + "script_name": "Skriptname", + "skip": "Überspringen", + "next": "Weiter", + "next_with_progress": "Nächster Schritt (Schritt {step} von {steps})", + "back": "Zurück", + "last": "Fertig", + "auto": "Automatisch", + "hide": "Verbergen", + "custom": "Benutzerdefiniert", + "resize_column_width": "Spaltenbreite anpassen", + "collapse": "Einklappen", + "expand": "Erweitern", + "import_script_placeholder": "Unterstützt die Eingabe von absoluten Links von Skripten, die mit .user.js enden, oder ScriptCat-Installationsseiten-Links\nMehrzeilige Eingabe möglich, eine pro Zeile\nBeispiel:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/zh-CN/script-show-page/1234", + "light": "Heller Modus", + "dark": "Dunkler Modus", + "enter_search_value": "Bitte geben Sie {{search}} für die Suche ein", + "no_message_content": "Kein Nachrichteninhalt", + "loading": "Wird geladen...", + "auth_type": "Authentifizierungstyp", + "url": "URL", + "username": "Benutzername", + "password": "Passwort", + "access_token_bearer": "Zugriffstoken (Bearer)", + "s3_bucket_name": "Bucket-Name", + "s3_region": "Region", + "s3_access_key_id": "Zugriffs-Schlüssel-ID", + "s3_secret_access_key": "Geheimer Zugriffsschlüssel", + "s3_custom_endpoint": "Benutzerdefinierter Endpunkt (optional)", + "cancel": "Abbrechen" +} diff --git a/src/locales/de-DE/editor.json b/src/locales/de-DE/editor.json new file mode 100644 index 000000000..960d8f051 --- /dev/null +++ b/src/locales/de-DE/editor.json @@ -0,0 +1,132 @@ +{ + "save": "Speichern", + "save_success": "Erfolgreich gespeichert", + "copy": "Kopieren", + "find": "Suchen", + "replace": "Ersetzen", + "select_all": "Alles auswählen", + "format": "Formatieren", + "back": "Zurück", + "more": "Mehr", + "file": "Datei", + "edit": "Bearbeiten", + "settings": "Einstellungen", + "run_settings": "Ausführungseinstellungen", + "code": "Code", + "script_setting": "Skripteinstellungen", + "storage": "Speicher", + "resource": "Ressourcen", + "search_resource": "Ressourcen suchen", + "search_storage": "Schlüssel suchen", + "record_count": "{{count}} Einträge", + "resource_count": "{{count}} Ressourcen · {{size}}", + "script_list": "Skriptliste", + "search_scripts": "Skripte suchen...", + "new_script": "Neues Skript", + "source": "Quelle", + "from_user": "Benutzer", + "from_script": "Skript", + "last_updated": "Zuletzt aktualisiert", + "run_at": "Ausführen bei", + "run_in": "Ausführen in", + "check_update": "Auf Updates prüfen", + "line_col": "Z {{line}}, Sp {{col}}", + "script_not_found": "Skript nicht gefunden", + "confirm_delete_script": "Skript „{{name}}“ löschen?", + "delete_success": "Erfolgreich gelöscht", + "delete_failed": "Löschen fehlgeschlagen", + "cancel": "Abbrechen", + "confirm": "Bestätigen", + "save_as": "Speichern unter", + "run": "Ausführen", + "debug": "Debuggen", + "script_storage": "Skript-Speicher", + "enter_key": "Bitte geben Sie den Schlüssel ein", + "key_placeholder": "Schlüssel", + "value_placeholder": "Wenn der Typ Objekt ist, geben Sie JSON-parsbare Daten ein", + "clear_success": "Bereinigung erfolgreich", + "confirm_clear": "Möchten Sie wirklich diesen Speicherplatz bereinigen?", + "script_resource": "Skript-Ressource", + "basic_info": "Grundinformationen", + "update_url": "Update-URL", + "add_permission": "Berechtigung hinzufügen", + "match": "Übereinstimmung", + "user_setting": "Benutzereinstellungen", + "confirm_delete_exclude": "Diese Ausnahme löschen bestätigen?", + "after_deleting_match_item": "Nach dem Löschen der Skript-Übereinstimmungseinträge werden sie automatisch zu den Übereinstimmungseinträgen hinzugefügt", + "confirm_delete_match": "Diese Übereinstimmung löschen bestätigen?", + "after_deleting_exclude_item": "Nach dem Löschen der Skript-Übereinstimmungseinträge werden sie automatisch zu den Ausnahmeeinträgen hinzugefügt", + "add_match": "Übereinstimmung hinzufügen", + "add_exclude": "Ausnahme hinzufügen", + "website_match": "Website-Übereinstimmung (@match)", + "website_exclude": "Website-Ausnahme (@exclude)", + "confirm_reset": "Zurücksetzen bestätigen?", + "undo": "Rückgängig", + "redo": "Wiederherstellen", + "cut": "Ausschneiden", + "paste": "Einfügen", + "user_config": "Benutzerkonfiguration", + "gm_api": "GM API", + "storage_api": "Speicher-API", + "use_file_system": "Verwendetes Dateisystem", + "open_directory": "Verzeichnis öffnen", + "account_validation_failed": "Kontoinformationen-Validierung fehlgeschlagen", + "not_set": "Nicht eingestellt", + "in_use": "In Verwendung", + "storage_error": "Speicherfehler", + "upload_to_cloud": "In die Cloud hochladen", + "save_failed": "Speichern fehlgeschlagen", + "exporting": "Wird exportiert...", + "upload_to": "Hochladen zu", + "value_export_expression": "Wert-Export-Ausdruck", + "overwrite_original_value_on_import": "Originalwert beim Import überschreiben", + "cookie_export_expression": "Cookie-Export-Ausdruck", + "overwrite_original_cookie_on_import": "Originalwert beim Import überschreiben", + "restore_default_values": "Standardwerte wiederherstellen", + "edit_conflict": "Bearbeitungskonflikt", + "confirm_override_when_edit_conflict": "Dieses Skript wurde in einer anderen Instanz bearbeitet. Beim Ersetzen werden diese Änderungen überschrieben. Möchten Sie stattdessen diese Version behalten?", + "save_abort_when_edit_conflict": "Dieses Skript wurde in einer anderen Instanz bearbeitet. Speichern abgebrochen.", + "scriptname_conflict": "Skriptname-Konflikt", + "confirm_save_when_scriptname_conflict": "Dieser Skriptname wird bereits von einem anderen Skript verwendet. Möchten Sie ihn trotzdem speichern?", + "save_abort_when_scriptname_conflict": "Dieser Skriptname wird bereits von einem anderen Skript verwendet. Speichern abgebrochen.", + "eslint_config_format_error": "eslint-Konfigurationsformat-Fehler", + "script_modified_leave_confirm": "Ihre Änderungen wurden noch nicht gespeichert. Wenn Sie die Seite verlassen, gehen die Änderungen verloren. Möchten Sie die Seite wirklich verlassen?", + "create_success_note": "Erstellung erfolgreich. Beachten Sie, dass Hintergrundskripte standardmäßig nicht aktiviert werden", + "save_as_failed": "Speichern unter fehlgeschlagen", + "save_as_success": "Speichern unter erfolgreich", + "only_background_scheduled_can_run": "Nur Hintergrund-/geplante Skripte können ausgeführt werden", + "preparing_script_resources": "Skript-Ressourcen werden vorbereitet...", + "build_success_message": "Build erfolgreich. Sie können die Entwicklertools auf der Erweiterungsseite öffnen und die Ausgabe in der Konsole anzeigen", + "script_storage_tooltip": "Kann die gespeicherten Skriptdaten verwalten (GM_value)", + "script_resource_tooltip": "Verwalte mit @resource, @require heruntergeladene Ressourcen", + "script_setting_tooltip": "Führe einige benutzerdefinierte Einstellungen für das Skript durch", + "script_modified_close_confirm": "Skript wurde geändert. Das Schließen führt zum Verlust der Änderungen. Fortfahren?", + "close_current_tab": "Aktuellen Tab schließen", + "close_other_tabs": "Andere Tabs schließen", + "close_left_tabs": "Linke Tabs schließen", + "close_right_tabs": "Rechte Tabs schließen", + "invalid_script_code": "Ungültiger Skriptcode", + "build_failed": "Build fehlgeschlagen", + "drag_script_here_to_upload": "Skript hierher ziehen zum Hochladen", + "watch_file_description": "Überwachen Sie Dateiänderungen und aktualisieren Sie Skripte automatisch. Beim Gebrauch stellen Sie sicher, dass der Skriptdateipfad unverändert bleibt und die Seite nicht geschlossen werden kann", + "watch_file": "Datei überwachen", + "stop_watch_file": "Überwachung stoppen", + "individual_edit": "Einzelbearbeitung", + "batch_edit": "Stapelbearbeitung", + "script_code": "Skriptcode", + "editor_config": "Editor-Konfiguration", + "editor_config_description": "Sie können sich an den compilerOptions in jsconfig.js orientieren, um die Konfiguration vorzunehmen", + "editor_type_definition": "Editor-Typdefinitionen", + "editor_type_definition_description": "Sie können Ihre eigenen Typdefinitionen anpassen, der Skript-Editor wird diese automatisch laden", + "eslint_rules_reset": "ESLint-Regeln wurden zurückgesetzt", + "eslint_rules_saved": "ESLint-Regeln wurden gespeichert", + "editor_config_reset": "Editor-Konfiguration wurde zurückgesetzt", + "editor_config_saved": "Editor-Konfiguration wurde gespeichert", + "editor_config_format_error": "Editor-Konfiguration Formatfehler", + "editor_type_definition_reset": "Editor-Typdefinitionen wurden zurückgesetzt", + "editor_type_definition_saved": "Editor-Typdefinitionen wurden gespeichert", + "editor": { + "show_script_list": "Skriptliste anzeigen", + "hide_script_list": "Skriptliste ausblenden" + } +} diff --git a/src/locales/de-DE/guide.json b/src/locales/de-DE/guide.json new file mode 100644 index 000000000..dfc8bb420 --- /dev/null +++ b/src/locales/de-DE/guide.json @@ -0,0 +1,25 @@ +{ + "start_title": "Willkommen bei der ScriptCat-Erweiterung", + "start_content": "Als nächstes stellen wir Ihnen die grundlegende Verwendung von ScriptCat vor", + "installed_scripts": "Ihre installierten Skripte werden hier angezeigt", + "script_list_title": "Skript-Markt", + "script_list_content": "Sie können Skripte aus dem Skript-Markt installieren. ScriptCat unterstützt neben Benutzerskripten auch Hintergrundskripte", + "script_list_enable_title": "Skript-Aktivierung", + "script_list_enable_content": "Skripte müssen aktiviert werden, um verwendet zu werden. Seitenskripte sind bei der Installation standardmäßig aktiviert, Hintergrundskripte sind bei der Installation standardmäßig deaktiviert", + "script_list_apply_to_run_status_title": "Anwenden auf und Ausführungsstatus", + "script_list_apply_to_run_status_content": "Zeigt den Ausführungsstatus von Skripten an. Bewegen Sie die Maus über das Tag, um den Skripttyp zu sehen", + "script_list_sort_title": "Sortierung", + "script_list_sort_content": "Sie können die Skriptbezeichnungen per Drag & Drop sortieren.", + "script_list_update_title": "Zuletzt aktualisiert", + "script_list_update_content": "Durch Klicken auf die Spaltenüberschrift „Zuletzt aktualisiert“ wird geprüft, ob es Aktualisierungen des Skripts gibt.", + "script_list_action_title": "Aktion", + "script_list_action_content": "Die Bedienleiste ermöglicht den Zugriff auf die Skriptbearbeitung, die Steuerung der Skriptausführung und -beendigung (Hintergrundskripte) sowie auf die Einstellungen (siehe UserConfig). Über die Schaltflächen auf der rechten Seite können Sie auf erweiterte Filterfunktionen zugreifen und zwischen verschiedenen Ansichtsmodi wechseln.", + "tools_title": "Häufig verwendete Werkzeuge", + "tools_content": "Werkzeuge bieten Backup- und Entwicklungstools", + "tools_backup_title": "Backup", + "tools_backup_content": "Backups können Skripte speichern und Verluste vermeiden. Sie können Skriptdateien lokal exportieren oder lokale Skriptdateien laden. Sie können auch in die Cloud sichern, was praktischer ist.", + "setting_title": "Einstellungen", + "setting_content": "Einstellungen enthalten hauptsächlich häufig verwendete Einstellungsoptionen wie Sprache, Skript-Synchronisation, Update-Frequenz", + "setting_sync_title": "Update und Synchronisation", + "setting_sync_content": "Die Skript-Synchronisationsfunktion kann bequem den Skriptinhalt dieses Geräts in die Cloud synchronisieren. Wenn Sie mehrere Geräte haben, aktivieren Sie bitte die Option zum synchronisierten Löschen. Wenn Skripte auf diesem Gerät gelöscht werden, werden entsprechende Skripte aus der Cloud gelöscht und auch Skripte auf anderen Geräten gelöscht." +} diff --git a/src/locales/de-DE/index.ts b/src/locales/de-DE/index.ts new file mode 100644 index 000000000..bacd6d096 --- /dev/null +++ b/src/locales/de-DE/index.ts @@ -0,0 +1,11 @@ +export { default as agent } from "./agent.json"; +export { default as common } from "./common.json"; +export { default as editor } from "./editor.json"; +export { default as guide } from "./guide.json"; +export { default as install } from "./install.json"; +export { default as logs } from "./logs.json"; +export { default as permission } from "./permission.json"; +export { default as popup } from "./popup.json"; +export { default as script } from "./script.json"; +export { default as settings } from "./settings.json"; +export { default as tools } from "./tools.json"; diff --git a/src/locales/de-DE/install.json b/src/locales/de-DE/install.json new file mode 100644 index 000000000..869b8a434 --- /dev/null +++ b/src/locales/de-DE/install.json @@ -0,0 +1,207 @@ +{ + "data_import": "Datenimport", + "select_scripts_to_import": "Bitte wählen Sie die zu importierenden Skripte aus", + "select_all": "Alle auswählen", + "script_import_progress": "Skript-Import-Fortschritt", + "select_subscribes_to_import": "Bitte wählen Sie die zu importierenden Abonnements aus", + "subscribe_import_progress": "Abonnement-Import-Fortschritt", + "script": "Installieren", + "update_script": "Aktualisieren", + "subscribe": "Abonnement installieren", + "update_subscribe": "Abonnement aktualisieren", + "update_script_no_close": "Aktualisieren ohne das Fenster zu schließen", + "script_no_close": "Installieren, ohne das Fenster zu schließen", + "update_script_no_more_update": "Aktualisieren, aber nicht mehr nach Updates suchen", + "close_update_script_no_more_update": "Schließen und nicht nach Updates suchen", + "script_no_more_update": "Installieren, aber nicht mehr nach Updates suchen", + "invalid_link": "Ungültiger Link", + "subscribe_install_label": "Dieses Abonnement wird die folgenden Skripte installieren", + "subscribe_scripts_title": "Dieses Abonnement installiert die folgenden Skripte", + "subscribe_scripts_empty": "Dieses Abonnement deklariert noch keine Skripte", + "script_runs_in": "Skript wird auf den folgenden Websites ausgeführt", + "script_has_full_access_to": "Skript erhält vollständigen Zugriff auf die folgenden Adressen", + "script_requires": "Skript referenziert die folgenden externen Ressourcen", + "cookie_warning": "Achtung: Dieses Skript beantragt Cookie-Operationsberechtigung. Dies ist eine gefährliche Berechtigung, bitte stellen Sie die Sicherheit des Skripts sicher.", + "perm_card_title": "Dieses Skript erhält die folgenden Berechtigungen", + "perm_card_hint": "Bitte vor der Installation bestätigen", + "perm_card_empty": "Dieses Skript fordert keine besonderen Berechtigungen an", + "perm_match_label": "Läuft auf", + "perm_match_summary": "Das Skript läuft auf diesen Websites und verändert die Seiten", + "perm_connect_label": "Cross-Origin-Zugriff", + "perm_connect_summary": "Kann Anfragen an die folgenden Domains senden und deren Daten lesen", + "perm_grant_label": "GM-Funktionen", + "perm_grant_summary": "Kann die folgenden GM-APIs aufrufen", + "perm_require_label": "Externe Ressourcen", + "perm_require_summary": "Lädt die folgenden Drittanbieter-Skripte und Ressourcen", + "badge_background": "Hintergrund", + "badge_scheduled": "Geplant", + "enabled_label": "Aktiviert", + "schedule_cron_label": "Geplante Aufgabe", + "schedule_next_run": "Nächste Ausführung", + "schedule_background_desc": "Läuft automatisch, solange der Browser geöffnet ist", + "code_lines": "{{count}} Zeilen", + "code_copy": "Code kopieren", + "code_collapse": "Einklappen", + "code_expand": "Ausklappen", + "loading_title": "Skript wird geladen", + "loading_desc": "Skriptinhalt wird von der Quelle heruntergeladen und ausgewertet", + "error_retry": "Erneut versuchen", + "error_invalid_desc": "Es fehlt ein gültiger Installationsquellen-Parameter, das Skript kann nicht geladen werden.", + "context_install": "Skript installieren", + "context_update": "Skript aktualisieren", + "background_script": "Hintergrundskript", + "scheduled_script": "Geplantes Skript", + "watching_status": "Dateiänderungen werden überwacht; nach dem Speichern wird automatisch neu installiert", + "watching_chip": "Überwachung", + "watching_title": "Dateiänderungen werden überwacht", + "watching_file_desc": "Beim Speichern von {{file}} wird das Skript automatisch neu installiert und synchronisiert", + "watching_last_sync": "Zuletzt synchronisiert {{time}}", + "warning_title": "Bitte bestätige, dass das Skript aus einer vertrauenswürdigen Quelle stammt", + "warning_risk_connect": "es kann auf alle Domains zugreifen", + "warning_risk_antifeature": "es deklariert Anti-Features", + "warning_risk_join": " und ", + "warning_risk_tail": " — mit Vorsicht installieren.", + "action_note_install": "Mit der Installation vertraust du der Quelle und dem Autor dieses Skripts", + "action_note_update": "Mit der Aktualisierung akzeptierst du diese Code- und Berechtigungsänderung", + "action_note_subscribe": "Mit der Installation vertraust du diesem Abonnement und seinem Autor", + "action_note_watching": "Überwachung — Speichern aktualisiert automatisch; zum manuellen Vorgehen anhalten", + "context_skill_install": "Skill installieren", + "context_skill_update": "Skill aktualisieren", + "skill_kind": "KI-Skill", + "skill_prompt_title": "Prompt", + "skill_prompt_chip": "SKILL.md", + "skill_tools_title": "Werkzeuge", + "skill_config_title": "Konfiguration", + "skill_references_title": "Referenzen", + "skill_required": "Erforderlich", + "skill_secret": "Geheim", + "skill_install": "Skill installieren", + "skill_update": "Skill aktualisieren", + "skill_warning": "Der Skill fügt der KI einen Prompt hinzu und gewährt ihr das Recht, die folgenden Werkzeuge, GM-Funktionen und Konfigurationen aufzurufen. Nur aus seriösen Quellen installieren!", + "skill_warning_title": "Bitte bestätige, dass dieser Skill aus einer vertrauenswürdigen Quelle stammt", + "skill_warning_desc": "Nach der Installation fügt er der KI einen Prompt hinzu und gewährt die aufgeführten Werkzeuge (inkl. GM-Berechtigungen) sowie Lese-/Schreibzugriff auf die Konfiguration — mit Vorsicht installieren.", + "success": "Installation erfolgreich", + "install": { + "update_success": "Update erfolgreich" + }, + "failed": "Installation fehlgeschlagen", + "subscribe_success": "Abonnement erfolgreich", + "subscribe_failed": "Abonnement fehlgeschlagen", + "current_version": "Aktuelle Version", + "update_version": "Update-Version", + "updatepage": { + "title": "Sammelupdate", + "main_header": "Nach Updates suchen", + "last_check": "Zuletzt geprüft {{time}}", + "status_checking_updates": "Suche nach Updates...", + "updates_available": "{{count}} Updates verfügbar", + "ignored_count": "{{count}} ignoriert", + "selected_count": "{{selected}} / {{total}} ausgewählt", + "update_selected": "Auswahl aktualisieren ({{count}})", + "ignore_selected": "Auswahl ignorieren", + "update": "Aktualisieren", + "ignore": "Ignorieren", + "restore": "Wiederherstellen", + "restore_all": "Alle wiederherstellen", + "ignored_section": "Ignorierte Updates", + "auto_close": "Schließt automatisch in {{count}} s", + "col_script": "Skript", + "col_version": "Version", + "col_change": "Änderungen", + "col_source": "Quelle", + "col_action": "Aktionen", + "enabled": "Aktiviert", + "disabled": "Deaktiviert", + "codechange_major": "Große Änderung", + "codechange_noticeable": "Auffällige Änderung", + "codechange_tiny": "Kleine Änderung", + "tag_new_connect": "Neuer @connect", + "empty_title": "Alle Skripte sind aktuell", + "empty_desc": "{{count}} Skripte geprüft · Keine Updates verfügbar", + "similarity": "Ähnlichkeit", + "new_connects": "Neue @connect", + "toast_found": "{{count}} Skripte mit Updates gefunden", + "toast_uptodate": "Alle Skripte sind aktuell" + }, + "importpage": { + "title": "Datenimport", + "context_review": "Datenimport", + "context_importing": "Wird importiert", + "context_done": "Import abgeschlossen", + "selected_count": "{{selected}} / {{total}} ausgewählt", + "unimportable_count": "{{count}} nicht importierbar", + "count_scripts": "{{count}} Skripte", + "count_subscribes": "{{count}} Abonnements", + "col_script": "Skript", + "col_version": "Version", + "col_source": "Quelle", + "col_data": "Daten", + "col_status": "Status", + "col_enabled": "Aktiviert", + "op_add": "Neu", + "op_update": "Update", + "op_error": "Parse-Fehler", + "source_local": "Lokal erstellt", + "data_values": "{{count}} Einträge", + "data_resources": "mit Ressourcen", + "enable_after_import": "Nach Import aktivieren", + "row_error": "Datei beschädigt, Import nicht möglich", + "unknown_script": "Unbekanntes Skript", + "subscribe_section": "Abonnements", + "trust_hint": "Es werden nur ausgewählte Einträge wiederhergestellt; beim Import werden keine Daten gesendet", + "import_selected": "Auswahl importieren ({{count}})", + "importing_progress": "Backup wird wiederhergestellt · {{done}} / {{total}} abgeschlossen", + "importing_hint": "Diese Seite geöffnet lassen", + "importing_actionbar_hint": "Skripte und Daten werden wiederhergestellt, bitte diese Seite nicht schließen", + "importing_button": "Wird importiert…", + "cancel": "Abbrechen", + "status_pending": "Ausstehend", + "status_importing": "Wird importiert", + "status_done": "Importiert", + "status_skipped": "Übersprungen", + "done_title": "Import abgeschlossen", + "done_desc": "Die ausgewählten Skripte und Daten wurden wiederhergestellt", + "done_stat_scripts": "{{count}} Skripte", + "done_stat_subscribes": "{{count}} Abonnements", + "done_stat_values": "{{count}} Dateneinträge", + "view_scripts": "Skriptliste anzeigen", + "loading_title": "Backup-Datei wird analysiert", + "loading_desc": "Backup-Inhalt wird gelesen und geprüft", + "error_title": "Backup-Datei kann nicht gelesen werden", + "error_desc": "Die Backup-Datei ist möglicherweise beschädigt oder der Import-Link ist abgelaufen", + "invalid_desc": "Der Import-Link ist ungültig oder abgelaufen", + "retry": "Erneut versuchen", + "empty_title": "Nichts zum Importieren in diesem Backup", + "empty_desc": "Diese Backup-Datei enthält keine Skripte oder Abonnements" + }, + "downloading_status_text": "Wird heruntergeladen. {{bytes}} empfangen.", + "downloading_status_percent": "Wird heruntergeladen. {{bytes}} / {{total}} ({{percent}}%) empfangen.", + "page_please_wait": "Bitte warten Sie", + "page_loading": "Installationsseite wird geladen", + "page_load_failed": "Installationsseite konnte nicht geladen werden", + "invalid_page": "Ungültige Seite", + "background_script_tag": "Dies ist ein Hintergrundskript", + "scheduled_script_tag": "Dies ist ein geplantes Skript", + "from_legitimate_sources_warning": "Bitte installieren Sie Skripte aus legitimen Quellen! Unbekannte Skripte können Ihre Privatsphäre verletzen oder bösartige Operationen durchführen!", + "referral_link_title": "Empfehlungslink", + "referral_link_description": "Dieses Skript modifiziert oder leitet zu den Affiliate-Links des Autors um", + "ads_title": "Mit Werbung", + "ads_description": "Dieses Skript fügt Werbung auf den von Ihnen besuchten Seiten ein", + "payment_title": "Bezahltes Skript", + "payment_description": "Dieses Skript erfordert eine Zahlung für die normale Nutzung", + "miner_title": "Mining", + "miner_description": "Dieses Skript hat Mining-Verhalten", + "membership_title": "Mitgliedschaftsfunktion", + "membership_description": "Dieses Skript erfordert eine Mitgliedschaftsregistrierung für die normale Nutzung", + "tracking_title": "Informationsverfolgung", + "tracking_description": "Dieses Skript verfolgt Ihre Benutzerinformationen", + "script_info_load_failed": "Skript-Informationen laden fehlgeschlagen!", + "script_import_result": "Skript-Import-Ergebnis", + "failure_info": "Fehlerinformationen", + "source": "Installationsquelle", + "skill_prompt": "Prompt", + "skill_tools": "Werkzeuge", + "skill_config": "Konfiguration", + "skill_references": "Referenzen", + "skill_install_failed": "Skill-Installation fehlgeschlagen" +} diff --git a/src/locales/de-DE/logs.json b/src/locales/de-DE/logs.json new file mode 100644 index 000000000..160f016ad --- /dev/null +++ b/src/locales/de-DE/logs.json @@ -0,0 +1,59 @@ +{ + "log_title": "Ausführungsprotokoll", + "last_5_minutes": "Letzte 5 Minuten", + "last_15_minutes": "Letzte 15 Minuten", + "last_30_minutes": "Letzte 30 Minuten", + "last_1_hour": "Letzte 1 Stunde", + "last_3_hours": "Letzte 3 Stunden", + "last_6_hours": "Letzte 6 Stunden", + "last_12_hours": "Letzte 12 Stunden", + "last_24_hours": "Letzte 24 Stunden", + "last_7_days": "Letzte 7 Tage", + "query": "Abfrage", + "labels": "Labels", + "search_regex": "Suche (Regex unterstützt)", + "clean_schedule": "Geplante Bereinigung", + "days_ago_logs": "Tage alte Protokolle", + "delete_completed": "Löschen abgeschlossen", + "delete_current_logs": "Aktuelle Protokolle löschen", + "clear_completed": "Bereinigung abgeschlossen", + "clear_logs": "Protokolle bereinigen", + "now": "Jetzt", + "total_logs": "Insgesamt {{length}} Protokolle gefunden", + "filtered_logs": "Nach Filterung {{length}} Protokolle", + "enter_filter_conditions": "Bitte geben Sie Filterbedingungen für die Abfrage ein", + "last_updated": "Zuletzt aktualisiert", + "runtime": "Laufzeit", + "advanced": "Erweitert", + "label_filter": "Label-Filter", + "add_label": "Label hinzufügen", + "custom_range": "Benutzerdefiniert", + "back_to_top": "Nach oben", + "refresh": "Aktualisieren", + "all_levels": "Alle", + "total_count": "{{count}} gesamt", + "filtered_count": "{{count}} gefiltert", + "clear_logs_confirm": "Alle Protokolle bereinigen? Dies kann nicht rückgängig gemacht werden.", + "no_logs": "Keine Protokolle", + "refresh_off": "Aus", + "interval_5s": "5s", + "interval_10s": "10s", + "interval_30s": "30s", + "interval_1m": "1m", + "interval_5m": "5m", + "quick_range": "Schnellbereiche", + "absolute_range": "Absoluter Bereich", + "from_start": "Von (Start)", + "to_end": "Bis (Ende)", + "apply_range": "Bereich anwenden", + "auto_refresh_hint": "Wenn das Ende auf „Jetzt“ gesetzt ist, ruft die automatische Aktualisierung weiterhin die neuesten Protokolle ab.", + "group_minutes": "Minuten", + "group_hours": "Stunden", + "group_days": "Tage", + "weekdays_short": "So,Mo,Di,Mi,Do,Fr,Sa", + "year_month": "{{month}}/{{year}}", + "time": "Zeit", + "prev_month": "Voriger Monat", + "next_month": "Nächster Monat", + "live": "Live" +} diff --git a/src/locales/de-DE/permission.json b/src/locales/de-DE/permission.json new file mode 100644 index 000000000..44e861c86 --- /dev/null +++ b/src/locales/de-DE/permission.json @@ -0,0 +1,42 @@ +{ + "permission": "Berechtigung", + "permission_value": "Berechtigungswert", + "allow": "Erlauben", + "permission_management": "Berechtigungsverwaltung", + "permission_cors": "Cross-Origin (CORS)", + "permission_cookie": "Cookies verwalten", + "allow_once": "Einmal erlauben", + "deny_once": "Einmal verweigern", + "script_accessing_cross_origin_resource": "Skript versucht auf Cross-Origin-Ressourcen zuzugreifen", + "confirm_operation_description": "Bitte bestätigen Sie, ob Sie dem Skript erlauben, diese Operation durchzuführen. Das Skript kann auch ein @connect-Tag hinzufügen, um diese Option zu überspringen", + "request_domain": "Anfrage-Domain", + "request_url": "Anfrage-URL", + "access_cookie_content": "Skript versucht auf Website-Cookie-Inhalte zuzugreifen", + "confirm_script_operation": "Bitte bestätigen Sie, ob Sie dem Skript erlauben, diese Operation durchzuführen. Cookies sind wichtige Benutzerdaten, bitte autorisieren Sie nur vertrauenswürdige Skripte.", + "cookie_domain": "Cookie-Domain", + "script_operation_title": "Skript versucht den Skript-Sync-Speicherplatz zu bearbeiten", + "script_operation_description": "Bitte bestätigen Sie, ob Sie dem Skript erlauben, diese Operation durchzuführen. Nach der Erlaubnis kann das Skript den von Ihnen eingestellten Speicherplatz bearbeiten. Das Skript erstellt ein app/${dir}-Verzeichnis unter dem Speicherplatz zur Verwendung", + "script_permission_content": "Skript", + "extension_site_access_title": "ScriptCat benötigt Website-Zugriff", + "extension_site_access_description": "Gewähren Sie dem Browser Website-Zugriff auf diesen Ursprung, damit ScriptCat die Anfrage ausführen kann. Dadurch wird die Website-Zugriffseinstellung der Erweiterung geändert.", + "extension_site_access_content": "Website", + "request_permission": "Beantragen Sie Genehmigungen", + "allow_user_script_guide": "'Nutzerskripts zulassen' ist derzeit nicht aktiviert, daher können die Skripte nicht richtig ausgeführt werden. 👉Hier klicken, um zu erfahren, wie man es aktiviert", + "user_script_type": "Benutzerskript", + "auth_duration": "Autorisierungsdauer", + "duration_once": "Nur dieses Mal", + "duration_temporary": "Temporär", + "duration_permanent": "Dauerhaft", + "apply_to_all_domains": "Auf alle angefragten Domains anwenden", + "apply_to_all_domains_desc": "Gilt für alle Anfragen dieses Skripts (Platzhalter)", + "allow_action": "Erlauben", + "deny_action": "Verweigern", + "ignore_action": "Ignorieren", + "cancel_action": "Abbrechen", + "loading_confirm": "Autorisierungsanfrage wird geladen…", + "cookie_warning_title": "Hochsensible Berechtigung", + "cookie_warning_desc": "Cookies enthalten sensible Daten wie den Anmeldestatus. Autorisieren Sie nur vertrauenswürdige Skripte.", + "confirm_expired_title": "Autorisierungsanfrage abgelaufen", + "confirm_expired_desc": "Diese Autorisierungsanfrage ist abgelaufen oder wurde bereits bearbeitet. Bitte kehren Sie zur Seite zurück und lösen Sie sie erneut aus.", + "auto_close_in": "Das Fenster wird in {{second}}s automatisch geschlossen" +} diff --git a/src/locales/de-DE/popup.json b/src/locales/de-DE/popup.json new file mode 100644 index 000000000..ec4c9f43a --- /dev/null +++ b/src/locales/de-DE/popup.json @@ -0,0 +1,27 @@ +{ + "new_version_available": "Neue Version verfügbar", + "current_page_scripts": "Aktuelle Seiten-Ausführungsskripte", + "enabled_background_scripts": "Aktivierte und laufende Hintergrundskripte", + "menu_expand_num_before": "Wenn Menüelemente mehr als", + "menu_expand_num_after": "sind, automatisch verbergen", + "develop_mode_guide": "Der 'Entwicklermodus' ist derzeit nicht aktiviert, daher können die Skripte nicht richtig ausgeführt werden. 👉Hier klicken, um zu erfahren, wie man ihn aktiviert", + "lower_version_browser_guide": "Ihr Browser ist zu veraltet, daher können die Skripte nicht richtig ausgeführt werden. 👉Hier klicken, um mehr zu erfahren", + "click_to_reload": "👉Zum Neuladen klicken", + "page_in_blacklist": "Die aktuelle Seite ist auf der Blacklist und kann keine Skripte verwenden", + "ext_update_notification": "ScriptCat-Erweiterung wurde aktualisiert", + "ext_update_notification_desc": "Aktuelle Version: {{version}}, Details finden Sie im Changelog", + "script_menu_display": "Von Skript registrierte Menüs", + "badge_type_none": "Nicht anzeigen", + "badge_type_run_count": "Ausführungsanzahl", + "badge_type_script_count": "Skriptanzahl", + "script_menu": "Skriptmenü", + "display_right_click_menu": "Rechtsklickmenü anzeigen", + "display_right_click_menu_desc": "Skriptmenü im Rechtsklickmenü des Browsers anzeigen", + "expand_count": "Erweiterte Anzahl", + "auto_collapse_when_exceeds": "Automatisch einklappen, wenn diese Anzahl überschritten wird", + "allow_user_script_guide": "'Nutzerskripts zulassen' ist derzeit nicht aktiviert, daher können die Skripte nicht richtig ausgeführt werden. 👉Hier klicken, um zu erfahren, wie man es aktiviert", + "request_permission": "Beantragen Sie Genehmigungen", + "show_more_scripts": "+{{count}} Skripte", + "use_on_mobile": "ScriptCat auf dem Handy nutzen", + "scan_qr_to_install": "QR-Code scannen, um ScriptCat auf dem Handy zu installieren" +} diff --git a/src/locales/de-DE/script.json b/src/locales/de-DE/script.json new file mode 100644 index 000000000..d5b931025 --- /dev/null +++ b/src/locales/de-DE/script.json @@ -0,0 +1,115 @@ +{ + "import_link": "Link-Import", + "import_link_failure": "Link-Import fehlgeschlagen", + "create_user_script": "Neues Benutzerskript erstellen", + "create_background_script": "Neues Hintergrundskript erstellen", + "create_scheduled_script": "Neues geplantes Skript erstellen", + "import_by_local": "Lokaler Import", + "import_local_failure": "Lokaler Import fehlgeschlagen", + "import_local_success": "Lokaler Import erfolgreich", + "create_script": "Neues Skript erstellen", + "installed_scripts": "Installierte Skripte", + "nav_scripts": "Skripte", + "subscribe": "Abonnieren", + "subscribe_scripts_count": "{{count}} Skripte", + "enter_subscribe_name": "Bitte geben Sie den Abonnement-Namen ein", + "subscribe_url": "Abonnement-URL", + "confirm_delete_subscription": "Möchten Sie dieses Abonnement wirklich löschen? Zugehörige Skripte werden ebenfalls gelöscht", + "list": { + "confirm_delete": "Möchten Sie wirklich löschen? Beachten Sie, dass diese Operation nicht rückgängig gemacht werden kann!", + "confirm_update": "Möchten Sie wirklich aktualisieren? Beachten Sie, dass diese Operation nicht rückgängig gemacht werden kann!" + }, + "apply_to_run_status": "Anwenden auf / Ausführungsstatus", + "sorting": "Sortierung", + "foreground_page_script_tooltip": "Vordergrund-Seitenskript, wird auf angegebenen Seiten ausgeführt", + "background_script_tooltip": "Hintergrundskript, wird im Hintergrund ausgeführt, wenn aktiviert", + "scheduled_script_tooltip": "Geplantes Skript, nächste Ausführungszeit:", + "running": "Wird ausgeführt", + "completed": "Ausführung abgeschlossen", + "source_subscribe_link": "Abonnement-Link", + "source_local_script": "Lokales Skript", + "source_script_link": "Skript-Link", + "by_manual_creation": "Lokal durch Code-Bearbeitung erstellt", + "confirm_delete_script": "Möchten Sie dieses Skript wirklich löschen?", + "confirm_delete_scripts_content": "Möchten Sie die ausgewählten {{count}} Skripte wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "confirm_delete_script_content": "Möchten Sie das Skript „{{name}}“ wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "delete_failed": "Löschen fehlgeschlagen", + "enter_script_name": "Bitte geben Sie den Skriptnamen ein", + "update_not_supported": "Dieses Skript unterstützt keine Update-Überprüfung", + "checking_for_updates": "Suche nach Updates...", + "new_version_available": "Neue Version verfügbar", + "latest_version": "Neueste Version", + "checked_for_all_selected": "Alle ausgewählten Skripte auf Updates überprüft", + "update_check_failed": "Update-Überprüfung fehlgeschlagen", + "stopping_script": "Skript wird gestoppt", + "script_stopped": "Skript gestoppt", + "starting_script": "Skript wird gestartet...", + "starting_updates": "Batch-Update wird gestartet...", + "script_started": "Skript gestartet", + "operation_failed": "Vorgang fehlgeschlagen", + "batch_operations": "Batch-Operationen", + "scripts_pinned_to_top": "Das ausgewählte Skript wurde angepinnt", + "unknown_operation": "Unbekannte Operation", + "page_script": "Seitenskript", + "homepage": "Skript-Startseite", + "script_website": "Skript-Website", + "script_source": "Skript-Quellcode", + "bug_feedback_script_support": "Bug-Feedback/Skript-Support-Website", + "script_total_runs": "Dieses Skript wurde insgesamt {{runNum}} Mal ausgeführt, {{runNumByIframe}} Mal in iframes", + "script_total_runs_single": "Dieses Skript wurde {{runNum}} Mal ausgeführt", + "script_disabled": "Dieses Skript ist nicht aktiviert", + "cron_oncetype": { + "minute": "{{next}} (jede Minute ausgeführt)", + "hour": "{{next}} (jede Stunde ausgeführt)", + "day": "{{next}} (jeden Tag ausgeführt)", + "month": "{{next}} (jeden Monat ausgeführt)", + "week": "{{next}} (jede Woche ausgeführt)" + }, + "cron_invalid_expr": "Ungültiger Cron-Ausdruck", + "scheduled_script_description_title": "Dies ist ein geplantes Skript. Wenn aktiviert, wird es zu bestimmten Zeiten automatisch ausgeführt und kann im Panel manuell gesteuert werden.", + "scheduled_script_description_description_expr": "Geplante Aufgaben-Ausdruck", + "scheduled_script_description_description_next": "Letzte Ausführungszeit:", + "background_script_description": "Dies ist ein Hintergrundskript. Wenn aktiviert, wird es automatisch einmal ausgeführt, wenn der Browser geöffnet wird, und kann im Panel manuell gesteuert werden.", + "background_script": "Hintergrundskript", + "scheduled_script": "Geplantes Skript", + "script_status_tooltip": "Sie können den Aktivierungsstatus von Skripten steuern. Gewöhnliche Tampermonkey-Skripte sind standardmäßig aktiviert, während Hintergrund- und geplante Skripte standardmäßig deaktiviert sind.", + "subscribe_source_tooltip": "Dies ist eine Abonnementquelle. Wenn Sie das Abonnement öffnen, wird das Abonnementskript automatisch installiert.", + "script_name_cannot_be_set_to_empty": "Skriptname kann nicht leer gesetzt werden", + "search_scripts": "Suchskripte", + "script_list": { + "sidebar": { + "stopped": "Gestoppt", + "all": "Alle", + "normal_script": "Normales Skript", + "status": "Status" + } + }, + "tags": "Tags", + "input_tags_placeholder": "Tags eingeben, Enter drücken um zu bestätigen", + "switch_to_card_mode": "Wechseln Sie in den Kartenmodus", + "switch_to_table_mode": "In den Tabellenmodus wechseln", + "open_sidebar": "Öffne die Seitenleiste", + "close_sidebar": "Schließe die Seitenleiste", + "error_metadata_invalid": "MetaData-Block ist ungültig", + "error_script_name_required": "Skriptname ist erforderlich", + "error_script_version_required": "Skript @version ist erforderlich", + "error_script_namespace_required": "Skript @namespace ist erforderlich", + "error_cron_invalid": "Ungültiger Cron-Ausdruck: {{expr}}", + "error_script_type_mismatch": "Skripttyp stimmt nicht überein: Normale und Hintergrundskripte können nicht konvertiert werden", + "error_old_script_code_missing": "Alter Skriptcode nicht gefunden", + "error_subscribe_name_required": "Abonnementname ist erforderlich", + "error_grant_conflict": "@grant deklariert sowohl 'none' als auch GM API", + "error_metadata_line_duplicated": "In den Metadaten befinden sich doppelte Deklarationen.", + "create_group": "Erstellen", + "import_group": "Importieren", + "import_local_script": "Lokales Skript importieren", + "link_import": "Per Link importieren", + "import_skill": "Skill importieren", + "link_import_desc": "Skript- / Abonnement-Links einfügen, einer pro Zeile", + "link_import_placeholder": "https://example.com/script.user.js", + "link_import_hint": "Unterstützt Benutzerskripte / Abonnements / Skill-Links", + "not_a_valid_script": "Kein gültiges Benutzerskript oder SkillScript", + "import_done": "Import abgeschlossen: {{success}} erfolgreich · {{fail}} fehlgeschlagen", + "drop_to_install": "Skripte oder Skills hierher ziehen, um sie zu installieren", + "drop_to_install_hint": ".js Benutzerskripte / Abonnements · .zip Skill-Pakete hierher ziehen" +} diff --git a/src/locales/de-DE/settings.json b/src/locales/de-DE/settings.json new file mode 100644 index 000000000..e0f63a071 --- /dev/null +++ b/src/locales/de-DE/settings.json @@ -0,0 +1,119 @@ +{ + "general": "Allgemein", + "language": "Sprache", + "help_translate": "Bei der Übersetzung helfen", + "script_sync": "Skript-Synchronisation", + "sync_delete": "Synchronisiertes Löschen", + "sync_delete_desc": "Wenn aktiviert, wird das Skript beim Löschen als gelöscht markiert und andere Geräte erkennen diesen Status und löschen das Skript ebenfalls. Wenn deaktiviert, wird das Skript direkt aus dem lokalen und Cloud-Speicher gelöscht, was zu wiederholten Synchronisierungsproblemen führen kann, wenn mehrere Geräte verwendet werden.", + "enable_script_sync_to": "Skript-Synchronisation aktivieren zu", + "script_subscription_check_interval": "Skript-/Abonnement-Update-Überprüfungsintervall", + "never": "Nie", + "6_hours": "6 Stunden", + "12_hours": "12 Stunden", + "every_day": "Täglich", + "every_week": "Wöchentlich", + "update_disabled_scripts": "Deaktivierte Skripte aktualisieren", + "silent_update_non_critical_changes": "Nicht-kritische Änderungen still aktualisieren", + "enable_eslint": "ESLint aktivieren", + "eslint_rules": "ESLint-Regeln", + "enter_eslint_rules": "Bitte geben Sie ESLint-Regeln ein. Sie können die Konfiguration von https://eslint.org/play/ herunterladen", + "language_change_tip": "Sprachwechsel erfolgreich", + "backup": "Sicherung", + "local": "Lokal", + "export_file": "Datei exportieren", + "import_file": "Datei importieren", + "cloud": "Cloud", + "backup_to": "Sichern zu", + "preparing_backup": "Sicherung in die Cloud wird vorbereitet", + "backup_success": "Sicherung erfolgreich", + "backup_failed": "Sicherung fehlgeschlagen", + "no_backup_files": "Keine Sicherungsdateien", + "backup_list": "Sicherungsliste", + "open_backup_dir": "Sicherungsverzeichnis öffnen", + "confirm_delete_backup_file": "Löschen der Sicherungsdatei bestätigen", + "backup_strategy": "Sicherungsstrategie", + "under_construction": "Im Aufbau", + "sync_system_connect_failed": "Synchronisationssystem-Verbindung fehlgeschlagen", + "sync_system_closed": "Synchronisation geschlossen", + "sync_system_closed_description": "Synchronisationsfunktion ist geschlossen, bitte neu konfigurieren", + "export_success": "Export erfolgreich", + "get_backup_dir_url_failed": "Backup-Verzeichnis-Adresse abrufen fehlgeschlagen", + "get_backup_files_failed": "Backup-Dateien abrufen fehlgeschlagen", + "baidu_netdisk": "Baidu Netdisk", + "netdisk_unbind": "{{provider}} trennen", + "netdisk_unbind_confirm": "Das {{provider}}-Konto trennen?", + "netdisk_unbind_success": "{{provider}}-Konto getrennt", + "netdisk_unbind_error": "{{provider}}-Konto konnte nicht getrennt werden", + "save_only_current_group": "Speichern ist nur für die aktuelle Gruppe wirksam", + "security": "Sicherheit", + "blacklist_pages": "Blacklist-Seiten", + "blacklist_placeholder": "Verbieten Sie ScriptCat die Ausführung von Skripten auf den folgenden Seiten. Mehrere Seiten durch Zeilenwechsel trennen, z.B.:\nhttps://*.example.com", + "expression_format_error": "Ausdrucksformat-Fehler", + "migration_confirm_message": "Das erneute Versuchen der Speicher-Engine-Migration wird vorhandene Daten ändern. Bitte bestätigen Sie. Details siehe: https://docs.scriptcat.org/docs/change/v0.17/", + "retry_migration": "Speicher-Engine-Migration erneut versuchen", + "sync_status": "Synchronisierungsstatus", + "interface_settings": "Benutzeroberfläche", + "select_interface_language": "Sprache der Benutzeroberfläche auswählen", + "extension_icon_badge": "Erweiterungs-Icon-Badge", + "display_type": "Anzeigetyp", + "extension_icon_badge_type": "Zahlentyp der auf dem Erweiterungs-Icon angezeigten Badge", + "background_color": "Hintergrundfarbe", + "badge_background_color_desc": "Badge-Hintergrundfarbe", + "text_color": "Textfarbe", + "badge_text_color_desc": "Badge-Textfarbe", + "badge_type_none": "Nicht anzeigen", + "badge_type_run_count": "Ausführungsanzahl", + "badge_type_script_count": "Skriptanzahl", + "script_menu": "Skriptmenü", + "display_right_click_menu": "Rechtsklickmenü anzeigen", + "display_right_click_menu_desc": "Skriptmenü im Rechtsklickmenü des Browsers anzeigen", + "expand_count": "Erweiterte Anzahl", + "auto_collapse_when_exceeds": "Automatisch einklappen, wenn diese Anzahl überschritten wird", + "script_update_check_frequency": "Häufigkeit der Skript-Aktualisierungsprüfung", + "script_auto_update_frequency": "Frequenz der automatischen Skript-Update-Prüfung", + "update_options": "Update-Optionen", + "control_script_update_behavior": "Skript-Update-Verhalten kontrollieren", + "blacklist_pages_desc": "Verbietet Skripts auf bestimmten Seiten zu laufen, unterstützt Wildcards", + "development_tools": "Entwicklertools", + "check_script_code_quality": "Skriptcode-Qualität und Fehler prüfen", + "custom_eslint_rules_config": "Benutzerdefinierte ESLint-Regelkonfiguration (JSON-Format)", + "script_run_env": { + "title": "Laufzeitumgebung", + "all": "Alle Bezeichnungen", + "normal-tabs": "Normale Tabs", + "incognito-tabs": "Inkognito-Tabs" + }, + "script_run_at": { + "title": "Ausführungszeitpunkt" + }, + "script_setting": { + "title": "Skript-Einstellungen", + "default": "default" + }, + "notification": { + "script_sync_delete": "Skript-Löschsynchronisation", + "script_sync_delete_desc": "Skript {{scriptName}} wurde gelöscht", + "subscribe_update": "Abonnement {{subscribeName}} wurde aktualisiert", + "subscribe_update_desc": "Neue Skripte: {{newScripts}}\nGelöschte Skripte: {{deletedScripts}}" + }, + "enable_background": { + "title": "Hintergrundausführung aktivieren", + "description": "Wenn aktiviert, bleibt der Browser auch nach dem Schließen aller Fenster im Hintergrund aktiv und minimiert sich in den Tray, bis Sie den Browser manuell beenden. Dadurch können Hintergrundskripte weiterhin ausgeführt werden.", + "enable_failed": "Aktivierung fehlgeschlagen", + "disable_failed": "Deaktivierung fehlgeschlagen", + "prompt_title": "Hintergrundausführung aktivieren?", + "prompt_description": "Dies ist ein {{scriptType}}. Die Aktivierung der Hintergrundausführung ermöglicht es dem Skript, nach dem Schließen des Browsers weiter zu laufen.", + "enable_now": "Jetzt aktivieren", + "maybe_later": "Vielleicht später", + "settings_hint": "Sie können diese Option jederzeit in den Einstellungen ändern." + }, + "favicon_service": "Favicon-Dienst", + "favicon_service_desc": "Dienst zum Abrufen von Website-Symbolen auswählen", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_google": "Google", + "favicon_service_duckduckgo": "DuckDuckGo", + "favicon_service_icon-horse": "Icon Horse", + "favicon_service_local": "Lokal abrufen", + "cloud_sync_account_verification": "Cloud-Sync-Kontoinformationen werden überprüft...", + "cloud_sync_verification_failed": "Cloud-Sync-Kontoinformationen-Überprüfung fehlgeschlagen" +} diff --git a/src/locales/de-DE/tools.json b/src/locales/de-DE/tools.json new file mode 100644 index 000000000..f2950bb29 --- /dev/null +++ b/src/locales/de-DE/tools.json @@ -0,0 +1,17 @@ +{ + "development_tool": "Entwicklungstool", + "vscode_url": "VSCode-Adresse", + "auto_connect_vscode_service": "Automatisch mit VSCode-Service verbinden", + "connect": "Verbinden", + "connection_success": "Verbindung erfolgreich", + "connection_failed": "Verbindung fehlgeschlagen", + "select_import_script": "Bitte wählen Sie das zu importierende Skript auf der neuen Seite aus", + "import_error": "Import-Fehler", + "pulling_data_from_cloud": "Daten werden aus der Cloud abgerufen", + "pull_failed": "Abrufen fehlgeschlagen", + "restore": "Wiederherstellen", + "local_backup": "Lokale Sicherung", + "cloud_backup": "Cloud-Sicherung", + "auto_backup": "Automatische Sicherung", + "data_migration": "Datenmigration" +} diff --git a/src/locales/de-DE/translation.json b/src/locales/de-DE/translation.json deleted file mode 100644 index 745ed4d5e..000000000 --- a/src/locales/de-DE/translation.json +++ /dev/null @@ -1,770 +0,0 @@ -{ - "sentence-separator": ". ", - "import_link": "Link-Import", - "import_link_failure": "Link-Import fehlgeschlagen", - "create_user_script": "Neues Benutzerskript erstellen", - "create_background_script": "Neues Hintergrundskript erstellen", - "create_scheduled_script": "Neues geplantes Skript erstellen", - "import_by_local": "Lokaler Import", - "import_local_failure": "Lokaler Import fehlgeschlagen", - "import_local_success": "Lokaler Import erfolgreich", - "create_script": "Neues Skript erstellen", - "user_guide": "Benutzerhandbuch", - "api_docs": "API-Dokumentation", - "development_guide": "Entwicklungshandbuch", - "script_gallery": "Skript-Galerie", - "community_forum": "Community-Forum", - "external_links": "Externe Links", - "system_follow": "System folgen", - "no_data": "Keine Daten", - "installed_scripts": "Installierte Skripte", - "subscribe": "Abonnieren", - "logs": "Protokolle", - "tools": "Werkzeuge", - "find": "Suchen", - "replace": "Ersetzen", - "settings": "Einstellungen", - "hide_main_sidebar": "Seitenleiste einklappen", - "show_main_sidebar": "Seitenleiste ausklappen", - "guide": "Einsteigeranleitung", - "helpcenter": "Hilfezentrum", - "general": "Allgemein", - "language": "Sprache", - "help_translate": "Bei der Übersetzung helfen", - "script_sync": "Skript-Synchronisation", - "sync_delete": "Löschen synchronisieren", - "sync_delete_desc": "Wenn aktiviert, wird das Skript beim Löschen als gelöscht markiert und andere Geräte erkennen diesen Status und löschen das Skript ebenfalls. Wenn deaktiviert, wird das Skript direkt aus dem lokalen und Cloud-Speicher gelöscht, was zu wiederholten Synchronisierungsproblemen führen kann, wenn mehrere Geräte verwendet werden.", - "enable_script_sync_to": "Skript-Synchronisation aktivieren zu", - "save": "Speichern", - "save_as": "Speichern unter", - "file": "Datei", - "run": "Ausführen", - "debug": "Debuggen", - "cloud_sync_account_verification": "Cloud-Sync-Kontoinformationen werden überprüft...", - "cloud_sync_verification_failed": "Cloud-Sync-Kontoinformationen-Überprüfung fehlgeschlagen", - "save_success": "Erfolgreich gespeichert", - "reset_success": "Erfolgreich zurückgesetzt", - "update": "Aktualisieren", - "check_update": "Nach Updates suchen", - "script_subscription_check_interval": "Skript-/Abonnement-Update-Überprüfungsintervall", - "never": "Nie", - "6_hours": "6 Stunden", - "12_hours": "12 Stunden", - "every_day": "Täglich", - "every_week": "Wöchentlich", - "update_disabled_scripts": "Deaktivierte Skripte aktualisieren", - "silent_update_non_critical_changes": "Nicht kritische Änderungen still aktualisieren", - "enable_eslint": "ESLint aktivieren", - "eslint_rules": "ESLint-Regeln", - "enter_eslint_rules": "Bitte geben Sie ESLint-Regeln ein. Sie können die Konfiguration von https://eslint.org/play/ herunterladen", - "language_change_tip": "Sprachwechsel erfolgreich", - "backup": "Sicherung", - "local": "Lokal", - "export_file": "Datei exportieren", - "import_file": "Datei importieren", - "cloud": "Cloud", - "backup_to": "Sichern zu", - "preparing_backup": "Sicherung in die Cloud wird vorbereitet", - "backup_success": "Sicherung erfolgreich", - "backup_failed": "Sicherung fehlgeschlagen", - "no_backup_files": "Keine Sicherungsdateien", - "backup_list": "Sicherungsliste", - "open_backup_dir": "Sicherungsverzeichnis öffnen", - "confirm_delete": "Löschen bestätigen", - "confirm_delete_backup_file": "Löschen der Sicherungsdatei bestätigen", - "confirm_update": "Update bestätigen", - "delete_success": "Erfolgreich gelöscht", - "deleting": "Wird gelöscht", - "backup_strategy": "Sicherungsstrategie", - "under_construction": "In Entwicklung", - "development_tool": "Entwicklungstool", - "vscode_url": "VSCode-Adresse", - "auto_connect_vscode_service": "Automatisch mit VSCode-Service verbinden", - "connect": "Verbinden", - "connection_success": "Verbindung erfolgreich", - "connection_failed": "Verbindung fehlgeschlagen", - "select_import_script": "Bitte wählen Sie das zu importierende Skript auf der neuen Seite aus", - "import_error": "Import-Fehler", - "pulling_data_from_cloud": "Daten werden aus der Cloud abgerufen", - "pull_failed": "Abrufen fehlgeschlagen", - "restore": "Wiederherstellen", - "log_title": "Ausführungsprotokoll", - "last_5_minutes": "Letzte 5 Minuten", - "last_15_minutes": "Letzte 15 Minuten", - "last_30_minutes": "Letzte 30 Minuten", - "last_1_hour": "Letzte 1 Stunde", - "last_3_hours": "Letzte 3 Stunden", - "last_6_hours": "Letzte 6 Stunden", - "last_12_hours": "Letzte 12 Stunden", - "last_24_hours": "Letzte 24 Stunden", - "last_7_days": "Letzte 7 Tage", - "query": "Abfrage", - "labels": "Kennzeichnungen", - "search_regex": "Suche (Regex unterstützt)", - "clean_schedule": "Geplante Bereinigung", - "days_ago_logs": "Tage alte Protokolle", - "delete_completed": "Löschen abgeschlossen", - "delete_current_logs": "Aktuelle Protokolle löschen", - "clear_completed": "Bereinigung abgeschlossen", - "clear_logs": "Protokolle bereinigen", - "to": " - ", - "now": "Jetzt", - "total_logs": "{{length}} Protokolle gefunden", - "filtered_logs": "Nach Filterung {{length}} Protokolle", - "enter_filter_conditions": "Bitte Filterbedingungen eingeben", - "permission": "Berechtigung", - "enter_subscribe_name": "Bitte geben Sie den Abonnement-Namen ein", - "subscribe_url": "Abonnement-URL", - "confirm_delete_subscription": "Möchten Sie dieses Abonnement wirklich löschen? Zugehörige Skripte werden ebenfalls gelöscht", - "list": { - "confirm_delete": "Möchten Sie wirklich löschen? Beachten Sie, dass diese Operation nicht rückgängig gemacht werden kann!", - "confirm_update": "Möchten Sie wirklich aktualisieren? Beachten Sie, dass diese Operation nicht rückgängig gemacht werden kann!" - }, - "enable": "Aktivieren", - "script_list_enable_width": 120, - "script_list_last_updated_width": 120, - "script_list_apply_to_run_status_width": 140, - "subscribe_list_enable_width": 100, - "disable": "Deaktivieren", - "name": "Name", - "version": "Version", - "apply_to_run_status": "Anwenden auf / Ausführungsstatus", - "source": "Quelle", - "home": "Startseite", - "sorting": "Sortierung", - "last_updated": "Zuletzt aktualisiert", - "action": "Aktion", - "foreground_page_script_tooltip": "Vordergrund-Seitenskript, wird auf angegebenen Seiten ausgeführt", - "background_script_tooltip": "Hintergrundskript, wird im Hintergrund ausgeführt, wenn aktiviert", - "scheduled_script_tooltip": "Geplantes Skript, nächste Ausführungszeit:", - "running": "Wird ausgeführt", - "completed": "Ausführung abgeschlossen", - "source_subscribe_link": "Abonnement-Link", - "source_local_script": "Lokales Skript", - "source_script_link": "Skript-Link", - "by_manual_creation": "Lokal durch Code-Bearbeitung erstellt", - "confirm_delete_script": "Möchten Sie dieses Skript wirklich löschen?", - "confirm_delete_script_content": "Möchten Sie das Skript „{{name}}“ wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", - "delete_failed": "Löschen fehlgeschlagen", - "enter_script_name": "Bitte geben Sie den Skriptnamen ein", - "update_not_supported": "Dieses Skript unterstützt keine Update-Überprüfung", - "checking_for_updates": "Suche nach Updates...", - "new_version_available": "Neue Version verfügbar", - "latest_version": "Neueste Version", - "checked_for_all_selected": "Alle ausgewählten Skripte auf Updates überprüft", - "update_check_failed": "Update-Überprüfung fehlgeschlagen", - "script_import_failed": "Skriptimport fehlgeschlagen", - "install_page_open_failed": "Installationsseite konnte nicht geöffnet werden", - "stopping_script": "Skript wird gestoppt", - "script_stopped": "Skript gestoppt", - "starting_script": "Skript wird gestartet...", - "starting_updates": "Batch-Update wird gestartet...", - "script_started": "Skript gestartet", - "operation_failed": "Vorgang fehlgeschlagen", - "batch_operations": "Batch-Operationen", - "export": "Exportieren", - "delete": "Löschen", - "pin_to_top": "Klebrig", - "scripts_pinned_to_top": "Das ausgewählte Skript wurde angepinnt", - "unknown_operation": "Unbekannte Operation", - "confirm": "Bestätigen", - "close": "Schließen", - "page_script": "Seitenskript", - "homepage": "Skript-Startseite", - "script_website": "Skript-Website", - "script_source": "Skript-Quellcode", - "bug_feedback_script_support": "Bug-Feedback / Skript-Support-Website", - "config": "Konfiguration", - "key": "key", - "value": "value", - "add": "Hinzufügen", - "type": "Typ", - "edit_value": "Wert bearbeiten", - "add_value": "Wert hinzufügen", - "update_success": "Änderung erfolgreich", - "add_success": "Hinzufügen erfolgreich", - "script_storage": "Skript-Speicher", - "enter_key": "Bitte key eingeben", - "key_placeholder": "key", - "value_placeholder": "Wenn der Typ object ist, geben Sie Daten ein, die als JSON geparst werden können", - "clear": "Bereinigen", - "clear_success": "Bereinigung erfolgreich", - "confirm_clear": "Möchten Sie wirklich diesen Speicherplatz bereinigen?", - "type_string": "string", - "type_number": "number", - "type_boolean": "boolean", - "type_object": "object", - "confirm_delete_resource": "Möchten Sie diese Ressource wirklich löschen? Beim nächsten Start wird diese Ressource neu geladen", - "confirm_clear_resource": "Möchten Sie wirklich diese Ressourcen bereinigen? Beim nächsten Start werden die Ressourcen neu geladen", - "script_resource": "Skript-Ressource", - "permission_value": "Berechtigungswert", - "allow": "Erlauben", - "yes": "Ja", - "no": "Nein", - "confirm_delete_permission": "Sind Sie sicher, dass Sie diese Berechtigung löschen möchten?", - "basic_info": "Grundinformationen", - "update_url": "Update-URL", - "permission_management": "Berechtigungsverwaltung", - "add_permission": "Berechtigung hinzufügen", - "permission_cors": "Cross-Origin (CORS)", - "permission_cookie": "Cookies verwalten", - "match": "Übereinstimmung", - "user_setting": "Benutzereinstellungen", - "confirm_delete_exclude": "Diese Ausnahme löschen bestätigen?", - "after_deleting_match_item": "Nach dem Löschen der Skript-Übereinstimmungseinträge werden sie automatisch zu den Übereinstimmungseinträgen hinzugefügt", - "confirm_delete_match": "Diese Übereinstimmung löschen bestätigen?", - "after_deleting_exclude_item": "Nach dem Löschen der Skript-Übereinstimmungseinträge werden sie automatisch zu den Ausnahmeeinträgen hinzugefügt", - "add_match": "Übereinstimmung hinzufügen", - "add_exclude": "Ausnahme hinzufügen", - "website_match": "Website-Übereinstimmung (@match)", - "reset": "Zurücksetzen", - "website_exclude": "Website-Ausnahme (@exclude)", - "confirm_reset": "Zurücksetzen bestätigen?", - "script_total_runs": "Dieses Skript wurde insgesamt {{runNum}} Mal ausgeführt, {{runNumByIframe}} Mal in iframes", - "script_total_runs_single": "Dieses Skript wurde {{runNum}} Mal ausgeführt", - "script_disabled": "Dieses Skript ist nicht aktiviert", - "run_once": "Einmal ausführen", - "stop": "Stoppen", - "edit": "Bearbeiten", - "undo": "Rückgängig", - "redo": "Wiederherstellen", - "cut": "Ausschneiden", - "copy": "Kopieren", - "paste": "Einfügen", - "format": "Formatieren", - "exclude_on": "Wiederherstellen auf $0 zur Ausführung", - "exclude_off": "Ausschließen auf $0 zur Ausführung", - "user_config": "Benutzerkonfiguration", - "gm_api": "GM API", - "storage_api": "Speicher-API", - "use_file_system": "Verwendetes Dateisystem", - "open_directory": "Verzeichnis öffnen", - "account_validation_failed": "Kontoinformationen-Validierung fehlgeschlagen", - "not_set": "Nicht eingestellt", - "in_use": "Aktiviert", - "storage_error": "Speicherfehler", - "upload_to_cloud": "In die Cloud hochladen", - "save_failed": "Speichern fehlgeschlagen", - "exporting": "Wird exportiert...", - "upload_to": "Hochladen zu", - "value_export_expression": "Wert-Export-Ausdruck", - "overwrite_original_value_on_import": "Originalwert beim Import überschreiben", - "cookie_export_expression": "Cookie-Export-Ausdruck", - "overwrite_original_cookie_on_import": "Originalwert beim Import überschreiben", - "restore_default_values": "Standardwerte wiederherstellen", - "get_confirm_error": "Bestätigungsinformationen abrufen fehlgeschlagen", - "confirm_error": "Bestätigung fehlgeschlagen", - "ignore": "Ignorieren", - "allow_once": "Einmal erlauben", - "temporary_allow": "Diese {{permissionContent}} temporär erlauben", - "temporary_allow_all": "Alle {{permissionContent}} temporär erlauben", - "permanent_allow": "Diese {{permissionContent}} dauerhaft erlauben", - "permanent_allow_all": "Alle {{permissionContent}} dauerhaft erlauben", - "deny_once": "Einmal verweigern", - "temporary_deny": "Diese {{permissionContent}} temporär verweigern", - "temporary_deny_all": "Alle {{permissionContent}} temporär verweigern", - "permanent_deny": "Diese {{permissionContent}} dauerhaft verweigern", - "permanent_deny_all": "Alle {{permissionContent}} dauerhaft verweigern", - "data_import": "Datenimport", - "import": "Importieren", - "select_scripts_to_import": "Bitte wählen Sie die zu importierenden Skripte aus", - "select_all": "Alle auswählen", - "script_import_progress": "Skript-Import-Fortschritt", - "select_subscribes_to_import": "Bitte wählen Sie die zu importierenden Abonnements aus", - "subscribe_import_progress": "Abonnement-Import-Fortschritt", - "author": "Autor", - "description": "Beschreibung", - "operation": "Vorgang", - "error": "Fehler", - "unknown": "Unbekannt", - "add_new": "Neu hinzufügen", - "no_operation": "Keine Operation", - "enable_script": "Skript aktivieren", - "local_creation": "Lokale Erstellung", - "import_success": "Import erfolgreich", - "install_script": "Installieren", - "update_script": "Aktualisieren", - "install_subscribe": "Abonnement installieren", - "update_subscribe": "Abonnement aktualisieren", - "update_script_no_close": "Aktualisieren ohne das Fenster zu schließen", - "install_script_no_close": "Installieren, ohne das Fenster zu schließen", - "update_script_no_more_update": "Aktualisieren, aber nicht mehr nach Updates suchen", - "close_update_script_no_more_update": "Schließen und nicht nach Updates suchen", - "install_script_no_more_update": "Installieren, aber nicht mehr nach Updates suchen", - "invalid_link": "Ungültiger Link", - "subscribe_install_label": "Dieses Abonnement wird die folgenden Skripte installieren", - "script_runs_in": "Skript wird auf den folgenden Websites ausgeführt", - "script_has_full_access_to": "Skript erhält vollständigen Zugriff auf die folgenden Adressen", - "script_requires": "Skript referenziert die folgenden externen Ressourcen", - "cookie_warning": "Achtung: Dieses Skript beantragt Cookie-Operationsberechtigung. Dies ist eine gefährliche Berechtigung, bitte stellen Sie die Sicherheit des Skripts sicher.", - "cron_oncetype": { - "minute": "{{next}} (jede Minute ausgeführt)", - "hour": "{{next}} (jede Stunde ausgeführt)", - "day": "{{next}} (jeden Tag ausgeführt)", - "month": "{{next}} (jeden Monat ausgeführt)", - "week": "{{next}} (jede Woche ausgeführt)" - }, - "cron_invalid_expr": "Ungültiger Cron-Ausdruck", - "scheduled_script_description_title": "Dies ist ein geplantes Skript. Wenn aktiviert, wird es zu bestimmten Zeiten automatisch ausgeführt und kann im Panel manuell gesteuert werden.", - "scheduled_script_description_description_expr": "Geplante Aufgaben-Ausdruck", - "scheduled_script_description_description_next": "Letzte Ausführungszeit:", - "background_script_description": "Dies ist ein Hintergrundskript. Wenn aktiviert, wird es automatisch einmal ausgeführt, wenn der Browser geöffnet wird, und kann im Panel manuell gesteuert werden.", - "install_success": "Installation erfolgreich", - "install": { - "update_success": "Update erfolgreich" - }, - "install_failed": "Installation fehlgeschlagen", - "subscribe_success": "Abonnement erfolgreich", - "subscribe_failed": "Abonnement fehlgeschlagen", - "current_version": "Aktuelle Version", - "update_version": "Update-Version", - "updatepage": { - "main_header": "Nach Updates suchen", - "header_site_specific": "Updates für diese Website verfügbar ($0)", - "header_site_all": "Updates verfügbar", - "header_ignored": "Verfügbare, aber ignorierte Updates", - "update_all": "Alle aktualisieren", - "ignore_all": "Alle ignorieren", - "update": "Aktualisieren", - "ignore": "Ignorieren", - "old_version_": "Alte Version: ", - "new_version_": "Neue Version: ", - "enabled": "Aktiviert", - "tooltip_enabled": "Dieses Skript ist aktiviert.", - "disabled": "Deaktiviert", - "tooltip_disabled": "Dieses Skript ist deaktiviert.", - "similarity_": "Ähnlichkeit: ", - "codechange_major": "Große Änderung", - "codechange_noticeable": "Auffällige Änderung", - "codechange_tiny": "Kleine Änderung", - "new_connects_": "Neue @connect: ", - "tag_new_connect": "@connect hinzugefügt", - "status_last_check": "Letzte Überprüfung: $0", - "status_checking_updates": "Suche nach Updates...", - "status_no_update": "Keine Updates erforderlich", - "status_n_update": "$0 Updates erforderlich", - "status_n_ignored": "$0 Updates ignoriert", - "status_autoclose": "Automatisches Schließen in $0 Sekunden", - "header_other_update": "Andere verfügbare Updates" - }, - "downloading_status_text": "Wird heruntergeladen. {{bytes}} empfangen.", - "install_page_please_wait": "Bitte warten Sie", - "install_page_loading": "Installationsseite wird geladen", - "install_page_load_failed": "Installationsseite konnte nicht geladen werden", - "invalid_page": "Ungültige Seite", - "background_script_tag": "Dies ist ein Hintergrundskript", - "scheduled_script_tag": "Dies ist ein geplantes Skript", - "background_script": "Hintergrundskript", - "scheduled_script": "Geplantes Skript", - "install_from_legitimate_sources_warning": "Bitte installieren Sie Skripte aus legitimen Quellen! Unbekannte Skripte können Ihre Privatsphäre verletzen oder bösartige Operationen durchführen!", - "antifeature_referral_link_title": "Empfehlungslink", - "antifeature_referral_link_description": "Dieses Skript modifiziert oder leitet zu den Affiliate-Links des Autors um", - "antifeature_ads_title": "Mit Werbung", - "antifeature_ads_description": "Dieses Skript fügt Werbung auf den von Ihnen besuchten Seiten ein", - "antifeature_payment_title": "Bezahltes Skript", - "antifeature_payment_description": "Dieses Skript erfordert eine Zahlung für die normale Nutzung", - "antifeature_miner_title": "Kryptomining", - "antifeature_miner_description": "Dieses Skript hat Mining-Verhalten", - "antifeature_membership_title": "Mitgliedschaftsfunktion", - "antifeature_membership_description": "Dieses Skript erfordert eine Mitgliedschaftsregistrierung für die normale Nutzung", - "antifeature_tracking_title": "Informationsverfolgung", - "antifeature_tracking_description": "Dieses Skript verfolgt Ihre Benutzerinformationen", - "script_info_load_failed": "Skript-Informationen laden fehlgeschlagen!", - "script_status_tooltip": "Sie können den Aktivierungsstatus von Skripten steuern. Gewöhnliche Tampermonkey-Skripte sind standardmäßig aktiviert, während Hintergrund- und geplante Skripte standardmäßig deaktiviert sind.", - "subscribe_source_tooltip": "Dies ist eine Abonnementquelle. Wenn Sie das Abonnement öffnen, wird das Abonnementskript automatisch installiert.", - "get_script": "Skript abrufen", - "report_issue": "Bug / Problemfeedback", - "project_docs": "Projektdokumentation", - "community": "Gemeinschaft", - "popup": { - "new_version_available": "Neue Version verfügbar" - }, - "current_page_scripts": "Aktuelle Seiten-Ausführungsskripte", - "enabled_background_scripts": "Aktivierte und laufende Hintergrundskripte", - "script_accessing_cross_origin_resource": "Skript versucht auf Cross-Origin-Ressourcen zuzugreifen", - "confirm_operation_description": "Bitte bestätigen Sie, ob Sie dem Skript erlauben, diese Operation durchzuführen. Das Skript kann auch ein @connect-Tag hinzufügen, um diese Option zu überspringen", - "extension_site_access_title": "ScriptCat benötigt Website-Zugriff", - "extension_site_access_description": "Gewähren Sie dem Browser Website-Zugriff auf diesen Ursprung, damit ScriptCat die Anfrage ausführen kann. Dadurch wird die Website-Zugriffseinstellung der Erweiterung geändert.", - "extension_site_access_content": "Website", - "domain": "Domain", - "script_name": "Skriptname", - "request_domain": "Anfrage-Domain", - "request_url": "Anfrage-URL", - "access_cookie_content": "Skript versucht auf Website-Cookie-Inhalte zuzugreifen", - "confirm_script_operation": "Bitte bestätigen Sie, ob Sie dem Skript erlauben, diese Operation durchzuführen. Cookies sind wichtige Benutzerdaten, bitte autorisieren Sie nur vertrauenswürdige Skripte.", - "cookie_domain": "Cookie-Domain", - "script_operation_title": "Skript versucht, auf den Speicherplatz zuzugreifen", - "script_operation_description": "Bitte bestätigen Sie, ob Sie dem Skript erlauben, diese Operation durchzuführen. Nach der Erlaubnis kann das Skript den von Ihnen eingestellten Speicherplatz bearbeiten. Das Skript erstellt ein app/${dir}-Verzeichnis unter dem Speicherplatz zur Verwendung", - "script_permission_content": "Skript", - "sync_system_connect_failed": "Synchronisationssystem-Verbindung fehlgeschlagen", - "sync_system_closed": "Synchronisierung ist deaktiviert", - "sync_system_closed_description": "Die Synchronisierungsfunktion ist deaktiviert. Bitte neu konfigurieren.", - "auth_type": "Authentifizierungstyp", - "url": "URL", - "username": "Benutzername", - "password": "Passwort", - "access_token_bearer": "Zugriffstoken (Bearer)", - "s3_bucket_name": "Bucket-Name", - "s3_region": "Region", - "s3_access_key_id": "Zugriffs-Schlüssel-ID", - "s3_secret_access_key": "Geheimer Zugriffsschlüssel", - "s3_custom_endpoint": "Benutzerdefinierter Endpunkt (optional)", - "skip": "Überspringen", - "next": "Weiter", - "next_with_progress": "Nächster Schritt (Schritt {step} von {steps})", - "back": "Zurück", - "last": "Fertig", - "start_guide_title": "Willkommen bei der ScriptCat-Erweiterung", - "start_guide_content": "Als nächstes stellen wir Ihnen die grundlegende Verwendung von ScriptCat vor", - "guide_installed_scripts": "Ihre installierten Skripte werden hier angezeigt", - "guide_script_list_title": "Skript-Markt", - "guide_script_list_content": "Sie können Skripte aus dem Skript-Markt installieren. ScriptCat unterstützt neben Benutzerskripten auch Hintergrundskripte", - "guide_script_list_enable_title": "Skript-Aktivierung", - "guide_script_list_enable_content": "Skripte müssen aktiviert werden, um verwendet zu werden. Seitenskripte sind bei der Installation standardmäßig aktiviert, Hintergrundskripte sind bei der Installation standardmäßig deaktiviert", - "guide_script_list_apply_to_run_status_title": "Anwenden auf und Ausführungsstatus", - "guide_script_list_apply_to_run_status_content": "Zeigt den Ausführungsstatus von Skripten an. Bewegen Sie die Maus über das Tag, um den Skripttyp zu sehen", - "guide_script_list_sort_title": "Sortierung", - "guide_script_list_sort_content": "Sie können die Skriptbezeichnungen per Drag & Drop sortieren.", - "guide_script_list_update_title": "Zuletzt aktualisiert", - "guide_script_list_update_content": "Durch Klicken auf die Spaltenüberschrift „Zuletzt aktualisiert“ wird geprüft, ob es Aktualisierungen des Skripts gibt.", - "guide_script_list_action_title": "Vorgang", - "guide_script_list_action_content": "Die Bedienleiste ermöglicht den Zugriff auf die Skriptbearbeitung, die Steuerung der Skriptausführung und -beendigung (Hintergrundskripte) sowie auf die Einstellungen (siehe UserConfig). Über die Schaltflächen auf der rechten Seite können Sie auf erweiterte Filterfunktionen zugreifen und zwischen verschiedenen Ansichtsmodi wechseln.", - "guide_tools_title": "Häufig verwendete Werkzeuge", - "guide_tools_content": "Werkzeuge bieten Backup- und Entwicklungstools", - "guide_tools_backup_title": "Sicherung", - "guide_tools_backup_content": "Backups können Skripte speichern und Verluste vermeiden. Sie können Skriptdateien lokal exportieren oder lokale Skriptdateien laden. Sie können auch in die Cloud sichern, was praktischer ist.", - "guide_setting_title": "Einstellungen", - "guide_setting_content": "Einstellungen enthalten hauptsächlich häufig verwendete Einstellungsoptionen wie Sprache, Skript-Synchronisation, Update-Frequenz", - "guide_setting_sync_title": "Update und Synchronisation", - "guide_setting_sync_content": "Die Skript-Synchronisationsfunktion kann bequem den Skriptinhalt dieses Geräts in die Cloud synchronisieren. Wenn Sie mehrere Geräte haben, aktivieren Sie bitte die Option zum synchronisierten Löschen. Wenn Skripte auf diesem Gerät gelöscht werden, werden entsprechende Skripte aus der Cloud gelöscht und auch Skripte auf anderen Geräten gelöscht.", - "auto": "Automatisch", - "hide": "Verbergen", - "custom": "Benutzerdefiniert", - "resize_column_width": "Spaltenbreite anpassen", - "collapse": "Einklappen", - "expand": "Erweitern", - "menu_expand_num_before": "Wenn Menüelemente mehr als", - "menu_expand_num_after": "sind, automatisch verbergen", - "script_name_cannot_be_set_to_empty": "Skriptname kann nicht leer gesetzt werden", - "edit_conflict": "Bearbeitungskonflikt", - "confirm_override_when_edit_conflict": "Dieses Skript wurde in einer anderen Instanz bearbeitet. Beim Ersetzen werden diese Änderungen überschrieben. Möchten Sie stattdessen diese Version behalten?", - "save_abort_when_edit_conflict": "Dieses Skript wurde in einer anderen Instanz bearbeitet. Speichern abgebrochen.", - "scriptname_conflict": "Skriptname-Konflikt", - "confirm_save_when_scriptname_conflict": "Dieser Skriptname wird bereits von einem anderen Skript verwendet. Möchten Sie ihn trotzdem speichern?", - "save_abort_when_scriptname_conflict": "Dieser Skriptname wird bereits von einem anderen Skript verwendet. Speichern abgebrochen.", - "eslint_config_format_error": "eslint-Konfigurationsformat-Fehler", - "export_success": "Export erfolgreich", - "get_backup_dir_url_failed": "Backup-Verzeichnis-Adresse abrufen fehlgeschlagen", - "get_backup_files_failed": "Backup-Dateien abrufen fehlgeschlagen", - "request_permission": "Beantragen Sie Genehmigungen", - "develop_mode_guide": "Der 'Entwicklermodus' ist derzeit nicht aktiviert, daher können die Skripte nicht richtig ausgeführt werden. 👉Hier klicken, um zu erfahren, wie man ihn aktiviert", - "allow_user_script_guide": "'Nutzerskripts zulassen' ist derzeit nicht aktiviert, daher können die Skripte nicht richtig ausgeführt werden. 👉Hier klicken, um zu erfahren, wie man es aktiviert", - "lower_version_browser_guide": "Ihr Browser ist zu veraltet, daher können die Skripte nicht richtig ausgeführt werden. 👉Hier klicken, um mehr zu erfahren", - "click_to_reload": "👉Zum Neuladen klicken", - "page_in_blacklist": "Die aktuelle Seite ist auf der Blacklist und kann keine Skripte verwenden", - "baidu_netdisk": "Baidu Netdisk", - "netdisk_unbind": "{{provider}} trennen", - "netdisk_unbind_confirm": "Das {{provider}}-Konto trennen?", - "netdisk_unbind_success": "{{provider}}-Konto getrennt", - "netdisk_unbind_error": "{{provider}}-Konto konnte nicht getrennt werden", - "save_only_current_group": "Speichern ist nur für die aktuelle Gruppe wirksam", - "script_import_result": "Skript-Import-Ergebnis", - "failure_info": "Fehlerinformationen", - "security": "Sicherheit", - "blacklist_pages": "Blacklist-Seiten", - "blacklist_placeholder": "Verbieten Sie ScriptCat die Ausführung von Skripten auf den folgenden Seiten. Mehrere Seiten durch Zeilenwechsel trennen, z.B.:\nhttps://*.example.com", - "expression_format_error": "Ausdrucksformat-Fehler", - "migration_confirm_message": "Das erneute Versuchen der Speicher-Engine-Migration wird vorhandene Daten ändern. Bitte bestätigen Sie. Details siehe: https://docs.scriptcat.org/docs/change/v0.17/", - "retry_migration": "Speicher-Engine-Migration erneut versuchen", - "script_modified_leave_confirm": "Ihre Änderungen wurden noch nicht gespeichert. Wenn Sie die Seite verlassen, gehen die Änderungen verloren. Möchten Sie die Seite wirklich verlassen?", - "create_success_note": "Erstellung erfolgreich. Beachten Sie, dass Hintergrundskripte standardmäßig nicht aktiviert werden", - "save_as_failed": "Speichern unter fehlgeschlagen", - "save_as_success": "Speichern unter erfolgreich", - "only_background_scheduled_can_run": "Nur Hintergrund-/geplante Skripte können ausgeführt werden", - "preparing_script_resources": "Skript-Ressourcen werden vorbereitet...", - "build_success_message": "Build erfolgreich. Sie können die Entwicklertools auf der Erweiterungsseite öffnen und die Ausgabe in der Konsole anzeigen", - "script_storage_tooltip": "Kann die gespeicherten Skriptdaten verwalten (GM_value)", - "script_resource_tooltip": "Verwalte mit @resource, @require heruntergeladene Ressourcen", - "script_setting_tooltip": "Führe einige benutzerdefinierte Einstellungen für das Skript durch", - "script_modified_close_confirm": "Skript wurde geändert. Das Schließen führt zum Verlust der Änderungen. Fortfahren?", - "close_current_tab": "Aktuellen Tab schließen", - "close_other_tabs": "Andere Tabs schließen", - "close_left_tabs": "Linke Tabs schließen", - "close_right_tabs": "Rechte Tabs schließen", - "import_script_placeholder": "Unterstützt die Eingabe von absoluten Links von Skripten, die mit .user.js enden, oder ScriptCat-Installationsseiten-Links\nMehrzeilige Eingabe möglich, eine pro Zeile\nBeispiel:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/zh-CN/script-show-page/1234", - "invalid_script_code": "Ungültiger Skriptcode", - "build_failed": "Build fehlgeschlagen", - "drag_script_here_to_upload": "Skript hierher ziehen zum Hochladen", - "sync_status": "Synchronisierungsstatus", - "search_scripts": "Suchskripte", - "ext_update_notification": "ScriptCat-Erweiterung wurde aktualisiert", - "ext_update_notification_desc": "Aktuelle Version: {{version}}, Details finden Sie im Changelog", - "watch_file_description": "Überwachen Sie Dateiänderungen und aktualisieren Sie Skripte automatisch. Beim Gebrauch stellen Sie sicher, dass der Skriptdateipfad unverändert bleibt und die Seite nicht geschlossen werden kann", - "watch_file": "Datei überwachen", - "stop_watch_file": "Überwachung stoppen", - "script_menu_display": "Von Skript registrierte Menüs", - "badge_type_none": "Nicht anzeigen", - "badge_type_run_count": "Ausführungsanzahl", - "badge_type_script_count": "Skriptanzahl", - "interface_settings": "Benutzeroberfläche", - "select_interface_language": "Sprache der Benutzeroberfläche auswählen", - "extension_icon_badge": "Erweiterungs-Icon-Badge", - "display_type": "Anzeigetyp", - "extension_icon_badge_type": "Zahlentyp der auf dem Erweiterungs-Icon angezeigten Badge", - "background_color": "Hintergrundfarbe", - "badge_background_color_desc": "Badge-Hintergrundfarbe", - "text_color": "Textfarbe", - "badge_text_color_desc": "Badge-Textfarbe", - "script_menu": "Skriptmenü", - "display_right_click_menu": "Rechtsklickmenü anzeigen", - "display_right_click_menu_desc": "Skriptmenü im Rechtsklickmenü des Browsers anzeigen", - "expand_count": "Erweiterte Anzahl", - "auto_collapse_when_exceeds": "Automatisch einklappen, wenn diese Anzahl überschritten wird", - "script_update_check_frequency": "Häufigkeit der Skript-Aktualisierungsprüfung", - "script_auto_update_frequency": "Frequenz der automatischen Skript-Update-Prüfung", - "update_options": "Update-Optionen", - "control_script_update_behavior": "Skript-Update-Verhalten kontrollieren", - "blacklist_pages_desc": "Verbietet Skripts auf bestimmten Seiten zu laufen, unterstützt Wildcards", - "development_tools": "Entwicklertools", - "check_script_code_quality": "Skriptcode-Qualität und Fehler prüfen", - "custom_eslint_rules_config": "Benutzerdefinierte ESLint-Regelkonfiguration (JSON-Format)", - "light": "Heller Modus", - "dark": "Dunkler Modus", - "individual_edit": "Einzelbearbeitung", - "batch_edit": "Stapelbearbeitung", - "script_code": "Skriptcode", - "enter_search_value": "Bitte geben Sie {{search}} für die Suche ein", - "script_run_env": { - "title": "Laufzeitumgebung", - "all": "Alle Bezeichnungen", - "normal-tabs": "Normale Tabs", - "incognito-tabs": "Inkognito-Tabs" - }, - "script_run_at": { - "title": "Ausführungszeitpunkt" - }, - "script_setting": { - "title": "Skript-Einstellungen", - "default": "default" - }, - "editor_config": "Editor-Konfiguration", - "editor_config_description": "Sie können sich an den compilerOptions in jsconfig.js orientieren, um die Konfiguration vorzunehmen", - "editor_type_definition": "Editor-Typdefinitionen", - "editor_type_definition_description": "Sie können Ihre eigenen Typdefinitionen anpassen, der Skript-Editor wird diese automatisch laden", - "eslint_rules_reset": "ESLint-Regeln wurden zurückgesetzt", - "eslint_rules_saved": "ESLint-Regeln wurden gespeichert", - "editor_config_reset": "Editor-Konfiguration wurde zurückgesetzt", - "editor_config_saved": "Editor-Konfiguration wurde gespeichert", - "editor_config_format_error": "Editor-Konfiguration Formatfehler", - "editor_type_definition_reset": "Editor-Typdefinitionen wurden zurückgesetzt", - "editor_type_definition_saved": "Editor-Typdefinitionen wurden gespeichert", - "script_list": { - "sidebar": { - "stopped": "Gestoppt", - "all": "Alle", - "normal_script": "Normales Skript", - "status": "Status" - } - }, - "tags": "Schlagwörter", - "install_source": "Installationsquelle", - "input_tags_placeholder": "Schlagwörter eingeben, Enter drücken um zu bestätigen", - "switch_to_card_mode": "Wechseln Sie in den Kartenmodus", - "switch_to_table_mode": "In den Tabellenmodus wechseln", - "open_sidebar": "Öffne die Seitenleiste", - "close_sidebar": "Schließe die Seitenleiste", - "no_message_content": "Kein Nachrichteninhalt", - "error_metadata_invalid": "MetaData-Block ist ungültig", - "error_script_name_required": "Skriptname ist erforderlich", - "error_script_version_required": "Skript @version ist erforderlich", - "error_script_namespace_required": "Skript @namespace ist erforderlich", - "error_cron_invalid": "Ungültiger Cron-Ausdruck: {{expr}}", - "error_script_type_mismatch": "Skripttyp stimmt nicht überein: Normale und Hintergrundskripte können nicht konvertiert werden", - "error_old_script_code_missing": "Alter Skriptcode nicht gefunden", - "error_subscribe_name_required": "Abonnementname ist erforderlich", - "error_grant_conflict": "@grant deklariert sowohl 'none' als auch GM API", - "error_metadata_line_duplicated": "In den Metadaten befinden sich doppelte Deklarationen.", - "notification": { - "script_sync_delete": "Skriptlöschung synchronisieren", - "script_sync_delete_desc": "Skript {{scriptName}} wurde gelöscht", - "subscribe_update": "Abonnement {{subscribeName}} wurde aktualisiert", - "subscribe_update_desc": "Neue Skripte: {{newScripts}}\nGelöschte Skripte: {{deletedScripts}}" - }, - "loading": "Wird geladen...", - "runtime": "Laufzeit", - "enable_background": { - "title": "Hintergrundausführung aktivieren", - "description": "Wenn aktiviert, bleibt der Browser auch nach dem Schließen aller Fenster im Hintergrund aktiv und minimiert sich in den Tray, bis Sie den Browser manuell beenden. Dadurch können Hintergrundskripte weiterhin ausgeführt werden.", - "enable_failed": "Aktivierung fehlgeschlagen", - "disable_failed": "Deaktivierung fehlgeschlagen", - "prompt_title": "Hintergrundausführung aktivieren?", - "prompt_description": "Dies ist ein {{scriptType}}. Die Aktivierung der Hintergrundausführung ermöglicht es dem Skript, nach dem Schließen des Browsers weiter zu laufen.", - "enable_now": "Jetzt aktivieren", - "maybe_later": "Vielleicht später", - "settings_hint": "Sie können diese Option jederzeit in den Einstellungen ändern." - }, - "favicon_service": "Favicon-Dienst", - "favicon_service_desc": "Dienst zum Abrufen von Website-Symbolen auswählen", - "favicon_service_scriptcat": "ScriptCat", - "favicon_service_google": "Google", - "favicon_service_duckduckgo": "DuckDuckGo", - "favicon_service_icon-horse": "Icon Horse", - "favicon_service_local": "Lokal abrufen", - "editor": { - "show_script_list": "Skriptliste anzeigen", - "hide_script_list": "Skriptliste ausblenden" - }, - "agent": "AI Agent", - "agent_chat": "Chat", - "agent_provider": "Model Service", - "agent_mcp": "MCP", - "agent_skills": "Skills", - "agent_provider_title": "Model Service", - "agent_provider_select": "AI Provider", - "agent_provider_api_base_url": "API Base URL", - "agent_provider_api_key": "API Key", - "agent_provider_model": "Default Model", - "agent_provider_test_connection": "Test Connection", - "agent_provider_test_success": "Connection Successful", - "agent_provider_test_failed": "Connection Failed", - "agent_model_fetch": "Modelle abrufen", - "agent_model_fetch_failed": "Modellliste konnte nicht abgerufen werden", - "agent_model_name": "Name", - "agent_model_add": "Add Model", - "agent_model_edit": "Edit", - "agent_model_copy": "Copy", - "agent_model_delete": "Delete", - "agent_model_set_default": "Set as Default", - "agent_model_default_label": "Default", - "agent_model_delete_confirm": "Are you sure to delete this model?", - "agent_model_max_tokens": "Max Output Tokens", - "agent_model_no_models": "No models configured", - "agent_model_vision_support": "Supports vision input", - "agent_model_image_output": "Supports image output", - "agent_model_capabilities": "Fähigkeiten", - "agent_model_supports_vision": "Bildeingabe", - "agent_model_supports_image_output": "Bildausgabe", - "agent_coming_soon": "Coming soon...", - "agent_skills_title": "Skills Management", - "agent_skills_add": "Add Skill", - "agent_skills_empty": "No skills installed", - "agent_skills_tools": "Tools", - "agent_skills_references": "References", - "agent_skills_detail": "Skill Details", - "agent_skills_edit_prompt": "Prompt", - "agent_skills_install": "Install Skill", - "agent_skills_install_url": "Import from URL", - "agent_skills_install_paste": "Paste SKILL.md", - "agent_skills_uninstall": "Uninstall", - "agent_skills_uninstall_confirm": "Are you sure to uninstall Skill \"{{name}}\"?", - "agent_skills_save_success": "Saved successfully", - "agent_skills_install_success": "Installed successfully", - "agent_skills_fetch_failed": "Fetch failed", - "agent_skills_add_script": "Add Script", - "agent_skills_add_reference": "Add Reference", - "agent_skills_install_zip": "Upload ZIP", - "agent_skills_install_zip_hint": "Click to select a .zip file", - "agent_skills_prompt": "Prompt", - "agent_skills_installed_at": "Installed at", - "agent_skills_refresh": "Refresh", - "agent_skills_refresh_success": "Refreshed successfully", - "agent_skills_tool_code": "Tool Code", - "agent_skills_click_to_view_code": "Click tool name to view code", - "agent_skills_config": "Konfiguration", - "agent_skills_config_saved": "Konfiguration gespeichert", - "agent_skills_check_updates": "Updates prüfen", - "agent_skills_no_updates": "Alle Skills sind aktuell", - "agent_skills_updates_available": "Skill-Update(s) verfügbar", - "agent_skills_update": "Aktualisieren", - "agent_skills_update_success": "Erfolgreich aktualisiert", - "agent_skills_url_placeholder": "SKILL.cat.md URL eingeben", - "agent_chat_new": "New Chat", - "agent_chat_delete": "Delete Chat", - "agent_chat_delete_confirm": "Delete this conversation?", - "agent_chat_no_conversations": "No conversations", - "agent_chat_input_placeholder": "Type a message...", - "agent_chat_send": "Send", - "agent_chat_stop": "Stop", - "agent_chat_thinking": "Thinking", - "agent_chat_tool_call": "Tool Call", - "agent_chat_error": "Error occurred", - "agent_chat_no_model": "No model configured. Please add one in Model Service first.", - "agent_chat_model_select": "Select Model", - "agent_chat_rename": "Rename", - "agent_chat_copy": "Copy", - "agent_chat_copy_success": "Copied", - "agent_chat_regenerate": "Regenerate", - "agent_chat_streaming": "Generating...", - "agent_chat_newline": "for new line", - "agent_chat_welcome_hint": "Ask me anything about your scripts", - "agent_chat_welcome_start": "Create a conversation to get started", - "agent_chat_tokens": "tokens", - "agent_chat_first_token": "TTFT", - "agent_chat_tools_count": "{{count}} tools", - "agent_chat_tools_enabled": "Tools enabled", - "agent_chat_tools_disabled": "Tools disabled", - "agent_chat_tools_enabled_tip": "Tools enabled — click to disable", - "agent_chat_tools_disabled_tip": "Tools disabled — click to enable", - "agent_chat_background_enabled_tip": "Hintergrundmodus AN — Konversation läuft nach dem Schließen der Seite weiter, klicken zum Deaktivieren", - "agent_chat_background_disabled_tip": "Hintergrundmodus AUS — Konversation stoppt beim Schließen der Seite, klicken zum Aktivieren", - "agent_chat_delete_round": "Delete", - "agent_chat_copy_message": "Copy", - "agent_chat_edit_message": "Bearbeiten", - "agent_chat_save_and_send": "Speichern & Senden", - "agent_chat_cancel_edit": "Abbrechen", - "agent_chat_message_queued": "In Warteschlange", - "agent_chat_cancel_message": "Senden abbrechen", - "agent_permission_title": "Das Skript fordert die Verwendung des Agent-Gesprächs an", - "agent_permission_describe": "Dieses Skript fordert Zugang zur Agent-Gesprächsfunktion an, was API-Token verbraucht. Erlauben Sie nur vertrauenswürdigen Skripten.", - "agent_permission_content": "Agent-Gespräch", - "agent_opfs": "OPFS", - "agent_opfs_title": "OPFS Dateibrowser", - "agent_opfs_empty": "Leeres Verzeichnis", - "agent_opfs_name": "Name", - "agent_opfs_size": "Größe", - "agent_opfs_type": "Typ", - "agent_opfs_modified": "Zuletzt geändert", - "agent_opfs_delete_confirm": "Sind Sie sicher, dass Sie löschen möchten?", - "agent_opfs_delete_success": "Gelöscht", - "agent_opfs_file": "Datei", - "agent_opfs_directory": "Verzeichnis", - "agent_opfs_preview": "Vorschau", - "agent_opfs_root": "Stammverzeichnis", - "agent_dom_permission_title": "Das Skript fordert DOM-Zugriff an", - "agent_dom_permission_describe": "Dieses Skript fordert die Fähigkeit an, das DOM der Webseite zu lesen und zu manipulieren (Klicken, Formulare ausfüllen, Navigation, Screenshot usw.). Erlauben Sie nur vertrauenswürdigen Skripten.", - "agent_dom_permission_content": "Agent DOM-Operationen", - "agent_mcp_title": "MCP-Server", - "agent_mcp_add_server": "Server hinzufügen", - "agent_mcp_no_servers": "Keine MCP-Server konfiguriert", - "agent_mcp_test_connection": "Testen", - "agent_mcp_name_url_required": "Name und URL sind erforderlich", - "agent_mcp_optional": "optional", - "agent_mcp_custom_headers": "Benutzerdefinierte Header", - "agent_mcp_enabled": "Aktiviert", - "agent_mcp_detail": "Details", - "agent_mcp_tools": "Werkzeuge", - "agent_mcp_resources": "Ressourcen", - "agent_mcp_prompts": "Prompts", - "agent_mcp_no_tools": "Keine Werkzeuge verfügbar", - "agent_mcp_no_resources": "Keine Ressourcen verfügbar", - "agent_mcp_no_prompts": "Keine Prompts verfügbar", - "agent_mcp_loading": "Laden...", - "agent_mcp_parameters": "Parameter", - "agent_settings": "Einstellungen", - "agent_settings_title": "Agent-Einstellungen", - "agent_model_settings": "Modelleinstellungen", - "agent_summary_model": "Zusammenfassungsmodell", - "agent_summary_model_desc": "Für Web-Zusammenfassungen. Wenn nicht eingestellt, wird das Standardmodell verwendet", - "agent_summary_model_placeholder": "Standardmodell verwenden", - "agent_search_settings": "Sucheinstellungen", - "agent_search_engine": "Suchmaschine", - "agent_search_engine_baidu": "Baidu", - "agent_search_google_api_key": "Google API Key", - "agent_search_google_cse_id": "Benutzerdefinierte Suchmaschinen-ID", - "agent_settings_saved": "Einstellungen gespeichert", - "agent_settings_save_failed": "Einstellungen konnten nicht gespeichert werden", - "agent_search_engine_tip_bing": "Standard-Suchmaschine mit breiter globaler Abdeckung, keine zusätzliche Konfiguration erforderlich.", - "agent_search_engine_tip_duckduckgo": "Datenschutzorientierte Suchmaschine, kein API-Schlüssel erforderlich.", - "agent_search_engine_tip_baidu": "Optimiert für chinesische Inhalte, bessere Ergebnisse für chinesische Suchanfragen.", - "agent_search_engine_tip_google": "Hochwertige Suchergebnisse, erfordert einen Google API Key und eine benutzerdefinierte Suchmaschinen-ID." -} \ No newline at end of file diff --git a/src/locales/en-US/agent.json b/src/locales/en-US/agent.json new file mode 100644 index 000000000..529af3671 --- /dev/null +++ b/src/locales/en-US/agent.json @@ -0,0 +1,252 @@ +{ + "title": "AI Agent", + "docs": "Docs", + "chat": "Chat", + "provider": "Model Service", + "mcp": "MCP", + "skills": "Skills", + "provider_title": "Model Service", + "provider_subtitle": "Manage AI model providers and connections", + "provider_select": "AI Provider", + "provider_api_base_url": "API Base URL", + "provider_api_key": "API Key", + "provider_model": "Default Model", + "provider_test_connection": "Test Connection", + "provider_test_success": "Connection Successful", + "provider_test_failed": "Connection Failed", + "provider_test_hint": "The test sends a minimal request to verify the configuration", + "provider_docs": "Docs", + "provider_count_hint": "The default model is used for new conversations", + "model_count": "{{count}} models", + "model_fetch": "Fetch Models", + "model_fetch_failed": "Failed to fetch models", + "model_name": "Name", + "model_add": "Add Model", + "model_edit": "Edit", + "model_copy": "Copy", + "model_delete": "Delete", + "model_set_default": "Set as Default", + "model_default_label": "Default", + "model_delete_confirm": "Are you sure to delete this model?", + "model_max_tokens": "Max Output Tokens", + "model_context_window": "Context Window", + "model_no_models": "No models configured", + "model_no_models_desc": "Add your first model to start using the AI Agent", + "model_vision_support": "Supports vision input", + "model_image_output": "Supports image output", + "model_capabilities": "Capabilities", + "model_supports_vision": "Vision Input", + "model_supports_image_output": "Image Output", + "coming_soon": "Coming soon...", + "skills_title": "Skills Management", + "skills_subtitle": "Manage agent skill packages (SKILL.md prompts, scripts, references)", + "skills_add": "Add Skill", + "skills_empty": "No skills installed", + "skills_empty_desc": "Upload a ZIP or import your first skill from a URL", + "skills_tools": "Tools", + "skills_references": "References", + "skills_detail": "Skill Details", + "skills_edit_prompt": "Prompt", + "skills_install": "Install Skill", + "skills_install_url": "Import from URL", + "skills_install_paste": "Paste SKILL.md", + "skills_uninstall": "Uninstall", + "skills_uninstall_confirm": "Are you sure to uninstall Skill \"{{name}}\"?", + "skills_save_success": "Saved successfully", + "skills_install_success": "Installed successfully", + "skills_fetch_failed": "Fetch failed", + "skills_add_script": "Add Script", + "skills_add_reference": "Add Reference", + "skills_install_zip": "Upload ZIP", + "skills_install_zip_hint": "Click to select a .zip file", + "skills_prompt": "Prompt", + "skills_installed_at": "Installed at", + "skills_refresh": "Refresh", + "skills_refresh_success": "Refreshed successfully", + "skills_tool_code": "Tool Code", + "skills_click_to_view_code": "Click tool name to view code", + "skills_config": "Config", + "skills_config_saved": "Config saved", + "skills_check_updates": "Check Updates", + "skills_no_updates": "All skills are up to date", + "skills_updates_available": "update(s) available", + "skills_update": "Update", + "skills_docs": "Docs", + "skills_count": "{{count}} skills installed", + "skills_update_count": "{{count}} updates available", + "skills_update_available": "Update {{version}}", + "skills_references_short": "Refs", + "skills_configurable": "Configurable", + "skills_open_config": "Open config", + "skills_update_success": "Updated successfully", + "skills_url_placeholder": "Enter SKILL.cat.md URL", + "chat_new": "New Chat", + "chat_delete": "Delete Chat", + "chat_delete_confirm": "Delete this conversation?", + "chat_no_conversations": "No conversations", + "chat_search_placeholder": "Search conversations…", + "chat_search_no_results": "No matching conversations", + "chat_export": "Export", + "chat_input_placeholder": "Type a message...", + "chat_send": "Send", + "chat_stop": "Stop", + "chat_thinking": "Thinking", + "chat_tool_call": "Tool Call", + "chat_tool_arguments": "Arguments", + "chat_tool_result": "Result", + "chat_error": "Error occurred", + "chat_no_model": "No model configured. Please add one in Model Service first.", + "chat_model_select": "Select Model", + "chat_rename": "Rename", + "chat_copy": "Copy", + "chat_copy_success": "Copied", + "chat_regenerate": "Regenerate", + "chat_streaming": "Generating...", + "chat_retrying": "Retrying ({{attempt}}/{{max}})...", + "chat_attach_image": "Attach image", + "chat_attach_file": "Attach file", + "chat_welcome_hint": "Ask me anything about your scripts", + "chat_welcome_start": "Create a conversation to get started", + "chat_tokens": "tokens", + "chat_first_token": "TTFT", + "chat_tools_count": "{{count}} tools", + "chat_tools_enabled": "Tools enabled", + "chat_tools_disabled": "Tools disabled", + "chat_tools_enabled_tip": "Tools enabled — click to disable", + "chat_tools_disabled_tip": "Tools disabled — click to enable", + "chat_background_enabled_tip": "Background mode ON — conversation keeps running after closing the page, click to disable", + "chat_background_disabled_tip": "Background mode OFF — conversation stops when the page is closed, click to enable", + "chat_delete_round": "Delete", + "chat_copy_message": "Copy", + "chat_edit_message": "Edit", + "chat_save_and_send": "Save & Send", + "chat_cancel_edit": "Cancel", + "chat_message_queued": "Queued", + "chat_cancel_message": "Cancel send", + "permission_title": "The script is requesting to use Agent conversation", + "permission_describe": "This script requests Agent conversation access, which will consume API tokens. Only grant access to trusted scripts.", + "permission_content": "Agent Conversation", + "opfs": "OPFS", + "opfs_title": "OPFS File Browser", + "opfs_empty": "Empty directory", + "opfs_name": "Name", + "opfs_size": "Size", + "opfs_type": "Type", + "opfs_modified": "Last Modified", + "opfs_delete_confirm": "Are you sure to delete?", + "opfs_delete_success": "Deleted", + "opfs_file": "File", + "opfs_directory": "Directory", + "opfs_preview": "Preview", + "opfs_subtitle": "Origin Private File System · Agent private storage", + "opfs_refresh": "Refresh", + "opfs_upload": "Upload", + "opfs_actions": "Actions", + "opfs_item_count": "{{count}} items", + "opfs_empty_desc": "No files in this directory yet", + "opfs_upload_success": "Uploaded", + "opfs_upload_failed": "Upload failed", + "opfs_type_directory": "Folder", + "opfs_type_image": "Image", + "opfs_type_text": "Text", + "opfs_type_binary": "Binary", + "opfs_root": "Root", + "dom_permission_title": "The script is requesting DOM operation access", + "dom_permission_describe": "This script requests the ability to read and manipulate web page DOM (click, fill forms, navigate, screenshot, etc.). Only grant access to trusted scripts.", + "dom_permission_content": "Agent DOM Operations", + "mcp_title": "MCP Servers", + "mcp_subtitle": "Connect MCP servers to extend agent tools, resources and prompts", + "mcp_add_server": "Add Server", + "mcp_no_servers": "No MCP servers configured", + "mcp_no_servers_desc": "Add your first MCP server to connect external tools and data", + "mcp_status_connected": "Connected", + "mcp_status_failed": "Connection failed", + "mcp_status_untested": "Not tested", + "mcp_has_key": "Secret key", + "mcp_headers_count": "{{count}} headers", + "mcp_count_servers": "{{count}} servers", + "mcp_count_connected": "{{count}} connected", + "mcp_count_tools": "{{count}} tools available", + "mcp_docs": "Docs", + "mcp_test_connection": "Test", + "mcp_name_url_required": "Name and URL are required", + "mcp_optional": "optional", + "mcp_custom_headers": "Custom Headers", + "mcp_enabled": "Enabled", + "mcp_detail": "Details", + "mcp_tools": "Tools", + "mcp_resources": "Resources", + "mcp_prompts": "Prompts", + "mcp_no_tools": "No tools available", + "mcp_no_resources": "No resources available", + "mcp_no_prompts": "No prompts available", + "mcp_loading": "Loading...", + "mcp_parameters": "Parameters", + "tasks": "Tasks", + "tasks_title": "Scheduled Tasks", + "tasks_subtitle": "Run agent tasks automatically on a cron schedule", + "tasks_docs": "Docs", + "tasks_count_total": "{{count}} tasks", + "tasks_count_enabled": "{{count}} enabled", + "tasks_create": "Create Task", + "tasks_edit": "Edit Task", + "tasks_mode": "Mode", + "tasks_mode_internal": "Internal", + "tasks_mode_event": "Event", + "tasks_mode_internal_short": "Internal", + "tasks_mode_event_short": "Event", + "tasks_cron": "Cron Expression", + "tasks_next_run": "Next Run", + "tasks_last_status": "Last Status", + "tasks_run_now": "Run Now", + "tasks_history": "History", + "tasks_prompt": "Prompt", + "tasks_max_iterations": "Max Iterations", + "tasks_notify": "Notify on Complete", + "tasks_notify_desc": "Send a browser notification when the task completes", + "tasks_no_tasks": "No scheduled tasks", + "tasks_no_tasks_desc": "Create your first scheduled task to run the agent automatically", + "tasks_no_runs": "No run history yet", + "tasks_delete_confirm": "Are you sure to delete this task?", + "tasks_clear_runs": "Clear History", + "tasks_clear_runs_confirm": "Are you sure to clear the run history?", + "tasks_event_hint": "When triggered, the script that created this task will be notified", + "tasks_event_trigger": "Event Trigger", + "tasks_name_cron_required": "Name and Cron expression are required", + "tasks_model_select": "Select Model", + "tasks_skills": "Skills", + "tasks_skills_auto": "Auto load all", + "tasks_conversation_id": "Continue conversation ID (optional)", + "tasks_run_status_success": "Success", + "tasks_run_status_error": "Error", + "tasks_run_status_running": "Running", + "tasks_run_duration": "Duration", + "tasks_run_usage": "Usage", + "tasks_run_conversation": "View Conversation", + "tasks_run_time": "Time", + "tasks_run_status": "Status", + "tasks_never_run": "Never run", + "settings": "Settings", + "settings_title": "Agent Settings", + "settings_subtitle": "Model, search and general preferences · changes apply instantly", + "settings_cat_model": "Model", + "settings_cat_search": "Search", + "model_settings": "Model Settings", + "summary_model": "Summary Model", + "summary_model_desc": "Model for summarization, falls back to default if not set", + "summary_model_placeholder": "Use default model", + "search_settings": "Search Settings", + "search_engine": "Search Engine", + "search_engine_desc": "The retrieval source used by the web_search tool.", + "search_engine_baidu": "Baidu", + "search_google_api_key": "Google API Key", + "search_google_api_key_desc": "Used to call the Custom Search JSON API.", + "search_google_cse_id": "Custom Search Engine ID", + "search_google_cse_id_desc": "The cx parameter of the Programmable Search Engine.", + "settings_saved": "Settings saved", + "settings_save_failed": "Failed to save settings", + "search_engine_tip_bing": "Default search engine with broad global coverage, no extra configuration needed.", + "search_engine_tip_duckduckgo": "Privacy-focused search engine, no API key required.", + "search_engine_tip_baidu": "Optimized for Chinese content, better results for Chinese queries.", + "search_engine_tip_google": "High-quality search results, requires a Google API Key and Custom Search Engine ID." +} diff --git a/src/locales/en-US/common.json b/src/locales/en-US/common.json new file mode 100644 index 000000000..a6ac0b44e --- /dev/null +++ b/src/locales/en-US/common.json @@ -0,0 +1,131 @@ +{ + "user_guide": "User Guide", + "api_docs": "API Docs", + "development_guide": "Development Guide", + "script_gallery": "Script Gallery", + "community_forum": "Community Forum", + "external_links": "External Links", + "system_follow": "Follow System", + "no_data": "No Data", + "logs": "Logs", + "tools": "Tools", + "find": "Find", + "replace": "Replace", + "settings": "Settings", + "change_theme": "Change Theme", + "hide_main_sidebar": "Collapse sidebar", + "show_main_sidebar": "Expand sidebar", + "menu": "Menu", + "guide": "Guide", + "helpcenter": "Help Center", + "save": "Save", + "file": "File", + "save_success": "Saved successfully", + "reset_success": "Reset successfully", + "update": "Update", + "check_update": "Check Update", + "confirm_delete": "Confirm Deletion", + "confirm_update": "Confirm Update", + "delete_success": "Delete Successful", + "deleting": "Deleting", + "enable": "Enable", + "script_list_enable_width": 100, + "script_list_last_updated_width": 150, + "script_list_apply_to_run_status_width": 110, + "subscribe_list_enable_width": 120, + "disable": "Disable", + "name": "Name", + "version": "Version", + "source": "Source", + "home": "Home", + "action": "Action", + "export": "Export", + "delete": "Delete", + "pin_to_top": "Pin Top", + "confirm": "Confirm", + "close": "Close", + "config": "Config", + "key": "Key", + "value": "Value", + "add": "Add", + "type": "Type", + "size": "Size", + "download": "Download", + "edit_value": "Edit Value", + "add_value": "Add Value", + "update_success": "Update Successful", + "add_success": "Add Successful", + "clear": "Clear", + "type_string": "String", + "type_number": "Number", + "type_boolean": "Boolean", + "type_object": "Object", + "confirm_delete_resource": "Are you sure you want to delete this resource? This resource will reload on the next startup.", + "confirm_clear_resource": "Are you sure you want to clear these resources? Resources will reload on the next startup.", + "yes": "Yes", + "no": "No", + "confirm_delete_permission": "Are you sure you want to delete this permission?", + "reset": "Reset", + "run_once": "Run Once", + "stop": "Stop", + "edit": "Edit", + "copy": "Copy", + "exclude_on": "Reinstate $0's execution", + "exclude_off": "Exclude $0's exeuction", + "get_confirm_error": "Get Confirmation Information Failed", + "confirm_error": "Confirmation Failed", + "ignore": "Ignore", + "temporary_allow": "Temporarily Allow This {{permissionContent}}", + "temporary_allow_all": "Temporarily Allow All {{permissionContent}}", + "permanent_allow": "Permanently Allow This {{permissionContent}}", + "permanent_allow_all": "Permanently Allow All {{permissionContent}}", + "temporary_deny": "Temporarily Deny This {{permissionContent}}", + "temporary_deny_all": "Temporarily Deny All {{permissionContent}}", + "permanent_deny": "Permanently Deny This {{permissionContent}}", + "permanent_deny_all": "Permanently Deny All {{permissionContent}}", + "import": "Import", + "author": "Author", + "description": "Description", + "operation": "Operation", + "error": "Error", + "unknown": "Unknown", + "add_new": "Add New", + "no_operation": "No Operation", + "enable_script": "Enable Script", + "local_creation": "Local Creation", + "import_success": "Import Successful", + "get_script": "Get Script", + "report_issue": "Report Issue", + "project_docs": "Project Documentation", + "community": "Community", + "domain": "Domain", + "script_name": "Script Name", + "skip": "Skip", + "next": "Next", + "next_with_progress": "Next (Step {step} of {steps})", + "back": "Back", + "last": "Finish", + "auto": "Auto", + "hide": "Hide", + "custom": "Custom", + "resize_column_width": "Resize Column", + "collapse": "Collapse", + "expand": "Expand", + "import_script_placeholder": "Support for an absolute link at the end of .user.js or a link to a scripting cat installation page\ncan be filled in multiple lines, each entry\nExample:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/en/script-show-page/1234", + "light": "Light", + "dark": "Dark", + "enter_search_value": "Enter {{search}} to search", + "no_message_content": "No Message Content", + "loading": "Loading...", + "auth_type": "Authentication Type", + "url": "URL", + "username": "Username", + "password": "Password", + "access_token_bearer": "Access Token (Bearer)", + "s3_bucket_name": "Bucket Name", + "s3_region": "Region", + "s3_access_key_id": "Access Key ID", + "s3_secret_access_key": "Secret Access Key", + "s3_custom_endpoint": "Custom Endpoint (Optional)", + "cancel": "Cancel" +} diff --git a/src/locales/en-US/editor.json b/src/locales/en-US/editor.json new file mode 100644 index 000000000..c97a089f7 --- /dev/null +++ b/src/locales/en-US/editor.json @@ -0,0 +1,132 @@ +{ + "save": "Save", + "save_success": "Saved successfully", + "copy": "Copy", + "find": "Find", + "replace": "Replace", + "select_all": "Select All", + "format": "Format", + "back": "Back", + "more": "More", + "file": "File", + "edit": "Edit", + "settings": "Settings", + "run_settings": "Run Settings", + "code": "Code", + "script_setting": "Script Settings", + "storage": "Storage", + "resource": "Resources", + "search_resource": "Search resources", + "search_storage": "Search key", + "record_count": "{{count}} records", + "resource_count": "{{count}} resources · {{size}}", + "script_list": "Script List", + "search_scripts": "Search scripts...", + "new_script": "New Script", + "source": "Source", + "from_user": "User", + "from_script": "Script", + "last_updated": "Last Updated", + "run_at": "Run At", + "run_in": "Run In", + "check_update": "Check Update", + "line_col": "Ln {{line}}, Col {{col}}", + "script_not_found": "Script not found", + "confirm_delete_script": "Delete script \"{{name}}\"?", + "delete_success": "Deleted successfully", + "delete_failed": "Delete failed", + "cancel": "Cancel", + "confirm": "Confirm", + "save_as": "Save as", + "run": "Run", + "debug": "Debug", + "script_storage": "Script Storage", + "enter_key": "Please Enter Key", + "key_placeholder": "Key", + "value_placeholder": "When the type is object, please enter data that can be JSON parsed.", + "clear_success": "Clear Successful", + "confirm_clear": "Are you sure you want to clear this storage space?", + "script_resource": "Script Resource", + "basic_info": "Basic Information", + "update_url": "Update URL", + "add_permission": "Add Permission", + "match": "Match", + "user_setting": "User Setting", + "confirm_delete_exclude": "Confirm deletion of this exclusion?", + "after_deleting_match_item": "After the script's match items are deleted, they will be automatically added to the match items", + "confirm_delete_match": "Confirm deletion of this match?", + "after_deleting_exclude_item": "After deleting the script's match item, it will be automatically added to the exclusion items", + "add_match": "Add Match", + "add_exclude": "Add Exclude", + "website_match": "Website Match (@match)", + "website_exclude": "Website Exclude (@exclude)", + "confirm_reset": "Confirm reset?", + "undo": "Undo", + "redo": "Redo", + "cut": "Cut", + "paste": "Paste", + "user_config": "User Config", + "gm_api": "GM Api", + "storage_api": "Storage API", + "use_file_system": "File System in Use", + "open_directory": "Open Directory", + "account_validation_failed": "Account Validation Failed", + "not_set": "Not Set", + "in_use": "In Use", + "storage_error": "Storage Error", + "upload_to_cloud": "Upload to Cloud", + "save_failed": "Save Failed", + "exporting": "Exporting...", + "upload_to": "Upload To", + "value_export_expression": "Value Export Expression", + "overwrite_original_value_on_import": "Overwrite Original Value on Import", + "cookie_export_expression": "Cookie Export Expression", + "overwrite_original_cookie_on_import": "Overwrite Original Cookie on Import", + "restore_default_values": "Restore Default Values", + "edit_conflict": "Edit Conflict", + "confirm_override_when_edit_conflict": "This script was edited in another instance. Replacing it will overwrite those changes. Would you like to keep this version instead?", + "save_abort_when_edit_conflict": "The script was edited in another instance. Save aborted.", + "scriptname_conflict": "Script Name Conflict", + "confirm_save_when_scriptname_conflict": "This script name is already used by another script. Do you still want to save it?", + "save_abort_when_scriptname_conflict": "This script name is already used by another script. Save aborted.", + "eslint_config_format_error": "eslint configuration format error", + "script_modified_leave_confirm": "Your changes have not been saved. If you leave, your changes will be lost. Are you sure you want to leave?", + "create_success_note": "New script successfully created. Note that the background script will not be enabled by default.", + "save_as_failed": "Save As Failed", + "save_as_success": "Saved Successfully", + "only_background_scheduled_can_run": "Only background script/crontab scripts can run", + "preparing_script_resources": "Preparing script resource...", + "build_success_message": "Build successfully, open developer tools to view output in the console", + "script_storage_tooltip": "Can manage the stored script data (GM_value)", + "script_resource_tooltip": "Manage @resource, @required downloaded resources", + "script_setting_tooltip": "Make some custom settings for scripts", + "script_modified_close_confirm": "The script has been modified. Changes will be lost after closing, continue?", + "close_current_tab": "Close current tab", + "close_other_tabs": "Close others", + "close_left_tabs": "Close tabs to the left", + "close_right_tabs": "Close tabs to the right", + "invalid_script_code": "Bad Code", + "build_failed": "Failed to Build", + "drag_script_here_to_upload": "Drag script here to upload it", + "watch_file_description": "Monitor file changes and automatically update scripts. When using, please make sure the script file path remains unchanged and the page cannot be closed.", + "watch_file": "Watch file", + "stop_watch_file": "Stop watch", + "individual_edit": "Individual Edit", + "batch_edit": "Batch Edit", + "script_code": "Script Code", + "editor_config": "Editor Configuration", + "editor_config_description": "You can refer to the compilerOptions in jsconfig.js for configuration", + "editor_type_definition": "Editor Type Definition", + "editor_type_definition_description": "You can customize your own type definitions, and the script editor will automatically load these type definitions", + "eslint_rules_reset": "ESLint Rules Reset", + "eslint_rules_saved": "ESLint Rules Saved", + "editor_config_reset": "Editor Configuration Reset", + "editor_config_saved": "Editor Configuration Saved", + "editor_config_format_error": "Editor Configuration Format Error", + "editor_type_definition_reset": "Editor Type Definition Reset", + "editor_type_definition_saved": "Editor Type Definition Saved", + "editor": { + "show_script_list": "Show Script List", + "hide_script_list": "Hide Script List" + } +} diff --git a/src/locales/en-US/guide.json b/src/locales/en-US/guide.json new file mode 100644 index 000000000..a5602fb67 --- /dev/null +++ b/src/locales/en-US/guide.json @@ -0,0 +1,25 @@ +{ + "start_title": "Welcome to the ScriptCat Extension", + "start_content": "Next, we will introduce the basic usage of ScriptCat to you.", + "installed_scripts": "The scripts you have installed will be displayed here.", + "script_list_title": "Script Market", + "script_list_content": "You can install scripts from the Script Market. In addition to supporting user scripts, ScriptCat also supports background scripts.", + "script_list_enable_title": "Enable Scripts", + "script_list_enable_content": "Scripts need to be enabled to be used. Page scripts are enabled by default upon installation, while background scripts are disabled by default.", + "script_list_apply_to_run_status_title": "Apply To and Run Status", + "script_list_apply_to_run_status_content": "The running status of scripts is displayed here. Hover over the tags to view the script type.", + "script_list_sort_title": "Sort", + "script_list_sort_content": "You can drag the tag of the script to sort", + "script_list_update_title": "Last Updated", + "script_list_update_content": "Click the last update column label to check for updates to the script", + "script_list_action_title": "Action", + "script_list_action_content": "The operation bar allows access to script editing, control script execution and termination (background scripts), and settings (see UserConfig). The buttons on the right allow access to advanced filtering and toggling view modes.", + "tools_title": "Common Tools", + "tools_content": "The tools provide backup and development features.", + "tools_backup_title": "Backup", + "tools_backup_content": "Backup allows you to save scripts to avoid loss. You can export script files to your local machine or load script files from your local machine. You can also back up to the cloud for added convenience.", + "setting_title": "Settings", + "setting_content": "The settings mainly include language, script synchronization, update frequency, and other common options.", + "setting_sync_title": "Update and Sync", + "setting_sync_content": "The script synchronization feature allows you to easily sync the script content of this device to the cloud. If you select the option to sync deletions, when a script is deleted on this device, it will also be deleted from the cloud. You can also update the script version here to get more powerful features." +} diff --git a/src/locales/en-US/index.ts b/src/locales/en-US/index.ts new file mode 100644 index 000000000..bacd6d096 --- /dev/null +++ b/src/locales/en-US/index.ts @@ -0,0 +1,11 @@ +export { default as agent } from "./agent.json"; +export { default as common } from "./common.json"; +export { default as editor } from "./editor.json"; +export { default as guide } from "./guide.json"; +export { default as install } from "./install.json"; +export { default as logs } from "./logs.json"; +export { default as permission } from "./permission.json"; +export { default as popup } from "./popup.json"; +export { default as script } from "./script.json"; +export { default as settings } from "./settings.json"; +export { default as tools } from "./tools.json"; diff --git a/src/locales/en-US/install.json b/src/locales/en-US/install.json new file mode 100644 index 000000000..7e16f6a0b --- /dev/null +++ b/src/locales/en-US/install.json @@ -0,0 +1,207 @@ +{ + "data_import": "Data Import", + "select_scripts_to_import": "Please select the scripts you want to import", + "select_all": "Select All", + "script_import_progress": "Import Progress for Scripts", + "select_subscribes_to_import": "Please select the Subscribes you want to import", + "subscribe_import_progress": "Import Progress for Subscribes", + "script": "Install Script", + "update_script": "Update Script", + "subscribe": "Install Subscribe", + "update_subscribe": "Update Subscribe", + "update_script_no_close": "Update Script without closing window", + "script_no_close": "Install Script without closing window", + "update_script_no_more_update": "Update Script and disable update check", + "close_update_script_no_more_update": "Close and do not check for updates", + "script_no_more_update": "Install Script and disable update check", + "invalid_link": "Invalid Link", + "subscribe_install_label": "This Subscribe will install the following scripts", + "subscribe_scripts_title": "This subscription will install the following scripts", + "subscribe_scripts_empty": "This subscription declares no scripts yet", + "script_runs_in": "Script will run on the following websites", + "script_has_full_access_to": "Script will have full access to the following URLs", + "script_requires": "Script requires the following external resources", + "cookie_warning": "Please note, this script requests access to Cookie permissions, which is a dangerous permission. Please verify the security of the script.", + "perm_card_title": "This script will be granted the following permissions", + "perm_card_hint": "Please confirm before installing", + "perm_card_empty": "This script does not request any special permissions", + "perm_match_label": "Runs on", + "perm_match_summary": "The script runs on these websites and modifies the pages", + "perm_connect_label": "Cross-origin access", + "perm_connect_summary": "Can send requests to and read data from the following domains", + "perm_grant_label": "GM capabilities", + "perm_grant_summary": "Can call the following GM APIs", + "perm_require_label": "External resources", + "perm_require_summary": "Loads the following third-party scripts and resources", + "badge_background": "Background", + "badge_scheduled": "Scheduled", + "enabled_label": "Enabled", + "schedule_cron_label": "Scheduled task", + "schedule_next_run": "Next run", + "schedule_background_desc": "Runs automatically while the browser is open", + "code_lines": "{{count}} lines", + "code_copy": "Copy code", + "code_collapse": "Collapse", + "code_expand": "Expand", + "loading_title": "Loading script", + "loading_desc": "Downloading and parsing the script content from the source", + "error_retry": "Retry", + "error_invalid_desc": "A valid install source parameter is missing, so the script cannot be loaded.", + "context_install": "Install Script", + "context_update": "Update Script", + "background_script": "Background Script", + "scheduled_script": "Scheduled Script", + "watching_status": "Watching file changes; will reinstall automatically after saving", + "watching_chip": "Watching", + "watching_title": "Watching file changes", + "watching_file_desc": "Saving {{file}} will reinstall and sync the script automatically", + "watching_last_sync": "Last synced {{time}}", + "warning_title": "Please confirm the script comes from a trusted source", + "warning_risk_connect": "it can access all domains", + "warning_risk_antifeature": "it declares anti-features", + "warning_risk_join": " and ", + "warning_risk_tail": " — install with caution.", + "action_note_install": "Installing means you trust this script's source and author", + "action_note_update": "Updating means you accept this code and permission change", + "action_note_subscribe": "Installing means you trust this subscription and its author", + "action_note_watching": "Watching — saving the file auto-updates; stop to act manually", + "context_skill_install": "Install Skill", + "context_skill_update": "Update Skill", + "skill_kind": "AI Skill", + "skill_prompt_title": "Prompt", + "skill_prompt_chip": "SKILL.md", + "skill_tools_title": "Tools", + "skill_config_title": "Configuration", + "skill_references_title": "References", + "skill_required": "Required", + "skill_secret": "Secret", + "skill_install": "Install Skill", + "skill_update": "Update Skill", + "skill_warning": "The skill injects a prompt into the AI and grants it permission to call the following tools, GM capabilities, and configuration. Install only from legitimate sources!", + "skill_warning_title": "Please confirm this skill comes from a trusted source", + "skill_warning_desc": "After installing, it injects a prompt into the AI and grants the listed tools (incl. GM permissions) and config read/write — install with caution.", + "success": "Install Successful", + "install": { + "update_success": "Update Successful" + }, + "failed": "Install Failed", + "subscribe_success": "Subscribe Successful", + "subscribe_failed": "Subscribe Failed", + "current_version": "Current Version", + "update_version": "Update Version", + "updatepage": { + "title": "Batch Update", + "main_header": "Check for Updates", + "last_check": "Last checked {{time}}", + "status_checking_updates": "Checking the updates...", + "updates_available": "{{count}} updates available", + "ignored_count": "{{count}} ignored", + "selected_count": "{{selected}} / {{total}} selected", + "update_selected": "Update Selected ({{count}})", + "ignore_selected": "Ignore Selected", + "update": "Update", + "ignore": "Ignore", + "restore": "Restore", + "restore_all": "Restore All", + "ignored_section": "Ignored Updates", + "auto_close": "Auto-closing in {{count}}s", + "col_script": "Script", + "col_version": "Version", + "col_change": "Changes", + "col_source": "Source", + "col_action": "Actions", + "enabled": "Enabled", + "disabled": "Disabled", + "codechange_major": "Major Change", + "codechange_noticeable": "Noticeable Change", + "codechange_tiny": "Minor Change", + "tag_new_connect": "New @connect", + "empty_title": "All scripts are up to date", + "empty_desc": "Checked {{count}} scripts · No updates available", + "similarity": "Similarity", + "new_connects": "New @connect", + "toast_found": "Found {{count}} scripts with updates", + "toast_uptodate": "All scripts are up to date" + }, + "importpage": { + "title": "Data Import", + "context_review": "Data Import", + "context_importing": "Importing", + "context_done": "Import Complete", + "selected_count": "{{selected}} / {{total}} selected", + "unimportable_count": "{{count}} can't be imported", + "count_scripts": "{{count}} scripts", + "count_subscribes": "{{count}} subscriptions", + "col_script": "Script", + "col_version": "Version", + "col_source": "Source", + "col_data": "Data", + "col_status": "Status", + "col_enabled": "Enabled", + "op_add": "New", + "op_update": "Update", + "op_error": "Parse Failed", + "source_local": "Created locally", + "data_values": "{{count}} items", + "data_resources": "with resources", + "enable_after_import": "Enable after import", + "row_error": "File corrupted, cannot import", + "unknown_script": "Unknown script", + "subscribe_section": "Subscriptions", + "trust_hint": "Only the items you select are restored; importing sends no data anywhere", + "import_selected": "Import Selected ({{count}})", + "importing_progress": "Restoring backup · {{done}} / {{total}} done", + "importing_hint": "Keep this page open", + "importing_actionbar_hint": "Restoring scripts and data, please don't close this page", + "importing_button": "Importing…", + "cancel": "Cancel", + "status_pending": "Pending", + "status_importing": "Importing", + "status_done": "Imported", + "status_skipped": "Skipped", + "done_title": "Import Complete", + "done_desc": "The selected scripts and data have been restored", + "done_stat_scripts": "{{count}} scripts", + "done_stat_subscribes": "{{count}} subscriptions", + "done_stat_values": "{{count}} data items", + "view_scripts": "View Script List", + "loading_title": "Parsing backup file", + "loading_desc": "Reading and verifying backup contents", + "error_title": "Cannot read backup file", + "error_desc": "The backup file may be corrupted, or the import link has expired", + "invalid_desc": "The import link is invalid or has expired", + "retry": "Retry", + "empty_title": "Nothing to import in this backup", + "empty_desc": "This backup file contains no scripts or subscriptions" + }, + "downloading_status_text": "Downloading. Received {{bytes}}.", + "downloading_status_percent": "Downloading. Received {{bytes}} / {{total}} ({{percent}}%).", + "page_please_wait": "Please wait", + "page_loading": "Loading Installation Page", + "page_load_failed": "Failed to Load Installation Page", + "invalid_page": "Invalid Page", + "background_script_tag": "This is a Background Script", + "scheduled_script_tag": "This is a Scheduled Script", + "from_legitimate_sources_warning": "Please install scripts from legitimate sources! Unknown scripts may invade your privacy or conduct malicious operations.", + "referral_link_title": "Referral Link", + "referral_link_description": "This script modifies or redirects to the author's referral link", + "ads_title": "Ads", + "ads_description": "This script inserts ads on the pages you visit", + "payment_title": "Payment", + "payment_description": "This script requires payment to be used properly", + "miner_title": "Mining", + "miner_description": "This script engages in mining activities", + "membership_title": "Membership Features", + "membership_description": "This script requires registration as a member to be used properly", + "tracking_title": "Tracking", + "tracking_description": "This script tracks your user information", + "script_info_load_failed": "Failed to load script information", + "script_import_result": "Script Import Results", + "failure_info": "Failure Information", + "source": "Install Source", + "skill_prompt": "Prompt", + "skill_tools": "Tools", + "skill_config": "Configuration", + "skill_references": "References", + "skill_install_failed": "Skill installation failed" +} diff --git a/src/locales/en-US/logs.json b/src/locales/en-US/logs.json new file mode 100644 index 000000000..3d4394c46 --- /dev/null +++ b/src/locales/en-US/logs.json @@ -0,0 +1,59 @@ +{ + "log_title": "Runtime Logs", + "last_5_minutes": "Last 5 Minutes", + "last_15_minutes": "Last 15 Minutes", + "last_30_minutes": "Last 30 Minutes", + "last_1_hour": "Last 1 Hour", + "last_3_hours": "Last 3 Hours", + "last_6_hours": "Last 6 Hours", + "last_12_hours": "Last 12 Hours", + "last_24_hours": "Last 24 Hours", + "last_7_days": "Last 7 Days", + "query": "Query", + "labels": "Labels", + "search_regex": "Search (Supports Regex)", + "clean_schedule": "Automatically Clean Up Logs Older Than", + "days_ago_logs": "Days", + "delete_completed": "Deletion Completed", + "delete_current_logs": "Delete Current Logs", + "clear_completed": "Clearing Completed", + "clear_logs": "Clear Logs", + "now": "Now", + "total_logs": "A total of {{length}} logs were queried", + "filtered_logs": "{{length}} logs after filtering", + "enter_filter_conditions": "Please Enter Filter Conditions for Query", + "last_updated": "Last Updated", + "runtime": "Runtime", + "advanced": "Advanced", + "label_filter": "Label Filter", + "add_label": "Add Label", + "custom_range": "Custom Range", + "back_to_top": "Back to Top", + "refresh": "Refresh", + "all_levels": "All", + "total_count": "{{count}} total", + "filtered_count": "{{count}} filtered", + "clear_logs_confirm": "Clear all logs? This cannot be undone.", + "no_logs": "No logs", + "refresh_off": "Off", + "interval_5s": "5s", + "interval_10s": "10s", + "interval_30s": "30s", + "interval_1m": "1m", + "interval_5m": "5m", + "quick_range": "Quick Ranges", + "absolute_range": "Absolute Range", + "from_start": "From (Start)", + "to_end": "To (End)", + "apply_range": "Apply Range", + "auto_refresh_hint": "When the end is set to \"Now\", auto-refresh keeps pulling the latest logs.", + "group_minutes": "Minutes", + "group_hours": "Hours", + "group_days": "Days", + "weekdays_short": "Su,Mo,Tu,We,Th,Fr,Sa", + "year_month": "{{month}}/{{year}}", + "time": "Time", + "prev_month": "Previous Month", + "next_month": "Next Month", + "live": "Live" +} diff --git a/src/locales/en-US/permission.json b/src/locales/en-US/permission.json new file mode 100644 index 000000000..adfb9045e --- /dev/null +++ b/src/locales/en-US/permission.json @@ -0,0 +1,42 @@ +{ + "permission": "Permission", + "permission_value": "Permission Value", + "allow": "Allow", + "permission_management": "Permission Management", + "permission_cors": "Cross-domain (cors)", + "permission_cookie": "Manage Cookies", + "allow_once": "Allow Once", + "deny_once": "Deny Once", + "script_accessing_cross_origin_resource": "Script is attempting to access a cross-origin resource", + "confirm_operation_description": "Please confirm if you allow the script to perform this operation. The script can also add the @connect tag to bypass this option.", + "request_domain": "Request Domain", + "request_url": "Request URL", + "access_cookie_content": "The script is attempting to access website cookie content", + "confirm_script_operation": "Please confirm if you allow the script to perform this operation. Cookies contain important user data, so only grant access to trusted scripts.", + "cookie_domain": "Cookie Domain", + "script_operation_title": "Script is attempting to access script synchronization storage", + "script_operation_description": "Please confirm whether you allow the script to perform this operation. If allowed, the script will be able to access the storage space you have set up and create a directory app/${dir} within it.", + "script_permission_content": "Script", + "extension_site_access_title": "ScriptCat needs site access", + "extension_site_access_description": "Grant browser site access to this origin so ScriptCat can perform the request. This changes the extension's site access setting.", + "extension_site_access_content": "Site", + "request_permission": "Request Permission", + "allow_user_script_guide": "'Allow User Scripts' is currently not enabled, so the scripts cannot run properly. 👉tap to learn how to enable", + "user_script_type": "User Script", + "auth_duration": "Authorization Duration", + "duration_once": "Just Once", + "duration_temporary": "Temporary", + "duration_permanent": "Permanent", + "apply_to_all_domains": "Apply to all requested domains", + "apply_to_all_domains_desc": "Takes effect for all of this script's requests (wildcard)", + "allow_action": "Allow", + "deny_action": "Deny", + "ignore_action": "Ignore", + "cancel_action": "Cancel", + "loading_confirm": "Loading authorization request…", + "cookie_warning_title": "Highly sensitive permission", + "cookie_warning_desc": "Cookies contain sensitive data such as login state. Only authorize trusted scripts.", + "confirm_expired_title": "Authorization request expired", + "confirm_expired_desc": "This authorization request has timed out or already been handled. Please return to the page and trigger it again.", + "auto_close_in": "Window will close automatically in {{second}}s" +} diff --git a/src/locales/en-US/popup.json b/src/locales/en-US/popup.json new file mode 100644 index 000000000..836f25186 --- /dev/null +++ b/src/locales/en-US/popup.json @@ -0,0 +1,27 @@ +{ + "new_version_available": "New Version Available", + "current_page_scripts": "Current Page Running Scripts", + "enabled_background_scripts": "Enabled and Running Background Scripts", + "menu_expand_num_before": "Menu items more than", + "menu_expand_num_after": "will be hidden.", + "develop_mode_guide": "'Developer mode' is currently not enabled, so the scripts cannot run properly. 👉tap to learn how to enable", + "lower_version_browser_guide": "Your browser is too outdated, so the scripts cannot run properly. 👉Click me to learn more", + "click_to_reload": "👉Click to Reload", + "page_in_blacklist": "The current page is blacklisted, cannot use script", + "ext_update_notification": "Scriptcat extension updated", + "ext_update_notification_desc": "Current version: {{version}}, please see the update log for details", + "script_menu_display": "Script Registered Menu", + "badge_type_none": "None", + "badge_type_run_count": "Run Count", + "badge_type_script_count": "Script Count", + "script_menu": "Script Menu", + "display_right_click_menu": "Show right-click menu", + "display_right_click_menu_desc": "Show script menu in browser right-click menu", + "expand_count": "Expand Count", + "auto_collapse_when_exceeds": "Auto collapse when exceeds this number", + "allow_user_script_guide": "'Allow User Scripts' is currently not enabled, so the scripts cannot run properly. 👉tap to learn how to enable", + "request_permission": "Request Permission", + "show_more_scripts": "+{{count}} scripts", + "use_on_mobile": "Use ScriptCat on mobile", + "scan_qr_to_install": "Scan the QR code to install ScriptCat on your phone" +} diff --git a/src/locales/en-US/script.json b/src/locales/en-US/script.json new file mode 100644 index 000000000..3e8322202 --- /dev/null +++ b/src/locales/en-US/script.json @@ -0,0 +1,115 @@ +{ + "import_link": "Link Import", + "import_link_failure": "Link Import Failed", + "create_user_script": "Create User Script", + "create_background_script": "Create Background Script", + "create_scheduled_script": "Create Scheduled Script", + "import_by_local": "Local Import", + "import_local_failure": "Local Import Failed", + "import_local_success": "Local Import Suceeded", + "create_script": "Create Script", + "installed_scripts": "Installed Scripts", + "nav_scripts": "Scripts", + "subscribe": "Subscribe", + "subscribe_scripts_count": "{{count}} scripts", + "enter_subscribe_name": "Please Enter Subscribe Name", + "subscribe_url": "Subscribe URL", + "confirm_delete_subscription": "Are you sure you want to delete this Subscribe? Related scripts will also be deleted.", + "list": { + "confirm_delete": "Are you sure you want to delete? Please note that this is an irreversible operation.", + "confirm_update": "Are you sure you want to update? Please note that this is an irreversible operation." + }, + "apply_to_run_status": "Apply To / Run Status", + "sorting": "Sorting", + "foreground_page_script_tooltip": "Foreground page script, runs on the specified page", + "background_script_tooltip": "Background script, will run in background when enabled", + "scheduled_script_tooltip": "Scheduled script, next run time:", + "running": "Running", + "completed": "Completed", + "source_subscribe_link": "Subscribe Link", + "source_local_script": "Local Script", + "source_script_link": "Script Link", + "by_manual_creation": "Created locally through code editing", + "confirm_delete_script": "Are you sure you want to delete this script?", + "confirm_delete_scripts_content": "Are you sure you want to delete the selected {{count}} scripts? This action cannot be undone.", + "confirm_delete_script_content": "Are you sure you want to delete the script \"{{name}}\"? This action cannot be undone.", + "delete_failed": "Deletion Failed", + "enter_script_name": "Please enter script name", + "update_not_supported": "This script does not support update checks", + "checking_for_updates": "Checking for updates...", + "new_version_available": "New Version Available", + "latest_version": "Latest Version", + "checked_for_all_selected": "Checked updates for all selected", + "update_check_failed": "Update Check Failed", + "stopping_script": "Stopping Script", + "script_stopped": "Script Stopped", + "starting_script": "Starting Script...", + "starting_updates": "Starting Batch Updates...", + "script_started": "Script Started", + "operation_failed": "Operation failed", + "batch_operations": "Batch Operations", + "scripts_pinned_to_top": "The selected script has been pinned", + "unknown_operation": "Unknown Operation", + "page_script": "Page Script", + "homepage": "Homepage", + "script_website": "Script Website", + "script_source": "Script Source Code", + "bug_feedback_script_support": "Bug Feedback/Script Support", + "script_total_runs": "The script has run {{runNum}} times, {{runNumByIframe}} times in iframes", + "script_total_runs_single": "The script has run {{runNum}} times", + "script_disabled": "The script is not enabled", + "cron_oncetype": { + "minute": "{{next}} (runs every minute)", + "hour": "{{next}} (runs every hour)", + "day": "{{next}} (runs every day)", + "month": "{{next}} (runs every month)", + "week": "{{next}} (runs every week)" + }, + "cron_invalid_expr": "Invalid cron expression", + "scheduled_script_description_title": "This is a scheduled script, which will automatically run at a specific time once enabled and can be manually controlled in the panel.", + "scheduled_script_description_description_expr": "Scheduled task expression:", + "scheduled_script_description_description_next": "Most recent run time:", + "background_script_description": "This is a background script, which will automatically run once when the browser opens once enabled, and can be manually controlled in the panel.", + "background_script": "Background Script", + "scheduled_script": "Scheduled Script", + "script_status_tooltip": "You can control the enable status of scripts. Ordinary Tampermonkey scripts are enabled by default, and Background scripts and Scheduled scripts are disabled by default.", + "subscribe_source_tooltip": "This is a subscription source. When you open the subscription, the subscription script will be automatically installed.", + "script_name_cannot_be_set_to_empty": "Script name cannot be empty", + "search_scripts": "Search script", + "script_list": { + "sidebar": { + "stopped": "Stopped", + "all": "All", + "normal_script": "Normal Script", + "status": "Status" + } + }, + "tags": "Tags", + "input_tags_placeholder": "Enter tags, press Enter to confirm", + "switch_to_card_mode": "Switch to Card Mode", + "switch_to_table_mode": "Switch to Table Mode", + "open_sidebar": "Open Sidebar", + "close_sidebar": "Close Sidebar", + "error_metadata_invalid": "Invalid MetaData block", + "error_script_name_required": "Script name is required", + "error_script_version_required": "Script @version is required", + "error_script_namespace_required": "Script @namespace is required", + "error_cron_invalid": "Invalid cron expression: {{expr}}", + "error_script_type_mismatch": "Script type mismatch: normal and background scripts cannot be converted", + "error_old_script_code_missing": "Previous script code not found", + "error_subscribe_name_required": "Subscription name is required", + "error_grant_conflict": "@grant declares both 'none' and GM API", + "error_metadata_line_duplicated": "There are duplicate declarations in the metadata.", + "create_group": "Create", + "import_group": "Import", + "import_local_script": "Import Local Script", + "link_import": "Import from URL", + "import_skill": "Import Skill", + "link_import_desc": "Paste script / subscription URLs, one per line", + "link_import_placeholder": "https://example.com/script.user.js", + "link_import_hint": "Supports user scripts / subscriptions / Skill URLs", + "not_a_valid_script": "Not a valid user script or SkillScript", + "import_done": "Import done: {{success}} succeeded · {{fail}} failed", + "drop_to_install": "Drop scripts or Skills here to install", + "drop_to_install_hint": "Drop .js user scripts / subscriptions · .zip Skill packages" +} diff --git a/src/locales/en-US/settings.json b/src/locales/en-US/settings.json new file mode 100644 index 000000000..f70f4326e --- /dev/null +++ b/src/locales/en-US/settings.json @@ -0,0 +1,119 @@ +{ + "general": "General", + "language": "Language", + "help_translate": "Help us translate", + "script_sync": "Script Sync", + "sync_delete": "Sync Delete", + "sync_delete_desc": "When enabled, scripts will be marked as deleted when removed, and other devices will detect this status and delete the script accordingly. When disabled, scripts will be deleted directly from both local and cloud storage, which may cause repeated sync issues if multiple devices are in use.", + "enable_script_sync_to": "Enable Script Sync to", + "script_subscription_check_interval": "Script/Subscription Update Check Interval", + "never": "Never", + "6_hours": "6 Hours", + "12_hours": "12 Hours", + "every_day": "Every Day", + "every_week": "Every Week", + "update_disabled_scripts": "Update Disabled Scripts", + "silent_update_non_critical_changes": "Silently Update Non-critical Changes", + "enable_eslint": "Enable ESLint", + "eslint_rules": "ESLint Rules", + "enter_eslint_rules": "Please enter ESLint rules. Configurations can be downloaded from https://eslint.org/play/.", + "language_change_tip": "Language Change Successful", + "backup": "Backup", + "local": "Local", + "export_file": "Export File", + "import_file": "Import File", + "cloud": "Cloud", + "backup_to": "Backup To", + "preparing_backup": "Preparing Backup to Cloud", + "backup_success": "Backup Successful", + "backup_failed": "Backup Failed", + "no_backup_files": "No Backup Files", + "backup_list": "Backup List", + "open_backup_dir": "Open Backup Directory", + "confirm_delete_backup_file": "Confirm Delete Backup File", + "backup_strategy": "Backup Strategy", + "under_construction": "Under Construction", + "sync_system_connect_failed": "Sync system connection failed", + "sync_system_closed": "Sync turned off", + "sync_system_closed_description": "Sync is disabled, please configure again", + "export_success": "Dump success saved", + "get_backup_dir_url_failed": "Failed to get backup directory address", + "get_backup_files_failed": "Failed to fetch backups", + "baidu_netdisk": "BaiduNetdisk", + "netdisk_unbind": "Unbind {{provider}}", + "netdisk_unbind_confirm": "Unbind the {{provider}} account?", + "netdisk_unbind_success": "{{provider}} account unbound", + "netdisk_unbind_error": "Failed to unbind {{provider}} account", + "save_only_current_group": "Save only for current group", + "security": "Security", + "blacklist_pages": "Blacklist Page", + "blacklist_placeholder": "Disable ScriptCat to run scripts on pages like\nhttps://*.example.com", + "expression_format_error": "Condition expression format error", + "migration_confirm_message": "Retry the migration storage engine to modify existing data. Please confirm, see:https://docs.scriptcat.org/docs/change/v0.17/.", + "retry_migration": "Retry Migration Storage Engine", + "sync_status": "Sync Status", + "interface_settings": "Interface", + "select_interface_language": "Select interface display language", + "extension_icon_badge": "Extension Icon Badge", + "display_type": "Display Type", + "extension_icon_badge_type": "Number type displayed on extension icon", + "background_color": "Background Color", + "badge_background_color_desc": "Badge background color", + "text_color": "Text Color", + "badge_text_color_desc": "Badge text color", + "badge_type_none": "None", + "badge_type_run_count": "Run Count", + "badge_type_script_count": "Script Count", + "script_menu": "Script Menu", + "display_right_click_menu": "Show right-click menu", + "display_right_click_menu_desc": "Show script menu in browser right-click menu", + "expand_count": "Expand Count", + "auto_collapse_when_exceeds": "Auto collapse when exceeds this number", + "script_update_check_frequency": "Script Update Check Frequency", + "script_auto_update_frequency": "Script automatic update check frequency", + "update_options": "Update Options", + "control_script_update_behavior": "Control script update behavior", + "blacklist_pages_desc": "Prevent scripts from running on specified pages, supports wildcards", + "development_tools": "Development Tools", + "check_script_code_quality": "Check script code quality and errors", + "custom_eslint_rules_config": "Custom ESLint rules configuration (JSON format)", + "script_run_env": { + "title": "Operating environment", + "all": "All", + "normal-tabs": "Normal tags", + "incognito-tabs": "Incognito tags" + }, + "script_run_at": { + "title": "Run Timing" + }, + "script_setting": { + "title": "Script Setting", + "default": "default" + }, + "notification": { + "script_sync_delete": "Script Sync Delete", + "script_sync_delete_desc": "Script {{scriptName}} has been deleted", + "subscribe_update": "Subscribe {{subscribeName}} has been updated", + "subscribe_update_desc": "New scripts: {{newScripts}}\nDeleted scripts: {{deletedScripts}}" + }, + "enable_background": { + "title": "Enable Background Running", + "description": "When enabled, the browser stays running in the background after you close all windows, minimizing to the tray until you manually quit Chrome. This allows background scripts to keep running.", + "enable_failed": "Failed to enable", + "disable_failed": "Failed to disable", + "prompt_title": "Enable Background Running?", + "prompt_description": "This is a {{scriptType}}. Enabling background running allows the script to continue running after the browser is closed.", + "enable_now": "Enable Now", + "maybe_later": "Maybe Later", + "settings_hint": "You can change this option in settings at any time." + }, + "favicon_service": "Favicon Service", + "favicon_service_desc": "Choose the service for fetching website icons", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_google": "Google", + "favicon_service_duckduckgo": "DuckDuckGo", + "favicon_service_icon-horse": "Icon Horse", + "favicon_service_local": "Local Fetch", + "cloud_sync_account_verification": "Cloud Sync Account Verification in Progress...", + "cloud_sync_verification_failed": "Cloud Sync Account Verification Failed" +} diff --git a/src/locales/en-US/tools.json b/src/locales/en-US/tools.json new file mode 100644 index 000000000..8af838a02 --- /dev/null +++ b/src/locales/en-US/tools.json @@ -0,0 +1,17 @@ +{ + "development_tool": "Development Tool", + "vscode_url": "VSCode URL", + "auto_connect_vscode_service": "Auto Connect VSCode Service", + "connect": "Connect", + "connection_success": "Connection Successful", + "connection_failed": "Connection Failed", + "select_import_script": "Please Select the Script to Import in the New Page", + "import_error": "Import Error", + "pulling_data_from_cloud": "Pulling Data from Cloud", + "pull_failed": "Pull Failed", + "restore": "Restore", + "local_backup": "Local Backup", + "cloud_backup": "Cloud Backup", + "auto_backup": "Auto Backup", + "data_migration": "Data Migration" +} diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json deleted file mode 100644 index 5713b5069..000000000 --- a/src/locales/en-US/translation.json +++ /dev/null @@ -1,808 +0,0 @@ -{ - "sentence-separator": ". ", - "import_link": "Link Import", - "import_link_failure": "Link Import Failed", - "create_user_script": "Create User Script", - "create_background_script": "Create Background Script", - "create_scheduled_script": "Create Scheduled Script", - "import_by_local": "Local Import", - "import_local_failure": "Local Import Failed", - "import_local_success": "Local Import Suceeded", - "create_script": "Create Script", - "user_guide": "User Guide", - "api_docs": "API Docs", - "development_guide": "Development Guide", - "script_gallery": "Script Gallery", - "community_forum": "Community Forum", - "external_links": "External Links", - "system_follow": "Follow System", - "no_data": "No Data", - "installed_scripts": "Installed Scripts", - "subscribe": "Subscribe", - "logs": "Logs", - "tools": "Tools", - "find": "Find", - "replace": "Replace", - "settings": "Settings", - "hide_main_sidebar": "Collapse sidebar", - "show_main_sidebar": "Expand sidebar", - "guide": "Guide", - "helpcenter": "Help Center", - "general": "General", - "language": "Language", - "help_translate": "Help us translate", - "script_sync": "Script Sync", - "sync_delete": "Sync Delete", - "sync_delete_desc": "When enabled, scripts will be marked as deleted when removed, and other devices will detect this status and delete the script accordingly. When disabled, scripts will be deleted directly from both local and cloud storage, which may cause repeated sync issues if multiple devices are in use.", - "enable_script_sync_to": "Enable Script Sync to", - "save": "Save", - "save_as": "Save as", - "file": "File", - "run": "Run", - "debug": "Debug", - "cloud_sync_account_verification": "Cloud Sync Account Verification in Progress...", - "cloud_sync_verification_failed": "Cloud Sync Account Verification Failed", - "save_success": "Saved successfully", - "reset_success": "Reset successfully", - "update": "Update", - "check_update": "Check Update", - "script_subscription_check_interval": "Script/Subscription Update Check Interval", - "never": "Never", - "6_hours": "6 Hours", - "12_hours": "12 Hours", - "every_day": "Every Day", - "every_week": "Every Week", - "update_disabled_scripts": "Update Disabled Scripts", - "silent_update_non_critical_changes": "Silently Update Non-critical Changes", - "enable_eslint": "Enable ESLint", - "eslint_rules": "ESLint Rules", - "enter_eslint_rules": "Please enter ESLint rules. Configurations can be downloaded from https://eslint.org/play/.", - "language_change_tip": "Language Change Successful", - "backup": "Backup", - "local": "Local", - "export_file": "Export File", - "import_file": "Import File", - "cloud": "Cloud", - "backup_to": "Backup To", - "preparing_backup": "Preparing Backup to Cloud", - "backup_success": "Backup Successful", - "backup_failed": "Backup Failed", - "no_backup_files": "No Backup Files", - "backup_list": "Backup List", - "open_backup_dir": "Open Backup Directory", - "confirm_delete": "Confirm Deletion", - "confirm_delete_backup_file": "Confirm Delete Backup File", - "confirm_update": "Confirm Update", - "delete_success": "Delete Successful", - "deleting": "Deleting", - "backup_strategy": "Backup Strategy", - "under_construction": "Under Construction", - "development_tool": "Development Tool", - "vscode_url": "VSCode URL", - "auto_connect_vscode_service": "Auto Connect VSCode Service", - "connect": "Connect", - "connection_success": "Connection Successful", - "connection_failed": "Connection Failed", - "select_import_script": "Please Select the Script to Import in the New Page", - "import_error": "Import Error", - "pulling_data_from_cloud": "Pulling Data from Cloud", - "pull_failed": "Pull Failed", - "restore": "Restore", - "log_title": "Runtime Logs", - "last_5_minutes": "Last 5 Minutes", - "last_15_minutes": "Last 15 Minutes", - "last_30_minutes": "Last 30 Minutes", - "last_1_hour": "Last 1 Hour", - "last_3_hours": "Last 3 Hours", - "last_6_hours": "Last 6 Hours", - "last_12_hours": "Last 12 Hours", - "last_24_hours": "Last 24 Hours", - "last_7_days": "Last 7 Days", - "query": "Query", - "labels": "Labels", - "search_regex": "Search (Supports Regex)", - "clean_schedule": "Automatically Clean Up Logs Older Than", - "days_ago_logs": "Days", - "delete_completed": "Deletion Completed", - "delete_current_logs": "Delete Current Logs", - "clear_completed": "Clearing Completed", - "clear_logs": "Clear Logs", - "to": " - ", - "now": "Now", - "total_logs": "{{length}} logs found", - "filtered_logs": "{{length}} logs after filtering", - "enter_filter_conditions": "Please enter filter conditions", - "permission": "Permission", - "enter_subscribe_name": "Please Enter Subscribe Name", - "subscribe_url": "Subscribe URL", - "confirm_delete_subscription": "Are you sure you want to delete this Subscribe? Related scripts will also be deleted.", - "list": { - "confirm_delete": "Are you sure you want to delete? Please note that this is an irreversible operation.", - "confirm_update": "Are you sure you want to update? Please note that this is an irreversible operation." - }, - "enable": "Enable", - "script_list_enable_width": 100, - "script_list_last_updated_width": 150, - "script_list_apply_to_run_status_width": 110, - "subscribe_list_enable_width": 120, - "disable": "Disable", - "name": "Name", - "version": "Version", - "apply_to_run_status": "Apply To / Run Status", - "source": "Source", - "home": "Home", - "sorting": "Sorting", - "last_updated": "Last Updated", - "action": "Action", - "foreground_page_script_tooltip": "Foreground page script, runs on the specified page", - "background_script_tooltip": "Background script, will run in background when enabled", - "scheduled_script_tooltip": "Scheduled script, next run time:", - "running": "Running", - "completed": "Completed", - "source_subscribe_link": "Subscribe Link", - "source_local_script": "Local Script", - "source_script_link": "Script Link", - "by_manual_creation": "Created locally through code editing", - "confirm_delete_script": "Are you sure you want to delete this script?", - "confirm_delete_script_content": "Are you sure you want to delete the script \"{{name}}\"? This action cannot be undone.", - "delete_failed": "Deletion Failed", - "enter_script_name": "Please enter script name", - "update_not_supported": "This script does not support update checks", - "checking_for_updates": "Checking for updates...", - "new_version_available": "New Version Available", - "latest_version": "Latest Version", - "checked_for_all_selected": "Checked updates for all selected", - "update_check_failed": "Update Check Failed", - "script_import_failed": "Script Import Failed", - "install_page_open_failed": "Failed to Open Install Page", - "stopping_script": "Stopping Script", - "script_stopped": "Script Stopped", - "starting_script": "Starting Script...", - "starting_updates": "Starting Batch Updates...", - "script_started": "Script Started", - "operation_failed": "Operation failed", - "batch_operations": "Batch Operations", - "export": "Export", - "delete": "Delete", - "pin_to_top": "Pin Top", - "scripts_pinned_to_top": "The selected script has been pinned", - "unknown_operation": "Unknown Operation", - "confirm": "Confirm", - "close": "Close", - "page_script": "Page Script", - "homepage": "Homepage", - "script_website": "Script Website", - "script_source": "Script Source Code", - "bug_feedback_script_support": "Bug Feedback/Script Support", - "config": "Config", - "key": "Key", - "value": "Value", - "add": "Add", - "type": "Type", - "edit_value": "Edit Value", - "add_value": "Add Value", - "update_success": "Update Successful", - "add_success": "Add Successful", - "script_storage": "Script Storage", - "enter_key": "Please Enter Key", - "key_placeholder": "Key", - "value_placeholder": "When the type is object, please enter data that can be JSON parsed.", - "clear": "Clear", - "clear_success": "Clear Successful", - "confirm_clear": "Are you sure you want to clear this storage space?", - "type_string": "String", - "type_number": "Number", - "type_boolean": "Boolean", - "type_object": "Object", - "confirm_delete_resource": "Are you sure you want to delete this resource? This resource will reload on the next startup.", - "confirm_clear_resource": "Are you sure you want to clear these resources? Resources will reload on the next startup.", - "script_resource": "Script Resource", - "permission_value": "Permission Value", - "allow": "Allow", - "yes": "Yes", - "no": "No", - "confirm_delete_permission": "Are you sure you want to delete this permission?", - "basic_info": "Basic Information", - "update_url": "Update URL", - "permission_management": "Permission Management", - "add_permission": "Add Permission", - "permission_cors": "Cross-domain (CORS)", - "permission_cookie": "Manage Cookies", - "match": "Match", - "user_setting": "User Setting", - "confirm_delete_exclude": "Confirm deletion of this exclusion?", - "after_deleting_match_item": "After the script's match items are deleted, they will be automatically added to the match items", - "confirm_delete_match": "Confirm deletion of this match?", - "after_deleting_exclude_item": "After deleting the script's match item, it will be automatically added to the exclusion items", - "add_match": "Add Match", - "add_exclude": "Add Exclude", - "website_match": "Website Match (@match)", - "reset": "Reset", - "website_exclude": "Website Exclude (@exclude)", - "confirm_reset": "Confirm reset?", - "script_total_runs": "The script has run {{runNum}} times, {{runNumByIframe}} times in iframes", - "script_total_runs_single": "The script has run {{runNum}} times", - "script_disabled": "The script is not enabled", - "run_once": "Run Once", - "stop": "Stop", - "edit": "Edit", - "undo": "Undo", - "redo": "Redo", - "cut": "Cut", - "copy": "Copy", - "paste": "Paste", - "format": "Format", - "exclude_on": "Reinstate $0's execution", - "exclude_off": "Exclude $0's exeuction", - "user_config": "User Config", - "gm_api": "GM API", - "storage_api": "Storage API", - "use_file_system": "File System in Use", - "open_directory": "Open Directory", - "account_validation_failed": "Account Validation Failed", - "not_set": "Not Set", - "in_use": "In Use", - "storage_error": "Storage Error", - "upload_to_cloud": "Upload to Cloud", - "save_failed": "Save Failed", - "exporting": "Exporting...", - "upload_to": "Upload To", - "value_export_expression": "Value Export Expression", - "overwrite_original_value_on_import": "Overwrite Original Value on Import", - "cookie_export_expression": "Cookie Export Expression", - "overwrite_original_cookie_on_import": "Overwrite Original Cookie on Import", - "restore_default_values": "Restore Default Values", - "get_confirm_error": "Get Confirmation Information Failed", - "confirm_error": "Confirmation Failed", - "ignore": "Ignore", - "allow_once": "Allow Once", - "temporary_allow": "Temporarily Allow This {{permissionContent}}", - "temporary_allow_all": "Temporarily Allow All {{permissionContent}}", - "permanent_allow": "Permanently Allow This {{permissionContent}}", - "permanent_allow_all": "Permanently Allow All {{permissionContent}}", - "deny_once": "Deny Once", - "temporary_deny": "Temporarily Deny This {{permissionContent}}", - "temporary_deny_all": "Temporarily Deny All {{permissionContent}}", - "permanent_deny": "Permanently Deny This {{permissionContent}}", - "permanent_deny_all": "Permanently Deny All {{permissionContent}}", - "data_import": "Data Import", - "import": "Import", - "select_scripts_to_import": "Please select the scripts you want to import", - "select_all": "Select All", - "script_import_progress": "Import Progress for Scripts", - "select_subscribes_to_import": "Please select the Subscribes you want to import", - "subscribe_import_progress": "Import Progress for Subscribes", - "author": "Author", - "description": "Description", - "operation": "Operation", - "error": "Error", - "unknown": "Unknown", - "add_new": "Add New", - "no_operation": "No Operation", - "enable_script": "Enable Script", - "local_creation": "Local Creation", - "import_success": "Import Successful", - "install_script": "Install Script", - "update_script": "Update Script", - "install_subscribe": "Install Subscribe", - "update_subscribe": "Update Subscribe", - "update_script_no_close": "Update Script without closing window", - "install_script_no_close": "Install Script without closing window", - "update_script_no_more_update": "Update Script and disable update check", - "close_update_script_no_more_update": "Close and do not check for updates", - "install_script_no_more_update": "Install Script and disable update check", - "invalid_link": "Invalid Link", - "subscribe_install_label": "This Subscribe will install the following scripts", - "script_runs_in": "Script will run on the following websites", - "script_has_full_access_to": "Script will have full access to the following URLs", - "script_requires": "Script requires the following external resources", - "cookie_warning": "Please note, this script requests access to Cookie permissions, which is a dangerous permission. Please verify the security of the script.", - "cron_oncetype": { - "minute": "{{next}} (runs every minute)", - "hour": "{{next}} (runs every hour)", - "day": "{{next}} (runs every day)", - "month": "{{next}} (runs every month)", - "week": "{{next}} (runs every week)" - }, - "cron_invalid_expr": "Invalid cron expression", - "scheduled_script_description_title": "This is a scheduled script, which will automatically run at a specific time once enabled and can be manually controlled in the panel.", - "scheduled_script_description_description_expr": "Scheduled task expression:", - "scheduled_script_description_description_next": "Most recent run time:", - "background_script_description": "This is a background script, which will automatically run once when the browser opens once enabled, and can be manually controlled in the panel.", - "install_success": "Install Successful", - "install": { - "update_success": "Update Successful" - }, - "install_failed": "Install Failed", - "subscribe_success": "Subscribe Successful", - "subscribe_failed": "Subscribe Failed", - "current_version": "Current Version", - "update_version": "Update Version", - "updatepage": { - "main_header": "Check for Updates", - "header_site_specific": "Updates available for this website ($0)", - "header_site_all": "Updates available", - "header_ignored": "Updates available but ignored", - "update_all": "Update All", - "ignore_all": "Ignore All", - "update": "Update", - "ignore": "Ignore", - "old_version_": "Old Version: ", - "new_version_": "New Version: ", - "enabled": "Enabled", - "tooltip_enabled": "This script is enabled.", - "disabled": "Disabled", - "tooltip_disabled": "This script is disabled.", - "similarity_": "Similarity: ", - "codechange_major": "Major Change", - "codechange_noticeable": "Noticeable Change", - "codechange_tiny": "Minor Change", - "new_connects_": "New @connect: ", - "tag_new_connect": "@connect Added", - "status_last_check": "Last Check: $0", - "status_checking_updates": "Checking the updates...", - "status_no_update": "No updates required", - "status_n_update": "$0 updates required", - "status_n_ignored": "$0 updates ignored", - "status_autoclose": "Auto-closing in $0 seconds", - "header_other_update": "Other available updates" - }, - "downloading_status_text": "Downloading. Received {{bytes}}.", - "install_page_please_wait": "Please wait", - "install_page_loading": "Loading Installation Page", - "install_page_load_failed": "Failed to Load Installation Page", - "invalid_page": "Invalid Page", - "background_script_tag": "This is a Background Script", - "scheduled_script_tag": "This is a Scheduled Script", - "background_script": "Background Script", - "scheduled_script": "Scheduled Script", - "install_from_legitimate_sources_warning": "Please install scripts from legitimate sources! Unknown scripts may invade your privacy or conduct malicious operations.", - "antifeature_referral_link_title": "Referral Link", - "antifeature_referral_link_description": "This script modifies or redirects to the author's referral link", - "antifeature_ads_title": "Ads", - "antifeature_ads_description": "This script inserts ads on the pages you visit", - "antifeature_payment_title": "Payment", - "antifeature_payment_description": "This script requires payment to be used properly", - "antifeature_miner_title": "Mining", - "antifeature_miner_description": "This script engages in mining activities", - "antifeature_membership_title": "Membership Features", - "antifeature_membership_description": "This script requires registration as a member to be used properly", - "antifeature_tracking_title": "Tracking", - "antifeature_tracking_description": "This script tracks your user information", - "script_info_load_failed": "Failed to load script information", - "script_status_tooltip": "You can control the enable status of scripts. Ordinary Tampermonkey scripts are enabled by default, and Background scripts and Scheduled scripts are disabled by default.", - "subscribe_source_tooltip": "This is a subscription source. When you open the subscription, the subscription script will be automatically installed.", - "get_script": "Get Script", - "report_issue": "Bug / Issue Feedback", - "project_docs": "Project Documentation", - "community": "Community", - "popup": { - "new_version_available": "New Version Available" - }, - "current_page_scripts": "Current Page Running Scripts", - "enabled_background_scripts": "Enabled and Running Background Scripts", - "script_accessing_cross_origin_resource": "Script is attempting to access a cross-origin resource", - "confirm_operation_description": "Please confirm if you allow the script to perform this operation. The script can also add the @connect tag to bypass this option.", - "extension_site_access_title": "ScriptCat needs site access", - "extension_site_access_description": "Grant browser site access to this origin so ScriptCat can perform the request. This changes the extension's site access setting.", - "extension_site_access_content": "Site", - "domain": "Domain", - "script_name": "Script Name", - "request_domain": "Request Domain", - "request_url": "Request URL", - "access_cookie_content": "The script is attempting to access website cookie content", - "confirm_script_operation": "Please confirm if you allow the script to perform this operation. Cookies contain important user data, so only grant access to trusted scripts.", - "cookie_domain": "Cookie Domain", - "script_operation_title": "Script is attempting to access storage space", - "script_operation_description": "Please confirm whether you allow the script to perform this operation. If allowed, the script will be able to access the storage space you have set up and create a directory app/${dir} within it.", - "script_permission_content": "Script", - "sync_system_connect_failed": "Sync system connection failed", - "sync_system_closed": "Sync turned off", - "sync_system_closed_description": "Sync is disabled, please configure again", - "auth_type": "Authentication Type", - "url": "URL", - "username": "Username", - "password": "Password", - "access_token_bearer": "Access Token (Bearer)", - "s3_bucket_name": "Bucket Name", - "s3_region": "Region", - "s3_access_key_id": "Access Key ID", - "s3_secret_access_key": "Secret Access Key", - "s3_custom_endpoint": "Custom Endpoint (Optional)", - "skip": "Skip", - "next": "Next", - "next_with_progress": "Next (Step {step} of {steps})", - "back": "Back", - "last": "Finish", - "start_guide_title": "Welcome to the ScriptCat Extension", - "start_guide_content": "Next, we will introduce the basic usage of ScriptCat to you.", - "guide_installed_scripts": "The scripts you have installed will be displayed here.", - "guide_script_list_title": "Script Market", - "guide_script_list_content": "You can install scripts from the Script Market. In addition to supporting user scripts, ScriptCat also supports background scripts.", - "guide_script_list_enable_title": "Enable Scripts", - "guide_script_list_enable_content": "Scripts need to be enabled to be used. Page scripts are enabled by default upon installation, while background scripts are disabled by default.", - "guide_script_list_apply_to_run_status_title": "Apply To and Run Status", - "guide_script_list_apply_to_run_status_content": "The running status of scripts is displayed here. Hover over the tags to view the script type.", - "guide_script_list_sort_title": "Sort", - "guide_script_list_sort_content": "You can drag the tag of the script to sort", - "guide_script_list_update_title": "Last Updated", - "guide_script_list_update_content": "Click the last update column label to check for updates to the script", - "guide_script_list_action_title": "Operation", - "guide_script_list_action_content": "The operation bar allows access to script editing, control script execution and termination (background scripts), and settings (see UserConfig). The buttons on the right allow access to advanced filtering and toggling view modes.", - "guide_tools_title": "Common Tools", - "guide_tools_content": "The tools provide backup and development features.", - "guide_tools_backup_title": "Backup", - "guide_tools_backup_content": "Backup allows you to save scripts to avoid loss. You can export script files to your local machine or load script files from your local machine. You can also back up to the cloud for added convenience.", - "guide_setting_title": "Settings", - "guide_setting_content": "The settings mainly include language, script synchronization, update frequency, and other common options.", - "guide_setting_sync_title": "Update and Sync", - "guide_setting_sync_content": "The script synchronization feature allows you to easily sync the script content of this device to the cloud. If you select the option to sync deletions, when a script is deleted on this device, it will also be deleted from the cloud. You can also update the script version here to get more powerful features.", - "auto": "Auto", - "hide": "Hide", - "custom": "Custom", - "resize_column_width": "Resize Column", - "collapse": "Collapse", - "expand": "Expand", - "menu_expand_num_before": "Menu items more than", - "menu_expand_num_after": "will be hidden.", - "script_name_cannot_be_set_to_empty": "Script name cannot be empty", - "edit_conflict": "Edit Conflict", - "confirm_override_when_edit_conflict": "This script was edited in another instance. Replacing it will overwrite those changes. Would you like to keep this version instead?", - "save_abort_when_edit_conflict": "The script was edited in another instance. Save aborted.", - "scriptname_conflict": "Script Name Conflict", - "confirm_save_when_scriptname_conflict": "This script name is already used by another script. Do you still want to save it?", - "save_abort_when_scriptname_conflict": "This script name is already used by another script. Save aborted.", - "eslint_config_format_error": "eslint configuration format error", - "export_success": "Dump success saved", - "get_backup_dir_url_failed": "Failed to get backup directory address", - "get_backup_files_failed": "Failed to fetch backups", - "request_permission": "Request Permission", - "develop_mode_guide": "'Developer mode' is currently not enabled, so the scripts cannot run properly. 👉tap to learn how to enable", - "allow_user_script_guide": "'Allow User Scripts' is currently not enabled, so the scripts cannot run properly. 👉tap to learn how to enable", - "lower_version_browser_guide": "Your browser is too outdated, so the scripts cannot run properly. 👉Click me to learn more", - "click_to_reload": "👉Click to Reload", - "page_in_blacklist": "The current page is blacklisted, cannot use script", - "baidu_netdisk": "BaiduNetdisk", - "netdisk_unbind": "Unbind {{provider}}", - "netdisk_unbind_confirm": "Unbind the {{provider}} account?", - "netdisk_unbind_success": "{{provider}} account unbound", - "netdisk_unbind_error": "Failed to unbind {{provider}} account", - "save_only_current_group": "Save only for current group", - "script_import_result": "Script Import Results", - "failure_info": "Failure Information", - "security": "Security", - "blacklist_pages": "Blacklist Page", - "blacklist_placeholder": "Disable ScriptCat to run scripts on pages like\nhttps://*.example.com", - "expression_format_error": "Condition expression format error", - "migration_confirm_message": "Retry the migration storage engine to modify existing data. Please confirm, see:https://docs.scriptcat.org/docs/change/v0.17/.", - "retry_migration": "Retry Migration Storage Engine", - "script_modified_leave_confirm": "Your changes have not been saved. If you leave, your changes will be lost. Are you sure you want to leave?", - "create_success_note": "New script successfully created. Note that the background script will not be enabled by default.", - "save_as_failed": "Save As Failed", - "save_as_success": "Saved Successfully", - "only_background_scheduled_can_run": "Only background script/crontab scripts can run", - "preparing_script_resources": "Preparing script resource...", - "build_success_message": "Build successfully, open developer tools to view output in the console", - "script_storage_tooltip": "Can manage the stored script data (GM_value)", - "script_resource_tooltip": "Manage @resource, @required downloaded resources", - "script_setting_tooltip": "Make some custom settings for scripts", - "script_modified_close_confirm": "The script has been modified. Changes will be lost after closing, continue?", - "close_current_tab": "Close current tab", - "close_other_tabs": "Close others", - "close_left_tabs": "Close tabs to the left", - "close_right_tabs": "Close tabs to the right", - "import_script_placeholder": "Support for an absolute link at the end of .user.js or a link to a scripting cat installation page\ncan be filled in multiple lines, each entry\nExample:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/en/script-show-page/1234", - "invalid_script_code": "Bad Code", - "build_failed": "Failed to Build", - "drag_script_here_to_upload": "Drag script here to upload it", - "sync_status": "Sync Status", - "search_scripts": "Search script", - "ext_update_notification": "Scriptcat extension updated", - "ext_update_notification_desc": "Current version: {{version}}, please see the update log for details", - "watch_file_description": "Monitor file changes and automatically update scripts. When using, please make sure the script file path remains unchanged and the page cannot be closed.", - "watch_file": "Watch file", - "stop_watch_file": "Stop watch", - "script_menu_display": "Script Registered Menu", - "badge_type_none": "None", - "badge_type_run_count": "Run Count", - "badge_type_script_count": "Script Count", - "interface_settings": "Interface", - "select_interface_language": "Select interface display language", - "extension_icon_badge": "Extension Icon Badge", - "display_type": "Display Type", - "extension_icon_badge_type": "Number type displayed on extension icon", - "background_color": "Background Color", - "badge_background_color_desc": "Badge background color", - "text_color": "Text Color", - "badge_text_color_desc": "Badge text color", - "script_menu": "Script Menu", - "display_right_click_menu": "Show right-click menu", - "display_right_click_menu_desc": "Show script menu in browser right-click menu", - "expand_count": "Expand Count", - "auto_collapse_when_exceeds": "Auto collapse when exceeds this number", - "script_update_check_frequency": "Script Update Check Frequency", - "script_auto_update_frequency": "Script automatic update check frequency", - "update_options": "Update Options", - "control_script_update_behavior": "Control script update behavior", - "blacklist_pages_desc": "Prevent scripts from running on specified pages, supports wildcards", - "development_tools": "Development Tools", - "check_script_code_quality": "Check script code quality and errors", - "custom_eslint_rules_config": "Custom ESLint rules configuration (JSON format)", - "light": "Light", - "dark": "Dark", - "individual_edit": "Individual Edit", - "batch_edit": "Batch Edit", - "script_code": "Script Code", - "enter_search_value": "Enter {{search}} to search", - "script_run_env": { - "title": "Operating environment", - "all": "All", - "normal-tabs": "Normal tags", - "incognito-tabs": "Incognito tags" - }, - "script_run_at": { - "title": "Run Timing" - }, - "script_setting": { - "title": "Script Setting", - "default": "default" - }, - "editor_config": "Editor Configuration", - "editor_config_description": "You can refer to the compilerOptions in jsconfig.js for configuration", - "editor_type_definition": "Editor Type Definition", - "editor_type_definition_description": "You can customize your own type definitions, and the script editor will automatically load these type definitions", - "eslint_rules_reset": "ESLint Rules Reset", - "eslint_rules_saved": "ESLint Rules Saved", - "editor_config_reset": "Editor Configuration Reset", - "editor_config_saved": "Editor Configuration Saved", - "editor_config_format_error": "Editor Configuration Format Error", - "editor_type_definition_reset": "Editor Type Definition Reset", - "editor_type_definition_saved": "Editor Type Definition Saved", - "script_list": { - "sidebar": { - "stopped": "Stopped", - "all": "All", - "normal_script": "Normal Script", - "status": "Status" - } - }, - "tags": "Tags", - "install_source": "Install Source", - "input_tags_placeholder": "Enter tags, press Enter to confirm", - "switch_to_card_mode": "Switch to Card Mode", - "switch_to_table_mode": "Switch to Table Mode", - "open_sidebar": "Open Sidebar", - "close_sidebar": "Close Sidebar", - "no_message_content": "No Message Content", - "error_metadata_invalid": "Invalid MetaData block", - "error_script_name_required": "Script name is required", - "error_script_version_required": "Script @version is required", - "error_script_namespace_required": "Script @namespace is required", - "error_cron_invalid": "Invalid cron expression: {{expr}}", - "error_script_type_mismatch": "Script type mismatch: normal and background scripts cannot be converted", - "error_old_script_code_missing": "Previous script code not found", - "error_subscribe_name_required": "Subscription name is required", - "error_grant_conflict": "@grant declares both 'none' and GM API", - "error_metadata_line_duplicated": "There are duplicate declarations in the metadata.", - "notification": { - "script_sync_delete": "Sync Script Delete", - "script_sync_delete_desc": "Script {{scriptName}} has been deleted", - "subscribe_update": "Subscribe {{subscribeName}} has been updated", - "subscribe_update_desc": "New scripts: {{newScripts}}\nDeleted scripts: {{deletedScripts}}" - }, - "loading": "Loading...", - "runtime": "Runtime", - "enable_background": { - "title": "Enable Background Running", - "description": "When enabled, the browser stays running in the background after you close all windows, minimizing to the tray until you manually quit Chrome. This allows background scripts to keep running.", - "enable_failed": "Failed to enable", - "disable_failed": "Failed to disable", - "prompt_title": "Enable Background Running?", - "prompt_description": "This is a {{scriptType}}. Enabling background running allows the script to continue running after the browser is closed.", - "enable_now": "Enable Now", - "maybe_later": "Maybe Later", - "settings_hint": "You can change this option in settings at any time." - }, - "favicon_service": "Favicon Service", - "favicon_service_desc": "Choose the service for fetching website icons", - "favicon_service_scriptcat": "ScriptCat", - "favicon_service_google": "Google", - "favicon_service_duckduckgo": "DuckDuckGo", - "favicon_service_icon-horse": "Icon Horse", - "favicon_service_local": "Local Fetch", - "editor": { - "show_script_list": "Show Script List", - "hide_script_list": "Hide Script List" - }, - "agent": "AI Agent", - "agent_chat": "Chat", - "agent_provider": "Model Service", - "agent_mcp": "MCP", - "agent_skills": "Skills", - "agent_provider_title": "Model Service", - "agent_provider_select": "AI Provider", - "agent_provider_api_base_url": "API Base URL", - "agent_provider_api_key": "API Key", - "agent_provider_model": "Default Model", - "agent_provider_test_connection": "Test Connection", - "agent_provider_test_success": "Connection Successful", - "agent_provider_test_failed": "Connection Failed", - "agent_model_fetch": "Fetch Models", - "agent_model_fetch_failed": "Failed to fetch models", - "agent_model_name": "Name", - "agent_model_add": "Add Model", - "agent_model_edit": "Edit", - "agent_model_copy": "Copy", - "agent_model_delete": "Delete", - "agent_model_set_default": "Set as Default", - "agent_model_default_label": "Default", - "agent_model_delete_confirm": "Are you sure to delete this model?", - "agent_model_max_tokens": "Max Output Tokens", - "agent_model_context_window": "Context Window", - "agent_model_no_models": "No models configured", - "agent_model_vision_support": "Supports vision input", - "agent_model_image_output": "Supports image output", - "agent_model_capabilities": "Capabilities", - "agent_model_supports_vision": "Vision Input", - "agent_model_supports_image_output": "Image Output", - "agent_coming_soon": "Coming soon...", - "agent_skills_title": "Skills Management", - "agent_skills_add": "Add Skill", - "agent_skills_empty": "No skills installed", - "agent_skills_tools": "Tools", - "agent_skills_references": "References", - "agent_skills_detail": "Skill Details", - "agent_skills_edit_prompt": "Prompt", - "agent_skills_install": "Install Skill", - "agent_skills_install_url": "Import from URL", - "agent_skills_install_paste": "Paste SKILL.md", - "agent_skills_uninstall": "Uninstall", - "agent_skills_uninstall_confirm": "Are you sure to uninstall Skill \"{{name}}\"?", - "agent_skills_save_success": "Saved successfully", - "agent_skills_install_success": "Installed successfully", - "agent_skills_fetch_failed": "Fetch failed", - "agent_skills_add_script": "Add Script", - "agent_skills_add_reference": "Add Reference", - "agent_skills_install_zip": "Upload ZIP", - "agent_skills_install_zip_hint": "Click to select a .zip file", - "agent_skills_prompt": "Prompt", - "agent_skills_installed_at": "Installed at", - "agent_skills_refresh": "Refresh", - "agent_skills_refresh_success": "Refreshed successfully", - "agent_skills_tool_code": "Tool Code", - "agent_skills_click_to_view_code": "Click tool name to view code", - "agent_skills_config": "Config", - "agent_skills_config_saved": "Config saved", - "agent_skills_check_updates": "Check Updates", - "agent_skills_no_updates": "All skills are up to date", - "agent_skills_updates_available": "update(s) available", - "agent_skills_update": "Update", - "agent_skills_update_success": "Updated successfully", - "agent_skills_url_placeholder": "Enter SKILL.cat.md URL", - "agent_chat_new": "New Chat", - "agent_chat_delete": "Delete Chat", - "agent_chat_delete_confirm": "Delete this conversation?", - "agent_chat_no_conversations": "No conversations", - "agent_chat_input_placeholder": "Type a message...", - "agent_chat_send": "Send", - "agent_chat_stop": "Stop", - "agent_chat_thinking": "Thinking", - "agent_chat_tool_call": "Tool Call", - "agent_chat_error": "Error occurred", - "agent_chat_no_model": "No model configured. Please add one in Model Service first.", - "agent_chat_model_select": "Select Model", - "agent_chat_rename": "Rename", - "agent_chat_copy": "Copy", - "agent_chat_copy_success": "Copied", - "agent_chat_regenerate": "Regenerate", - "agent_chat_streaming": "Generating...", - "agent_chat_retrying": "Retrying ({{attempt}}/{{max}})...", - "agent_chat_newline": "for new line", - "agent_chat_attach_image": "Attach image", - "agent_chat_attach_file": "Attach file", - "agent_chat_welcome_hint": "Ask me anything about your scripts", - "agent_chat_welcome_start": "Create a conversation to get started", - "agent_chat_tokens": "tokens", - "agent_chat_first_token": "TTFT", - "agent_chat_tools_count": "{{count}} tools", - "agent_chat_tools_enabled": "Tools enabled", - "agent_chat_tools_disabled": "Tools disabled", - "agent_chat_tools_enabled_tip": "Tools enabled — click to disable", - "agent_chat_tools_disabled_tip": "Tools disabled — click to enable", - "agent_chat_background_enabled_tip": "Background mode ON — conversation keeps running after closing the page, click to disable", - "agent_chat_background_disabled_tip": "Background mode OFF — conversation stops when the page is closed, click to enable", - "agent_chat_delete_round": "Delete", - "agent_chat_copy_message": "Copy", - "agent_chat_edit_message": "Edit", - "agent_chat_save_and_send": "Save & Send", - "agent_chat_cancel_edit": "Cancel", - "agent_chat_message_queued": "Queued", - "agent_chat_cancel_message": "Cancel send", - "agent_permission_title": "The script is requesting to use Agent conversation", - "agent_permission_describe": "This script requests Agent conversation access, which will consume API tokens. Only grant access to trusted scripts.", - "agent_permission_content": "Agent Conversation", - "agent_opfs": "OPFS", - "agent_opfs_title": "OPFS File Browser", - "agent_opfs_empty": "Empty directory", - "agent_opfs_name": "Name", - "agent_opfs_size": "Size", - "agent_opfs_type": "Type", - "agent_opfs_modified": "Last Modified", - "agent_opfs_delete_confirm": "Are you sure to delete?", - "agent_opfs_delete_success": "Deleted", - "agent_opfs_file": "File", - "agent_opfs_directory": "Directory", - "agent_opfs_preview": "Preview", - "agent_opfs_root": "Root", - "agent_dom_permission_title": "The script is requesting DOM operation access", - "agent_dom_permission_describe": "This script requests the ability to read and manipulate web page DOM (click, fill forms, navigate, screenshot, etc.). Only grant access to trusted scripts.", - "agent_dom_permission_content": "Agent DOM Operations", - "agent_mcp_title": "MCP Servers", - "agent_mcp_add_server": "Add Server", - "agent_mcp_no_servers": "No MCP servers configured", - "agent_mcp_test_connection": "Test", - "agent_mcp_name_url_required": "Name and URL are required", - "agent_mcp_optional": "optional", - "agent_mcp_custom_headers": "Custom Headers", - "agent_mcp_enabled": "Enabled", - "agent_mcp_detail": "Details", - "agent_mcp_tools": "Tools", - "agent_mcp_resources": "Resources", - "agent_mcp_prompts": "Prompts", - "agent_mcp_no_tools": "No tools available", - "agent_mcp_no_resources": "No resources available", - "agent_mcp_no_prompts": "No prompts available", - "agent_mcp_loading": "Loading...", - "agent_mcp_parameters": "Parameters", - "agent_tasks": "Tasks", - "agent_tasks_title": "Scheduled Tasks", - "agent_tasks_create": "Create Task", - "agent_tasks_edit": "Edit Task", - "agent_tasks_mode_internal": "Internal", - "agent_tasks_mode_event": "Event", - "agent_tasks_cron": "Cron Expression", - "agent_tasks_next_run": "Next Run", - "agent_tasks_last_status": "Last Status", - "agent_tasks_run_now": "Run Now", - "agent_tasks_history": "History", - "agent_tasks_prompt": "Prompt", - "agent_tasks_max_iterations": "Max Iterations", - "agent_tasks_notify": "Notify on Complete", - "agent_tasks_no_tasks": "No scheduled tasks", - "agent_tasks_delete_confirm": "Are you sure to delete this task?", - "agent_tasks_clear_runs": "Clear History", - "agent_tasks_clear_runs_confirm": "Are you sure to clear the run history?", - "agent_tasks_event_hint": "When triggered, the script that created this task will be notified", - "agent_tasks_name_cron_required": "Name and Cron expression are required", - "agent_tasks_model_select": "Select Model", - "agent_tasks_skills": "Skills", - "agent_tasks_skills_auto": "Auto load all", - "agent_tasks_conversation_id": "Continue conversation ID (optional)", - "agent_tasks_run_status_success": "Success", - "agent_tasks_run_status_error": "Error", - "agent_tasks_run_status_running": "Running", - "agent_tasks_run_duration": "Duration", - "agent_tasks_run_usage": "Usage", - "agent_tasks_run_conversation": "View Conversation", - "agent_tasks_run_time": "Time", - "agent_tasks_run_status": "Status", - "agent_tasks_never_run": "Never run", - "agent_settings": "Settings", - "agent_settings_title": "Agent Settings", - "agent_doc_link": "Documentation", - "agent_model_settings": "Model Settings", - "agent_summary_model": "Summary Model", - "agent_summary_model_desc": "Model for summarization, falls back to default if not set", - "agent_summary_model_placeholder": "Use default model", - "agent_search_settings": "Search Settings", - "agent_search_engine": "Search Engine", - "agent_search_engine_baidu": "Baidu", - "agent_search_google_api_key": "Google API Key", - "agent_search_google_cse_id": "Custom Search Engine ID", - "agent_settings_saved": "Settings saved", - "agent_settings_save_failed": "Failed to save settings", - "agent_search_engine_tip_bing": "Default search engine with broad global coverage, no extra configuration needed.", - "agent_search_engine_tip_duckduckgo": "Privacy-focused search engine, no API key required.", - "agent_search_engine_tip_baidu": "Optimized for Chinese content, better results for Chinese queries.", - "agent_search_engine_tip_google": "High-quality search results, requires a Google API Key and Custom Search Engine ID." -} \ No newline at end of file diff --git a/src/locales/i18n-usage.test.ts b/src/locales/i18n-usage.test.ts new file mode 100644 index 000000000..239dc1fab --- /dev/null +++ b/src/locales/i18n-usage.test.ts @@ -0,0 +1,89 @@ +// can be tested with vitest-environment node +import { describe, it, expect } from "vitest"; +import fs from "fs"; +import path from "path"; + +// 防止 i18n key 命名空间写错(如 script 命名空间的 key 漏写 "script:" 前缀, +// 或误用 "popup." 点号代替 "popup:" 冒号),导致 UI 直接显示原始 key 而非译文。 +// 该测试静态扫描 src/pages 与 service worker 下所有 t("字面量")/i18n.t("字面量") +// 调用,按 i18next 解析规则在 zh-CN 资源中查找;带 defaultValue 的调用会回退到 +// 默认文案,不在拦截范围内。 + +const repoRoot = process.cwd(); +const localeDir = path.join(repoRoot, "src/locales/zh-CN"); +const scanDirs = [path.join(repoRoot, "src/pages"), path.join(repoRoot, "src/app/service/service_worker")]; + +const DEFAULT_NS = "common"; +const NS = fs + .readdirSync(localeDir) + .filter((f) => f.endsWith(".json")) + .map((f) => f.replace(/\.json$/, "")); + +const resources: Record = {}; +for (const ns of NS) { + resources[ns] = JSON.parse(fs.readFileSync(path.join(localeDir, `${ns}.json`), "utf8")); +} + +function resolveKey(ns: string, keyPath: string): string | undefined { + let cur: unknown = resources[ns]; + for (const seg of keyPath.split(".")) { + if (cur && typeof cur === "object" && seg in (cur as Record)) { + cur = (cur as Record)[seg]; + } else { + return undefined; + } + } + return typeof cur === "string" || typeof cur === "number" ? String(cur) : undefined; +} + +function listFiles(dir: string): string[] { + const out: string[] = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) out.push(...listFiles(full)); + else if (/\.tsx?$/.test(entry.name) && !/\.test\.tsx?$/.test(entry.name)) out.push(full); + } + return out; +} + +// 匹配 t("xxx")、i18n.t("xxx")、i18next.t("xxx")。 +// 裸 t( 用前置 (? { + T_CALL.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = T_CALL.exec(line))) { + const key = m[1]; + const rest = m[2]; + if (key === "" || key.includes("${") || key.includes("{{")) continue; // 动态/插值 key + const hasDefault = /defaultValue/.test(rest) || /defaultValue/.test(lines[idx + 1] ?? ""); + if (hasDefault) continue; // 有兜底文案,不会显示原始 key + let ns = DEFAULT_NS; + let keyPath = key; + const ci = key.indexOf(":"); + if (ci !== -1) { + ns = key.slice(0, ci); + keyPath = key.slice(ci + 1); + } + if (!NS.includes(ns)) continue; // 冒号并非命名空间(如 url),跳过 + if (resolveKey(ns, keyPath) === undefined) { + violations.push(`${path.relative(repoRoot, file)}:${idx + 1} t("${key}")`); + } + } + }); + } + return violations; +} + +describe("i18n 用法完整性", () => { + it("src/pages 与 service worker 下所有无 defaultValue 的 t() 调用都应能在 zh-CN 资源中解析(命名空间前缀正确)", () => { + const violations = findUnresolved(); + expect(violations, `以下 t() 调用无法解析,会直接显示原始 key:\n${violations.join("\n")}`).toEqual([]); + }); +}); diff --git a/src/locales/ja-JP/agent.json b/src/locales/ja-JP/agent.json new file mode 100644 index 000000000..00a965e03 --- /dev/null +++ b/src/locales/ja-JP/agent.json @@ -0,0 +1,252 @@ +{ + "title": "AI Agent", + "docs": "ドキュメント", + "chat": "チャット", + "provider": "モデルサービス", + "mcp": "MCP", + "skills": "Skills", + "provider_title": "モデルサービス", + "provider_subtitle": "Manage AI model providers and connections", + "provider_select": "AIサービスプロバイダー", + "provider_api_base_url": "APIアドレス", + "provider_api_key": "APIキー", + "provider_model": "デフォルトモデル", + "provider_test_connection": "接続テスト", + "provider_test_success": "接続成功", + "provider_test_failed": "接続失敗", + "provider_test_hint": "テストは設定を検証するため最小限のリクエストを送信します", + "provider_docs": "ドキュメント", + "provider_count_hint": "デフォルトモデルは新しい会話に使用されます", + "model_count": "{{count}} 個のモデル", + "model_fetch": "モデルを取得", + "model_fetch_failed": "モデルリストの取得に失敗しました", + "model_name": "名前", + "model_add": "モデルを追加", + "model_edit": "編集", + "model_copy": "コピー", + "model_delete": "削除", + "model_set_default": "デフォルトに設定", + "model_default_label": "デフォルト", + "model_delete_confirm": "このモデル設定を削除してもよろしいですか?", + "model_max_tokens": "最大出力トークン数", + "model_context_window": "コンテキストウィンドウ", + "model_no_models": "モデル設定がありません", + "model_no_models_desc": "Add your first model to start using the AI Agent", + "model_vision_support": "画像入力対応", + "model_image_output": "画像出力対応", + "model_capabilities": "モデル機能", + "model_supports_vision": "画像入力", + "model_supports_image_output": "画像出力", + "coming_soon": "開発中...", + "skills_title": "Skills 管理", + "skills_subtitle": "Manage agent skill packages (SKILL.md prompts, scripts, references)", + "skills_add": "Skill を追加", + "skills_empty": "インストール済みの Skill はありません", + "skills_empty_desc": "Upload a ZIP or import your first skill from a URL", + "skills_tools": "ツール", + "skills_references": "参考資料", + "skills_detail": "Skill 詳細", + "skills_edit_prompt": "プロンプト", + "skills_install": "Skill をインストール", + "skills_install_url": "URL からインポート", + "skills_install_paste": "SKILL.md を貼り付け", + "skills_uninstall": "アンインストール", + "skills_uninstall_confirm": "Skill「{{name}}」をアンインストールしますか?", + "skills_save_success": "保存しました", + "skills_install_success": "インストールしました", + "skills_fetch_failed": "取得に失敗しました", + "skills_add_script": "スクリプトを追加", + "skills_add_reference": "参考資料を追加", + "skills_install_zip": "ZIP アップロード", + "skills_install_zip_hint": "クリックして .zip ファイルを選択", + "skills_prompt": "プロンプト", + "skills_installed_at": "インストール日時", + "skills_refresh": "更新", + "skills_refresh_success": "更新しました", + "skills_tool_code": "ツールコード", + "skills_click_to_view_code": "ツール名をクリックしてコードを表示", + "skills_config": "設定", + "skills_config_saved": "設定を保存しました", + "skills_check_updates": "更新を確認", + "skills_no_updates": "すべてのSkillは最新です", + "skills_updates_available": "件のSkillに更新があります", + "skills_update": "更新", + "skills_docs": "ドキュメント", + "skills_count": "{{count}} 個のスキルがインストール済み", + "skills_update_count": "{{count}} 個に更新があります", + "skills_update_available": "{{version}} に更新", + "skills_references_short": "参考", + "skills_configurable": "設定可能", + "skills_open_config": "設定を開く", + "skills_update_success": "更新しました", + "skills_url_placeholder": "SKILL.cat.md URLを入力", + "chat_new": "新しいチャット", + "chat_delete": "チャットを削除", + "chat_delete_confirm": "この会話を削除しますか?", + "chat_no_conversations": "会話がありません", + "chat_search_placeholder": "会話を検索…", + "chat_search_no_results": "一致する会話がありません", + "chat_export": "エクスポート", + "chat_input_placeholder": "メッセージを入力...", + "chat_send": "送信", + "chat_stop": "停止", + "chat_thinking": "思考中", + "chat_tool_call": "ツール呼び出し", + "chat_tool_arguments": "引数", + "chat_tool_result": "結果", + "chat_error": "エラーが発生しました", + "chat_no_model": "モデルが設定されていません。先にモデルサービスで追加してください。", + "chat_model_select": "モデルを選択", + "chat_rename": "名前を変更", + "chat_copy": "コピー", + "chat_copy_success": "コピーしました", + "chat_regenerate": "再生成", + "chat_streaming": "生成中...", + "chat_retrying": "再試行中 ({{attempt}}/{{max}})...", + "chat_attach_image": "画像を追加", + "chat_attach_file": "ファイルを追加", + "chat_welcome_hint": "スクリプトに関する質問は何でもどうぞ", + "chat_welcome_start": "会話を作成して始めましょう", + "chat_tokens": "トークン", + "chat_first_token": "TTFT", + "chat_tools_count": "{{count}} ツール呼び出し", + "chat_tools_enabled": "ツール有効", + "chat_tools_disabled": "ツール無効", + "chat_tools_enabled_tip": "ツール有効 — クリックで無効化", + "chat_tools_disabled_tip": "ツール無効 — クリックで有効化", + "chat_background_enabled_tip": "バックグラウンドモード ON — ページを閉じても会話は継続します。クリックで無効化", + "chat_background_disabled_tip": "バックグラウンドモード OFF — ページを閉じると会話は停止します。クリックで有効化", + "chat_delete_round": "削除", + "chat_copy_message": "コピー", + "chat_edit_message": "編集", + "chat_save_and_send": "保存して送信", + "chat_cancel_edit": "キャンセル", + "chat_message_queued": "キュー中", + "chat_cancel_message": "送信をキャンセル", + "permission_title": "スクリプトが Agent 会話の使用をリクエストしています", + "permission_describe": "このスクリプトは Agent 会話機能の使用をリクエストしており、API トークンを消費します。信頼できるスクリプトにのみ許可してください。", + "permission_content": "Agent 会話", + "opfs": "OPFS", + "opfs_title": "OPFS ファイルブラウザ", + "opfs_empty": "空のディレクトリ", + "opfs_name": "名前", + "opfs_size": "サイズ", + "opfs_type": "種類", + "opfs_modified": "最終更新", + "opfs_delete_confirm": "削除してよろしいですか?", + "opfs_delete_success": "削除しました", + "opfs_file": "ファイル", + "opfs_directory": "ディレクトリ", + "opfs_preview": "プレビュー", + "opfs_subtitle": "Origin Private File System · Agent のプライベートストレージ", + "opfs_refresh": "更新", + "opfs_upload": "アップロード", + "opfs_actions": "操作", + "opfs_item_count": "{{count}} 項目", + "opfs_empty_desc": "このディレクトリにはまだファイルがありません", + "opfs_upload_success": "アップロードしました", + "opfs_upload_failed": "アップロードに失敗しました", + "opfs_type_directory": "フォルダー", + "opfs_type_image": "画像", + "opfs_type_text": "テキスト", + "opfs_type_binary": "バイナリ", + "opfs_root": "ルート", + "dom_permission_title": "スクリプトが DOM 操作アクセスをリクエストしています", + "dom_permission_describe": "このスクリプトはウェブページの DOM を読み取り操作する能力(クリック、フォーム入力、ナビゲーション、スクリーンショットなど)をリクエストしています。信頼できるスクリプトにのみ許可してください。", + "dom_permission_content": "Agent DOM 操作", + "mcp_title": "MCPサーバー", + "mcp_subtitle": "Connect MCP servers to extend agent tools, resources and prompts", + "mcp_add_server": "サーバーを追加", + "mcp_no_servers": "MCPサーバーが設定されていません", + "mcp_no_servers_desc": "Add your first MCP server to connect external tools and data", + "mcp_status_connected": "Connected", + "mcp_status_failed": "Connection failed", + "mcp_status_untested": "Not tested", + "mcp_has_key": "シークレットキー", + "mcp_headers_count": "{{count}} 個のヘッダー", + "mcp_count_servers": "{{count}} 個のサーバー", + "mcp_count_connected": "{{count}} 個接続済み", + "mcp_count_tools": "{{count}} 個のツールが利用可能", + "mcp_docs": "ドキュメント", + "mcp_test_connection": "テスト", + "mcp_name_url_required": "名前とURLは必須です", + "mcp_optional": "任意", + "mcp_custom_headers": "カスタムヘッダー", + "mcp_enabled": "有効", + "mcp_detail": "詳細", + "mcp_tools": "ツール", + "mcp_resources": "リソース", + "mcp_prompts": "プロンプト", + "mcp_no_tools": "利用可能なツールはありません", + "mcp_no_resources": "利用可能なリソースはありません", + "mcp_no_prompts": "利用可能なプロンプトはありません", + "mcp_loading": "読み込み中...", + "mcp_parameters": "パラメータ", + "tasks": "定時タスク", + "tasks_title": "定時タスク管理", + "tasks_subtitle": "cron スケジュールで Agent タスクを自動実行します", + "settings": "設定", + "settings_title": "Agent 設定", + "settings_subtitle": "モデル、検索、一般設定 · 変更は即時反映されます", + "settings_cat_model": "モデル", + "settings_cat_search": "検索", + "model_settings": "モデル設定", + "summary_model": "要約モデル", + "summary_model_desc": "ウェブ要約などに使用。未設定の場合はデフォルトモデルを使用", + "summary_model_placeholder": "デフォルトモデルを使用", + "search_settings": "検索設定", + "search_engine": "検索エンジン", + "search_engine_desc": "web_search ツールが使用する検索ソースです。", + "search_engine_baidu": "Baidu", + "search_google_api_key": "Google API Key", + "search_google_api_key_desc": "Custom Search JSON API の呼び出しに使用します。", + "search_google_cse_id": "カスタム検索エンジン ID", + "search_google_cse_id_desc": "Programmable Search Engine の cx パラメーターです。", + "settings_saved": "設定を保存しました", + "settings_save_failed": "設定の保存に失敗しました", + "search_engine_tip_bing": "デフォルトの検索エンジン、グローバルに広くカバー、追加設定不要。", + "search_engine_tip_duckduckgo": "プライバシー重視の検索エンジン、API キー不要。", + "search_engine_tip_baidu": "中国語コンテンツに最適化、中国語の検索結果がより良好。", + "search_engine_tip_google": "高品質な検索結果、Google API Key とカスタム検索エンジン ID の設定が必要。", + "tasks_docs": "ドキュメント", + "tasks_count_total": "{{count}} 個のタスク", + "tasks_count_enabled": "{{count}} 個有効", + "tasks_create": "タスクを作成", + "tasks_edit": "タスクを編集", + "tasks_mode": "モード", + "tasks_mode_internal": "内部実行", + "tasks_mode_event": "イベント駆動", + "tasks_mode_internal_short": "内部", + "tasks_mode_event_short": "イベント", + "tasks_cron": "cron 式", + "tasks_next_run": "次回実行", + "tasks_last_status": "前回の状態", + "tasks_run_now": "今すぐ実行", + "tasks_history": "実行履歴", + "tasks_prompt": "プロンプト", + "tasks_max_iterations": "最大反復回数", + "tasks_notify": "完了通知", + "tasks_notify_desc": "タスク完了時にブラウザ通知を送信します", + "tasks_no_tasks": "定時タスクがありません", + "tasks_no_tasks_desc": "最初の定時タスクを作成して、Agent をスケジュール実行しましょう", + "tasks_no_runs": "実行記録がまだありません", + "tasks_delete_confirm": "この定時タスクを削除してもよろしいですか?", + "tasks_clear_runs": "履歴をクリア", + "tasks_clear_runs_confirm": "このタスクの実行履歴をクリアしてもよろしいですか?", + "tasks_event_hint": "タスクがトリガーされると、このタスクを作成したスクリプトに通知されます", + "tasks_event_trigger": "イベントトリガー", + "tasks_name_cron_required": "名前と cron 式は必須です", + "tasks_model_select": "モデルを選択", + "tasks_skills": "Skills", + "tasks_skills_auto": "すべて自動読み込み", + "tasks_conversation_id": "会話を継続する ID(任意)", + "tasks_run_status_success": "成功", + "tasks_run_status_error": "失敗", + "tasks_run_status_running": "実行中", + "tasks_run_duration": "所要時間", + "tasks_run_usage": "使用量", + "tasks_run_conversation": "会話を表示", + "tasks_run_time": "時刻", + "tasks_run_status": "状態", + "tasks_never_run": "未実行" +} diff --git a/src/locales/ja-JP/common.json b/src/locales/ja-JP/common.json new file mode 100644 index 000000000..2f9e88a30 --- /dev/null +++ b/src/locales/ja-JP/common.json @@ -0,0 +1,131 @@ +{ + "user_guide": "使用ガイド", + "api_docs": "APIドキュメント", + "development_guide": "開発ガイド", + "script_gallery": "スクリプトギャラリー", + "community_forum": "コミュニティフォーラム", + "external_links": "外部リンク", + "system_follow": "システムに準拠", + "no_data": "データがありません", + "logs": "ログ", + "tools": "ツール", + "find": "検索", + "replace": "置換", + "settings": "設定", + "change_theme": "背景色変更", + "hide_main_sidebar": "サイドバーを折りたたむ", + "show_main_sidebar": "サイドバーを展開", + "menu": "メニュー", + "guide": "初心者ガイド", + "helpcenter": "ヘルプセンター", + "save": "保存", + "file": "ファイル", + "save_success": "保存に成功しました", + "reset_success": "リセットに成功しました", + "update": "更新", + "check_update": "更新をチェック", + "confirm_delete": "削除を確認", + "confirm_update": "更新を確認", + "delete_success": "削除に成功しました", + "deleting": "削除中", + "enable": "有効", + "script_list_enable_width": 80, + "script_list_last_updated_width": 120, + "script_list_apply_to_run_status_width": 140, + "subscribe_list_enable_width": 100, + "disable": "無効", + "name": "名前", + "version": "バージョン", + "source": "ソース", + "home": "ホーム", + "action": "アクション", + "export": "エクスポート", + "delete": "削除", + "pin_to_top": "ピン", + "confirm": "確認", + "close": "閉じる", + "config": "設定", + "key": "キー", + "value": "値", + "add": "追加", + "type": "タイプ", + "size": "サイズ", + "download": "ダウンロード", + "edit_value": "値を編集", + "add_value": "値を追加", + "update_success": "変更に成功しました", + "add_success": "追加に成功しました", + "clear": "クリア", + "type_string": "文字列", + "type_number": "数値", + "type_boolean": "ブール値", + "type_object": "オブジェクト", + "confirm_delete_resource": "このリソースを削除してもよろしいですか?次回開始時にこのリソースが再読み込みされます", + "confirm_clear_resource": "本当にこれらのリソースをクリアしますか?次回開始時にリソースが再読み込みされます", + "yes": "はい", + "no": "いいえ", + "confirm_delete_permission": "この権限を削除してもよろしいですか?", + "reset": "リセット", + "run_once": "一度実行", + "stop": "中止", + "edit": "編集", + "copy": "コピー", + "exclude_on": "$0の実行を復元", + "exclude_off": "$0の実行を除外", + "get_confirm_error": "確認情報の取得に失敗しました", + "confirm_error": "確認に失敗しました", + "ignore": "無視", + "temporary_allow": "この{{permissionContent}}を一時的に許可", + "temporary_allow_all": "すべての{{permissionContent}}を一時的に許可", + "permanent_allow": "この{{permissionContent}}を永続的に許可", + "permanent_allow_all": "すべての{{permissionContent}}を永続的に許可", + "temporary_deny": "この{{permissionContent}}を一時的に拒否", + "temporary_deny_all": "すべての{{permissionContent}}を一時的に拒否", + "permanent_deny": "この{{permissionContent}}を永続的に拒否", + "permanent_deny_all": "すべての{{permissionContent}}を永続的に拒否", + "import": "インポート", + "author": "作者", + "description": "説明", + "operation": "アクション", + "error": "エラー", + "unknown": "不明", + "add_new": "新規追加", + "no_operation": "操作しない", + "enable_script": "スクリプトを有効にする", + "local_creation": "ローカル作成", + "import_success": "インポートに成功しました", + "get_script": "スクリプトを取得", + "report_issue": "バグ/問題フィードバック", + "project_docs": "プロジェクトドキュメント", + "community": "コミュニティ", + "domain": "ドメイン", + "script_name": "スクリプト名", + "skip": "スキップ", + "next": "次へ", + "next_with_progress": "次へ(ステップ{step}の全{steps}ステップ中)", + "back": "戻る", + "last": "完了", + "auto": "自動", + "hide": "非表示", + "custom": "カスタム", + "resize_column_width": "列幅を調整", + "collapse": "折りたたむ", + "expand": "展開", + "import_script_placeholder": ".user.js で終わるスクリプトの絶対リンクまたはScriptCatインストールページリンクの入力をサポート\n複数行で記入可能、1行につき1つ\n例:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/zh-CN/script-show-page/1234", + "light": "ライトモード", + "dark": "ダークモード", + "enter_search_value": "{{search}}を入力して検索してください", + "no_message_content": "メッセージ内容なし", + "loading": "読み込み中...", + "auth_type": "認証タイプ", + "url": "URL", + "username": "ユーザー名", + "password": "パスワード", + "access_token_bearer": "アクセストークン(Bearer)", + "s3_bucket_name": "バケット名", + "s3_region": "リージョン", + "s3_access_key_id": "アクセスキーID", + "s3_secret_access_key": "シークレットアクセスキー", + "s3_custom_endpoint": "カスタムエンドポイント(オプション)", + "cancel": "キャンセル" +} diff --git a/src/locales/ja-JP/editor.json b/src/locales/ja-JP/editor.json new file mode 100644 index 000000000..e9911c76f --- /dev/null +++ b/src/locales/ja-JP/editor.json @@ -0,0 +1,132 @@ +{ + "save": "保存", + "save_success": "保存しました", + "copy": "コピー", + "find": "検索", + "replace": "置換", + "select_all": "すべて選択", + "format": "フォーマット", + "back": "戻る", + "more": "その他", + "file": "ファイル", + "edit": "編集", + "settings": "設定", + "run_settings": "実行設定", + "code": "コード", + "script_setting": "スクリプト設定", + "storage": "ストレージ", + "resource": "リソース", + "search_resource": "リソースを検索", + "search_storage": "キーを検索", + "record_count": "{{count}} 件", + "resource_count": "{{count}} 個のリソース · 合計 {{size}}", + "script_list": "スクリプト一覧", + "search_scripts": "スクリプトを検索...", + "new_script": "新規スクリプト", + "source": "ソース", + "from_user": "ユーザー", + "from_script": "スクリプト", + "last_updated": "最終更新", + "run_at": "実行タイミング", + "run_in": "実行環境", + "check_update": "更新確認", + "line_col": "行 {{line}}, 列 {{col}}", + "script_not_found": "スクリプトが見つかりません", + "confirm_delete_script": "スクリプト「{{name}}」を削除しますか?", + "delete_success": "削除しました", + "delete_failed": "削除に失敗しました", + "cancel": "キャンセル", + "confirm": "確認", + "save_as": "名前を付けて保存", + "run": "実行", + "debug": "デバッグ", + "script_storage": "スクリプトストレージ", + "enter_key": "キーを入力してください", + "key_placeholder": "キー", + "value_placeholder": "タイプがobjectの場合、JSON解析可能なデータを入力してください", + "clear_success": "クリアに成功しました", + "confirm_clear": "本当にこのストレージスペースをクリアしますか?", + "script_resource": "スクリプトリソース", + "basic_info": "基本情報", + "update_url": "URLを更新", + "add_permission": "権限を追加", + "match": "マッチ", + "user_setting": "ユーザー設定", + "confirm_delete_exclude": "この除外を削除してもよろしいですか?", + "after_deleting_match_item": "スクリプト設定のマッチ項目を削除すると、自動的にマッチ項目に追加されます", + "confirm_delete_match": "このマッチを削除してもよろしいですか?", + "after_deleting_exclude_item": "スクリプト設定のマッチ項目を削除すると、自動的に除外項目に追加されます", + "add_match": "マッチを追加", + "add_exclude": "除外を追加", + "website_match": "ウェブサイトマッチ(@match)", + "website_exclude": "ウェブサイト除外(@exclude)", + "confirm_reset": "リセットしてもよろしいですか?", + "undo": "元に戻す", + "redo": "やり直し", + "cut": "切り取り", + "paste": "貼り付け", + "user_config": "ユーザー設定", + "gm_api": "GM API", + "storage_api": "ストレージAPI", + "use_file_system": "使用するファイルシステム", + "open_directory": "ディレクトリを開く", + "account_validation_failed": "アカウント情報の確認に失敗しました", + "not_set": "未設定", + "in_use": "活発な", + "storage_error": "ストレージエラー", + "upload_to_cloud": "クラウドにアップロード", + "save_failed": "保存に失敗しました", + "exporting": "エクスポート中...", + "upload_to": "アップロード先", + "value_export_expression": "値エクスポート式", + "overwrite_original_value_on_import": "インポート時に元の値を上書き", + "cookie_export_expression": "Cookieエクスポート式", + "overwrite_original_cookie_on_import": "インポート時に元の値を上書き", + "restore_default_values": "デフォルト値を復元", + "edit_conflict": "編集の競合", + "confirm_override_when_edit_conflict": "このスクリプトは別のインスタンスで編集されています。置き換えるとその変更は上書きされます。このバージョンを保持しますか?", + "save_abort_when_edit_conflict": "このスクリプトは別のインスタンスで編集されています。保存は中止されました。", + "scriptname_conflict": "スクリプト名の競合", + "confirm_save_when_scriptname_conflict": "このスクリプト名はすでに別のスクリプトで使用されています。それでも保存しますか?", + "save_abort_when_scriptname_conflict": "このスクリプト名はすでに別のスクリプトで使用されています。保存を中止しました。", + "eslint_config_format_error": "ESLint設定フォーマットエラー", + "script_modified_leave_confirm": "現在の内容はまだ保存されていません。このままページを離れると変更は失われます。よろしいですか?", + "create_success_note": "作成に成功しました。バックグラウンドスクリプトはデフォルトで有効になりませんのでご注意ください", + "save_as_failed": "名前を付けて保存に失敗しました", + "save_as_success": "名前を付けて保存に成功しました", + "only_background_scheduled_can_run": "バックグラウンドスクリプト/スケジュールスクリプトのみ実行できます", + "preparing_script_resources": "スクリプトリソースを準備中...", + "build_success_message": "ビルドに成功しました。拡張機能ページで開発者ツールを開き、コンソールで出力を確認できます", + "script_storage_tooltip": "スクリプト保存データ(GM_value)を管理できます", + "script_resource_tooltip": "@resource、@requireでダウンロードしたリソースを管理", + "script_setting_tooltip": "スクリプトのカスタム設定を行う", + "script_modified_close_confirm": "スクリプトが変更されています。閉じると変更が失われます。続行しますか?", + "close_current_tab": "現在のタブを閉じる", + "close_other_tabs": "他のタブを閉じる", + "close_left_tabs": "左側のタブを閉じる", + "close_right_tabs": "右側のタブを閉じる", + "invalid_script_code": "無効なスクリプトコード", + "build_failed": "ビルドに失敗しました", + "drag_script_here_to_upload": "スクリプトをここにドラッグしてアップロード", + "watch_file_description": "ファイルの変更を監視し、スクリプトを自動更新します。使用する際は、スクリプトファイルのパスが変更されず、ページが閉じられないことを確認してください。", + "watch_file": "ファイルの監視", + "stop_watch_file": "監視を停止", + "individual_edit": "個別編集", + "batch_edit": "一括編集", + "script_code": "スクリプトコード", + "editor_config": "エディタ設定", + "editor_config_description": "jsconfig.jsのcompilerOptionsを参考に設定できます", + "editor_type_definition": "エディタ型定義", + "editor_type_definition_description": "独自の型定義をカスタマイズでき、スクリプトエディタがこれらの型定義を自動的に読み込みます", + "eslint_rules_reset": "ESLintルールがリセットされました", + "eslint_rules_saved": "ESLintルールが保存されました", + "editor_config_reset": "エディタ設定がリセットされました", + "editor_config_saved": "エディタ設定が保存されました", + "editor_config_format_error": "エディタ設定のフォーマットエラー", + "editor_type_definition_reset": "エディタ型定義がリセットされました", + "editor_type_definition_saved": "エディタ型定義が保存されました", + "editor": { + "show_script_list": "スクリプトリストを表示", + "hide_script_list": "スクリプトリストを非表示" + } +} diff --git a/src/locales/ja-JP/guide.json b/src/locales/ja-JP/guide.json new file mode 100644 index 000000000..f606a85d5 --- /dev/null +++ b/src/locales/ja-JP/guide.json @@ -0,0 +1,25 @@ +{ + "start_title": "ScriptCat拡張機能へようこそ", + "start_content": "次に、ScriptCatの基本的な使用方法をご紹介します", + "installed_scripts": "インストールしたスクリプトはここに表示されます", + "script_list_title": "スクリプトセンター", + "script_list_content": "スクリプトセンターからスクリプトをインストールできます。ScriptCatはユーザースクリプトに加えてバックグラウンドスクリプトもサポートしています", + "script_list_enable_title": "スクリプト有効化", + "script_list_enable_content": "スクリプトは有効にしないと使用できません。ページスクリプトはインストール時にデフォルトで有効、バックグラウンドスクリプトはインストール時にデフォルトで無効です", + "script_list_apply_to_run_status_title": "適用先と実行状態", + "script_list_apply_to_run_status_content": "スクリプトの実行状態を表示します。タグにマウスを合わせるとスクリプトタイプを確認できます", + "script_list_sort_title": "ソート", + "script_list_sort_content": "スクリプトをドラッグして順番を付ける事ができます。", + "script_list_update_title": "最終更新", + "script_list_update_content": "「最終更新」列ラベルをクリックすると、スクリプトの更新がチェックされます。", + "script_list_action_title": "アクション", + "script_list_action_content": "操作バーでは、スクリプトの編集、制御スクリプトの実行と終了(バックグラウンドスクリプト)、および設定(UserConfig を参照)にアクセスできます。右側のボタンからは、高度なフィルタリングや表示モードの切り替えにアクセスできます。", + "tools_title": "常用ツール", + "tools_content": "ツールにはバックアップと開発のツールが提供されています", + "tools_backup_title": "バックアップ", + "tools_backup_content": "バックアップはスクリプトを保存し、紛失を避けることができます。スクリプトファイルをローカルにエクスポートしたり、ローカルのスクリプトファイルを読み込んだりできます。クラウドにバックアップすることもでき、より便利です。", + "setting_title": "設定", + "setting_content": "設定には主に言語、スクリプト同期、更新頻度などの常用設定項目が含まれています", + "setting_sync_title": "更新と同期", + "setting_sync_content": "スクリプト同期機能により、このデバイスのスクリプトコンテンツをクラウドに便利に同期できます。複数のデバイスがある場合は、同期削除オプションをチェックしてください。このデバイスでスクリプトが削除されると、クラウドから対応するスクリプトが削除され、他のデバイスのスクリプトも削除されます。" +} diff --git a/src/locales/ja-JP/index.ts b/src/locales/ja-JP/index.ts new file mode 100644 index 000000000..bacd6d096 --- /dev/null +++ b/src/locales/ja-JP/index.ts @@ -0,0 +1,11 @@ +export { default as agent } from "./agent.json"; +export { default as common } from "./common.json"; +export { default as editor } from "./editor.json"; +export { default as guide } from "./guide.json"; +export { default as install } from "./install.json"; +export { default as logs } from "./logs.json"; +export { default as permission } from "./permission.json"; +export { default as popup } from "./popup.json"; +export { default as script } from "./script.json"; +export { default as settings } from "./settings.json"; +export { default as tools } from "./tools.json"; diff --git a/src/locales/ja-JP/install.json b/src/locales/ja-JP/install.json new file mode 100644 index 000000000..f3ae05e95 --- /dev/null +++ b/src/locales/ja-JP/install.json @@ -0,0 +1,207 @@ +{ + "data_import": "データインポート", + "select_scripts_to_import": "インポートするスクリプトを選択してください", + "select_all": "すべて選択", + "script_import_progress": "スクリプトインポート進行状況", + "select_subscribes_to_import": "インポートするサブスクライブを選択してください", + "subscribe_import_progress": "サブスクライブインポート進行状況", + "script": "インストール", + "update_script": "更新", + "subscribe": "サブスクライブをインストール", + "update_subscribe": "サブスクライブを更新", + "update_script_no_close": "ウィンドウを閉じずに更新する", + "script_no_close": "ウィンドウを閉じずにインストールする", + "update_script_no_more_update": "更新しますが、今後は更新を確認しません", + "close_update_script_no_more_update": "閉じて更新をチェックしない", + "script_no_more_update": "インストールするが、今後は更新をチェックしない", + "invalid_link": "無効なリンク", + "subscribe_install_label": "このサブスクライブは以下のスクリプトをインストールします", + "subscribe_scripts_title": "このサブスクリプションは以下のスクリプトをインストールします", + "subscribe_scripts_empty": "このサブスクリプションはまだスクリプトを宣言していません", + "script_runs_in": "スクリプトは以下のウェブサイトで実行されます", + "script_has_full_access_to": "スクリプトは以下のアドレスへの完全なアクセス権限を取得します", + "script_requires": "スクリプトは以下の外部リソースを参照しています", + "cookie_warning": "注意:このスクリプトはCookieの操作権限をリクエストします。これは危険な権限ですので、スクリプトの安全性を確認してください。", + "perm_card_title": "このスクリプトには以下の権限が付与されます", + "perm_card_hint": "インストール前に確認してください", + "perm_card_empty": "このスクリプトは特別な権限を要求しません", + "perm_match_label": "実行サイト", + "perm_match_summary": "スクリプトはこれらのサイトで動作し、ページを変更します", + "perm_connect_label": "クロスオリジンアクセス", + "perm_connect_summary": "以下のドメインへリクエストを送信し、データを読み取れます", + "perm_grant_label": "GM 機能", + "perm_grant_summary": "以下の GM API を呼び出せます", + "perm_require_label": "外部リソース", + "perm_require_summary": "以下のサードパーティスクリプトとリソースを読み込みます", + "badge_background": "バックグラウンド", + "badge_scheduled": "スケジュール", + "enabled_label": "有効", + "schedule_cron_label": "スケジュールタスク", + "schedule_next_run": "次回実行", + "schedule_background_desc": "ブラウザが開いている間、自動的に実行されます", + "code_lines": "{{count}} 行", + "code_copy": "コードをコピー", + "code_collapse": "折りたたむ", + "code_expand": "展開", + "loading_title": "スクリプトを読み込み中", + "loading_desc": "ソースからスクリプトの内容をダウンロードして解析しています", + "error_retry": "再試行", + "error_invalid_desc": "有効なインストール元のパラメーターがないため、スクリプトを読み込めません。", + "context_install": "スクリプトのインストール", + "context_update": "スクリプトの更新", + "background_script": "バックグラウンドスクリプト", + "scheduled_script": "スケジュールスクリプト", + "watching_status": "ファイルの変更を監視中。保存すると自動的に再インストールされます", + "watching_chip": "監視中", + "watching_title": "ファイルの変更を監視中", + "watching_file_desc": "{{file}} を保存すると、自動的に再インストールしてスクリプトを同期します", + "watching_last_sync": "最終同期 {{time}}", + "warning_title": "信頼できるソースのスクリプトか確認してください", + "warning_risk_connect": "すべてのドメインにアクセスできます", + "warning_risk_antifeature": "アンチフィーチャーが宣言されています", + "warning_risk_join": "、", + "warning_risk_tail": " — 慎重にインストールしてください。", + "action_note_install": "インストールは、このスクリプトのソースと作者を信頼することを意味します", + "action_note_update": "更新は、このコードと権限の変更を受け入れることを意味します", + "action_note_subscribe": "インストールは、このサブスクリプションとその作者を信頼することを意味します", + "action_note_watching": "監視中 — ファイルを保存すると自動更新されます。停止すると手動操作できます", + "context_skill_install": "スキルのインストール", + "context_skill_update": "スキルの更新", + "skill_kind": "AI スキル", + "skill_prompt_title": "プロンプト", + "skill_prompt_chip": "SKILL.md", + "skill_tools_title": "ツール", + "skill_config_title": "設定項目", + "skill_references_title": "参考資料", + "skill_required": "必須", + "skill_secret": "シークレット", + "skill_install": "Skillをインストール", + "skill_update": "Skillを更新", + "skill_warning": "スキルは AI にプロンプトを注入し、以下のツール、GM 機能、設定を呼び出す権限を付与します。正規のソースからインストールしてください!", + "skill_warning_title": "このスキルが信頼できるソースのものか確認してください", + "skill_warning_desc": "インストール後、AI にプロンプトを注入し、記載されたツール(GM 権限を含む)と設定の読み書きを付与します — 慎重にインストールしてください。", + "success": "インストールに成功しました", + "install": { + "update_success": "更新に成功しました" + }, + "failed": "インストールに失敗しました", + "subscribe_success": "サブスクライブに成功しました", + "subscribe_failed": "サブスクライブに失敗しました", + "current_version": "現在のバージョン", + "update_version": "更新バージョン", + "updatepage": { + "title": "一括更新", + "main_header": "更新を確認", + "last_check": "最終チェック {{time}}", + "status_checking_updates": "更新を確認中...", + "updates_available": "{{count}} 件の更新", + "ignored_count": "{{count}} 件を無視", + "selected_count": "{{selected}} / {{total}} 件選択中", + "update_selected": "選択を更新 ({{count}})", + "ignore_selected": "選択を無視", + "update": "更新", + "ignore": "無視", + "restore": "復元", + "restore_all": "すべて復元", + "ignored_section": "無視した更新", + "auto_close": "{{count}} 秒後に自動で閉じます", + "col_script": "スクリプト", + "col_version": "バージョン", + "col_change": "変更", + "col_source": "提供元", + "col_action": "操作", + "enabled": "有効", + "disabled": "無効", + "codechange_major": "大きな変更", + "codechange_noticeable": "目立つ変更", + "codechange_tiny": "小さな変更", + "tag_new_connect": "@connect 追加", + "empty_title": "すべてのスクリプトは最新です", + "empty_desc": "{{count}} 件のスクリプトを確認 · 利用可能な更新はありません", + "similarity": "類似度", + "new_connects": "新しい @connect", + "toast_found": "{{count}} 件の更新可能なスクリプトが見つかりました", + "toast_uptodate": "すべてのスクリプトは最新です" + }, + "importpage": { + "title": "データインポート", + "context_review": "データインポート", + "context_importing": "インポート中", + "context_done": "インポート完了", + "selected_count": "{{selected}} / {{total}} 件を選択中", + "unimportable_count": "{{count}} 件はインポートできません", + "count_scripts": "{{count}} 件のスクリプト", + "count_subscribes": "{{count}} 件のサブスクリプション", + "col_script": "スクリプト", + "col_version": "バージョン", + "col_source": "ソース", + "col_data": "データ", + "col_status": "状態", + "col_enabled": "有効", + "op_add": "新規", + "op_update": "更新", + "op_error": "解析失敗", + "source_local": "ローカルで作成", + "data_values": "{{count}} 件", + "data_resources": "リソースあり", + "enable_after_import": "インポート後に有効化", + "row_error": "ファイルが破損しており、インポートできません", + "unknown_script": "不明なスクリプト", + "subscribe_section": "サブスクリプション", + "trust_hint": "選択した項目のみを復元します。インポートでデータが送信されることはありません", + "import_selected": "選択した項目をインポート ({{count}})", + "importing_progress": "バックアップを復元中 · {{done}} / {{total}} 完了", + "importing_hint": "このページを開いたままにしてください", + "importing_actionbar_hint": "スクリプトとデータを復元しています。このページを閉じないでください", + "importing_button": "インポート中…", + "cancel": "キャンセル", + "status_pending": "待機中", + "status_importing": "インポート中", + "status_done": "インポート済み", + "status_skipped": "スキップ", + "done_title": "インポート完了", + "done_desc": "選択したスクリプトとデータを復元しました", + "done_stat_scripts": "{{count}} 件のスクリプト", + "done_stat_subscribes": "{{count}} 件のサブスクリプション", + "done_stat_values": "{{count}} 件のデータ", + "view_scripts": "スクリプト一覧を表示", + "loading_title": "バックアップファイルを解析中", + "loading_desc": "バックアップの内容を読み込んで検証しています", + "error_title": "バックアップファイルを読み込めません", + "error_desc": "バックアップファイルが破損しているか、インポートリンクの有効期限が切れている可能性があります", + "invalid_desc": "インポートリンクが無効か、有効期限が切れています", + "retry": "再試行", + "empty_title": "このバックアップにインポートできる項目がありません", + "empty_desc": "このバックアップファイルにはスクリプトもサブスクリプションも含まれていません" + }, + "downloading_status_text": "ダウンロード中。{{bytes}} を受信しました。", + "downloading_status_percent": "ダウンロード中。{{bytes}} / {{total}}({{percent}}%)を受信しました。", + "page_please_wait": "しばらくお待ちください", + "page_loading": "インストールページを読み込み中", + "page_load_failed": "インストールページの読み込みに失敗しました", + "invalid_page": "無効なページ", + "background_script_tag": "これはバックグラウンドスクリプトです", + "scheduled_script_tag": "これはスケジュールスクリプトです", + "from_legitimate_sources_warning": "正当なソースからスクリプトをインストールしてください!不明なスクリプトはプライバシーを侵害したり、悪意のある操作を行う可能性があります!", + "referral_link_title": "紹介リンク", + "referral_link_description": "このスクリプトは作者のアフィリエイトリンクに変更またはリダイレクトします", + "ads_title": "広告付き", + "ads_description": "このスクリプトはアクセスしたページに広告を挿入します", + "payment_title": "有料スクリプト", + "payment_description": "このスクリプトは正常に使用するために支払いが必要です", + "miner_title": "マイニング", + "miner_description": "このスクリプトにはマイニング動作があります", + "membership_title": "メンバー機能", + "membership_description": "このスクリプトは正常に使用するためにメンバー登録が必要です", + "tracking_title": "情報追跡", + "tracking_description": "このスクリプトはユーザー情報を追跡します", + "script_info_load_failed": "スクリプト情報の読み込みに失敗しました!", + "script_import_result": "スクリプトインポート結果", + "failure_info": "失敗情報", + "source": "インストール元", + "skill_prompt": "プロンプト", + "skill_tools": "ツール", + "skill_config": "設定項目", + "skill_references": "参考資料", + "skill_install_failed": "Skillのインストールに失敗しました" +} diff --git a/src/locales/ja-JP/logs.json b/src/locales/ja-JP/logs.json new file mode 100644 index 000000000..c3981cff9 --- /dev/null +++ b/src/locales/ja-JP/logs.json @@ -0,0 +1,59 @@ +{ + "log_title": "実行ログ", + "last_5_minutes": "過去5分", + "last_15_minutes": "過去15分", + "last_30_minutes": "過去30分", + "last_1_hour": "過去1時間", + "last_3_hours": "過去3時間", + "last_6_hours": "過去6時間", + "last_12_hours": "過去12時間", + "last_24_hours": "過去24時間", + "last_7_days": "過去7日", + "query": "クエリ", + "labels": "ラベル", + "search_regex": "検索(正規表現サポート)", + "clean_schedule": "定期クリーニング", + "days_ago_logs": "日前のログ", + "delete_completed": "削除完了", + "delete_current_logs": "現在のログを削除", + "clear_completed": "クリア完了", + "clear_logs": "ログをクリア", + "now": "現在", + "total_logs": "合計{{length}}件のログが見つかりました", + "filtered_logs": "フィルタリング後{{length}}件のログ", + "enter_filter_conditions": "フィルタリング条件を入力してクエリしてください", + "last_updated": "最終更新", + "runtime": "ランタイム", + "advanced": "詳細", + "label_filter": "ラベル絞り込み", + "add_label": "ラベルを追加", + "custom_range": "カスタム範囲", + "back_to_top": "トップへ戻る", + "refresh": "更新", + "all_levels": "すべて", + "total_count": "全 {{count}} 件", + "filtered_count": "絞り込み {{count}} 件", + "clear_logs_confirm": "すべてのログを消去しますか?この操作は元に戻せません。", + "no_logs": "ログがありません", + "refresh_off": "オフ", + "interval_5s": "5秒", + "interval_10s": "10秒", + "interval_30s": "30秒", + "interval_1m": "1分", + "interval_5m": "5分", + "quick_range": "クイック範囲", + "absolute_range": "絶対時間範囲", + "from_start": "開始", + "to_end": "終了", + "apply_range": "範囲を適用", + "auto_refresh_hint": "終了を「現在」にすると、自動更新が最新のログを取得し続けます", + "group_minutes": "分", + "group_hours": "時間", + "group_days": "日", + "weekdays_short": "日,月,火,水,木,金,土", + "year_month": "{{year}}年{{month}}月", + "time": "時刻", + "prev_month": "前の月", + "next_month": "次の月", + "live": "リアルタイム" +} diff --git a/src/locales/ja-JP/permission.json b/src/locales/ja-JP/permission.json new file mode 100644 index 000000000..39c375630 --- /dev/null +++ b/src/locales/ja-JP/permission.json @@ -0,0 +1,42 @@ +{ + "permission": "権限", + "permission_value": "権限値", + "allow": "許可", + "permission_management": "権限管理", + "permission_cors": "クロスドメイン(CORS)", + "permission_cookie": "Cookieを管理", + "allow_once": "一度許可", + "deny_once": "一度拒否", + "script_accessing_cross_origin_resource": "スクリプトがクロスドメインリソースにアクセスしようとしています", + "confirm_operation_description": "スクリプトがこの操作を実行することを許可するかどうか確認してください。スクリプトは@connectタグを追加してこのオプションをスキップすることもできます", + "request_domain": "リクエストドメイン", + "request_url": "リクエストURL", + "access_cookie_content": "スクリプトがウェブサイトのCookieコンテンツにアクセスしようとしています", + "confirm_script_operation": "スクリプトがこの操作を実行することを許可するかどうか確認してください。Cookieは重要なユーザーデータですので、信頼できるスクリプトにのみ許可してください。", + "cookie_domain": "Cookieドメイン", + "script_operation_title": "スクリプトがスクリプト同期ストレージスペースを操作しようとしています", + "script_operation_description": "スクリプトがこの操作を実行することを許可するかどうか確認してください。許可すると、スクリプトが設定したストレージスペースの操作が許可され、スクリプトはストレージスペース下にapp/${dir}ディレクトリを作成して使用します", + "script_permission_content": "スクリプト", + "extension_site_access_title": "ScriptCat にサイトアクセス権限が必要です", + "extension_site_access_description": "ScriptCat がこのリクエストを実行できるように、このオリジンへのブラウザーのサイトアクセス権限を許可してください。これは拡張機能のサイトアクセス設定を変更します。", + "extension_site_access_content": "サイト", + "request_permission": "権限をリクエストする", + "allow_user_script_guide": "現在「ユーザー スクリプトを許可する」が有効ではないため、スクリプトは正常に動作しません。👉有効化の方法はこちら", + "user_script_type": "ユーザースクリプト", + "auth_duration": "許可の有効期間", + "duration_once": "今回のみ", + "duration_temporary": "一時的", + "duration_permanent": "永続的", + "apply_to_all_domains": "リクエストするすべてのドメインに適用", + "apply_to_all_domains_desc": "このスクリプトのすべてのリクエストに適用されます(ワイルドカード)", + "allow_action": "許可", + "deny_action": "拒否", + "ignore_action": "無視", + "cancel_action": "キャンセル", + "loading_confirm": "許可リクエストを読み込んでいます…", + "cookie_warning_title": "高機密性の権限", + "cookie_warning_desc": "Cookie にはログイン状態などの機密データが含まれます。信頼できるスクリプトにのみ許可してください。", + "confirm_expired_title": "許可リクエストの有効期限が切れました", + "confirm_expired_desc": "この許可リクエストはタイムアウトしたか、すでに処理されています。ページに戻ってもう一度実行してください。", + "auto_close_in": "{{second}} 秒後にウィンドウが自動的に閉じます" +} diff --git a/src/locales/ja-JP/popup.json b/src/locales/ja-JP/popup.json new file mode 100644 index 000000000..97d995399 --- /dev/null +++ b/src/locales/ja-JP/popup.json @@ -0,0 +1,27 @@ +{ + "new_version_available": "新しいバージョンが利用可能です", + "current_page_scripts": "現在のページ実行スクリプト", + "enabled_background_scripts": "有効および実行中のバックグラウンドスクリプト", + "menu_expand_num_before": "メニュー項目が", + "menu_expand_num_after": "個を超えると自動的に非表示", + "develop_mode_guide": "現在「デベロッパーモード」が有効ではないため、スクリプトは正常に動作しません。👉有効化の方法はこちら", + "lower_version_browser_guide": "ご使用のブラウザは古すぎるため、スクリプトは正常に動作しません。👉詳しくはこちら", + "click_to_reload": "👉再読み込みする", + "page_in_blacklist": "現在のページはブラックリストにあり、スクリプトを使用できません", + "ext_update_notification": "ScriptCat拡張機能が更新されました", + "ext_update_notification_desc": "現在のバージョン: {{version}}、詳細は更新ログをご覧ください", + "script_menu_display": "スクリプトが登録したメニュー", + "badge_type_none": "表示しない", + "badge_type_run_count": "実行回数", + "badge_type_script_count": "スクリプト数", + "script_menu": "スクリプトメニュー", + "display_right_click_menu": "右クリックメニューを表示", + "display_right_click_menu_desc": "ブラウザの右クリックメニューにスクリプトメニューを表示する", + "expand_count": "展開数", + "auto_collapse_when_exceeds": "この数を超えたときに自動的に折りたたむ", + "allow_user_script_guide": "現在「ユーザー スクリプトを許可する」が有効ではないため、スクリプトは正常に動作しません。👉有効化の方法はこちら", + "request_permission": "権限をリクエストする", + "show_more_scripts": "+{{count}} 個のスクリプト", + "use_on_mobile": "スマホで ScriptCat を使う", + "scan_qr_to_install": "QR コードをスキャンしてスマホに ScriptCat をインストール" +} diff --git a/src/locales/ja-JP/script.json b/src/locales/ja-JP/script.json new file mode 100644 index 000000000..9c760aeb4 --- /dev/null +++ b/src/locales/ja-JP/script.json @@ -0,0 +1,115 @@ +{ + "import_link": "リンクインポート", + "import_link_failure": "リンクインポートに失敗しました", + "create_user_script": "新しいユーザースクリプトを作成", + "create_background_script": "新しいバックグラウンドスクリプトを作成", + "create_scheduled_script": "新しいスケジュールスクリプトを作成", + "import_by_local": "ローカルインポート", + "import_local_failure": "ローカルインポートに失敗しました", + "import_local_success": "ローカルインポートに成功しました", + "create_script": "新しいスクリプトを作成", + "installed_scripts": "インストール済みのスクリプト", + "nav_scripts": "スクリプト", + "subscribe": "サブスクライブ", + "subscribe_scripts_count": "{{count}} 個のスクリプト", + "enter_subscribe_name": "サブスクライブ名を入力してください", + "subscribe_url": "サブスクライブURL", + "confirm_delete_subscription": "このサブスクライブを削除してもよろしいですか?関連するスクリプトも削除されます", + "list": { + "confirm_delete": "削除してもよろしいですか?この操作は元に戻せません!", + "confirm_update": "更新してもよろしいですか?この操作は元に戻せません!" + }, + "apply_to_run_status": "適用先/実行状態", + "sorting": "ソート", + "foreground_page_script_tooltip": "フォアグラウンドページスクリプト、指定されたページで実行されます", + "background_script_tooltip": "バックグラウンドスクリプト、有効にするとバックグラウンドで実行されます", + "scheduled_script_tooltip": "スケジュールスクリプト、次回実行時間:", + "running": "実行中", + "completed": "実行完了", + "source_subscribe_link": "サブスクライブリンク", + "source_local_script": "ローカルスクリプト", + "source_script_link": "スクリプトリンク", + "by_manual_creation": "コード編集によってローカルで作成された", + "confirm_delete_script": "このスクリプトを削除してもよろしいですか?", + "confirm_delete_scripts_content": "選択した {{count}} 個のスクリプトを削除しますか?これは元に戻すことはできません。", + "confirm_delete_script_content": "スクリプト\"{{name}}\"を削除しますか?これは元に戻すことはできません。", + "delete_failed": "削除に失敗しました", + "enter_script_name": "スクリプト名を入力してください", + "update_not_supported": "このスクリプトは更新チェックをサポートしていません", + "checking_for_updates": "更新をチェック中...", + "new_version_available": "新しいバージョンがあります", + "latest_version": "最新バージョンです", + "checked_for_all_selected": "選択されたすべてのスクリプトの更新をチェックしました", + "update_check_failed": "更新チェックに失敗しました", + "stopping_script": "スクリプトを停止中", + "script_stopped": "スクリプトが停止しました", + "starting_script": "スクリプトを開始中...", + "starting_updates": "一括更新を開始中...", + "script_started": "スクリプトが開始しました", + "operation_failed": "操作に失敗しました", + "batch_operations": "一括操作", + "scripts_pinned_to_top": "選択されたスクリプトは固定されました", + "unknown_operation": "不明な操作", + "page_script": "ページスクリプト", + "homepage": "スクリプトホームページ", + "script_website": "スクリプトサイト", + "script_source": "スクリプトソースコード", + "bug_feedback_script_support": "バグフィードバック/スクリプトサポートサイト", + "script_total_runs": "このスクリプトは合計{{runNum}}回実行され、iframe上で{{runNumByIframe}}回実行されました", + "script_total_runs_single": "このスクリプトは{{runNum}}回実行されました", + "script_disabled": "このスクリプトは有効になっていません", + "cron_oncetype": { + "minute": "{{next}}(毎分実行)", + "hour": "{{next}}(毎時間実行)", + "day": "{{next}}(毎日実行)", + "month": "{{next}}(毎月実行)", + "week": "{{next}}(毎週実行)" + }, + "cron_invalid_expr": "不正な cron 式です", + "scheduled_script_description_title": "これはスケジュールスクリプトです。有効にすると特定の時間に自動実行され、手動操作も可能です。", + "scheduled_script_description_description_expr": "スケジュールタスク表現:", + "scheduled_script_description_description_next": "最近の実行時間:", + "background_script_description": "これはバックグラウンドスクリプトです。有効にするとブラウザを開いたときに自動的に一度実行され、パネルで手動制御できます。", + "background_script": "バックグラウンドスクリプト", + "scheduled_script": "スケジュールスクリプト", + "script_status_tooltip": "スクリプトの有効状態を管理できます。通常の Tampermonkey スクリプトはデフォルトで有効ですが、バックグラウンドスクリプトとスケジュールスクリプトはデフォルトで無効になっています。", + "subscribe_source_tooltip": "これはサブスクリプションソースです。サブスクリプションを開くと、サブスクリプションスクリプトが自動的にインストールされます。", + "script_name_cannot_be_set_to_empty": "スクリプト名を空に設定することはできません", + "search_scripts": "検索スクリプト", + "script_list": { + "sidebar": { + "stopped": "停止済み", + "all": "すべて", + "normal_script": "通常スクリプト", + "status": "状態" + } + }, + "tags": "タグ", + "input_tags_placeholder": "タグを入力し、Enterを押して確認", + "switch_to_card_mode": "カードモードに切り替え", + "switch_to_table_mode": "テーブルモードに切り替え", + "open_sidebar": "サイドバーを開く", + "close_sidebar": "サイドバーを閉じる", + "error_metadata_invalid": "MetaData ブロックが不正です", + "error_script_name_required": "スクリプト名は必須です", + "error_script_version_required": "@version は必須です", + "error_script_namespace_required": "@namespace は必須です", + "error_cron_invalid": "無効な cron 式です: {{expr}}", + "error_script_type_mismatch": "スクリプト種別が不一致です(通常とバックグラウンドは相互変換不可)", + "error_old_script_code_missing": "既存スクリプトのコードが見つかりません", + "error_subscribe_name_required": "サブスクライブ名は必須です", + "error_grant_conflict": "@grant に 'none' と GM API の両方が指定されています", + "error_metadata_line_duplicated": "メタデータ内に重複した宣言があります。", + "create_group": "新規作成", + "import_group": "インポート", + "import_local_script": "ローカルスクリプトをインポート", + "link_import": "URLからインポート", + "import_skill": "Skillをインポート", + "link_import_desc": "スクリプト / サブスクライブのURLを貼り付けてください(1行に1つ)", + "link_import_placeholder": "https://example.com/script.user.js", + "link_import_hint": "ユーザースクリプト / サブスクライブ / Skill URLに対応", + "not_a_valid_script": "有効なユーザースクリプトまたはSkillScriptではありません", + "import_done": "インポート完了: 成功 {{success}} · 失敗 {{fail}}", + "drop_to_install": "スクリプトまたはSkillをここにドロップしてインストール", + "drop_to_install_hint": ".jsユーザースクリプト / サブスクライブ · .zip Skillパッケージをドロップ" +} diff --git a/src/locales/ja-JP/settings.json b/src/locales/ja-JP/settings.json new file mode 100644 index 000000000..abcc8b92d --- /dev/null +++ b/src/locales/ja-JP/settings.json @@ -0,0 +1,119 @@ +{ + "general": "一般", + "language": "言語", + "help_translate": "翻訳に協力", + "script_sync": "スクリプト同期", + "sync_delete": "同期削除", + "sync_delete_desc": "有効にすると、スクリプト削除時に削除マークが付けられ、他のデバイスがその状態を検出してスクリプトを削除します。無効にすると、ローカルとクラウドのスクリプトが直接削除されるため、複数のデバイスを使用している場合、スクリプトが繰り返し同期される可能性があります。", + "enable_script_sync_to": "スクリプト同期を有効にする", + "script_subscription_check_interval": "スクリプト/サブスクライブの更新チェック間隔", + "never": "なし", + "6_hours": "6時間", + "12_hours": "12時間", + "every_day": "毎日", + "every_week": "毎週", + "update_disabled_scripts": "無効なスクリプトを更新する", + "silent_update_non_critical_changes": "重要ではない変更を静かに更新する", + "enable_eslint": "ESLintを有効にする", + "eslint_rules": "ESLintルール", + "enter_eslint_rules": "ESLintルールを入力してください。https://eslint.org/play/ から設定をダウンロードできます", + "language_change_tip": "言語の切り替えが成功しました", + "backup": "バックアップ", + "local": "ローカル", + "export_file": "ファイルをエクスポート", + "import_file": "ファイルをインポート", + "cloud": "クラウド", + "backup_to": "バックアップ先", + "preparing_backup": "クラウドへのバックアップを準備中", + "backup_success": "バックアップに成功しました", + "backup_failed": "バックアップに失敗しました", + "no_backup_files": "バックアップファイルがありません", + "backup_list": "バックアップリスト", + "open_backup_dir": "バックアップディレクトリを開く", + "confirm_delete_backup_file": "バックアップファイルの削除を確認", + "backup_strategy": "バックアップ戦略", + "under_construction": "建設中", + "sync_system_connect_failed": "同期システムの接続に失敗しました", + "sync_system_closed": "同期が閉じられました", + "sync_system_closed_description": "同期機能が閉じられています。再設定してください", + "export_success": "エクスポートに成功しました", + "get_backup_dir_url_failed": "バックアップディレクトリアドレスの取得に失敗しました", + "get_backup_files_failed": "バックアップファイルの取得に失敗しました", + "baidu_netdisk": "百度ネットディスク", + "netdisk_unbind": "{{provider}} の連携を解除", + "netdisk_unbind_confirm": "{{provider}} のアカウント連携を解除しますか?", + "netdisk_unbind_success": "{{provider}} のアカウント連携を解除しました", + "netdisk_unbind_error": "{{provider}} のアカウント解除に失敗しました", + "save_only_current_group": "保存は現在のグループにのみ有効です", + "security": "セキュリティ", + "blacklist_pages": "ブラックリストページ", + "blacklist_placeholder": "以下のページでScriptCatのスクリプト実行を禁止します。複数のページは改行文字で区切ってください。例:\nhttps://*.example.com", + "expression_format_error": "式フォーマットエラー", + "migration_confirm_message": "ストレージエンジンの移行を再試行すると既存のデータが変更されます。確認してください。詳細はこちら:https://docs.scriptcat.org/docs/change/v0.17/", + "retry_migration": "ストレージエンジンの移行を再試行", + "sync_status": "同期状態", + "interface_settings": "インターフェース", + "select_interface_language": "インターフェース表示言語を選択", + "extension_icon_badge": "拡張機能アイコンバッジ", + "display_type": "表示タイプ", + "extension_icon_badge_type": "拡張機能アイコンに表示される数字タイプ", + "background_color": "背景色", + "badge_background_color_desc": "バッジ背景色", + "text_color": "テキスト色", + "badge_text_color_desc": "バッジテキスト色", + "badge_type_none": "表示しない", + "badge_type_run_count": "実行回数", + "badge_type_script_count": "スクリプト数", + "script_menu": "スクリプトメニュー", + "display_right_click_menu": "右クリックメニューを表示", + "display_right_click_menu_desc": "ブラウザの右クリックメニューにスクリプトメニューを表示する", + "expand_count": "展開数", + "auto_collapse_when_exceeds": "この数を超えたときに自動的に折りたたむ", + "script_update_check_frequency": "スクリプト更新の確認頻度", + "script_auto_update_frequency": "スクリプトの自動更新確認頻度", + "update_options": "更新オプション", + "control_script_update_behavior": "スクリプト更新の動作を制御", + "blacklist_pages_desc": "指定されたページでスクリプトの実行を禁止、ワイルドカードをサポート", + "development_tools": "開発ツール", + "check_script_code_quality": "スクリプトコードの品質とエラーをチェック", + "custom_eslint_rules_config": "カスタムESLintルール設定(JSON形式)", + "script_run_env": { + "title": "実行環境", + "all": "すべてのタブ", + "normal-tabs": "通常のタブ", + "incognito-tabs": "シークレットタブ" + }, + "script_run_at": { + "title": "実行タイミング" + }, + "script_setting": { + "title": "スクリプト設定", + "default": "デフォルト" + }, + "notification": { + "script_sync_delete": "スクリプト同期削除", + "script_sync_delete_desc": "スクリプト {{scriptName}} が削除されました", + "subscribe_update": "サブスクリプション {{subscribeName}} が更新されました", + "subscribe_update_desc": "新しいスクリプト: {{newScripts}}\n削除されたスクリプト: {{deletedScripts}}" + }, + "enable_background": { + "title": "バックグラウンド実行を有効にする", + "description": "有効にすると、すべてのウィンドウを閉じた後もブラウザはバックグラウンドで動作し、手動で終了するまでトレイに最小化されます。これにより、バックグラウンドスクリプトの実行が続きます。", + "enable_failed": "有効化に失敗しました", + "disable_failed": "無効化に失敗しました", + "prompt_title": "バックグラウンド実行を有効にしますか?", + "prompt_description": "これは{{scriptType}}です。バックグラウンド実行を有効にすると、ブラウザを閉じた後もスクリプトの実行が続きます。", + "enable_now": "今すぐ有効にする", + "maybe_later": "後で", + "settings_hint": "設定ページでいつでも変更できます。" + }, + "favicon_service": "Favicon サービス", + "favicon_service_desc": "ウェブサイトアイコンの取得サービスを選択", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_google": "Google", + "favicon_service_duckduckgo": "DuckDuckGo", + "favicon_service_icon-horse": "Icon Horse", + "favicon_service_local": "ローカル取得", + "cloud_sync_account_verification": "クラウド同期アカウント情報を確認中...", + "cloud_sync_verification_failed": "クラウド同期アカウント情報の確認に失敗しました" +} diff --git a/src/locales/ja-JP/tools.json b/src/locales/ja-JP/tools.json new file mode 100644 index 000000000..c860d43d8 --- /dev/null +++ b/src/locales/ja-JP/tools.json @@ -0,0 +1,17 @@ +{ + "development_tool": "開発ツール", + "vscode_url": "VSCodeアドレス", + "auto_connect_vscode_service": "VSCodeサービスに自動接続", + "connect": "接続", + "connection_success": "接続に成功しました", + "connection_failed": "接続に失敗しました", + "select_import_script": "新しいページでインポートするスクリプトを選択してください", + "import_error": "インポートエラー", + "pulling_data_from_cloud": "クラウドからデータを取得中", + "pull_failed": "取得に失敗しました", + "restore": "復元", + "local_backup": "ローカルバックアップ", + "cloud_backup": "クラウドバックアップ", + "auto_backup": "自動バックアップ", + "data_migration": "データ移行" +} diff --git a/src/locales/ja-JP/translation.json b/src/locales/ja-JP/translation.json deleted file mode 100644 index b3ef31eff..000000000 --- a/src/locales/ja-JP/translation.json +++ /dev/null @@ -1,770 +0,0 @@ -{ - "sentence-separator": "。", - "import_link": "リンクインポート", - "import_link_failure": "リンクインポートに失敗しました", - "create_user_script": "新しいユーザースクリプトを作成", - "create_background_script": "新しいバックグラウンドスクリプトを作成", - "create_scheduled_script": "新しいスケジュールスクリプトを作成", - "import_by_local": "ローカルインポート", - "import_local_failure": "ローカルインポートに失敗しました", - "import_local_success": "ローカルインポートに成功しました", - "create_script": "新しいスクリプトを作成", - "user_guide": "使用ガイド", - "api_docs": "APIドキュメント", - "development_guide": "開発ガイド", - "script_gallery": "スクリプトギャラリー", - "community_forum": "コミュニティフォーラム", - "external_links": "外部リンク", - "system_follow": "システムに準拠", - "no_data": "データがありません", - "installed_scripts": "インストール済みのスクリプト", - "subscribe": "サブスクライブ", - "logs": "ログ", - "tools": "ツール", - "find": "検索", - "replace": "置換", - "settings": "設定", - "hide_main_sidebar": "サイドバーを折りたたむ", - "show_main_sidebar": "サイドバーを展開", - "guide": "初心者ガイド", - "helpcenter": "ヘルプセンター", - "general": "一般", - "language": "言語", - "help_translate": "翻訳に協力", - "script_sync": "スクリプト同期", - "sync_delete": "削除状態を同期", - "sync_delete_desc": "有効にすると、スクリプト削除時に削除済みとしてマークされ、他のデバイスでもその状態を検出してスクリプトを削除します。無効にすると、ローカルとクラウドから直接削除されるため、複数のデバイスで使用している場合に同期の不整合が繰り返し発生することがあります。", - "enable_script_sync_to": "スクリプト同期を有効にする", - "save": "保存", - "save_as": "名前を付けて保存", - "file": "ファイル", - "run": "実行", - "debug": "デバッグ", - "cloud_sync_account_verification": "クラウド同期アカウント情報を確認中...", - "cloud_sync_verification_failed": "クラウド同期アカウント情報の確認に失敗しました", - "save_success": "保存に成功しました", - "reset_success": "リセットに成功しました", - "update": "更新", - "check_update": "更新をチェック", - "script_subscription_check_interval": "スクリプト/サブスクライブの更新チェック間隔", - "never": "なし", - "6_hours": "6時間", - "12_hours": "12時間", - "every_day": "毎日", - "every_week": "毎週", - "update_disabled_scripts": "無効なスクリプトを更新する", - "silent_update_non_critical_changes": "重要でない変更をサイレントで更新する", - "enable_eslint": "ESLintを有効にする", - "eslint_rules": "ESLintルール", - "enter_eslint_rules": "ESLintルールを入力してください。https://eslint.org/play/ から設定をダウンロードできます", - "language_change_tip": "言語の切り替えが成功しました", - "backup": "バックアップ", - "local": "ローカル", - "export_file": "ファイルをエクスポート", - "import_file": "ファイルをインポート", - "cloud": "クラウド", - "backup_to": "バックアップ先", - "preparing_backup": "クラウドへのバックアップを準備中", - "backup_success": "バックアップに成功しました", - "backup_failed": "バックアップに失敗しました", - "no_backup_files": "バックアップファイルがありません", - "backup_list": "バックアップリスト", - "open_backup_dir": "バックアップディレクトリを開く", - "confirm_delete": "削除を確認", - "confirm_delete_backup_file": "バックアップファイルの削除を確認", - "confirm_update": "更新を確認", - "delete_success": "削除に成功しました", - "deleting": "削除中", - "backup_strategy": "バックアップ戦略", - "under_construction": "開発中", - "development_tool": "開発ツール", - "vscode_url": "VSCodeアドレス", - "auto_connect_vscode_service": "VSCodeサービスに自動接続", - "connect": "接続", - "connection_success": "接続に成功しました", - "connection_failed": "接続に失敗しました", - "select_import_script": "新しいページでインポートするスクリプトを選択してください", - "import_error": "インポートエラー", - "pulling_data_from_cloud": "クラウドからデータを取得中", - "pull_failed": "取得に失敗しました", - "restore": "復元", - "log_title": "実行ログ", - "last_5_minutes": "過去5分", - "last_15_minutes": "過去15分", - "last_30_minutes": "過去30分", - "last_1_hour": "過去1時間", - "last_3_hours": "過去3時間", - "last_6_hours": "過去6時間", - "last_12_hours": "過去12時間", - "last_24_hours": "過去24時間", - "last_7_days": "過去7日", - "query": "検索", - "labels": "ラベル", - "search_regex": "検索(正規表現対応)", - "clean_schedule": "定期クリーニング", - "days_ago_logs": "日前のログ", - "delete_completed": "削除完了", - "delete_current_logs": "現在のログを削除", - "clear_completed": "クリア完了", - "clear_logs": "ログをクリア", - "to": " ~ ", - "now": "直近", - "total_logs": "{{length}}件のログが見つかりました", - "filtered_logs": "絞り込み結果: {{length}}件", - "enter_filter_conditions": "絞り込み条件を入力してください", - "permission": "権限", - "enter_subscribe_name": "サブスクライブ名を入力してください", - "subscribe_url": "サブスクライブURL", - "confirm_delete_subscription": "このサブスクライブを削除してもよろしいですか?関連するスクリプトも削除されます", - "list": { - "confirm_delete": "削除してもよろしいですか?この操作は元に戻せません!", - "confirm_update": "更新してもよろしいですか?この操作は元に戻せません!" - }, - "enable": "有効", - "script_list_enable_width": 80, - "script_list_last_updated_width": 120, - "script_list_apply_to_run_status_width": 140, - "subscribe_list_enable_width": 100, - "disable": "無効", - "name": "名前", - "version": "バージョン", - "apply_to_run_status": "適用先/実行状態", - "source": "ソース", - "home": "ホーム", - "sorting": "ソート", - "last_updated": "最終更新", - "action": "操作", - "foreground_page_script_tooltip": "フォアグラウンドページスクリプト、指定されたページで実行されます", - "background_script_tooltip": "バックグラウンドスクリプト、有効にするとバックグラウンドで実行されます", - "scheduled_script_tooltip": "スケジュールスクリプト、次回実行時間:", - "running": "実行中", - "completed": "実行完了", - "source_subscribe_link": "サブスクライブリンク", - "source_local_script": "ローカルスクリプト", - "source_script_link": "スクリプトリンク", - "by_manual_creation": "コード編集によってローカルで作成された", - "confirm_delete_script": "このスクリプトを削除してもよろしいですか?", - "confirm_delete_script_content": "スクリプト\"{{name}}\"を削除しますか?これは元に戻すことはできません。", - "delete_failed": "削除に失敗しました", - "enter_script_name": "スクリプト名を入力してください", - "update_not_supported": "このスクリプトは更新チェックをサポートしていません", - "checking_for_updates": "更新をチェック中...", - "new_version_available": "新しいバージョンがあります", - "latest_version": "最新バージョンです", - "checked_for_all_selected": "選択されたすべてのスクリプトの更新をチェックしました", - "update_check_failed": "更新チェックに失敗しました", - "script_import_failed": "スクリプトのインポートに失敗しました", - "install_page_open_failed": "インストールページを開けませんでした", - "stopping_script": "スクリプトを停止中", - "script_stopped": "スクリプトが停止しました", - "starting_script": "スクリプトを開始中...", - "starting_updates": "一括更新を開始中...", - "script_started": "スクリプトが開始しました", - "operation_failed": "操作に失敗しました", - "batch_operations": "一括操作", - "export": "エクスポート", - "delete": "削除", - "pin_to_top": "ピン", - "scripts_pinned_to_top": "選択されたスクリプトは固定されました", - "unknown_operation": "不明な操作", - "confirm": "確認", - "close": "閉じる", - "page_script": "ページスクリプト", - "homepage": "スクリプトホームページ", - "script_website": "スクリプトサイト", - "script_source": "スクリプトソースコード", - "bug_feedback_script_support": "バグフィードバック/スクリプトサポートサイト", - "config": "設定", - "key": "キー", - "value": "値", - "add": "追加", - "type": "タイプ", - "edit_value": "値を編集", - "add_value": "値を追加", - "update_success": "変更に成功しました", - "add_success": "追加に成功しました", - "script_storage": "スクリプトストレージ", - "enter_key": "キーを入力してください", - "key_placeholder": "キー", - "value_placeholder": "タイプがobjectの場合、JSON解析可能なデータを入力してください", - "clear": "クリア", - "clear_success": "クリアに成功しました", - "confirm_clear": "本当にこのストレージスペースをクリアしますか?", - "type_string": "文字列", - "type_number": "数値", - "type_boolean": "ブール値", - "type_object": "オブジェクト", - "confirm_delete_resource": "このリソースを削除してもよろしいですか?次回開始時にこのリソースが再読み込みされます", - "confirm_clear_resource": "本当にこれらのリソースをクリアしますか?次回開始時にリソースが再読み込みされます", - "script_resource": "スクリプトリソース", - "permission_value": "権限値", - "allow": "許可", - "yes": "はい", - "no": "いいえ", - "confirm_delete_permission": "この権限を削除してもよろしいですか?", - "basic_info": "基本情報", - "update_url": "URLを更新", - "permission_management": "権限管理", - "add_permission": "権限を追加", - "permission_cors": "クロスドメイン(CORS)", - "permission_cookie": "Cookieを管理", - "match": "マッチ", - "user_setting": "ユーザー設定", - "confirm_delete_exclude": "この除外を削除してもよろしいですか?", - "after_deleting_match_item": "スクリプト設定のマッチ項目を削除すると、自動的にマッチ項目に追加されます", - "confirm_delete_match": "このマッチを削除してもよろしいですか?", - "after_deleting_exclude_item": "スクリプト設定のマッチ項目を削除すると、自動的に除外項目に追加されます", - "add_match": "対象を追加", - "add_exclude": "除外を追加", - "website_match": "対象サイト(@match)", - "reset": "リセット", - "website_exclude": "除外サイト(@exclude)", - "confirm_reset": "リセットしてもよろしいですか?", - "script_total_runs": "このスクリプトは合計{{runNum}}回実行され、iframe上で{{runNumByIframe}}回実行されました", - "script_total_runs_single": "このスクリプトは{{runNum}}回実行されました", - "script_disabled": "このスクリプトは有効になっていません", - "run_once": "一度実行", - "stop": "中止", - "edit": "編集", - "undo": "元に戻す", - "redo": "やり直し", - "cut": "切り取り", - "copy": "コピー", - "paste": "貼り付け", - "format": "フォーマット", - "exclude_on": "$0の実行を復元", - "exclude_off": "$0の実行を除外", - "user_config": "ユーザー設定", - "gm_api": "GM API", - "storage_api": "ストレージAPI", - "use_file_system": "使用するファイルシステム", - "open_directory": "ディレクトリを開く", - "account_validation_failed": "アカウント情報の確認に失敗しました", - "not_set": "未設定", - "in_use": "有効", - "storage_error": "ストレージエラー", - "upload_to_cloud": "クラウドにアップロード", - "save_failed": "保存に失敗しました", - "exporting": "エクスポート中...", - "upload_to": "アップロード先", - "value_export_expression": "値エクスポート式", - "overwrite_original_value_on_import": "インポート時に元の値を上書き", - "cookie_export_expression": "Cookieエクスポート式", - "overwrite_original_cookie_on_import": "インポート時に元の値を上書き", - "restore_default_values": "デフォルト値を復元", - "get_confirm_error": "確認情報の取得に失敗しました", - "confirm_error": "確認に失敗しました", - "ignore": "無視", - "allow_once": "一度許可", - "temporary_allow": "この{{permissionContent}}を一時的に許可", - "temporary_allow_all": "すべての{{permissionContent}}を一時的に許可", - "permanent_allow": "この{{permissionContent}}を永続的に許可", - "permanent_allow_all": "すべての{{permissionContent}}を永続的に許可", - "deny_once": "一度拒否", - "temporary_deny": "この{{permissionContent}}を一時的に拒否", - "temporary_deny_all": "すべての{{permissionContent}}を一時的に拒否", - "permanent_deny": "この{{permissionContent}}を永続的に拒否", - "permanent_deny_all": "すべての{{permissionContent}}を永続的に拒否", - "data_import": "データインポート", - "import": "インポート", - "select_scripts_to_import": "インポートするスクリプトを選択してください", - "select_all": "すべて選択", - "script_import_progress": "スクリプトインポート進行状況", - "select_subscribes_to_import": "インポートするサブスクライブを選択してください", - "subscribe_import_progress": "サブスクライブインポート進行状況", - "author": "作者", - "description": "説明", - "operation": "操作", - "error": "エラー", - "unknown": "不明", - "add_new": "新規追加", - "no_operation": "操作しない", - "enable_script": "スクリプトを有効にする", - "local_creation": "ローカル作成", - "import_success": "インポートに成功しました", - "install_script": "インストール", - "update_script": "更新", - "install_subscribe": "サブスクライブをインストール", - "update_subscribe": "サブスクライブを更新", - "update_script_no_close": "ウィンドウを閉じずに更新する", - "install_script_no_close": "ウィンドウを閉じずにインストールする", - "update_script_no_more_update": "更新しますが、今後は更新を確認しません", - "close_update_script_no_more_update": "閉じて更新をチェックしない", - "install_script_no_more_update": "インストールするが、今後は更新をチェックしない", - "invalid_link": "無効なリンク", - "subscribe_install_label": "このサブスクライブは以下のスクリプトをインストールします", - "script_runs_in": "スクリプトは以下のウェブサイトで実行されます", - "script_has_full_access_to": "スクリプトは以下のアドレスへの完全なアクセス権限を取得します", - "script_requires": "スクリプトは以下の外部リソースを参照しています", - "cookie_warning": "注意:このスクリプトはCookieの操作権限をリクエストします。これは危険な権限ですので、スクリプトの安全性を確認してください。", - "cron_oncetype": { - "minute": "{{next}}(毎分実行)", - "hour": "{{next}}(毎時間実行)", - "day": "{{next}}(毎日実行)", - "month": "{{next}}(毎月実行)", - "week": "{{next}}(毎週実行)" - }, - "cron_invalid_expr": "不正な cron 式です", - "scheduled_script_description_title": "これはスケジュールスクリプトです。有効にすると特定の時間に自動実行され、手動操作も可能です。", - "scheduled_script_description_description_expr": "スケジュールタスク表現:", - "scheduled_script_description_description_next": "最近の実行時間:", - "background_script_description": "これはバックグラウンドスクリプトです。有効にするとブラウザを開いたときに自動的に一度実行され、パネルで手動制御できます。", - "install_success": "インストールに成功しました", - "install": { - "update_success": "更新に成功しました" - }, - "install_failed": "インストールに失敗しました", - "subscribe_success": "サブスクライブに成功しました", - "subscribe_failed": "サブスクライブに失敗しました", - "current_version": "現在のバージョン", - "update_version": "更新バージョン", - "updatepage": { - "main_header": "更新を確認", - "header_site_specific": "このサイトに利用可能な更新があります($0)", - "header_site_all": "利用可能な更新があります", - "header_ignored": "利用可能な更新(無視済み)", - "update_all": "すべて更新", - "ignore_all": "すべて無視", - "update": "更新", - "ignore": "無視", - "old_version_": "旧バージョン:", - "new_version_": "新バージョン:", - "enabled": "有効", - "tooltip_enabled": "このスクリプトは有効です。", - "disabled": "無効", - "tooltip_disabled": "このスクリプトは無効です。", - "similarity_": "類似度:", - "codechange_major": "大きな変更", - "codechange_noticeable": "目立つ変更", - "codechange_tiny": "小さな変更", - "new_connects_": "新しい @connect:", - "tag_new_connect": "@connect 追加", - "status_last_check": "最終確認:$0", - "status_checking_updates": "更新を確認中...", - "status_no_update": "更新は不要です", - "status_n_update": "$0 件の更新が必要です", - "status_n_ignored": "$0 件の更新が無視されました", - "status_autoclose": "$0 秒後に自動的に閉じます", - "header_other_update": "その他の利用可能な更新" - }, - "downloading_status_text": "ダウンロード中。{{bytes}} を受信しました。", - "install_page_please_wait": "しばらくお待ちください", - "install_page_loading": "インストールページを読み込み中", - "install_page_load_failed": "インストールページの読み込みに失敗しました", - "invalid_page": "無効なページ", - "background_script_tag": "これはバックグラウンドスクリプトです", - "scheduled_script_tag": "これはスケジュールスクリプトです", - "background_script": "バックグラウンドスクリプト", - "scheduled_script": "スケジュールスクリプト", - "install_from_legitimate_sources_warning": "正当なソースからスクリプトをインストールしてください!不明なスクリプトはプライバシーを侵害したり、悪意のある操作を行う可能性があります!", - "antifeature_referral_link_title": "紹介リンク", - "antifeature_referral_link_description": "このスクリプトは作者のアフィリエイトリンクに変更またはリダイレクトします", - "antifeature_ads_title": "広告付き", - "antifeature_ads_description": "このスクリプトはアクセスしたページに広告を挿入します", - "antifeature_payment_title": "有料スクリプト", - "antifeature_payment_description": "このスクリプトは正常に使用するために支払いが必要です", - "antifeature_miner_title": "マイニング", - "antifeature_miner_description": "このスクリプトにはマイニング動作があります", - "antifeature_membership_title": "メンバー機能", - "antifeature_membership_description": "このスクリプトは正常に使用するためにメンバー登録が必要です", - "antifeature_tracking_title": "情報追跡", - "antifeature_tracking_description": "このスクリプトはユーザー情報を追跡します", - "script_info_load_failed": "スクリプト情報の読み込みに失敗しました!", - "script_status_tooltip": "スクリプトの有効状態を管理できます。通常の Tampermonkey スクリプトはデフォルトで有効ですが、バックグラウンドスクリプトとスケジュールスクリプトはデフォルトで無効になっています。", - "subscribe_source_tooltip": "これはサブスクリプションソースです。サブスクリプションを開くと、サブスクリプションスクリプトが自動的にインストールされます。", - "get_script": "スクリプトを取得", - "report_issue": "バグ報告 / 問題のフィードバック", - "project_docs": "プロジェクトドキュメント", - "community": "コミュニティ", - "popup": { - "new_version_available": "新しいバージョンが利用可能です" - }, - "current_page_scripts": "現在のページ実行スクリプト", - "enabled_background_scripts": "有効および実行中のバックグラウンドスクリプト", - "script_accessing_cross_origin_resource": "スクリプトがクロスドメインリソースにアクセスしようとしています", - "confirm_operation_description": "スクリプトがこの操作を実行することを許可するかどうか確認してください。スクリプトは@connectタグを追加してこのオプションをスキップすることもできます", - "extension_site_access_title": "ScriptCat にサイトアクセス権限が必要です", - "extension_site_access_description": "ScriptCat がこのリクエストを実行できるように、このオリジンへのブラウザーのサイトアクセス権限を許可してください。これは拡張機能のサイトアクセス設定を変更します。", - "extension_site_access_content": "サイト", - "domain": "ドメイン", - "script_name": "スクリプト名", - "request_domain": "リクエストドメイン", - "request_url": "リクエストURL", - "access_cookie_content": "スクリプトがウェブサイトのCookieコンテンツにアクセスしようとしています", - "confirm_script_operation": "スクリプトがこの操作を実行することを許可するかどうか確認してください。Cookieは重要なユーザーデータですので、信頼できるスクリプトにのみ許可してください。", - "cookie_domain": "Cookieドメイン", - "script_operation_title": "スクリプトがストレージにアクセスしようとしています", - "script_operation_description": "スクリプトがこの操作を実行することを許可するかどうか確認してください。許可すると、スクリプトが設定したストレージスペースの操作が許可され、スクリプトはストレージスペース下にapp/${dir}ディレクトリを作成して使用します", - "script_permission_content": "スクリプト", - "sync_system_connect_failed": "同期システムの接続に失敗しました", - "sync_system_closed": "同期が無効になっています", - "sync_system_closed_description": "同期機能が無効になっています。再設定してください", - "auth_type": "認証タイプ", - "url": "URL", - "username": "ユーザー名", - "password": "パスワード", - "access_token_bearer": "アクセストークン(Bearer)", - "s3_bucket_name": "バケット名", - "s3_region": "リージョン", - "s3_access_key_id": "アクセスキーID", - "s3_secret_access_key": "シークレットアクセスキー", - "s3_custom_endpoint": "カスタムエンドポイント(オプション)", - "skip": "スキップ", - "next": "次へ", - "next_with_progress": "次へ(ステップ{step}の全{steps}ステップ中)", - "back": "戻る", - "last": "完了", - "start_guide_title": "ScriptCat拡張機能へようこそ", - "start_guide_content": "次に、ScriptCatの基本的な使用方法をご紹介します", - "guide_installed_scripts": "インストールしたスクリプトはここに表示されます", - "guide_script_list_title": "スクリプトセンター", - "guide_script_list_content": "スクリプトセンターからスクリプトをインストールできます。ScriptCatはユーザースクリプトに加えてバックグラウンドスクリプトもサポートしています", - "guide_script_list_enable_title": "スクリプト有効化", - "guide_script_list_enable_content": "スクリプトは有効にしないと使用できません。ページスクリプトはインストール時にデフォルトで有効、バックグラウンドスクリプトはインストール時にデフォルトで無効です", - "guide_script_list_apply_to_run_status_title": "適用先と実行状態", - "guide_script_list_apply_to_run_status_content": "スクリプトの実行状態を表示します。タグにマウスを合わせるとスクリプトタイプを確認できます", - "guide_script_list_sort_title": "ソート", - "guide_script_list_sort_content": "スクリプトをドラッグして順番を付ける事ができます。", - "guide_script_list_update_title": "最終更新", - "guide_script_list_update_content": "「最終更新」列ラベルをクリックすると、スクリプトの更新がチェックされます。", - "guide_script_list_action_title": "操作", - "guide_script_list_action_content": "操作バーでは、スクリプトの編集、スクリプトの実行・停止(バックグラウンドスクリプト)、および設定(UserConfig を参照)にアクセスできます。右側のボタンからは、高度な絞り込みや表示モードの切り替えにアクセスできます。", - "guide_tools_title": "常用ツール", - "guide_tools_content": "ツールにはバックアップと開発のツールが提供されています", - "guide_tools_backup_title": "バックアップ", - "guide_tools_backup_content": "バックアップはスクリプトを保存し、紛失を避けることができます。スクリプトファイルをローカルにエクスポートしたり、ローカルのスクリプトファイルを読み込んだりできます。クラウドにバックアップすることもでき、より便利です。", - "guide_setting_title": "設定", - "guide_setting_content": "設定には主に言語、スクリプト同期、更新頻度などの常用設定項目が含まれています", - "guide_setting_sync_title": "更新と同期", - "guide_setting_sync_content": "スクリプト同期機能により、このデバイスのスクリプトコンテンツをクラウドに便利に同期できます。複数のデバイスがある場合は、「削除状態を同期」オプションを有効にしてください。このデバイスでスクリプトが削除されると、クラウドから対応するスクリプトが削除され、他のデバイスのスクリプトも削除されます。", - "auto": "自動", - "hide": "非表示", - "custom": "カスタム", - "resize_column_width": "列幅を調整", - "collapse": "折りたたむ", - "expand": "展開", - "menu_expand_num_before": "メニュー項目が", - "menu_expand_num_after": "個を超えると自動的に非表示", - "script_name_cannot_be_set_to_empty": "スクリプト名を空に設定することはできません", - "edit_conflict": "編集の競合", - "confirm_override_when_edit_conflict": "このスクリプトは別のインスタンスで編集されています。置き換えるとその変更は上書きされます。このバージョンを保持しますか?", - "save_abort_when_edit_conflict": "このスクリプトは別のインスタンスで編集されています。保存は中止されました。", - "scriptname_conflict": "スクリプト名の競合", - "confirm_save_when_scriptname_conflict": "このスクリプト名はすでに別のスクリプトで使用されています。それでも保存しますか?", - "save_abort_when_scriptname_conflict": "このスクリプト名はすでに別のスクリプトで使用されています。保存を中止しました。", - "eslint_config_format_error": "ESLint設定フォーマットエラー", - "export_success": "エクスポートに成功しました", - "get_backup_dir_url_failed": "バックアップディレクトリアドレスの取得に失敗しました", - "get_backup_files_failed": "バックアップファイルの取得に失敗しました", - "request_permission": "権限をリクエストする", - "develop_mode_guide": "現在「デベロッパーモード」が有効ではないため、スクリプトは正常に動作しません。👉有効化の方法はこちら", - "allow_user_script_guide": "現在「ユーザー スクリプトを許可する」が有効ではないため、スクリプトは正常に動作しません。👉有効化の方法はこちら", - "lower_version_browser_guide": "ご使用のブラウザは古すぎるため、スクリプトは正常に動作しません。👉詳しくはこちら", - "click_to_reload": "👉再読み込みする", - "page_in_blacklist": "現在のページはブラックリストにあり、スクリプトを使用できません", - "baidu_netdisk": "百度ネットディスク", - "netdisk_unbind": "{{provider}} の連携を解除", - "netdisk_unbind_confirm": "{{provider}} のアカウント連携を解除しますか?", - "netdisk_unbind_success": "{{provider}} のアカウント連携を解除しました", - "netdisk_unbind_error": "{{provider}} のアカウント解除に失敗しました", - "save_only_current_group": "保存は現在のグループにのみ有効です", - "script_import_result": "スクリプトインポート結果", - "failure_info": "失敗情報", - "security": "セキュリティ", - "blacklist_pages": "ブラックリストページ", - "blacklist_placeholder": "以下のページでScriptCatのスクリプト実行を禁止します。複数のページは改行文字で区切ってください。例:\nhttps://*.example.com", - "expression_format_error": "式フォーマットエラー", - "migration_confirm_message": "ストレージエンジンの移行を再試行すると既存のデータが変更されます。確認してください。詳細はこちら:https://docs.scriptcat.org/docs/change/v0.17/", - "retry_migration": "ストレージエンジンの移行を再試行", - "script_modified_leave_confirm": "現在の内容はまだ保存されていません。このままページを離れると変更は失われます。よろしいですか?", - "create_success_note": "作成に成功しました。バックグラウンドスクリプトはデフォルトで有効になりませんのでご注意ください", - "save_as_failed": "名前を付けて保存に失敗しました", - "save_as_success": "名前を付けて保存に成功しました", - "only_background_scheduled_can_run": "バックグラウンドスクリプト/スケジュールスクリプトのみ実行できます", - "preparing_script_resources": "スクリプトリソースを準備中...", - "build_success_message": "ビルドに成功しました。拡張機能ページで開発者ツールを開き、コンソールで出力を確認できます", - "script_storage_tooltip": "スクリプト保存データ(GM_value)を管理できます", - "script_resource_tooltip": "@resource、@requireでダウンロードしたリソースを管理", - "script_setting_tooltip": "スクリプトのカスタム設定を行う", - "script_modified_close_confirm": "スクリプトが変更されています。閉じると変更が失われます。続行しますか?", - "close_current_tab": "現在のタブを閉じる", - "close_other_tabs": "他のタブを閉じる", - "close_left_tabs": "左側のタブを閉じる", - "close_right_tabs": "右側のタブを閉じる", - "import_script_placeholder": ".user.js で終わるスクリプトの絶対リンクまたはScriptCatインストールページリンクの入力をサポート\n複数行で記入可能、1行につき1つ\n例:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/zh-CN/script-show-page/1234", - "invalid_script_code": "無効なスクリプトコード", - "build_failed": "ビルドに失敗しました", - "drag_script_here_to_upload": "スクリプトをここにドラッグしてアップロード", - "sync_status": "同期状態", - "search_scripts": "検索スクリプト", - "ext_update_notification": "ScriptCat拡張機能が更新されました", - "ext_update_notification_desc": "現在のバージョン: {{version}}、詳細は更新ログをご覧ください", - "watch_file_description": "ファイルの変更を監視し、スクリプトを自動更新します。使用する際は、スクリプトファイルのパスが変更されず、ページが閉じられないことを確認してください。", - "watch_file": "ファイルの監視", - "stop_watch_file": "監視を停止", - "script_menu_display": "スクリプトが登録したメニュー", - "badge_type_none": "表示しない", - "badge_type_run_count": "実行回数", - "badge_type_script_count": "スクリプト数", - "interface_settings": "インターフェース", - "select_interface_language": "インターフェース表示言語を選択", - "extension_icon_badge": "拡張機能アイコンバッジ", - "display_type": "表示タイプ", - "extension_icon_badge_type": "拡張機能アイコンに表示される数字タイプ", - "background_color": "背景色", - "badge_background_color_desc": "バッジ背景色", - "text_color": "テキスト色", - "badge_text_color_desc": "バッジテキスト色", - "script_menu": "スクリプトメニュー", - "display_right_click_menu": "右クリックメニューを表示", - "display_right_click_menu_desc": "ブラウザの右クリックメニューにスクリプトメニューを表示する", - "expand_count": "展開数", - "auto_collapse_when_exceeds": "この数を超えたときに自動的に折りたたむ", - "script_update_check_frequency": "スクリプト更新の確認頻度", - "script_auto_update_frequency": "スクリプトの自動更新確認頻度", - "update_options": "更新オプション", - "control_script_update_behavior": "スクリプト更新の動作を制御", - "blacklist_pages_desc": "指定されたページでスクリプトの実行を禁止、ワイルドカードをサポート", - "development_tools": "開発ツール", - "check_script_code_quality": "スクリプトコードの品質とエラーをチェック", - "custom_eslint_rules_config": "カスタムESLintルール設定(JSON形式)", - "light": "ライトモード", - "dark": "ダークモード", - "individual_edit": "個別編集", - "batch_edit": "一括編集", - "script_code": "スクリプトコード", - "enter_search_value": "{{search}}を入力して検索してください", - "script_run_env": { - "title": "実行環境", - "all": "すべてのタブ", - "normal-tabs": "通常のタブ", - "incognito-tabs": "シークレットタブ" - }, - "script_run_at": { - "title": "実行タイミング" - }, - "script_setting": { - "title": "スクリプト設定", - "default": "デフォルト" - }, - "editor_config": "エディタ設定", - "editor_config_description": "jsconfig.jsのcompilerOptionsを参考に設定できます", - "editor_type_definition": "エディタ型定義", - "editor_type_definition_description": "独自の型定義をカスタマイズでき、スクリプトエディタがこれらの型定義を自動的に読み込みます", - "eslint_rules_reset": "ESLintルールがリセットされました", - "eslint_rules_saved": "ESLintルールが保存されました", - "editor_config_reset": "エディタ設定がリセットされました", - "editor_config_saved": "エディタ設定が保存されました", - "editor_config_format_error": "エディタ設定のフォーマットエラー", - "editor_type_definition_reset": "エディタ型定義がリセットされました", - "editor_type_definition_saved": "エディタ型定義が保存されました", - "script_list": { - "sidebar": { - "stopped": "停止済み", - "all": "すべて", - "normal_script": "通常スクリプト", - "status": "状態" - } - }, - "tags": "タグ", - "install_source": "インストール元", - "input_tags_placeholder": "タグを入力し、Enterを押して確認", - "switch_to_card_mode": "カードモードに切り替え", - "switch_to_table_mode": "テーブルモードに切り替え", - "open_sidebar": "サイドバーを開く", - "close_sidebar": "サイドバーを閉じる", - "no_message_content": "メッセージ内容なし", - "error_metadata_invalid": "MetaData ブロックが不正です", - "error_script_name_required": "スクリプト名は必須です", - "error_script_version_required": "@version は必須です", - "error_script_namespace_required": "@namespace は必須です", - "error_cron_invalid": "無効な cron 式です: {{expr}}", - "error_script_type_mismatch": "スクリプト種別が不一致です(通常とバックグラウンドは相互変換不可)", - "error_old_script_code_missing": "既存スクリプトのコードが見つかりません", - "error_subscribe_name_required": "サブスクライブ名は必須です", - "error_grant_conflict": "@grant に 'none' と GM API の両方が指定されています", - "error_metadata_line_duplicated": "メタデータ内に重複した宣言があります。", - "notification": { - "script_sync_delete": "スクリプトの削除状態を同期", - "script_sync_delete_desc": "スクリプト {{scriptName}} が削除されました", - "subscribe_update": "サブスクリプション {{subscribeName}} が更新されました", - "subscribe_update_desc": "新しいスクリプト: {{newScripts}}\n削除されたスクリプト: {{deletedScripts}}" - }, - "loading": "読み込み中...", - "runtime": "ランタイム", - "enable_background": { - "title": "バックグラウンド実行を有効にする", - "description": "有効にすると、すべてのウィンドウを閉じた後もブラウザはバックグラウンドで動作し、手動で終了するまでトレイに最小化されます。これにより、バックグラウンドスクリプトの実行が続きます。", - "enable_failed": "有効化に失敗しました", - "disable_failed": "無効化に失敗しました", - "prompt_title": "バックグラウンド実行を有効にしますか?", - "prompt_description": "これは{{scriptType}}です。バックグラウンド実行を有効にすると、ブラウザを閉じた後もスクリプトの実行が続きます。", - "enable_now": "今すぐ有効にする", - "maybe_later": "後で", - "settings_hint": "設定ページでいつでも変更できます。" - }, - "favicon_service": "Favicon サービス", - "favicon_service_desc": "ウェブサイトアイコンの取得サービスを選択", - "favicon_service_scriptcat": "ScriptCat", - "favicon_service_google": "Google", - "favicon_service_duckduckgo": "DuckDuckGo", - "favicon_service_icon-horse": "Icon Horse", - "favicon_service_local": "ローカル取得", - "editor": { - "show_script_list": "スクリプトリストを表示", - "hide_script_list": "スクリプトリストを非表示" - }, - "agent": "AI Agent", - "agent_chat": "チャット", - "agent_provider": "モデルサービス", - "agent_mcp": "MCP", - "agent_skills": "Skills", - "agent_provider_title": "モデルサービス", - "agent_provider_select": "AIサービスプロバイダー", - "agent_provider_api_base_url": "APIアドレス", - "agent_provider_api_key": "APIキー", - "agent_provider_model": "デフォルトモデル", - "agent_provider_test_connection": "接続テスト", - "agent_provider_test_success": "接続成功", - "agent_provider_test_failed": "接続失敗", - "agent_model_fetch": "モデルを取得", - "agent_model_fetch_failed": "モデルリストの取得に失敗しました", - "agent_model_name": "名前", - "agent_model_add": "モデルを追加", - "agent_model_edit": "編集", - "agent_model_copy": "コピー", - "agent_model_delete": "削除", - "agent_model_set_default": "デフォルトに設定", - "agent_model_default_label": "デフォルト", - "agent_model_delete_confirm": "このモデル設定を削除してもよろしいですか?", - "agent_model_max_tokens": "最大出力トークン数", - "agent_model_no_models": "モデル設定がありません", - "agent_model_vision_support": "画像入力対応", - "agent_model_image_output": "画像出力対応", - "agent_model_capabilities": "モデル機能", - "agent_model_supports_vision": "画像入力", - "agent_model_supports_image_output": "画像出力", - "agent_coming_soon": "開発中...", - "agent_skills_title": "Skills 管理", - "agent_skills_add": "Skill を追加", - "agent_skills_empty": "インストール済みの Skill はありません", - "agent_skills_tools": "ツール", - "agent_skills_references": "参考資料", - "agent_skills_detail": "Skill 詳細", - "agent_skills_edit_prompt": "プロンプト", - "agent_skills_install": "Skill をインストール", - "agent_skills_install_url": "URL からインポート", - "agent_skills_install_paste": "SKILL.md を貼り付け", - "agent_skills_uninstall": "アンインストール", - "agent_skills_uninstall_confirm": "Skill「{{name}}」をアンインストールしますか?", - "agent_skills_save_success": "保存しました", - "agent_skills_install_success": "インストールしました", - "agent_skills_fetch_failed": "取得に失敗しました", - "agent_skills_add_script": "スクリプトを追加", - "agent_skills_add_reference": "参考資料を追加", - "agent_skills_install_zip": "ZIP アップロード", - "agent_skills_install_zip_hint": "クリックして .zip ファイルを選択", - "agent_skills_prompt": "プロンプト", - "agent_skills_installed_at": "インストール日時", - "agent_skills_refresh": "更新", - "agent_skills_refresh_success": "更新しました", - "agent_skills_tool_code": "ツールコード", - "agent_skills_click_to_view_code": "ツール名をクリックしてコードを表示", - "agent_skills_config": "設定", - "agent_skills_config_saved": "設定を保存しました", - "agent_skills_check_updates": "更新を確認", - "agent_skills_no_updates": "すべてのSkillは最新です", - "agent_skills_updates_available": "件のSkillに更新があります", - "agent_skills_update": "更新", - "agent_skills_update_success": "更新しました", - "agent_skills_url_placeholder": "SKILL.cat.md URLを入力", - "agent_chat_new": "新しいチャット", - "agent_chat_delete": "チャットを削除", - "agent_chat_delete_confirm": "この会話を削除しますか?", - "agent_chat_no_conversations": "会話がありません", - "agent_chat_input_placeholder": "メッセージを入力...", - "agent_chat_send": "送信", - "agent_chat_stop": "停止", - "agent_chat_thinking": "思考中", - "agent_chat_tool_call": "ツール呼び出し", - "agent_chat_error": "エラーが発生しました", - "agent_chat_no_model": "モデルが設定されていません。先にモデルサービスで追加してください。", - "agent_chat_model_select": "モデルを選択", - "agent_chat_rename": "名前を変更", - "agent_chat_copy": "コピー", - "agent_chat_copy_success": "コピーしました", - "agent_chat_regenerate": "再生成", - "agent_chat_streaming": "生成中...", - "agent_chat_newline": "改行", - "agent_chat_welcome_hint": "スクリプトに関する質問は何でもどうぞ", - "agent_chat_welcome_start": "会話を作成して始めましょう", - "agent_chat_tokens": "トークン", - "agent_chat_first_token": "TTFT", - "agent_chat_tools_count": "{{count}} ツール呼び出し", - "agent_chat_tools_enabled": "ツール有効", - "agent_chat_tools_disabled": "ツール無効", - "agent_chat_tools_enabled_tip": "ツール有効 — クリックで無効化", - "agent_chat_tools_disabled_tip": "ツール無効 — クリックで有効化", - "agent_chat_background_enabled_tip": "バックグラウンドモード ON — ページを閉じても会話は継続します。クリックで無効化", - "agent_chat_background_disabled_tip": "バックグラウンドモード OFF — ページを閉じると会話は停止します。クリックで有効化", - "agent_chat_delete_round": "削除", - "agent_chat_copy_message": "コピー", - "agent_chat_edit_message": "編集", - "agent_chat_save_and_send": "保存して送信", - "agent_chat_cancel_edit": "キャンセル", - "agent_chat_message_queued": "キュー中", - "agent_chat_cancel_message": "送信をキャンセル", - "agent_permission_title": "スクリプトが Agent 会話の使用をリクエストしています", - "agent_permission_describe": "このスクリプトは Agent 会話機能の使用をリクエストしており、API トークンを消費します。信頼できるスクリプトにのみ許可してください。", - "agent_permission_content": "Agent 会話", - "agent_opfs": "OPFS", - "agent_opfs_title": "OPFS ファイルブラウザ", - "agent_opfs_empty": "空のディレクトリ", - "agent_opfs_name": "名前", - "agent_opfs_size": "サイズ", - "agent_opfs_type": "種類", - "agent_opfs_modified": "最終更新", - "agent_opfs_delete_confirm": "削除してよろしいですか?", - "agent_opfs_delete_success": "削除しました", - "agent_opfs_file": "ファイル", - "agent_opfs_directory": "ディレクトリ", - "agent_opfs_preview": "プレビュー", - "agent_opfs_root": "ルート", - "agent_dom_permission_title": "スクリプトが DOM 操作アクセスをリクエストしています", - "agent_dom_permission_describe": "このスクリプトはウェブページの DOM を読み取り操作する能力(クリック、フォーム入力、ナビゲーション、スクリーンショットなど)をリクエストしています。信頼できるスクリプトにのみ許可してください。", - "agent_dom_permission_content": "Agent DOM 操作", - "agent_mcp_title": "MCPサーバー", - "agent_mcp_add_server": "サーバーを追加", - "agent_mcp_no_servers": "MCPサーバーが設定されていません", - "agent_mcp_test_connection": "テスト", - "agent_mcp_name_url_required": "名前とURLは必須です", - "agent_mcp_optional": "任意", - "agent_mcp_custom_headers": "カスタムヘッダー", - "agent_mcp_enabled": "有効", - "agent_mcp_detail": "詳細", - "agent_mcp_tools": "ツール", - "agent_mcp_resources": "リソース", - "agent_mcp_prompts": "プロンプト", - "agent_mcp_no_tools": "利用可能なツールはありません", - "agent_mcp_no_resources": "利用可能なリソースはありません", - "agent_mcp_no_prompts": "利用可能なプロンプトはありません", - "agent_mcp_loading": "読み込み中...", - "agent_mcp_parameters": "パラメータ", - "agent_settings": "設定", - "agent_settings_title": "Agent 設定", - "agent_model_settings": "モデル設定", - "agent_summary_model": "要約モデル", - "agent_summary_model_desc": "ウェブ要約などに使用。未設定の場合はデフォルトモデルを使用", - "agent_summary_model_placeholder": "デフォルトモデルを使用", - "agent_search_settings": "検索設定", - "agent_search_engine": "検索エンジン", - "agent_search_engine_baidu": "Baidu", - "agent_search_google_api_key": "Google API Key", - "agent_search_google_cse_id": "カスタム検索エンジン ID", - "agent_settings_saved": "設定を保存しました", - "agent_settings_save_failed": "設定の保存に失敗しました", - "agent_search_engine_tip_bing": "デフォルトの検索エンジン、グローバルに広くカバー、追加設定不要。", - "agent_search_engine_tip_duckduckgo": "プライバシー重視の検索エンジン、API キー不要。", - "agent_search_engine_tip_baidu": "中国語コンテンツに最適化、中国語の検索結果がより良好。", - "agent_search_engine_tip_google": "高品質な検索結果、Google API Key とカスタム検索エンジン ID の設定が必要。" -} \ No newline at end of file diff --git a/src/locales/locales.test.ts b/src/locales/locales.test.ts index 1ac16cf16..87f10f90e 100644 --- a/src/locales/locales.test.ts +++ b/src/locales/locales.test.ts @@ -1,3 +1,4 @@ +// can be tested with vitest-environment node import { describe, it, expect } from "vitest"; import { i18nName, i18nDescription } from "./locales"; import type { SCMetadata } from "@App/app/repo/scripts"; diff --git a/src/locales/locales.ts b/src/locales/locales.ts index 57d2359ed..39cd8beaf 100644 --- a/src/locales/locales.ts +++ b/src/locales/locales.ts @@ -5,13 +5,13 @@ import { initReactI18next } from "react-i18next"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import type { SCMetadata } from "@App/app/repo/scripts"; -import enUS from "./en-US/translation.json"; -import viVN from "./vi-VN/translation.json"; -import zhCN from "./zh-CN/translation.json"; -import zhTW from "./zh-TW/translation.json"; -import jaJP from "./ja-JP/translation.json"; -import deDE from "./de-DE/translation.json"; -import ruRU from "./ru-RU/translation.json"; +import * as enUS from "./en-US"; +import * as zhCN from "./zh-CN"; +import * as zhTW from "./zh-TW"; +import * as jaJP from "./ja-JP"; +import * as deDE from "./de-DE"; +import * as viVN from "./vi-VN"; +import * as ruRU from "./ru-RU"; import "dayjs/locale/en"; import "dayjs/locale/vi"; import "dayjs/locale/zh-cn"; @@ -36,21 +36,37 @@ export const initLocalesPromise = new Promise((resolve) => { initLocalesResolve = resolve; }); +const NS = [ + "common", + "popup", + "script", + "editor", + "settings", + "install", + "agent", + "logs", + "guide", + "tools", + "permission", +] as const; + export function initLanguage(lng: string = "en-US"): void { i18n.use(initReactI18next).init({ fallbackLng: "en-US", - lng: lng, // 优先使用localStorage中的语言设置 + lng: lng, + ns: [...NS], + defaultNS: "common", interpolation: { - escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape + escapeValue: false, }, resources: { - "en-US": { title: "English", translation: enUS }, - "zh-CN": { title: "简体中文", translation: zhCN }, - "zh-TW": { title: "繁体中文", translation: zhTW }, - "ja-JP": { title: "日本語", translation: jaJP }, - "de-DE": { title: "Deutsch", translation: deDE }, - "vi-VN": { title: "Tiếng Việt", translation: viVN }, - "ru-RU": { title: "Русский", translation: ruRU }, + "en-US": { title: "English", ...enUS }, + "zh-CN": { title: "简体中文", ...zhCN }, + "zh-TW": { title: "繁体中文", ...zhTW }, + "ja-JP": { title: "日本語", ...jaJP }, + "de-DE": { title: "Deutsch", ...deDE }, + "vi-VN": { title: "Tiếng Việt", ...viVN }, + "ru-RU": { title: "Русский", ...ruRU }, }, }); @@ -138,7 +154,7 @@ export function matchLanguage(): Promise { // 遍历数组寻找匹配语言 for (let i = 0; i < acceptLanguages.length; i += 1) { const lng = acceptLanguages[i]; - if (i18n.hasResourceBundle(lng, "translation")) { + if (i18n.hasResourceBundle(lng, "common")) { return lng; } } diff --git a/src/locales/ru-RU/agent.json b/src/locales/ru-RU/agent.json new file mode 100644 index 000000000..590470e0f --- /dev/null +++ b/src/locales/ru-RU/agent.json @@ -0,0 +1,252 @@ +{ + "title": "AI Agent", + "docs": "Документация", + "chat": "Chat", + "provider": "Model Service", + "mcp": "MCP", + "skills": "Skills", + "provider_title": "Model Service", + "provider_subtitle": "Manage AI model providers and connections", + "provider_select": "AI Provider", + "provider_api_base_url": "API Base URL", + "provider_api_key": "API Key", + "provider_model": "Default Model", + "provider_test_connection": "Test Connection", + "provider_test_success": "Connection Successful", + "provider_test_failed": "Connection Failed", + "provider_test_hint": "Тест отправляет минимальный запрос для проверки конфигурации", + "provider_docs": "Документация", + "provider_count_hint": "Модель по умолчанию используется для новых бесед", + "model_count": "{{count}} моделей", + "model_fetch": "Получить модели", + "model_fetch_failed": "Не удалось получить список моделей", + "model_name": "Name", + "model_add": "Add Model", + "model_edit": "Edit", + "model_copy": "Copy", + "model_delete": "Delete", + "model_set_default": "Set as Default", + "model_default_label": "Default", + "model_delete_confirm": "Are you sure to delete this model?", + "model_max_tokens": "Max Output Tokens", + "model_context_window": "Контекстное окно", + "model_no_models": "No models configured", + "model_no_models_desc": "Add your first model to start using the AI Agent", + "model_vision_support": "Supports vision input", + "model_image_output": "Supports image output", + "model_capabilities": "Возможности", + "model_supports_vision": "Ввод изображений", + "model_supports_image_output": "Вывод изображений", + "coming_soon": "Coming soon...", + "skills_title": "Skills Management", + "skills_subtitle": "Manage agent skill packages (SKILL.md prompts, scripts, references)", + "skills_add": "Add Skill", + "skills_empty": "No skills installed", + "skills_empty_desc": "Upload a ZIP or import your first skill from a URL", + "skills_tools": "Tools", + "skills_references": "References", + "skills_detail": "Skill Details", + "skills_edit_prompt": "Prompt", + "skills_install": "Install Skill", + "skills_install_url": "Import from URL", + "skills_install_paste": "Paste SKILL.md", + "skills_uninstall": "Uninstall", + "skills_uninstall_confirm": "Are you sure to uninstall Skill \"{{name}}\"?", + "skills_save_success": "Saved successfully", + "skills_install_success": "Installed successfully", + "skills_fetch_failed": "Fetch failed", + "skills_add_script": "Add Script", + "skills_add_reference": "Add Reference", + "skills_install_zip": "Upload ZIP", + "skills_install_zip_hint": "Click to select a .zip file", + "skills_prompt": "Prompt", + "skills_installed_at": "Installed at", + "skills_refresh": "Refresh", + "skills_refresh_success": "Refreshed successfully", + "skills_tool_code": "Tool Code", + "skills_click_to_view_code": "Click tool name to view code", + "skills_config": "Config", + "skills_config_saved": "Config saved", + "skills_check_updates": "Check Updates", + "skills_no_updates": "All skills are up to date", + "skills_updates_available": "update(s) available", + "skills_update": "Update", + "skills_docs": "Документация", + "skills_count": "Установлено навыков: {{count}}", + "skills_update_count": "Доступно обновлений: {{count}}", + "skills_update_available": "Обновить {{version}}", + "skills_references_short": "Справка", + "skills_configurable": "Настраиваемый", + "skills_open_config": "Открыть настройки", + "skills_update_success": "Updated successfully", + "skills_url_placeholder": "Enter SKILL.cat.md URL", + "chat_new": "New Chat", + "chat_delete": "Delete Chat", + "chat_delete_confirm": "Delete this conversation?", + "chat_no_conversations": "No conversations", + "chat_search_placeholder": "Поиск бесед…", + "chat_search_no_results": "Нет подходящих бесед", + "chat_export": "Экспорт", + "chat_input_placeholder": "Type a message...", + "chat_send": "Send", + "chat_stop": "Stop", + "chat_thinking": "Thinking", + "chat_tool_call": "Tool Call", + "chat_tool_arguments": "Аргументы", + "chat_tool_result": "Результат", + "chat_error": "Error occurred", + "chat_no_model": "No model configured. Please add one in Model Service first.", + "chat_model_select": "Select Model", + "chat_rename": "Rename", + "chat_copy": "Copy", + "chat_copy_success": "Copied", + "chat_regenerate": "Regenerate", + "chat_streaming": "Generating...", + "chat_retrying": "Повтор ({{attempt}}/{{max}})...", + "chat_attach_image": "Прикрепить изображение", + "chat_attach_file": "Прикрепить файл", + "chat_welcome_hint": "Ask me anything about your scripts", + "chat_welcome_start": "Create a conversation to get started", + "chat_tokens": "tokens", + "chat_first_token": "TTFT", + "chat_tools_count": "{{count}} tools", + "chat_tools_enabled": "Tools enabled", + "chat_tools_disabled": "Tools disabled", + "chat_tools_enabled_tip": "Tools enabled — click to disable", + "chat_tools_disabled_tip": "Tools disabled — click to enable", + "chat_background_enabled_tip": "Фоновый режим ВКЛ — диалог продолжит работу после закрытия страницы, нажмите для отключения", + "chat_background_disabled_tip": "Фоновый режим ВЫКЛ — диалог остановится при закрытии страницы, нажмите для включения", + "chat_delete_round": "Delete", + "chat_copy_message": "Copy", + "chat_edit_message": "Редактировать", + "chat_save_and_send": "Сохранить и отправить", + "chat_cancel_edit": "Отмена", + "chat_message_queued": "В очереди", + "chat_cancel_message": "Отменить отправку", + "permission_title": "Скрипт запрашивает использование Agent разговора", + "permission_describe": "Этот скрипт запрашивает доступ к функции Agent разговора, что будет потреблять API токены. Разрешайте только доверенным скриптам.", + "permission_content": "Agent разговор", + "opfs": "OPFS", + "opfs_title": "Файловый браузер OPFS", + "opfs_empty": "Пустая директория", + "opfs_name": "Имя", + "opfs_size": "Размер", + "opfs_type": "Тип", + "opfs_modified": "Последнее изменение", + "opfs_delete_confirm": "Вы уверены, что хотите удалить?", + "opfs_delete_success": "Удалено", + "opfs_file": "Файл", + "opfs_directory": "Директория", + "opfs_preview": "Предпросмотр", + "opfs_subtitle": "Origin Private File System · Приватное хранилище агента", + "opfs_refresh": "Обновить", + "opfs_upload": "Загрузить", + "opfs_actions": "Действия", + "opfs_item_count": "{{count}} элементов", + "opfs_empty_desc": "В этом каталоге пока нет файлов", + "opfs_upload_success": "Загружено", + "opfs_upload_failed": "Не удалось загрузить", + "opfs_type_directory": "Папка", + "opfs_type_image": "Изображение", + "opfs_type_text": "Текст", + "opfs_type_binary": "Двоичный", + "opfs_root": "Корень", + "dom_permission_title": "Скрипт запрашивает доступ к операциям DOM", + "dom_permission_describe": "Этот скрипт запрашивает возможность читать и управлять DOM веб-страницы (клик, заполнение форм, навигация, скриншот и т.д.). Разрешайте только доверенным скриптам.", + "dom_permission_content": "Agent DOM операции", + "mcp_title": "MCP серверы", + "mcp_subtitle": "Connect MCP servers to extend agent tools, resources and prompts", + "mcp_add_server": "Добавить сервер", + "mcp_no_servers": "MCP серверы не настроены", + "mcp_no_servers_desc": "Add your first MCP server to connect external tools and data", + "mcp_status_connected": "Connected", + "mcp_status_failed": "Connection failed", + "mcp_status_untested": "Not tested", + "mcp_has_key": "Секретный ключ", + "mcp_headers_count": "{{count}} заголовков", + "mcp_count_servers": "{{count}} серверов", + "mcp_count_connected": "{{count}} подключено", + "mcp_count_tools": "Доступно инструментов: {{count}}", + "mcp_docs": "Документация", + "mcp_test_connection": "Тест", + "mcp_name_url_required": "Имя и URL обязательны", + "mcp_optional": "необязательно", + "mcp_custom_headers": "Пользовательские заголовки", + "mcp_enabled": "Включено", + "mcp_detail": "Подробности", + "mcp_tools": "Инструменты", + "mcp_resources": "Ресурсы", + "mcp_prompts": "Промпты", + "mcp_no_tools": "Нет доступных инструментов", + "mcp_no_resources": "Нет доступных ресурсов", + "mcp_no_prompts": "Нет доступных промптов", + "mcp_loading": "Загрузка...", + "mcp_parameters": "Параметры", + "tasks": "Задачи", + "tasks_title": "Управление задачами", + "tasks_subtitle": "Автоматически запускать задачи Agent по расписанию cron", + "settings": "Настройки", + "settings_title": "Настройки Agent", + "settings_subtitle": "Модель, поиск и общие настройки · изменения применяются сразу", + "settings_cat_model": "Модель", + "settings_cat_search": "Поиск", + "model_settings": "Настройки модели", + "summary_model": "Модель для резюме", + "summary_model_desc": "Используется для суммаризации веб-страниц. При отсутствии используется модель по умолчанию", + "summary_model_placeholder": "Использовать модель по умолчанию", + "search_settings": "Настройки поиска", + "search_engine": "Поисковая система", + "search_engine_desc": "Источник поиска, используемый инструментом web_search.", + "search_engine_baidu": "Baidu", + "search_google_api_key": "Google API Key", + "search_google_api_key_desc": "Используется для вызова Custom Search JSON API.", + "search_google_cse_id": "ID пользовательской поисковой системы", + "search_google_cse_id_desc": "Параметр cx для Programmable Search Engine.", + "settings_saved": "Настройки сохранены", + "settings_save_failed": "Не удалось сохранить настройки", + "search_engine_tip_bing": "Поисковая система по умолчанию с широким глобальным охватом, дополнительная настройка не требуется.", + "search_engine_tip_duckduckgo": "Поисковая система с акцентом на конфиденциальность, API-ключ не требуется.", + "search_engine_tip_baidu": "Оптимизирован для китайского контента, лучшие результаты для запросов на китайском языке.", + "search_engine_tip_google": "Высококачественные результаты поиска, требуется Google API Key и ID пользовательской поисковой системы.", + "tasks_docs": "Документация", + "tasks_count_total": "{{count}} задач", + "tasks_count_enabled": "{{count}} включено", + "tasks_create": "Создать задачу", + "tasks_edit": "Изменить задачу", + "tasks_mode": "Режим", + "tasks_mode_internal": "Внутреннее выполнение", + "tasks_mode_event": "По событию", + "tasks_mode_internal_short": "Внутренний", + "tasks_mode_event_short": "Событие", + "tasks_cron": "Выражение cron", + "tasks_next_run": "Следующий запуск", + "tasks_last_status": "Последний статус", + "tasks_run_now": "Запустить сейчас", + "tasks_history": "История", + "tasks_prompt": "Промпт", + "tasks_max_iterations": "Максимум итераций", + "tasks_notify": "Уведомлять о завершении", + "tasks_notify_desc": "Отправлять уведомление браузера после завершения задачи", + "tasks_no_tasks": "Нет задач по расписанию", + "tasks_no_tasks_desc": "Создайте первую задачу, чтобы Agent запускался автоматически", + "tasks_no_runs": "История выполнения пуста", + "tasks_delete_confirm": "Вы уверены, что хотите удалить эту задачу?", + "tasks_clear_runs": "Очистить историю", + "tasks_clear_runs_confirm": "Вы уверены, что хотите очистить историю выполнения?", + "tasks_event_hint": "При срабатывании скрипт, создавший эту задачу, получит уведомление", + "tasks_event_trigger": "Триггер события", + "tasks_name_cron_required": "Имя и выражение cron обязательны", + "tasks_model_select": "Выбрать модель", + "tasks_skills": "Skills", + "tasks_skills_auto": "Загружать все автоматически", + "tasks_conversation_id": "ID продолжаемого диалога (необязательно)", + "tasks_run_status_success": "Успешно", + "tasks_run_status_error": "Ошибка", + "tasks_run_status_running": "Выполняется", + "tasks_run_duration": "Длительность", + "tasks_run_usage": "Использование", + "tasks_run_conversation": "Открыть диалог", + "tasks_run_time": "Время", + "tasks_run_status": "Статус", + "tasks_never_run": "Не запускалась" +} diff --git a/src/locales/ru-RU/common.json b/src/locales/ru-RU/common.json new file mode 100644 index 000000000..6816a3269 --- /dev/null +++ b/src/locales/ru-RU/common.json @@ -0,0 +1,131 @@ +{ + "user_guide": "Руководство пользователя", + "api_docs": "Документация API", + "development_guide": "Руководство разработчика", + "script_gallery": "Галерея скриптов", + "community_forum": "Форум сообщества", + "external_links": "Внешние ссылки", + "system_follow": "Следовать системе", + "no_data": "Нет данных", + "logs": "Журналы", + "tools": "Инструменты", + "find": "Найти", + "replace": "Заменить", + "settings": "Настройки", + "change_theme": "Change Theme", + "hide_main_sidebar": "Свернуть боковую панель", + "show_main_sidebar": "Развернуть боковую панель", + "menu": "Меню", + "guide": "Руководство новичка", + "helpcenter": "Справочный центр", + "save": "Сохранить", + "file": "Файл", + "save_success": "Сохранено успешно", + "reset_success": "Сброс выполнен успешно", + "update": "Обновить", + "check_update": "Проверить обновления", + "confirm_delete": "Подтвердить удаление", + "confirm_update": "Подтвердить обновление", + "delete_success": "Успешно удалено", + "deleting": "Удаление", + "enable": "Включить", + "script_list_enable_width": 120, + "script_list_last_updated_width": 120, + "script_list_apply_to_run_status_width": 140, + "subscribe_list_enable_width": 100, + "disable": "Выключить", + "name": "Название", + "version": "Версия", + "source": "Источник", + "home": "Главная", + "action": "Действие", + "export": "Экспорт", + "delete": "Удалить", + "pin_to_top": "Закрепить сверху", + "confirm": "Подтвердить", + "close": "Закрыть", + "config": "Конфигурация", + "key": "ключ", + "value": "значение", + "add": "Добавить", + "type": "Тип", + "size": "Размер", + "download": "Скачать", + "edit_value": "Редактировать значение", + "add_value": "Добавить значение", + "update_success": "Изменение успешно", + "add_success": "Добавление успешно", + "clear": "Очистить", + "type_string": "строка", + "type_number": "число", + "type_boolean": "логический", + "type_object": "объект", + "confirm_delete_resource": "Вы уверены, что хотите удалить этот ресурс? При следующем запуске он будет перезагружен", + "confirm_clear_resource": "Вы действительно хотите очистить эти ресурсы? При следующем запуске они будут перезагружены", + "yes": "Да", + "no": "Нет", + "confirm_delete_permission": "Вы уверены, что хотите удалить это разрешение?", + "reset": "Сброс", + "run_once": "Запустить один раз", + "stop": "Остановить", + "edit": "Редактировать", + "copy": "Копировать", + "exclude_on": "Восстановить в $0 выполнении", + "exclude_off": "Исключить в $0 выполнении", + "get_confirm_error": "Ошибка получения информации подтверждения", + "confirm_error": "Ошибка подтверждения", + "ignore": "Игнорировать", + "temporary_allow": "Временно разрешить это {{permissionContent}}", + "temporary_allow_all": "Временно разрешить все {{permissionContent}}", + "permanent_allow": "Постоянно разрешить это {{permissionContent}}", + "permanent_allow_all": "Постоянно разрешить все {{permissionContent}}", + "temporary_deny": "Временно отклонить это {{permissionContent}}", + "temporary_deny_all": "Временно отклонить все {{permissionContent}}", + "permanent_deny": "Постоянно отклонить это {{permissionContent}}", + "permanent_deny_all": "Постоянно отклонить все {{permissionContent}}", + "import": "Импорт", + "author": "Автор", + "description": "Описание", + "operation": "Операция", + "error": "Ошибка", + "unknown": "Неизвестно", + "add_new": "Добавить новый", + "no_operation": "Без операций", + "enable_script": "Включить скрипт", + "local_creation": "Локальное создание", + "import_success": "Импорт успешен", + "get_script": "Получить скрипт", + "report_issue": "Обратная связь по багам/проблемам", + "project_docs": "Документация проекта", + "community": "Сообщество", + "domain": "Домен", + "script_name": "Название скрипта", + "skip": "Пропустить", + "next": "Далее", + "next_with_progress": "Следующий шаг (шаг {step} из {steps})", + "back": "Назад", + "last": "Завершить", + "auto": "Автоматически", + "hide": "Скрыть", + "custom": "Пользовательский", + "resize_column_width": "Изменить ширину столбца", + "collapse": "Свернуть", + "expand": "Развернуть", + "import_script_placeholder": "Поддерживается ввод абсолютных ссылок скриптов, заканчивающихся на .user.js, или ссылок страницы установки ScriptCat\nМожно заполнять в несколько строк, по одной на строку\nПример:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/zh-CN/script-show-page/1234", + "light": "Светлый режим", + "dark": "Темный режим", + "enter_search_value": "Введите {{search}} для поиска", + "no_message_content": "Нет содержимого сообщения", + "loading": "Загрузка...", + "auth_type": "Тип аутентификации", + "url": "URL", + "username": "Имя пользователя", + "password": "Пароль", + "access_token_bearer": "Токен доступа (Bearer)", + "s3_bucket_name": "Имя корзины", + "s3_region": "Регион", + "s3_access_key_id": "Идентификатор ключа доступа", + "s3_secret_access_key": "Секретный ключ доступа", + "s3_custom_endpoint": "Пользовательская конечная точка (необязательно)", + "cancel": "Отмена" +} diff --git a/src/locales/ru-RU/editor.json b/src/locales/ru-RU/editor.json new file mode 100644 index 000000000..234235bb5 --- /dev/null +++ b/src/locales/ru-RU/editor.json @@ -0,0 +1,132 @@ +{ + "save": "Сохранить", + "save_success": "Успешно сохранено", + "copy": "Копировать", + "find": "Найти", + "replace": "Заменить", + "select_all": "Выделить всё", + "format": "Форматировать", + "back": "Назад", + "more": "Ещё", + "file": "Файл", + "edit": "Правка", + "settings": "Настройки", + "run_settings": "Настройки запуска", + "code": "Код", + "script_setting": "Настройки скрипта", + "storage": "Хранилище", + "resource": "Ресурсы", + "search_resource": "Поиск ресурсов", + "search_storage": "Поиск ключа", + "record_count": "Записей: {{count}}", + "resource_count": "Ресурсов: {{count}} · {{size}}", + "script_list": "Список скриптов", + "search_scripts": "Поиск скриптов...", + "new_script": "Новый скрипт", + "source": "Источник", + "from_user": "Пользователь", + "from_script": "Скрипт", + "last_updated": "Последнее обновление", + "run_at": "Время запуска", + "run_in": "Среда запуска", + "check_update": "Проверить обновления", + "line_col": "Стр {{line}}, Кол {{col}}", + "script_not_found": "Скрипт не найден", + "confirm_delete_script": "Удалить скрипт «{{name}}»?", + "delete_success": "Успешно удалено", + "delete_failed": "Не удалось удалить", + "cancel": "Отмена", + "confirm": "Подтвердить", + "save_as": "Сохранить как", + "run": "Запустить", + "debug": "Отладка", + "script_storage": "Хранилище скрипта", + "enter_key": "Введите ключ", + "key_placeholder": "ключ", + "value_placeholder": "Когда тип - object, введите данные, которые можно парсить JSON", + "clear_success": "Очистка успешна", + "confirm_clear": "Вы действительно хотите очистить это хранилище?", + "script_resource": "Ресурсы скрипта", + "basic_info": "Основная информация", + "update_url": "URL обновления", + "add_permission": "Добавить разрешение", + "match": "Совпадение", + "user_setting": "Пользовательские настройки", + "confirm_delete_exclude": "Подтвердить удаление этого исключения?", + "after_deleting_match_item": "После удаления элемента совпадения, установленного скриптом, он автоматически добавится в список совпадений", + "confirm_delete_match": "Подтвердить удаление этого совпадения?", + "after_deleting_exclude_item": "После удаления элемента совпадения, установленного скриптом, он автоматически добавится в список исключений", + "add_match": "Добавить совпадение", + "add_exclude": "Добавить исключение", + "website_match": "Совпадение сайта (@match)", + "website_exclude": "Исключение сайта (@exclude)", + "confirm_reset": "Подтвердить сброс?", + "undo": "Отменить", + "redo": "Повторить", + "cut": "Вырезать", + "paste": "Вставить", + "user_config": "Конфигурация пользователя", + "gm_api": "GM API", + "storage_api": "Storage API", + "use_file_system": "Используемая файловая система", + "open_directory": "Открыть папку", + "account_validation_failed": "Ошибка проверки информации об аккаунте", + "not_set": "Не установлено", + "in_use": "Используется", + "storage_error": "Ошибка хранилища", + "upload_to_cloud": "Загрузить в облако", + "save_failed": "Ошибка сохранения", + "exporting": "Экспорт...", + "upload_to": "Загрузить в", + "value_export_expression": "Выражение экспорта значений", + "overwrite_original_value_on_import": "Перезаписать исходное значение при импорте", + "cookie_export_expression": "Выражение экспорта cookie", + "overwrite_original_cookie_on_import": "Перезаписать исходный cookie при импорте", + "restore_default_values": "Восстановить значения по умолчанию", + "edit_conflict": "Конфликт редактирования", + "confirm_override_when_edit_conflict": "Этот скрипт был изменён в другом экземпляре. Замена приведёт к перезаписи этих изменений. Хотите сохранить эту версию?", + "save_abort_when_edit_conflict": "Скрипт был изменён в другом экземпляре. Сохранение отменено.", + "scriptname_conflict": "Конфликт имени скрипта", + "confirm_save_when_scriptname_conflict": "Это имя скрипта уже используется другим скриптом. Вы всё равно хотите сохранить его?", + "save_abort_when_scriptname_conflict": "Это имя скрипта уже используется другим скриптом. Сохранение отменено.", + "eslint_config_format_error": "Ошибка формата конфигурации ESLint", + "script_modified_leave_confirm": "Изменения не сохранены. Если вы покинете страницу, они будут потеряны. Вы уверены, что хотите уйти?", + "create_success_note": "Создание успешно. Обратите внимание, что фоновые скрипты не включаются по умолчанию", + "save_as_failed": "Ошибка «Сохранить как»", + "save_as_success": "«Сохранить как» успешно", + "only_background_scheduled_can_run": "Только фоновые/запланированные скрипты могут выполняться", + "preparing_script_resources": "Подготовка ресурсов скрипта...", + "build_success_message": "Сборка успешна. Вы можете открыть инструменты разработчика на странице расширений и посмотреть вывод в консоли", + "script_storage_tooltip": "Можно управлять данными хранилища скрипта (GM_value)", + "script_resource_tooltip": "Управление ресурсами, загруженными через @resource, @require", + "script_setting_tooltip": "Настройка некоторых пользовательских параметров скрипта", + "script_modified_close_confirm": "Скрипт был изменен. Изменения будут потеряны после закрытия. Продолжить?", + "close_current_tab": "Закрыть текущую вкладку", + "close_other_tabs": "Закрыть другие вкладки", + "close_left_tabs": "Закрыть левые вкладки", + "close_right_tabs": "Закрыть правые вкладки", + "invalid_script_code": "Неверный код скрипта", + "build_failed": "Ошибка сборки", + "drag_script_here_to_upload": "Перетащите скрипт сюда для загрузки", + "watch_file_description": "Отслеживайте изменения файлов и автоматически обновляйте скрипты. При использовании убедитесь, что путь к файлу скрипта остаётся неизменным, а страница не может быть закрыта", + "watch_file": "Мониторинг файлов", + "stop_watch_file": "Остановить мониторинг", + "individual_edit": "Индивидуальное редактирование", + "batch_edit": "Пакетное редактирование", + "script_code": "Код скрипта", + "editor_config": "Конфигурация редактора", + "editor_config_description": "Вы можете настроить compilerOptions, ссылаясь на jsconfig.js", + "editor_type_definition": "Определения типов редактора", + "editor_type_definition_description": "Вы можете настроить собственные определения типов, которые редактор скриптов будет автоматически загружать", + "eslint_rules_reset": "Правила ESLint сброшены", + "eslint_rules_saved": "Правила ESLint сохранены", + "editor_config_reset": "Конфигурация редактора сброшена", + "editor_config_saved": "Конфигурация редактора сохранена", + "editor_config_format_error": "Ошибка формата конфигурации редактора", + "editor_type_definition_reset": "Определения типов редактора сброшены", + "editor_type_definition_saved": "Определения типов редактора сохранены", + "editor": { + "show_script_list": "Показать список скриптов", + "hide_script_list": "Скрыть список скриптов" + } +} diff --git a/src/locales/ru-RU/guide.json b/src/locales/ru-RU/guide.json new file mode 100644 index 000000000..13905dcdf --- /dev/null +++ b/src/locales/ru-RU/guide.json @@ -0,0 +1,25 @@ +{ + "start_title": "Добро пожаловать в расширение ScriptCat", + "start_content": "Далее мы познакомим вас с основными методами использования ScriptCat", + "installed_scripts": "Установленные вами скрипты будут отображаться здесь", + "script_list_title": "Магазин скриптов", + "script_list_content": "Скрипты можно устанавливать из магазина скриптов. ScriptCat поддерживает не только пользовательские скрипты, но и фоновые скрипты", + "script_list_enable_title": "Включение скрипта", + "script_list_enable_content": "Скрипты нужно включить для использования. Скрипты страниц включены по умолчанию при установке, фоновые скрипты выключены по умолчанию", + "script_list_apply_to_run_status_title": "Применить к и статус выполнения", + "script_list_apply_to_run_status_content": "Отображение статуса выполнения скрипта. Наведите курсор на тег, чтобы увидеть тип скрипта", + "script_list_sort_title": "Сортировка", + "script_list_sort_content": "Вы можете перетаскивать метки сценариев, чтобы сортировать их.", + "script_list_update_title": "Последнее обновление", + "script_list_update_content": "При нажатии на заголовок столбца «Последнее обновление» будет выполнена проверка наличия обновлений для скрипта.", + "script_list_action_title": "Действие", + "script_list_action_content": "Панель управления позволяет редактировать скрипты, управлять их выполнением и завершением (фоновые скрипты), а также настраивать параметры (см. UserConfig). Кнопки справа позволяют получить доступ к расширенной фильтрации и переключать режимы просмотра.", + "tools_title": "Часто используемые инструменты", + "tools_content": "В инструментах предоставлены функции резервного копирования и разработки", + "tools_backup_title": "Резервное копирование", + "tools_backup_content": "Резервное копирование может сохранять скрипты во избежание потери. Можно экспортировать файлы скриптов локально или загружать локальные файлы скриптов. Также можно создавать резервные копии в облаке для большего удобства.", + "setting_title": "Настройки", + "setting_content": "Настройки в основном включают язык, синхронизацию скриптов, частоту обновлений и другие часто используемые опции", + "setting_sync_title": "Обновление и синхронизация", + "setting_sync_content": "Функция синхронизации скриптов может удобно синхронизировать содержимое скриптов этого устройства в облако. Если у вас несколько устройств, отметьте опцию синхронизации удаления. Когда скрипт удаляется с этого устройства, соответствующий скрипт будет удален из облака, а также удален с других устройств." +} diff --git a/src/locales/ru-RU/index.ts b/src/locales/ru-RU/index.ts new file mode 100644 index 000000000..bacd6d096 --- /dev/null +++ b/src/locales/ru-RU/index.ts @@ -0,0 +1,11 @@ +export { default as agent } from "./agent.json"; +export { default as common } from "./common.json"; +export { default as editor } from "./editor.json"; +export { default as guide } from "./guide.json"; +export { default as install } from "./install.json"; +export { default as logs } from "./logs.json"; +export { default as permission } from "./permission.json"; +export { default as popup } from "./popup.json"; +export { default as script } from "./script.json"; +export { default as settings } from "./settings.json"; +export { default as tools } from "./tools.json"; diff --git a/src/locales/ru-RU/install.json b/src/locales/ru-RU/install.json new file mode 100644 index 000000000..15ff66a5b --- /dev/null +++ b/src/locales/ru-RU/install.json @@ -0,0 +1,207 @@ +{ + "data_import": "Импорт данных", + "select_scripts_to_import": "Выберите скрипты для импорта", + "select_all": "Выбрать все", + "script_import_progress": "Прогресс импорта скриптов", + "select_subscribes_to_import": "Выберите подписки для импорта", + "subscribe_import_progress": "Прогресс импорта подписок", + "script": "Установить", + "update_script": "Обновить", + "subscribe": "Установить подписку", + "update_subscribe": "Обновить подписку", + "update_script_no_close": "Обновить, не закрывая окно", + "script_no_close": "Установить, не закрывая окно", + "update_script_no_more_update": "Обновить, но больше не проверять обновления", + "close_update_script_no_more_update": "Закрыть и не проверять обновления", + "script_no_more_update": "Установить, но больше не проверять обновления", + "invalid_link": "Неверная ссылка", + "subscribe_install_label": "Эта подписка установит следующие скрипты", + "subscribe_scripts_title": "Эта подписка установит следующие скрипты", + "subscribe_scripts_empty": "Эта подписка пока не объявляет скриптов", + "script_runs_in": "Скрипт будет выполняться на следующих сайтах", + "script_has_full_access_to": "Скрипт получит полный доступ к следующим адресам", + "script_requires": "Скрипт ссылается на следующие внешние ресурсы", + "cookie_warning": "Обратите внимание, что этот скрипт запрашивает разрешения на операции с Cookie. Это опасное разрешение, пожалуйста, убедитесь в безопасности скрипта.", + "perm_card_title": "Этому скрипту будут предоставлены следующие разрешения", + "perm_card_hint": "Подтвердите перед установкой", + "perm_card_empty": "Этот скрипт не запрашивает особых разрешений", + "perm_match_label": "Запускается на", + "perm_match_summary": "Скрипт работает на этих сайтах и изменяет страницы", + "perm_connect_label": "Кросс-доменный доступ", + "perm_connect_summary": "Может отправлять запросы и читать данные следующих доменов", + "perm_grant_label": "Возможности GM", + "perm_grant_summary": "Может вызывать следующие GM API", + "perm_require_label": "Внешние ресурсы", + "perm_require_summary": "Загружает следующие сторонние скрипты и ресурсы", + "badge_background": "Фоновый", + "badge_scheduled": "По расписанию", + "enabled_label": "Включено", + "schedule_cron_label": "Задача по расписанию", + "schedule_next_run": "Следующий запуск", + "schedule_background_desc": "Запускается автоматически, пока браузер открыт", + "code_lines": "{{count}} строк", + "code_copy": "Копировать код", + "code_collapse": "Свернуть", + "code_expand": "Развернуть", + "loading_title": "Загрузка скрипта", + "loading_desc": "Загрузка и разбор содержимого скрипта из источника", + "error_retry": "Повторить", + "error_invalid_desc": "Отсутствует допустимый параметр источника установки, скрипт не может быть загружен.", + "context_install": "Установка скрипта", + "context_update": "Обновление скрипта", + "background_script": "Фоновый скрипт", + "scheduled_script": "Скрипт по расписанию", + "watching_status": "Отслеживание изменений файла; после сохранения будет переустановлен автоматически", + "watching_chip": "Отслеживание", + "watching_title": "Отслеживание изменений файла", + "watching_file_desc": "Сохранение {{file}} автоматически переустановит и синхронизирует скрипт", + "watching_last_sync": "Последняя синхронизация {{time}}", + "warning_title": "Убедитесь, что скрипт получен из доверенного источника", + "warning_risk_connect": "он может получить доступ ко всем доменам", + "warning_risk_antifeature": "он объявляет антифункции", + "warning_risk_join": " и ", + "warning_risk_tail": " — устанавливайте с осторожностью.", + "action_note_install": "Установка означает, что вы доверяете источнику и автору этого скрипта", + "action_note_update": "Обновление означает, что вы принимаете изменения кода и разрешений", + "action_note_subscribe": "Установка означает, что вы доверяете этой подписке и её автору", + "action_note_watching": "Отслеживание — сохранение файла обновляет автоматически; остановите для ручных действий", + "context_skill_install": "Установка навыка", + "context_skill_update": "Обновление навыка", + "skill_kind": "Навык ИИ", + "skill_prompt_title": "Промпт", + "skill_prompt_chip": "SKILL.md", + "skill_tools_title": "Инструменты", + "skill_config_title": "Настройки", + "skill_references_title": "Справочные материалы", + "skill_required": "Обязательно", + "skill_secret": "Секрет", + "skill_install": "Установить Skill", + "skill_update": "Обновить Skill", + "skill_warning": "Навык внедряет промпт в ИИ и предоставляет ему право вызывать следующие инструменты, возможности GM и настройки. Устанавливайте только из законных источников!", + "skill_warning_title": "Убедитесь, что этот навык получен из доверенного источника", + "skill_warning_desc": "После установки навык внедряет промпт в ИИ и предоставляет перечисленные инструменты (включая разрешения GM) и чтение/запись настроек — устанавливайте с осторожностью.", + "success": "Установка успешна", + "install": { + "update_success": "Обновление успешно" + }, + "failed": "Ошибка установки", + "subscribe_success": "Подписка успешна", + "subscribe_failed": "Ошибка подписки", + "current_version": "Текущая версия", + "update_version": "Версия обновления", + "updatepage": { + "title": "Пакетное обновление", + "main_header": "Проверка обновлений", + "last_check": "Последняя проверка {{time}}", + "status_checking_updates": "Проверка обновлений...", + "updates_available": "Доступно обновлений: {{count}}", + "ignored_count": "Игнорируется: {{count}}", + "selected_count": "Выбрано {{selected}} / {{total}}", + "update_selected": "Обновить выбранные ({{count}})", + "ignore_selected": "Игнорировать выбранные", + "update": "Обновить", + "ignore": "Игнорировать", + "restore": "Восстановить", + "restore_all": "Восстановить все", + "ignored_section": "Игнорируемые обновления", + "auto_close": "Автозакрытие через {{count}} с", + "col_script": "Скрипт", + "col_version": "Версия", + "col_change": "Изменения", + "col_source": "Источник", + "col_action": "Действия", + "enabled": "Включено", + "disabled": "Выключено", + "codechange_major": "Существенное изменение", + "codechange_noticeable": "Заметное изменение", + "codechange_tiny": "Незначительное изменение", + "tag_new_connect": "Новый @connect", + "empty_title": "Все скрипты обновлены", + "empty_desc": "Проверено скриптов: {{count}} · Нет доступных обновлений", + "similarity": "Сходство", + "new_connects": "Новые @connect", + "toast_found": "Найдено скриптов с обновлениями: {{count}}", + "toast_uptodate": "Все скрипты обновлены" + }, + "importpage": { + "title": "Импорт данных", + "context_review": "Импорт данных", + "context_importing": "Импорт", + "context_done": "Импорт завершён", + "selected_count": "Выбрано {{selected}} / {{total}}", + "unimportable_count": "{{count}} нельзя импортировать", + "count_scripts": "{{count}} скриптов", + "count_subscribes": "{{count}} подписок", + "col_script": "Скрипт", + "col_version": "Версия", + "col_source": "Источник", + "col_data": "Данные", + "col_status": "Статус", + "col_enabled": "Включён", + "op_add": "Новый", + "op_update": "Обновление", + "op_error": "Ошибка разбора", + "source_local": "Создан локально", + "data_values": "{{count}} записей", + "data_resources": "с ресурсами", + "enable_after_import": "Включить после импорта", + "row_error": "Файл повреждён, импорт невозможен", + "unknown_script": "Неизвестный скрипт", + "subscribe_section": "Подписки", + "trust_hint": "Восстанавливаются только выбранные элементы; импорт не отправляет никаких данных", + "import_selected": "Импортировать выбранное ({{count}})", + "importing_progress": "Восстановление резервной копии · {{done}} / {{total}} готово", + "importing_hint": "Не закрывайте эту страницу", + "importing_actionbar_hint": "Идёт восстановление скриптов и данных, не закрывайте эту страницу", + "importing_button": "Импорт…", + "cancel": "Отмена", + "status_pending": "В очереди", + "status_importing": "Импорт", + "status_done": "Импортирован", + "status_skipped": "Пропущен", + "done_title": "Импорт завершён", + "done_desc": "Выбранные скрипты и данные восстановлены", + "done_stat_scripts": "{{count}} скриптов", + "done_stat_subscribes": "{{count}} подписок", + "done_stat_values": "{{count}} записей данных", + "view_scripts": "Открыть список скриптов", + "loading_title": "Разбор файла резервной копии", + "loading_desc": "Чтение и проверка содержимого резервной копии", + "error_title": "Не удалось прочитать файл резервной копии", + "error_desc": "Файл резервной копии повреждён или ссылка на импорт устарела", + "invalid_desc": "Ссылка на импорт недействительна или устарела", + "retry": "Повторить", + "empty_title": "В этой резервной копии нечего импортировать", + "empty_desc": "Этот файл резервной копии не содержит скриптов или подписок" + }, + "downloading_status_text": "Загрузка. Получено {{bytes}}.", + "downloading_status_percent": "Загрузка. Получено {{bytes}} / {{total}} ({{percent}}%).", + "page_please_wait": "Пожалуйста, подождите", + "page_loading": "Загрузка страницы установки", + "page_load_failed": "Не удалось загрузить страницу установки", + "invalid_page": "Недействительная страница", + "background_script_tag": "Это фоновый скрипт", + "scheduled_script_tag": "Это запланированный скрипт", + "from_legitimate_sources_warning": "Пожалуйста, устанавливайте скрипты только из законных источников! Неизвестные скрипты могут нарушить вашу конфиденциальность или выполнить злонамеренные операции!", + "referral_link_title": "Реферальная ссылка", + "referral_link_description": "Этот скрипт изменяет или перенаправляет на реферальную ссылку автора", + "ads_title": "Содержит рекламу", + "ads_description": "Этот скрипт вставляет рекламу на посещаемые вами страницы", + "payment_title": "Платный скрипт", + "payment_description": "Этот скрипт требует оплаты для нормального использования", + "miner_title": "Майнинг", + "miner_description": "Этот скрипт содержит функции майнинга", + "membership_title": "Функции членства", + "membership_description": "Этот скрипт требует регистрации членства для нормального использования", + "tracking_title": "Отслеживание информации", + "tracking_description": "Этот скрипт отслеживает информацию о пользователе", + "script_info_load_failed": "Ошибка загрузки информации о скрипте!", + "script_import_result": "Результат импорта скрипта", + "failure_info": "Информация об ошибке", + "source": "Источник установки", + "skill_prompt": "Промпт", + "skill_tools": "Инструменты", + "skill_config": "Конфигурация", + "skill_references": "Справочные материалы", + "skill_install_failed": "Ошибка установки Skill" +} diff --git a/src/locales/ru-RU/logs.json b/src/locales/ru-RU/logs.json new file mode 100644 index 000000000..ba1618654 --- /dev/null +++ b/src/locales/ru-RU/logs.json @@ -0,0 +1,59 @@ +{ + "log_title": "Журнал выполнения", + "last_5_minutes": "Последние 5 минут", + "last_15_minutes": "Последние 15 минут", + "last_30_minutes": "Последние 30 минут", + "last_1_hour": "Последний 1 час", + "last_3_hours": "Последние 3 часа", + "last_6_hours": "Последние 6 часов", + "last_12_hours": "Последние 12 часов", + "last_24_hours": "Последние 24 часа", + "last_7_days": "Последние 7 дней", + "query": "Запрос", + "labels": "Метки", + "search_regex": "Поиск (поддерживает регулярные выражения)", + "clean_schedule": "Расписание очистки", + "days_ago_logs": "дней назад", + "delete_completed": "Удаление завершено", + "delete_current_logs": "Удалить текущие журналы", + "clear_completed": "Очистка завершена", + "clear_logs": "Очистить журналы", + "now": "Сейчас", + "total_logs": "Найдено {{length}} записей журнала", + "filtered_logs": "После фильтрации {{length}} записей журнала", + "enter_filter_conditions": "Введите условия фильтрации для поиска", + "last_updated": "Последнее обновление", + "runtime": "Время выполнения", + "advanced": "Расширенные", + "label_filter": "Фильтр меток", + "add_label": "Добавить метку", + "custom_range": "Произвольный", + "back_to_top": "Наверх", + "refresh": "Обновить", + "all_levels": "Все", + "total_count": "Всего {{count}}", + "filtered_count": "Отфильтровано {{count}}", + "clear_logs_confirm": "Очистить все журналы? Это действие необратимо.", + "no_logs": "Нет журналов", + "refresh_off": "Выкл", + "interval_5s": "5 с", + "interval_10s": "10 с", + "interval_30s": "30 с", + "interval_1m": "1 мин", + "interval_5m": "5 мин", + "quick_range": "Быстрые диапазоны", + "absolute_range": "Абсолютный диапазон", + "from_start": "От (начало)", + "to_end": "До (конец)", + "apply_range": "Применить диапазон", + "auto_refresh_hint": "Когда конец установлен на «Сейчас», автообновление продолжает загружать свежие журналы", + "group_minutes": "Минуты", + "group_hours": "Часы", + "group_days": "Дни", + "weekdays_short": "Вс,Пн,Вт,Ср,Чт,Пт,Сб", + "year_month": "{{month}}.{{year}}", + "time": "Время", + "prev_month": "Предыдущий месяц", + "next_month": "Следующий месяц", + "live": "В реальном времени" +} diff --git a/src/locales/ru-RU/permission.json b/src/locales/ru-RU/permission.json new file mode 100644 index 000000000..9812cd431 --- /dev/null +++ b/src/locales/ru-RU/permission.json @@ -0,0 +1,42 @@ +{ + "permission": "Разрешения", + "permission_value": "Значение разрешения", + "allow": "Разрешить", + "permission_management": "Управление разрешениями", + "permission_cors": "Кросс-домен (cors)", + "permission_cookie": "Управление cookie", + "allow_once": "Разрешить один раз", + "deny_once": "Отклонить один раз", + "script_accessing_cross_origin_resource": "Скрипт пытается получить доступ к кросс-доменному ресурсу", + "confirm_operation_description": "Пожалуйста, подтвердите, разрешаете ли вы скрипту выполнить эту операцию. Скрипт также может добавить тег @connect, чтобы пропустить эту опцию", + "request_domain": "Запрашиваемый домен", + "request_url": "URL запроса", + "access_cookie_content": "Скрипт пытается получить доступ к содержимому cookie сайта", + "confirm_script_operation": "Пожалуйста, подтвердите, разрешаете ли вы скрипту выполнить эту операцию. Cookie являются важными пользовательскими данными, обязательно предоставляйте разрешения только доверенным скриптам.", + "cookie_domain": "Домен Cookie", + "script_operation_title": "Скрипт пытается работать с пространством синхронизации скриптов", + "script_operation_description": "Пожалуйста, подтвердите, разрешаете ли вы скрипту выполнить эту операцию. После разрешения скрипт сможет работать с настроенным вами пространством хранения. Скрипт создаст папку app/${dir} в пространстве хранения для использования", + "script_permission_content": "скрипт", + "extension_site_access_title": "ScriptCat требуется доступ к сайту", + "extension_site_access_description": "Разрешите браузеру доступ к сайту для этого источника, чтобы ScriptCat мог выполнить запрос. Это изменит настройку доступа к сайтам для расширения.", + "extension_site_access_content": "Сайт", + "request_permission": "Запрос разрешения", + "allow_user_script_guide": "«Разрешить пользовательские скрипты» сейчас отключён, поэтому скрипты не могут работать корректно. 👉Нажмите, чтобы узнать, как включить", + "user_script_type": "Пользовательский скрипт", + "auth_duration": "Срок действия разрешения", + "duration_once": "Только сейчас", + "duration_temporary": "Временно", + "duration_permanent": "Постоянно", + "apply_to_all_domains": "Применить ко всем запрашиваемым доменам", + "apply_to_all_domains_desc": "Действует для всех запросов этого скрипта (подстановка)", + "allow_action": "Разрешить", + "deny_action": "Отклонить", + "ignore_action": "Игнорировать", + "cancel_action": "Отмена", + "loading_confirm": "Загрузка запроса разрешения…", + "cookie_warning_title": "Особо чувствительное разрешение", + "cookie_warning_desc": "Cookie содержат конфиденциальные данные, например состояние входа. Разрешайте только доверенным скриптам.", + "confirm_expired_title": "Запрос разрешения истёк", + "confirm_expired_desc": "Этот запрос разрешения превысил время ожидания или уже был обработан. Вернитесь на страницу и вызовите его снова.", + "auto_close_in": "Окно закроется автоматически через {{second}} с" +} diff --git a/src/locales/ru-RU/popup.json b/src/locales/ru-RU/popup.json new file mode 100644 index 000000000..9888aa636 --- /dev/null +++ b/src/locales/ru-RU/popup.json @@ -0,0 +1,27 @@ +{ + "new_version_available": "Доступна новая версия", + "current_page_scripts": "Скрипты, выполняющиеся на текущей странице", + "enabled_background_scripts": "Включенные и работающие фоновые скрипты", + "menu_expand_num_before": "Когда пунктов меню больше", + "menu_expand_num_after": ", автоматически скрывать", + "develop_mode_guide": "«Режим разработчика» сейчас отключён, поэтому скрипты не могут работать корректно. 👉Нажмите, чтобы узнать, как включить", + "lower_version_browser_guide": "Ваш браузер слишком устарел, поэтому скрипты не могут работать корректно. 👉Нажмите, чтобы узнать подробнее", + "click_to_reload": "👉Нажмите для перезагрузки", + "page_in_blacklist": "Текущая страница находится в черном списке, невозможно использовать скрипты", + "ext_update_notification": "Расширение ScriptCat обновлено", + "ext_update_notification_desc": "Текущая версия: {{version}}, подробности смотрите в журнале обновлений", + "script_menu_display": "Меню, зарегистрированные скриптом", + "badge_type_none": "Не отображать", + "badge_type_run_count": "Количество запусков", + "badge_type_script_count": "Количество скриптов", + "script_menu": "Меню скрипта", + "display_right_click_menu": "Показать контекстное меню", + "display_right_click_menu_desc": "Показать меню скрипта в контекстном меню браузера", + "expand_count": "Количество развернутых", + "auto_collapse_when_exceeds": "Автоматически сворачивать при превышении этого количества", + "allow_user_script_guide": "«Разрешить пользовательские скрипты» сейчас отключён, поэтому скрипты не могут работать корректно. 👉Нажмите, чтобы узнать, как включить", + "request_permission": "Запрос разрешения", + "show_more_scripts": "+{{count}} скриптов", + "use_on_mobile": "Используйте ScriptCat на телефоне", + "scan_qr_to_install": "Отсканируйте QR-код, чтобы установить ScriptCat на телефон" +} diff --git a/src/locales/ru-RU/script.json b/src/locales/ru-RU/script.json new file mode 100644 index 000000000..2de603728 --- /dev/null +++ b/src/locales/ru-RU/script.json @@ -0,0 +1,115 @@ +{ + "import_link": "Импорт по ссылке", + "import_link_failure": "Ошибка импорта по ссылке", + "create_user_script": "Создать пользовательский скрипт", + "create_background_script": "Создать фоновый скрипт", + "create_scheduled_script": "Создать скрипт по расписанию", + "import_by_local": "Локальный импорт", + "import_local_failure": "Ошибка локального импорта", + "import_local_success": "Локальный импорт успешен", + "create_script": "Создать скрипт", + "installed_scripts": "Установленные скрипты", + "nav_scripts": "Скрипты", + "subscribe": "Подписаться", + "subscribe_scripts_count": "{{count}} скриптов", + "enter_subscribe_name": "Введите название подписки", + "subscribe_url": "Адрес подписки", + "confirm_delete_subscription": "Вы уверены, что хотите удалить эту подписку? Связанные скрипты также будут удалены", + "list": { + "confirm_delete": "Вы уверены, что хотите удалить? Обратите внимание, что эта операция необратима!", + "confirm_update": "Подтвердить обновление? Обратите внимание, что эта операция необратима!" + }, + "apply_to_run_status": "Применить к / Статус выполнения", + "sorting": "Сортировка", + "foreground_page_script_tooltip": "Скрипт переднего плана, будет выполняться на указанных страницах", + "background_script_tooltip": "Фоновый скрипт, будет работать в фоне после включения", + "scheduled_script_tooltip": "Запланированный скрипт, следующее время выполнения:", + "running": "Выполняется", + "completed": "Выполнение завершено", + "source_subscribe_link": "Ссылка подписки", + "source_local_script": "Локальный скрипт", + "source_script_link": "Ссылка скрипта", + "by_manual_creation": "Создан локально через редактирование кода", + "confirm_delete_script": "Вы уверены, что хотите удалить этот скрипт?", + "confirm_delete_scripts_content": "Вы уверены, что хотите удалить выбранные скрипты ({{count}})? Эта операция необратима.", + "confirm_delete_script_content": "Вы уверены, что хотите удалить скрипт \"{{name}}\"? Эта операция необратима.", + "delete_failed": "Ошибка удаления", + "enter_script_name": "Введите название скрипта", + "update_not_supported": "Этот скрипт не поддерживает проверку обновлений", + "checking_for_updates": "Проверка обновлений...", + "new_version_available": "Доступна новая версия", + "latest_version": "Уже последняя версия", + "checked_for_all_selected": "Все выбранные скрипты проверены на обновления", + "update_check_failed": "Ошибка проверки обновлений", + "stopping_script": "Остановка скрипта", + "script_stopped": "Скрипт остановлен", + "starting_script": "Запуск скрипта...", + "starting_updates": "Массовое обновление...", + "script_started": "Скрипт запущен", + "operation_failed": "Операция не выполнена", + "batch_operations": "Пакетные операции", + "scripts_pinned_to_top": "Выбранные скрипты закреплены сверху", + "unknown_operation": "Неизвестная операция", + "page_script": "Скрипт страницы", + "homepage": "Главная страница скрипта", + "script_website": "Сайт скрипта", + "script_source": "Исходный код скрипта", + "bug_feedback_script_support": "Сайт обратной связи/поддержки скрипта", + "script_total_runs": "Этот скрипт выполнялся {{runNum}} раз(а), в iframe {{runNumByIframe}} раз(а)", + "script_total_runs_single": "Этот скрипт выполнялся {{runNum}} раз(а)", + "script_disabled": "Этот скрипт не включен", + "cron_oncetype": { + "minute": "{{next}} (выполняется каждую минуту)", + "hour": "{{next}} (выполняется каждый час)", + "day": "{{next}} (выполняется каждый день)", + "month": "{{next}} (выполняется каждый месяц)", + "week": "{{next}} (выполняется каждую неделю)" + }, + "cron_invalid_expr": "Неверное выражение cron", + "scheduled_script_description_title": "Это запланированный скрипт. После включения он будет автоматически выполняться в определенное время и может управляться вручную с панели.", + "scheduled_script_description_description_expr": "Выражение планировщика", + "scheduled_script_description_description_next": "Последнее время выполнения:", + "background_script_description": "Это фоновый скрипт. После включения он будет автоматически выполняться один раз при открытии браузера и может управляться вручную с панели.", + "background_script": "Фоновый скрипт", + "scheduled_script": "Запланированный скрипт", + "script_status_tooltip": "Вы можете управлять статусом включения скриптов. Обычные скрипты Tampermonkey включены по умолчанию, а фоновые и запланированные скрипты выключены по умолчанию.", + "subscribe_source_tooltip": "Это источник подписки. При открытии подписки скрипт подписки будет установлен автоматически.", + "script_name_cannot_be_set_to_empty": "Имя скрипта не может быть пустым", + "search_scripts": "Поиск скриптов", + "script_list": { + "sidebar": { + "stopped": "Остановлено", + "all": "Все", + "normal_script": "Обычный скрипт", + "status": "Статус" + } + }, + "tags": "Теги", + "input_tags_placeholder": "Введите теги, нажмите Enter для подтверждения", + "switch_to_card_mode": "Переключиться в режим карточек", + "switch_to_table_mode": "Переключиться в табличный режим", + "open_sidebar": "Открыть боковую панель", + "close_sidebar": "Закрыть боковую панель", + "error_metadata_invalid": "Неверный блок MetaData", + "error_script_name_required": "Имя скрипта обязательно", + "error_script_version_required": "Требуется @version скрипта", + "error_script_namespace_required": "Требуется @namespace скрипта", + "error_cron_invalid": "Неверное выражение cron: {{expr}}", + "error_script_type_mismatch": "Несоответствие типа скрипта: обычные и фоновые скрипты нельзя преобразовывать", + "error_old_script_code_missing": "Предыдущий код скрипта не найден", + "error_subscribe_name_required": "Имя подписки обязательно", + "error_grant_conflict": "@grant одновременно объявляет 'none' и GM API", + "error_metadata_line_duplicated": "В метаданных есть повторяющиеся объявления.", + "create_group": "Создать", + "import_group": "Импорт", + "import_local_script": "Импортировать локальный скрипт", + "link_import": "Импорт по ссылке", + "import_skill": "Импортировать Skill", + "link_import_desc": "Вставьте ссылки на скрипты / подписки, по одной на строку", + "link_import_placeholder": "https://example.com/script.user.js", + "link_import_hint": "Поддерживает ссылки на пользовательские скрипты / подписки / Skill", + "not_a_valid_script": "Не является допустимым пользовательским скриптом или SkillScript", + "import_done": "Импорт завершён: успешно {{success}} · ошибок {{fail}}", + "drop_to_install": "Перетащите скрипты или Skill для установки", + "drop_to_install_hint": "Перетащите .js скрипты / подписки · .zip пакеты Skill" +} diff --git a/src/locales/ru-RU/settings.json b/src/locales/ru-RU/settings.json new file mode 100644 index 000000000..739b5a1ca --- /dev/null +++ b/src/locales/ru-RU/settings.json @@ -0,0 +1,119 @@ +{ + "general": "Общие", + "language": "Язык", + "help_translate": "Помочь с переводом", + "script_sync": "Синхронизация скриптов", + "sync_delete": "Синхронизация удаления", + "sync_delete_desc": "При включении скрипт будет помечен как удаленный при удалении, и другие устройства обнаружат этот статус и удалят скрипт. При отключении скрипт будет удален непосредственно из локального и облачного хранилища, что может привести к повторной синхронизации, если используется несколько устройств.", + "enable_script_sync_to": "Включить синхронизацию скриптов в", + "script_subscription_check_interval": "Интервал проверки обновлений скриптов/подписок", + "never": "Никогда", + "6_hours": "6 часов", + "12_hours": "12 часов", + "every_day": "Каждый день", + "every_week": "Каждую неделю", + "update_disabled_scripts": "Обновлять отключенные скрипты", + "silent_update_non_critical_changes": "Автоматически обновлять некритические изменения", + "enable_eslint": "Включить ESLint", + "eslint_rules": "Правила ESLint", + "enter_eslint_rules": "Введите правила ESLint, конфигурацию можно скачать с https://eslint.org/play/", + "language_change_tip": "Язык успешно изменен", + "backup": "Резервная копия", + "local": "Локальный", + "export_file": "Экспорт файла", + "import_file": "Импорт файла", + "cloud": "Облако", + "backup_to": "Резервное копирование в", + "preparing_backup": "Подготовка резервного копирования в облако", + "backup_success": "Резервное копирование успешно", + "backup_failed": "Ошибка резервного копирования", + "no_backup_files": "Нет файлов резервных копий", + "backup_list": "Список резервных копий", + "open_backup_dir": "Открыть папку резервных копий", + "confirm_delete_backup_file": "Подтвердить удаление файла резервной копии", + "backup_strategy": "Стратегия резервного копирования", + "under_construction": "В разработке", + "sync_system_connect_failed": "Ошибка подключения к системе синхронизации", + "sync_system_closed": "Синхронизация отключена", + "sync_system_closed_description": "Функция синхронизации отключена, пожалуйста, настройте заново", + "export_success": "Экспорт успешен", + "get_backup_dir_url_failed": "Ошибка получения адреса папки резервных копий", + "get_backup_files_failed": "Ошибка получения файлов резервных копий", + "baidu_netdisk": "Baidu Netdisk", + "netdisk_unbind": "Отвязать {{provider}}", + "netdisk_unbind_confirm": "Отвязать аккаунт {{provider}}?", + "netdisk_unbind_success": "Аккаунт {{provider}} отвязан", + "netdisk_unbind_error": "Не удалось отвязать аккаунт {{provider}}", + "save_only_current_group": "Сохранение действует только для текущей группы", + "security": "Безопасность", + "blacklist_pages": "Страницы черного списка", + "blacklist_placeholder": "Запретить ScriptCat выполнять скрипты на следующих страницах. Разделяйте несколько страниц переносами строк, например:\nhttps://*.example.com", + "expression_format_error": "Ошибка формата выражения", + "migration_confirm_message": "Повторная попытка миграции движка хранения изменит существующие данные. Пожалуйста, подтвердите. Подробности см.: https://docs.scriptcat.org/docs/change/v0.17/", + "retry_migration": "Повторить миграцию движка хранения", + "sync_status": "Статус синхронизации", + "interface_settings": "Интерфейс", + "select_interface_language": "Выберите язык интерфейса", + "extension_icon_badge": "Значок на иконке расширения", + "display_type": "Тип отображения", + "extension_icon_badge_type": "Тип числа, отображаемого на иконке расширения", + "background_color": "Цвет фона", + "badge_background_color_desc": "Цвет фона значка", + "text_color": "Цвет текста", + "badge_text_color_desc": "Цвет текста значка", + "badge_type_none": "Не отображать", + "badge_type_run_count": "Количество запусков", + "badge_type_script_count": "Количество скриптов", + "script_menu": "Меню скрипта", + "display_right_click_menu": "Показать контекстное меню", + "display_right_click_menu_desc": "Показать меню скрипта в контекстном меню браузера", + "expand_count": "Количество развернутых", + "auto_collapse_when_exceeds": "Автоматически сворачивать при превышении этого количества", + "script_update_check_frequency": "Частота проверки обновления скрипта", + "script_auto_update_frequency": "Частота автоматической проверки обновлений скриптов", + "update_options": "Параметры обновления", + "control_script_update_behavior": "Управление поведением обновления скриптов", + "blacklist_pages_desc": "Запретить запуск скриптов на указанных страницах, поддерживает подстановочные знаки", + "development_tools": "Инструменты разработки", + "check_script_code_quality": "Проверить качество кода скрипта и ошибки", + "custom_eslint_rules_config": "Пользовательская конфигурация правил ESLint (формат JSON)", + "script_run_env": { + "title": "Среда выполнения", + "all": "Все вкладки", + "normal-tabs": "Обычные вкладки", + "incognito-tabs": "Приватные вкладки" + }, + "script_run_at": { + "title": "Время выполнения" + }, + "script_setting": { + "title": "Настройки скрипта", + "default": "По умолчанию" + }, + "notification": { + "script_sync_delete": "Синхронизация удаления скрипта", + "script_sync_delete_desc": "Скрипт {{scriptName}} был удален", + "subscribe_update": "Подписка {{subscribeName}} была обновлена", + "subscribe_update_desc": "Новые скрипты: {{newScripts}}\nУдаленные скрипты: {{deletedScripts}}" + }, + "enable_background": { + "title": "Включить фоновый режим", + "description": "Если включено, браузер продолжит работать в фоновом режиме после закрытия всех окон и будет свернут в трей, пока вы вручную не завершите его работу. Это позволяет фоновым скриптам продолжать выполняться.", + "enable_failed": "Не удалось включить", + "disable_failed": "Не удалось отключить", + "prompt_title": "Включить фоновое выполнение?", + "prompt_description": "Это {{scriptType}}. Включение фонового выполнения позволит скрипту продолжать работать после закрытия браузера.", + "enable_now": "Включить сейчас", + "maybe_later": "Может быть позже", + "settings_hint": "Вы можете изменить эту опцию в настройках в любое время." + }, + "favicon_service": "Сервис Favicon", + "favicon_service_desc": "Выберите сервис для получения значков сайтов", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_google": "Google", + "favicon_service_duckduckgo": "DuckDuckGo", + "favicon_service_icon-horse": "Icon Horse", + "favicon_service_local": "Локальное получение", + "cloud_sync_account_verification": "Проверка учетной записи облачной синхронизации...", + "cloud_sync_verification_failed": "Ошибка проверки учетной записи облачной синхронизации" +} diff --git a/src/locales/ru-RU/tools.json b/src/locales/ru-RU/tools.json new file mode 100644 index 000000000..35fae0887 --- /dev/null +++ b/src/locales/ru-RU/tools.json @@ -0,0 +1,17 @@ +{ + "development_tool": "Инструмент разработки", + "vscode_url": "Адрес VSCode", + "auto_connect_vscode_service": "Автоматически подключаться к службе VSCode", + "connect": "Подключить", + "connection_success": "Подключение успешно", + "connection_failed": "Ошибка подключения", + "select_import_script": "Выберите скрипт для импорта на новой странице", + "import_error": "Ошибка импорта", + "pulling_data_from_cloud": "Получение данных из облака", + "pull_failed": "Ошибка получения", + "restore": "Восстановить", + "local_backup": "Локальная резервная копия", + "cloud_backup": "Облачная резервная копия", + "auto_backup": "Автоматическая резервная копия", + "data_migration": "Миграция данных" +} diff --git a/src/locales/ru-RU/translation.json b/src/locales/ru-RU/translation.json deleted file mode 100644 index 08008cb9b..000000000 --- a/src/locales/ru-RU/translation.json +++ /dev/null @@ -1,770 +0,0 @@ -{ - "sentence-separator": ". ", - "import_link": "Импорт по ссылке", - "import_link_failure": "Ошибка импорта по ссылке", - "create_user_script": "Создать пользовательский скрипт", - "create_background_script": "Создать фоновый скрипт", - "create_scheduled_script": "Создать скрипт по расписанию", - "import_by_local": "Локальный импорт", - "import_local_failure": "Ошибка локального импорта", - "import_local_success": "Локальный импорт успешен", - "create_script": "Создать скрипт", - "user_guide": "Руководство пользователя", - "api_docs": "Документация API", - "development_guide": "Руководство разработчика", - "script_gallery": "Галерея скриптов", - "community_forum": "Форум сообщества", - "external_links": "Внешние ссылки", - "system_follow": "Следовать системе", - "no_data": "Нет данных", - "installed_scripts": "Установленные скрипты", - "subscribe": "Подписаться", - "logs": "Журналы", - "tools": "Инструменты", - "find": "Найти", - "replace": "Заменить", - "settings": "Настройки", - "hide_main_sidebar": "Свернуть боковую панель", - "show_main_sidebar": "Развернуть боковую панель", - "guide": "Руководство новичка", - "helpcenter": "Справочный центр", - "general": "Общие", - "language": "Язык", - "help_translate": "Помочь с переводом", - "script_sync": "Синхронизация скриптов", - "sync_delete": "Синхронизировать удаление", - "sync_delete_desc": "При включении скрипт будет помечен как удаленный при удалении, и другие устройства обнаружат этот статус и удалят скрипт. При отключении скрипт будет удален непосредственно из локального и облачного хранилища, что может привести к повторной синхронизации, если используется несколько устройств.", - "enable_script_sync_to": "Включить синхронизацию скриптов в", - "save": "Сохранить", - "save_as": "Сохранить как", - "file": "Файл", - "run": "Запустить", - "debug": "Отладка", - "cloud_sync_account_verification": "Проверка учетной записи облачной синхронизации...", - "cloud_sync_verification_failed": "Ошибка проверки учетной записи облачной синхронизации", - "save_success": "Сохранено успешно", - "reset_success": "Сброс выполнен успешно", - "update": "Обновить", - "check_update": "Проверить обновления", - "script_subscription_check_interval": "Интервал проверки обновлений скриптов/подписок", - "never": "Никогда", - "6_hours": "6 часов", - "12_hours": "12 часов", - "every_day": "Каждый день", - "every_week": "Каждую неделю", - "update_disabled_scripts": "Обновлять отключенные скрипты", - "silent_update_non_critical_changes": "Автоматически обновлять некритические изменения", - "enable_eslint": "Включить ESLint", - "eslint_rules": "Правила ESLint", - "enter_eslint_rules": "Введите правила ESLint, конфигурацию можно скачать с https://eslint.org/play/", - "language_change_tip": "Язык успешно изменен", - "backup": "Резервная копия", - "local": "Локальный", - "export_file": "Экспорт файла", - "import_file": "Импорт файла", - "cloud": "Облако", - "backup_to": "Резервное копирование в", - "preparing_backup": "Подготовка резервного копирования в облако", - "backup_success": "Резервное копирование успешно", - "backup_failed": "Ошибка резервного копирования", - "no_backup_files": "Нет файлов резервных копий", - "backup_list": "Список резервных копий", - "open_backup_dir": "Открыть папку резервных копий", - "confirm_delete": "Подтвердить удаление", - "confirm_delete_backup_file": "Подтвердить удаление файла резервной копии", - "confirm_update": "Подтвердить обновление", - "delete_success": "Успешно удалено", - "deleting": "Удаление", - "backup_strategy": "Стратегия резервного копирования", - "under_construction": "В разработке", - "development_tool": "Инструмент разработки", - "vscode_url": "Адрес VSCode", - "auto_connect_vscode_service": "Автоматически подключаться к службе VSCode", - "connect": "Подключить", - "connection_success": "Подключение успешно", - "connection_failed": "Ошибка подключения", - "select_import_script": "Выберите скрипт для импорта на новой странице", - "import_error": "Ошибка импорта", - "pulling_data_from_cloud": "Получение данных из облака", - "pull_failed": "Ошибка получения", - "restore": "Восстановить", - "log_title": "Журнал выполнения", - "last_5_minutes": "Последние 5 минут", - "last_15_minutes": "Последние 15 минут", - "last_30_minutes": "Последние 30 минут", - "last_1_hour": "Последний 1 час", - "last_3_hours": "Последние 3 часа", - "last_6_hours": "Последние 6 часов", - "last_12_hours": "Последние 12 часов", - "last_24_hours": "Последние 24 часа", - "last_7_days": "Последние 7 дней", - "query": "Запрос", - "labels": "Метки", - "search_regex": "Поиск (поддерживает регулярные выражения)", - "clean_schedule": "Расписание очистки", - "days_ago_logs": "дней назад", - "delete_completed": "Удаление завершено", - "delete_current_logs": "Удалить текущие журналы", - "clear_completed": "Очистка завершена", - "clear_logs": "Очистить журналы", - "to": " — ", - "now": "Сейчас", - "total_logs": "Найдено {{length}} записей журнала", - "filtered_logs": "После фильтрации {{length}} записей журнала", - "enter_filter_conditions": "Введите условия фильтрации", - "permission": "Разрешения", - "enter_subscribe_name": "Введите название подписки", - "subscribe_url": "Адрес подписки", - "confirm_delete_subscription": "Вы уверены, что хотите удалить эту подписку? Связанные скрипты также будут удалены", - "list": { - "confirm_delete": "Вы уверены, что хотите удалить? Обратите внимание, что эта операция необратима!", - "confirm_update": "Подтвердить обновление? Обратите внимание, что эта операция необратима!" - }, - "enable": "Включить", - "script_list_enable_width": 120, - "script_list_last_updated_width": 120, - "script_list_apply_to_run_status_width": 140, - "subscribe_list_enable_width": 100, - "disable": "Выключить", - "name": "Название", - "version": "Версия", - "apply_to_run_status": "Применить к / Статус выполнения", - "source": "Источник", - "home": "Главная", - "sorting": "Сортировка", - "last_updated": "Последнее обновление", - "action": "Действие", - "foreground_page_script_tooltip": "Скрипт переднего плана, будет выполняться на указанных страницах", - "background_script_tooltip": "Фоновый скрипт, будет работать в фоне после включения", - "scheduled_script_tooltip": "Запланированный скрипт, следующее время выполнения:", - "running": "Выполняется", - "completed": "Выполнение завершено", - "source_subscribe_link": "Ссылка подписки", - "source_local_script": "Локальный скрипт", - "source_script_link": "Ссылка скрипта", - "by_manual_creation": "Создан локально через редактирование кода", - "confirm_delete_script": "Вы уверены, что хотите удалить этот скрипт?", - "confirm_delete_script_content": "Вы уверены, что хотите удалить скрипт \"{{name}}\"? Эта операция необратима.", - "delete_failed": "Ошибка удаления", - "enter_script_name": "Введите название скрипта", - "update_not_supported": "Этот скрипт не поддерживает проверку обновлений", - "checking_for_updates": "Проверка обновлений...", - "new_version_available": "Доступна новая версия", - "latest_version": "Уже последняя версия", - "checked_for_all_selected": "Все выбранные скрипты проверены на обновления", - "update_check_failed": "Ошибка проверки обновлений", - "script_import_failed": "Не удалось импортировать скрипт", - "install_page_open_failed": "Не удалось открыть страницу установки", - "stopping_script": "Остановка скрипта", - "script_stopped": "Скрипт остановлен", - "starting_script": "Запуск скрипта...", - "starting_updates": "Массовое обновление...", - "script_started": "Скрипт запущен", - "operation_failed": "Операция не выполнена", - "batch_operations": "Пакетные операции", - "export": "Экспорт", - "delete": "Удалить", - "pin_to_top": "Закрепить сверху", - "scripts_pinned_to_top": "Выбранные скрипты закреплены сверху", - "unknown_operation": "Неизвестная операция", - "confirm": "Подтвердить", - "close": "Закрыть", - "page_script": "Скрипт страницы", - "homepage": "Главная страница скрипта", - "script_website": "Сайт скрипта", - "script_source": "Исходный код скрипта", - "bug_feedback_script_support": "Сайт обратной связи/поддержки скрипта", - "config": "Конфигурация", - "key": "key", - "value": "value", - "add": "Добавить", - "type": "Тип", - "edit_value": "Редактировать значение", - "add_value": "Добавить значение", - "update_success": "Изменение успешно", - "add_success": "Добавление успешно", - "script_storage": "Хранилище скрипта", - "enter_key": "Введите key", - "key_placeholder": "key", - "value_placeholder": "Если тип — object, введите данные, которые можно разобрать как JSON", - "clear": "Очистить", - "clear_success": "Очистка успешна", - "confirm_clear": "Вы действительно хотите очистить это хранилище?", - "type_string": "string", - "type_number": "number", - "type_boolean": "boolean", - "type_object": "object", - "confirm_delete_resource": "Вы уверены, что хотите удалить этот ресурс? При следующем запуске он будет перезагружен", - "confirm_clear_resource": "Вы действительно хотите очистить эти ресурсы? При следующем запуске они будут перезагружены", - "script_resource": "Ресурсы скрипта", - "permission_value": "Значение разрешения", - "allow": "Разрешить", - "yes": "Да", - "no": "Нет", - "confirm_delete_permission": "Вы уверены, что хотите удалить это разрешение?", - "basic_info": "Основная информация", - "update_url": "URL обновления", - "permission_management": "Управление разрешениями", - "add_permission": "Добавить разрешение", - "permission_cors": "Кросс-домен (CORS)", - "permission_cookie": "Управление cookie", - "match": "Совпадение", - "user_setting": "Пользовательские настройки", - "confirm_delete_exclude": "Подтвердить удаление этого исключения?", - "after_deleting_match_item": "После удаления элемента совпадения, установленного скриптом, он автоматически добавится в список совпадений", - "confirm_delete_match": "Подтвердить удаление этого совпадения?", - "after_deleting_exclude_item": "После удаления элемента совпадения, установленного скриптом, он автоматически добавится в список исключений", - "add_match": "Добавить совпадение", - "add_exclude": "Добавить исключение", - "website_match": "Совпадение сайта (@match)", - "reset": "Сброс", - "website_exclude": "Исключение сайта (@exclude)", - "confirm_reset": "Подтвердить сброс?", - "script_total_runs": "Этот скрипт выполнялся {{runNum}} раз(а), в iframe {{runNumByIframe}} раз(а)", - "script_total_runs_single": "Этот скрипт выполнялся {{runNum}} раз(а)", - "script_disabled": "Этот скрипт не включен", - "run_once": "Запустить один раз", - "stop": "Остановить", - "edit": "Редактировать", - "undo": "Отменить", - "redo": "Повторить", - "cut": "Вырезать", - "copy": "Копировать", - "paste": "Вставить", - "format": "Форматировать", - "exclude_on": "Восстановить в $0 выполнении", - "exclude_off": "Исключить в $0 выполнении", - "user_config": "Конфигурация пользователя", - "gm_api": "GM API", - "storage_api": "API хранилища", - "use_file_system": "Используемая файловая система", - "open_directory": "Открыть папку", - "account_validation_failed": "Ошибка проверки информации об аккаунте", - "not_set": "Не установлено", - "in_use": "Используется", - "storage_error": "Ошибка хранилища", - "upload_to_cloud": "Загрузить в облако", - "save_failed": "Ошибка сохранения", - "exporting": "Экспорт...", - "upload_to": "Загрузить в", - "value_export_expression": "Выражение экспорта значений", - "overwrite_original_value_on_import": "Перезаписать исходное значение при импорте", - "cookie_export_expression": "Выражение экспорта cookie", - "overwrite_original_cookie_on_import": "Перезаписать исходный cookie при импорте", - "restore_default_values": "Восстановить значения по умолчанию", - "get_confirm_error": "Ошибка получения информации подтверждения", - "confirm_error": "Ошибка подтверждения", - "ignore": "Игнорировать", - "allow_once": "Разрешить один раз", - "temporary_allow": "Временно разрешить это {{permissionContent}}", - "temporary_allow_all": "Временно разрешить все {{permissionContent}}", - "permanent_allow": "Постоянно разрешить это {{permissionContent}}", - "permanent_allow_all": "Постоянно разрешить все {{permissionContent}}", - "deny_once": "Отклонить один раз", - "temporary_deny": "Временно отклонить это {{permissionContent}}", - "temporary_deny_all": "Временно отклонить все {{permissionContent}}", - "permanent_deny": "Постоянно отклонить это {{permissionContent}}", - "permanent_deny_all": "Постоянно отклонить все {{permissionContent}}", - "data_import": "Импорт данных", - "import": "Импорт", - "select_scripts_to_import": "Выберите скрипты для импорта", - "select_all": "Выбрать все", - "script_import_progress": "Прогресс импорта скриптов", - "select_subscribes_to_import": "Выберите подписки для импорта", - "subscribe_import_progress": "Прогресс импорта подписок", - "author": "Автор", - "description": "Описание", - "operation": "Операция", - "error": "Ошибка", - "unknown": "Неизвестно", - "add_new": "Добавить новый", - "no_operation": "Без операций", - "enable_script": "Включить скрипт", - "local_creation": "Локальное создание", - "import_success": "Импорт успешен", - "install_script": "Установить", - "update_script": "Обновить", - "install_subscribe": "Установить подписку", - "update_subscribe": "Обновить подписку", - "update_script_no_close": "Обновить, не закрывая окно", - "install_script_no_close": "Установить, не закрывая окно", - "update_script_no_more_update": "Обновить, но больше не проверять обновления", - "close_update_script_no_more_update": "Закрыть и не проверять обновления", - "install_script_no_more_update": "Установить, но больше не проверять обновления", - "invalid_link": "Неверная ссылка", - "subscribe_install_label": "Эта подписка установит следующие скрипты", - "script_runs_in": "Скрипт будет выполняться на следующих сайтах", - "script_has_full_access_to": "Скрипт получит полный доступ к следующим адресам", - "script_requires": "Скрипт ссылается на следующие внешние ресурсы", - "cookie_warning": "Обратите внимание, что этот скрипт запрашивает разрешения на операции с Cookie. Это опасное разрешение, пожалуйста, убедитесь в безопасности скрипта.", - "cron_oncetype": { - "minute": "{{next}} (выполняется каждую минуту)", - "hour": "{{next}} (выполняется каждый час)", - "day": "{{next}} (выполняется каждый день)", - "month": "{{next}} (выполняется каждый месяц)", - "week": "{{next}} (выполняется каждую неделю)" - }, - "cron_invalid_expr": "Неверное выражение cron", - "scheduled_script_description_title": "Это запланированный скрипт. После включения он будет автоматически выполняться в определенное время и может управляться вручную с панели.", - "scheduled_script_description_description_expr": "Выражение планировщика", - "scheduled_script_description_description_next": "Последнее время выполнения:", - "background_script_description": "Это фоновый скрипт. После включения он будет автоматически выполняться один раз при открытии браузера и может управляться вручную с панели.", - "install_success": "Установка успешна", - "install": { - "update_success": "Обновление успешно" - }, - "install_failed": "Ошибка установки", - "subscribe_success": "Подписка успешна", - "subscribe_failed": "Ошибка подписки", - "current_version": "Текущая версия", - "update_version": "Версия обновления", - "updatepage": { - "main_header": "Проверка обновлений", - "header_site_specific": "Доступны обновления для этого сайта ($0)", - "header_site_all": "Доступны обновления", - "header_ignored": "Доступные обновления, но проигнорированные", - "update_all": "Обновить все", - "ignore_all": "Игнорировать все", - "update": "Обновить", - "ignore": "Игнорировать", - "old_version_": "Старая версия: ", - "new_version_": "Новая версия: ", - "enabled": "Включено", - "tooltip_enabled": "Этот скрипт включен.", - "disabled": "Выключено", - "tooltip_disabled": "Этот скрипт выключен.", - "similarity_": "Сходство: ", - "codechange_major": "Существенное изменение", - "codechange_noticeable": "Заметное изменение", - "codechange_tiny": "Незначительное изменение", - "new_connects_": "Новый @connect: ", - "tag_new_connect": "Добавлен @connect", - "status_last_check": "Последняя проверка: $0", - "status_checking_updates": "Проверка обновлений...", - "status_no_update": "Обновления не требуются", - "status_n_update": "$0 обновлений требуется", - "status_n_ignored": "$0 обновлений проигнорировано", - "status_autoclose": "Автоматическое закрытие через $0 сек.", - "header_other_update": "Другие доступные обновления" - }, - "downloading_status_text": "Загрузка. Получено {{bytes}}.", - "install_page_please_wait": "Пожалуйста, подождите", - "install_page_loading": "Загрузка страницы установки", - "install_page_load_failed": "Не удалось загрузить страницу установки", - "invalid_page": "Недействительная страница", - "background_script_tag": "Это фоновый скрипт", - "scheduled_script_tag": "Это запланированный скрипт", - "background_script": "Фоновый скрипт", - "scheduled_script": "Запланированный скрипт", - "install_from_legitimate_sources_warning": "Пожалуйста, устанавливайте скрипты только из законных источников! Неизвестные скрипты могут нарушить вашу конфиденциальность или выполнить злонамеренные операции!", - "antifeature_referral_link_title": "Реферальная ссылка", - "antifeature_referral_link_description": "Этот скрипт изменяет или перенаправляет на реферальную ссылку автора", - "antifeature_ads_title": "Содержит рекламу", - "antifeature_ads_description": "Этот скрипт вставляет рекламу на посещаемые вами страницы", - "antifeature_payment_title": "Платный скрипт", - "antifeature_payment_description": "Этот скрипт требует оплаты для нормального использования", - "antifeature_miner_title": "Майнинг", - "antifeature_miner_description": "Этот скрипт содержит функции майнинга", - "antifeature_membership_title": "Функции членства", - "antifeature_membership_description": "Этот скрипт требует регистрации членства для нормального использования", - "antifeature_tracking_title": "Отслеживание информации", - "antifeature_tracking_description": "Этот скрипт отслеживает информацию о пользователе", - "script_info_load_failed": "Ошибка загрузки информации о скрипте!", - "script_status_tooltip": "Вы можете управлять статусом включения скриптов. Обычные скрипты Tampermonkey включены по умолчанию, а фоновые и запланированные скрипты выключены по умолчанию.", - "subscribe_source_tooltip": "Это источник подписки. При открытии подписки скрипт подписки будет установлен автоматически.", - "get_script": "Получить скрипт", - "report_issue": "Отзыв об ошибке / проблеме", - "project_docs": "Документация проекта", - "community": "Сообщество", - "popup": { - "new_version_available": "Доступна новая версия" - }, - "current_page_scripts": "Скрипты, выполняющиеся на текущей странице", - "enabled_background_scripts": "Включенные и работающие фоновые скрипты", - "script_accessing_cross_origin_resource": "Скрипт пытается получить доступ к кросс-доменному ресурсу", - "confirm_operation_description": "Пожалуйста, подтвердите, разрешаете ли вы скрипту выполнить эту операцию. Скрипт также может добавить тег @connect, чтобы пропустить эту опцию", - "extension_site_access_title": "ScriptCat требуется доступ к сайту", - "extension_site_access_description": "Разрешите браузеру доступ к сайту для этого источника, чтобы ScriptCat мог выполнить запрос. Это изменит настройку доступа к сайтам для расширения.", - "extension_site_access_content": "Сайт", - "domain": "Домен", - "script_name": "Название скрипта", - "request_domain": "Запрашиваемый домен", - "request_url": "URL запроса", - "access_cookie_content": "Скрипт пытается получить доступ к содержимому cookie сайта", - "confirm_script_operation": "Пожалуйста, подтвердите, разрешаете ли вы скрипту выполнить эту операцию. Cookie являются важными пользовательскими данными, обязательно предоставляйте разрешения только доверенным скриптам.", - "cookie_domain": "Домен Cookie", - "script_operation_title": "Скрипт пытается получить доступ к пространству хранения", - "script_operation_description": "Пожалуйста, подтвердите, разрешаете ли вы скрипту выполнить эту операцию. После разрешения скрипт сможет работать с настроенным вами пространством хранения. Скрипт создаст папку app/${dir} в пространстве хранения для использования", - "script_permission_content": "скрипт", - "sync_system_connect_failed": "Ошибка подключения к системе синхронизации", - "sync_system_closed": "Синхронизация отключена", - "sync_system_closed_description": "Функция синхронизации отключена, пожалуйста, настройте заново", - "auth_type": "Тип аутентификации", - "url": "URL", - "username": "Имя пользователя", - "password": "Пароль", - "access_token_bearer": "Токен доступа (Bearer)", - "s3_bucket_name": "Имя корзины", - "s3_region": "Регион", - "s3_access_key_id": "Идентификатор ключа доступа", - "s3_secret_access_key": "Секретный ключ доступа", - "s3_custom_endpoint": "Пользовательская конечная точка (необязательно)", - "skip": "Пропустить", - "next": "Далее", - "next_with_progress": "Следующий шаг (шаг {step} из {steps})", - "back": "Назад", - "last": "Завершить", - "start_guide_title": "Добро пожаловать в расширение ScriptCat", - "start_guide_content": "Далее мы познакомим вас с основными методами использования ScriptCat", - "guide_installed_scripts": "Установленные вами скрипты будут отображаться здесь", - "guide_script_list_title": "Магазин скриптов", - "guide_script_list_content": "Скрипты можно устанавливать из магазина скриптов. ScriptCat поддерживает не только пользовательские скрипты, но и фоновые скрипты", - "guide_script_list_enable_title": "Включение скрипта", - "guide_script_list_enable_content": "Скрипты нужно включить для использования. Скрипты страниц включены по умолчанию при установке, фоновые скрипты выключены по умолчанию", - "guide_script_list_apply_to_run_status_title": "Применить к и статус выполнения", - "guide_script_list_apply_to_run_status_content": "Отображение статуса выполнения скрипта. Наведите курсор на тег, чтобы увидеть тип скрипта", - "guide_script_list_sort_title": "Сортировка", - "guide_script_list_sort_content": "Вы можете перетаскивать метки сценариев, чтобы сортировать их.", - "guide_script_list_update_title": "Последнее обновление", - "guide_script_list_update_content": "При нажатии на заголовок столбца «Последнее обновление» будет выполнена проверка наличия обновлений для скрипта.", - "guide_script_list_action_title": "Операция", - "guide_script_list_action_content": "Панель управления позволяет редактировать скрипты, управлять их выполнением и завершением (фоновые скрипты), а также настраивать параметры (см. UserConfig). Кнопки справа позволяют получить доступ к расширенной фильтрации и переключать режимы просмотра.", - "guide_tools_title": "Часто используемые инструменты", - "guide_tools_content": "В инструментах предоставлены функции резервного копирования и разработки", - "guide_tools_backup_title": "Резервное копирование", - "guide_tools_backup_content": "Резервное копирование может сохранять скрипты во избежание потери. Можно экспортировать файлы скриптов локально или загружать локальные файлы скриптов. Также можно создавать резервные копии в облаке для большего удобства.", - "guide_setting_title": "Настройки", - "guide_setting_content": "Настройки в основном включают язык, синхронизацию скриптов, частоту обновлений и другие часто используемые опции", - "guide_setting_sync_title": "Обновление и синхронизация", - "guide_setting_sync_content": "Функция синхронизации скриптов может удобно синхронизировать содержимое скриптов этого устройства в облако. Если у вас несколько устройств, отметьте опцию синхронизации удаления. Когда скрипт удаляется с этого устройства, соответствующий скрипт будет удален из облака, а также удален с других устройств.", - "auto": "Автоматически", - "hide": "Скрыть", - "custom": "Пользовательский", - "resize_column_width": "Изменить ширину столбца", - "collapse": "Свернуть", - "expand": "Развернуть", - "menu_expand_num_before": "Когда пунктов меню больше", - "menu_expand_num_after": ", автоматически скрывать", - "script_name_cannot_be_set_to_empty": "Имя скрипта не может быть пустым", - "edit_conflict": "Конфликт редактирования", - "confirm_override_when_edit_conflict": "Этот скрипт был изменён в другом экземпляре. Замена приведёт к перезаписи этих изменений. Хотите сохранить эту версию?", - "save_abort_when_edit_conflict": "Скрипт был изменён в другом экземпляре. Сохранение отменено.", - "scriptname_conflict": "Конфликт имени скрипта", - "confirm_save_when_scriptname_conflict": "Это имя скрипта уже используется другим скриптом. Вы всё равно хотите сохранить его?", - "save_abort_when_scriptname_conflict": "Это имя скрипта уже используется другим скриптом. Сохранение отменено.", - "eslint_config_format_error": "Ошибка формата конфигурации ESLint", - "export_success": "Экспорт успешен", - "get_backup_dir_url_failed": "Ошибка получения адреса папки резервных копий", - "get_backup_files_failed": "Ошибка получения файлов резервных копий", - "request_permission": "Запрос разрешения", - "develop_mode_guide": "«Режим разработчика» сейчас отключён, поэтому скрипты не могут работать корректно. 👉Нажмите, чтобы узнать, как включить", - "allow_user_script_guide": "«Разрешить пользовательские скрипты» сейчас отключён, поэтому скрипты не могут работать корректно. 👉Нажмите, чтобы узнать, как включить", - "lower_version_browser_guide": "Ваш браузер слишком устарел, поэтому скрипты не могут работать корректно. 👉Нажмите, чтобы узнать подробнее", - "click_to_reload": "👉Нажмите для перезагрузки", - "page_in_blacklist": "Текущая страница находится в черном списке, невозможно использовать скрипты", - "baidu_netdisk": "Baidu Netdisk", - "netdisk_unbind": "Отвязать {{provider}}", - "netdisk_unbind_confirm": "Отвязать аккаунт {{provider}}?", - "netdisk_unbind_success": "Аккаунт {{provider}} отвязан", - "netdisk_unbind_error": "Не удалось отвязать аккаунт {{provider}}", - "save_only_current_group": "Сохранение действует только для текущей группы", - "script_import_result": "Результат импорта скрипта", - "failure_info": "Информация об ошибке", - "security": "Безопасность", - "blacklist_pages": "Страницы черного списка", - "blacklist_placeholder": "Запретить ScriptCat выполнять скрипты на следующих страницах. Разделяйте несколько страниц переносами строк, например:\nhttps://*.example.com", - "expression_format_error": "Ошибка формата выражения", - "migration_confirm_message": "Повторная попытка миграции движка хранения изменит существующие данные. Пожалуйста, подтвердите. Подробности см.: https://docs.scriptcat.org/docs/change/v0.17/", - "retry_migration": "Повторить миграцию движка хранения", - "script_modified_leave_confirm": "Изменения не сохранены. Если вы покинете страницу, они будут потеряны. Вы уверены, что хотите уйти?", - "create_success_note": "Создание успешно. Обратите внимание, что фоновые скрипты не включаются по умолчанию", - "save_as_failed": "Ошибка «Сохранить как»", - "save_as_success": "«Сохранить как» успешно", - "only_background_scheduled_can_run": "Только фоновые/запланированные скрипты могут выполняться", - "preparing_script_resources": "Подготовка ресурсов скрипта...", - "build_success_message": "Сборка успешна. Вы можете открыть инструменты разработчика на странице расширений и посмотреть вывод в консоли", - "script_storage_tooltip": "Можно управлять данными хранилища скрипта (GM_value)", - "script_resource_tooltip": "Управление ресурсами, загруженными через @resource, @require", - "script_setting_tooltip": "Настройка некоторых пользовательских параметров скрипта", - "script_modified_close_confirm": "Скрипт был изменен. Изменения будут потеряны после закрытия. Продолжить?", - "close_current_tab": "Закрыть текущую вкладку", - "close_other_tabs": "Закрыть другие вкладки", - "close_left_tabs": "Закрыть левые вкладки", - "close_right_tabs": "Закрыть правые вкладки", - "import_script_placeholder": "Поддерживается ввод абсолютных ссылок скриптов, заканчивающихся на .user.js, или ссылок страницы установки ScriptCat\nМожно заполнять в несколько строк, по одной на строку\nПример:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/zh-CN/script-show-page/1234", - "invalid_script_code": "Неверный код скрипта", - "build_failed": "Ошибка сборки", - "drag_script_here_to_upload": "Перетащите скрипт сюда для загрузки", - "sync_status": "Статус синхронизации", - "search_scripts": "Поиск скриптов", - "ext_update_notification": "Расширение ScriptCat обновлено", - "ext_update_notification_desc": "Текущая версия: {{version}}, подробности смотрите в журнале обновлений", - "watch_file_description": "Отслеживайте изменения файлов и автоматически обновляйте скрипты. При использовании убедитесь, что путь к файлу скрипта остаётся неизменным, а страница не может быть закрыта", - "watch_file": "Мониторинг файлов", - "stop_watch_file": "Остановить мониторинг", - "script_menu_display": "Меню, зарегистрированные скриптом", - "badge_type_none": "Не отображать", - "badge_type_run_count": "Количество запусков", - "badge_type_script_count": "Количество скриптов", - "interface_settings": "Интерфейс", - "select_interface_language": "Выберите язык интерфейса", - "extension_icon_badge": "Значок на иконке расширения", - "display_type": "Тип отображения", - "extension_icon_badge_type": "Тип числа, отображаемого на иконке расширения", - "background_color": "Цвет фона", - "badge_background_color_desc": "Цвет фона значка", - "text_color": "Цвет текста", - "badge_text_color_desc": "Цвет текста значка", - "script_menu": "Меню скрипта", - "display_right_click_menu": "Показать контекстное меню", - "display_right_click_menu_desc": "Показать меню скрипта в контекстном меню браузера", - "expand_count": "Количество развернутых", - "auto_collapse_when_exceeds": "Автоматически сворачивать при превышении этого количества", - "script_update_check_frequency": "Частота проверки обновления скрипта", - "script_auto_update_frequency": "Частота автоматической проверки обновлений скриптов", - "update_options": "Параметры обновления", - "control_script_update_behavior": "Управление поведением обновления скриптов", - "blacklist_pages_desc": "Запретить запуск скриптов на указанных страницах, поддерживает подстановочные знаки", - "development_tools": "Инструменты разработки", - "check_script_code_quality": "Проверить качество кода скрипта и ошибки", - "custom_eslint_rules_config": "Пользовательская конфигурация правил ESLint (формат JSON)", - "light": "Светлый режим", - "dark": "Темный режим", - "individual_edit": "Индивидуальное редактирование", - "batch_edit": "Пакетное редактирование", - "script_code": "Код скрипта", - "enter_search_value": "Введите {{search}} для поиска", - "script_run_env": { - "title": "Среда выполнения", - "all": "Все вкладки", - "normal-tabs": "Обычные вкладки", - "incognito-tabs": "Приватные вкладки" - }, - "script_run_at": { - "title": "Время выполнения" - }, - "script_setting": { - "title": "Настройки скрипта", - "default": "По умолчанию" - }, - "editor_config": "Конфигурация редактора", - "editor_config_description": "Вы можете настроить compilerOptions, ссылаясь на jsconfig.js", - "editor_type_definition": "Определения типов редактора", - "editor_type_definition_description": "Вы можете настроить собственные определения типов, которые редактор скриптов будет автоматически загружать", - "eslint_rules_reset": "Правила ESLint сброшены", - "eslint_rules_saved": "Правила ESLint сохранены", - "editor_config_reset": "Конфигурация редактора сброшена", - "editor_config_saved": "Конфигурация редактора сохранена", - "editor_config_format_error": "Ошибка формата конфигурации редактора", - "editor_type_definition_reset": "Определения типов редактора сброшены", - "editor_type_definition_saved": "Определения типов редактора сохранены", - "script_list": { - "sidebar": { - "stopped": "Остановлено", - "all": "Все", - "normal_script": "Обычный скрипт", - "status": "Статус" - } - }, - "tags": "Теги", - "install_source": "Источник установки", - "input_tags_placeholder": "Введите теги, нажмите Enter для подтверждения", - "switch_to_card_mode": "Переключиться в режим карточек", - "switch_to_table_mode": "Переключиться в табличный режим", - "open_sidebar": "Открыть боковую панель", - "close_sidebar": "Закрыть боковую панель", - "no_message_content": "Нет содержимого сообщения", - "error_metadata_invalid": "Неверный блок MetaData", - "error_script_name_required": "Имя скрипта обязательно", - "error_script_version_required": "Требуется @version скрипта", - "error_script_namespace_required": "Требуется @namespace скрипта", - "error_cron_invalid": "Неверное выражение cron: {{expr}}", - "error_script_type_mismatch": "Несоответствие типа скрипта: обычные и фоновые скрипты нельзя преобразовывать", - "error_old_script_code_missing": "Предыдущий код скрипта не найден", - "error_subscribe_name_required": "Имя подписки обязательно", - "error_grant_conflict": "@grant одновременно объявляет 'none' и GM API", - "error_metadata_line_duplicated": "В метаданных есть повторяющиеся объявления.", - "notification": { - "script_sync_delete": "Синхронизация удаления скрипта", - "script_sync_delete_desc": "Скрипт {{scriptName}} был удален", - "subscribe_update": "Подписка {{subscribeName}} была обновлена", - "subscribe_update_desc": "Новые скрипты: {{newScripts}}\nУдаленные скрипты: {{deletedScripts}}" - }, - "loading": "Загрузка...", - "runtime": "Время выполнения", - "enable_background": { - "title": "Включить фоновый режим", - "description": "Если включено, браузер продолжит работать в фоновом режиме после закрытия всех окон и будет свернут в трей, пока вы вручную не завершите его работу. Это позволяет фоновым скриптам продолжать выполняться.", - "enable_failed": "Не удалось включить", - "disable_failed": "Не удалось отключить", - "prompt_title": "Включить фоновое выполнение?", - "prompt_description": "Это {{scriptType}}. Включение фонового выполнения позволит скрипту продолжать работать после закрытия браузера.", - "enable_now": "Включить сейчас", - "maybe_later": "Может быть позже", - "settings_hint": "Вы можете изменить эту опцию в настройках в любое время." - }, - "favicon_service": "Сервис Favicon", - "favicon_service_desc": "Выберите сервис для получения значков сайтов", - "favicon_service_scriptcat": "ScriptCat", - "favicon_service_google": "Google", - "favicon_service_duckduckgo": "DuckDuckGo", - "favicon_service_icon-horse": "Icon Horse", - "favicon_service_local": "Локальное получение", - "editor": { - "show_script_list": "Показать список скриптов", - "hide_script_list": "Скрыть список скриптов" - }, - "agent": "AI Agent", - "agent_chat": "Chat", - "agent_provider": "Model Service", - "agent_mcp": "MCP", - "agent_skills": "Skills", - "agent_provider_title": "Model Service", - "agent_provider_select": "AI Provider", - "agent_provider_api_base_url": "API Base URL", - "agent_provider_api_key": "API Key", - "agent_provider_model": "Default Model", - "agent_provider_test_connection": "Test Connection", - "agent_provider_test_success": "Connection Successful", - "agent_provider_test_failed": "Connection Failed", - "agent_model_fetch": "Получить модели", - "agent_model_fetch_failed": "Не удалось получить список моделей", - "agent_model_name": "Name", - "agent_model_add": "Add Model", - "agent_model_edit": "Edit", - "agent_model_copy": "Copy", - "agent_model_delete": "Delete", - "agent_model_set_default": "Set as Default", - "agent_model_default_label": "Default", - "agent_model_delete_confirm": "Are you sure to delete this model?", - "agent_model_max_tokens": "Max Output Tokens", - "agent_model_no_models": "No models configured", - "agent_model_vision_support": "Supports vision input", - "agent_model_image_output": "Supports image output", - "agent_model_capabilities": "Возможности", - "agent_model_supports_vision": "Ввод изображений", - "agent_model_supports_image_output": "Вывод изображений", - "agent_coming_soon": "Coming soon...", - "agent_skills_title": "Skills Management", - "agent_skills_add": "Add Skill", - "agent_skills_empty": "No skills installed", - "agent_skills_tools": "Tools", - "agent_skills_references": "References", - "agent_skills_detail": "Skill Details", - "agent_skills_edit_prompt": "Prompt", - "agent_skills_install": "Install Skill", - "agent_skills_install_url": "Import from URL", - "agent_skills_install_paste": "Paste SKILL.md", - "agent_skills_uninstall": "Uninstall", - "agent_skills_uninstall_confirm": "Are you sure to uninstall Skill \"{{name}}\"?", - "agent_skills_save_success": "Saved successfully", - "agent_skills_install_success": "Installed successfully", - "agent_skills_fetch_failed": "Fetch failed", - "agent_skills_add_script": "Add Script", - "agent_skills_add_reference": "Add Reference", - "agent_skills_install_zip": "Upload ZIP", - "agent_skills_install_zip_hint": "Click to select a .zip file", - "agent_skills_prompt": "Prompt", - "agent_skills_installed_at": "Installed at", - "agent_skills_refresh": "Refresh", - "agent_skills_refresh_success": "Refreshed successfully", - "agent_skills_tool_code": "Tool Code", - "agent_skills_click_to_view_code": "Click tool name to view code", - "agent_skills_config": "Config", - "agent_skills_config_saved": "Config saved", - "agent_skills_check_updates": "Check Updates", - "agent_skills_no_updates": "All skills are up to date", - "agent_skills_updates_available": "update(s) available", - "agent_skills_update": "Update", - "agent_skills_update_success": "Updated successfully", - "agent_skills_url_placeholder": "Enter SKILL.cat.md URL", - "agent_chat_new": "New Chat", - "agent_chat_delete": "Delete Chat", - "agent_chat_delete_confirm": "Delete this conversation?", - "agent_chat_no_conversations": "No conversations", - "agent_chat_input_placeholder": "Type a message...", - "agent_chat_send": "Send", - "agent_chat_stop": "Stop", - "agent_chat_thinking": "Thinking", - "agent_chat_tool_call": "Tool Call", - "agent_chat_error": "Error occurred", - "agent_chat_no_model": "No model configured. Please add one in Model Service first.", - "agent_chat_model_select": "Select Model", - "agent_chat_rename": "Rename", - "agent_chat_copy": "Copy", - "agent_chat_copy_success": "Copied", - "agent_chat_regenerate": "Regenerate", - "agent_chat_streaming": "Generating...", - "agent_chat_newline": "for new line", - "agent_chat_welcome_hint": "Ask me anything about your scripts", - "agent_chat_welcome_start": "Create a conversation to get started", - "agent_chat_tokens": "tokens", - "agent_chat_first_token": "TTFT", - "agent_chat_tools_count": "{{count}} tools", - "agent_chat_tools_enabled": "Tools enabled", - "agent_chat_tools_disabled": "Tools disabled", - "agent_chat_tools_enabled_tip": "Tools enabled — click to disable", - "agent_chat_tools_disabled_tip": "Tools disabled — click to enable", - "agent_chat_background_enabled_tip": "Фоновый режим ВКЛ — диалог продолжит работу после закрытия страницы, нажмите для отключения", - "agent_chat_background_disabled_tip": "Фоновый режим ВЫКЛ — диалог остановится при закрытии страницы, нажмите для включения", - "agent_chat_delete_round": "Delete", - "agent_chat_copy_message": "Copy", - "agent_chat_edit_message": "Редактировать", - "agent_chat_save_and_send": "Сохранить и отправить", - "agent_chat_cancel_edit": "Отмена", - "agent_chat_message_queued": "В очереди", - "agent_chat_cancel_message": "Отменить отправку", - "agent_permission_title": "Скрипт запрашивает использование Agent разговора", - "agent_permission_describe": "Этот скрипт запрашивает доступ к функции Agent разговора, что будет потреблять API токены. Разрешайте только доверенным скриптам.", - "agent_permission_content": "Agent разговор", - "agent_opfs": "OPFS", - "agent_opfs_title": "Файловый браузер OPFS", - "agent_opfs_empty": "Пустая директория", - "agent_opfs_name": "Имя", - "agent_opfs_size": "Размер", - "agent_opfs_type": "Тип", - "agent_opfs_modified": "Последнее изменение", - "agent_opfs_delete_confirm": "Вы уверены, что хотите удалить?", - "agent_opfs_delete_success": "Удалено", - "agent_opfs_file": "Файл", - "agent_opfs_directory": "Директория", - "agent_opfs_preview": "Предпросмотр", - "agent_opfs_root": "Корень", - "agent_dom_permission_title": "Скрипт запрашивает доступ к операциям DOM", - "agent_dom_permission_describe": "Этот скрипт запрашивает возможность читать и управлять DOM веб-страницы (клик, заполнение форм, навигация, скриншот и т.д.). Разрешайте только доверенным скриптам.", - "agent_dom_permission_content": "Agent DOM операции", - "agent_mcp_title": "MCP серверы", - "agent_mcp_add_server": "Добавить сервер", - "agent_mcp_no_servers": "MCP серверы не настроены", - "agent_mcp_test_connection": "Тест", - "agent_mcp_name_url_required": "Имя и URL обязательны", - "agent_mcp_optional": "необязательно", - "agent_mcp_custom_headers": "Пользовательские заголовки", - "agent_mcp_enabled": "Включено", - "agent_mcp_detail": "Подробности", - "agent_mcp_tools": "Инструменты", - "agent_mcp_resources": "Ресурсы", - "agent_mcp_prompts": "Промпты", - "agent_mcp_no_tools": "Нет доступных инструментов", - "agent_mcp_no_resources": "Нет доступных ресурсов", - "agent_mcp_no_prompts": "Нет доступных промптов", - "agent_mcp_loading": "Загрузка...", - "agent_mcp_parameters": "Параметры", - "agent_settings": "Настройки", - "agent_settings_title": "Настройки Agent", - "agent_model_settings": "Настройки модели", - "agent_summary_model": "Модель для резюме", - "agent_summary_model_desc": "Используется для суммаризации веб-страниц. При отсутствии используется модель по умолчанию", - "agent_summary_model_placeholder": "Использовать модель по умолчанию", - "agent_search_settings": "Настройки поиска", - "agent_search_engine": "Поисковая система", - "agent_search_engine_baidu": "Baidu", - "agent_search_google_api_key": "Google API Key", - "agent_search_google_cse_id": "ID пользовательской поисковой системы", - "agent_settings_saved": "Настройки сохранены", - "agent_settings_save_failed": "Не удалось сохранить настройки", - "agent_search_engine_tip_bing": "Поисковая система по умолчанию с широким глобальным охватом, дополнительная настройка не требуется.", - "agent_search_engine_tip_duckduckgo": "Поисковая система с акцентом на конфиденциальность, API-ключ не требуется.", - "agent_search_engine_tip_baidu": "Оптимизирован для китайского контента, лучшие результаты для запросов на китайском языке.", - "agent_search_engine_tip_google": "Высококачественные результаты поиска, требуется Google API Key и ID пользовательской поисковой системы." -} \ No newline at end of file diff --git a/src/locales/vi-VN/agent.json b/src/locales/vi-VN/agent.json new file mode 100644 index 000000000..13d516ac9 --- /dev/null +++ b/src/locales/vi-VN/agent.json @@ -0,0 +1,252 @@ +{ + "title": "AI Agent", + "docs": "Tài liệu", + "chat": "Chat", + "provider": "Model Service", + "mcp": "MCP", + "skills": "Skills", + "provider_title": "Model Service", + "provider_subtitle": "Manage AI model providers and connections", + "provider_select": "AI Provider", + "provider_api_base_url": "API Base URL", + "provider_api_key": "API Key", + "provider_model": "Default Model", + "provider_test_connection": "Test Connection", + "provider_test_success": "Connection Successful", + "provider_test_failed": "Connection Failed", + "provider_test_hint": "Kiểm tra sẽ gửi một yêu cầu tối thiểu để xác minh cấu hình", + "provider_docs": "Tài liệu", + "provider_count_hint": "Mô hình mặc định được dùng cho cuộc trò chuyện mới", + "model_count": "{{count}} mô hình", + "model_fetch": "Fetch Models", + "model_fetch_failed": "Failed to fetch models", + "model_name": "Name", + "model_add": "Add Model", + "model_edit": "Edit", + "model_copy": "Copy", + "model_delete": "Delete", + "model_set_default": "Set as Default", + "model_default_label": "Default", + "model_delete_confirm": "Are you sure to delete this model?", + "model_max_tokens": "Max Output Tokens", + "model_context_window": "Cửa sổ ngữ cảnh", + "model_no_models": "No models configured", + "model_no_models_desc": "Add your first model to start using the AI Agent", + "model_vision_support": "Supports vision input", + "model_image_output": "Supports image output", + "model_capabilities": "Khả năng", + "model_supports_vision": "Nhập hình ảnh", + "model_supports_image_output": "Xuất hình ảnh", + "coming_soon": "Coming soon...", + "skills_title": "Skills Management", + "skills_subtitle": "Manage agent skill packages (SKILL.md prompts, scripts, references)", + "skills_add": "Add Skill", + "skills_empty": "No skills installed", + "skills_empty_desc": "Upload a ZIP or import your first skill from a URL", + "skills_tools": "Tools", + "skills_references": "References", + "skills_detail": "Skill Details", + "skills_edit_prompt": "Prompt", + "skills_install": "Install Skill", + "skills_install_url": "Import from URL", + "skills_install_paste": "Paste SKILL.md", + "skills_uninstall": "Uninstall", + "skills_uninstall_confirm": "Are you sure to uninstall Skill \"{{name}}\"?", + "skills_save_success": "Saved successfully", + "skills_install_success": "Installed successfully", + "skills_fetch_failed": "Fetch failed", + "skills_add_script": "Add Script", + "skills_add_reference": "Add Reference", + "skills_install_zip": "Upload ZIP", + "skills_install_zip_hint": "Click to select a .zip file", + "skills_prompt": "Prompt", + "skills_installed_at": "Installed at", + "skills_refresh": "Refresh", + "skills_refresh_success": "Refreshed successfully", + "skills_tool_code": "Tool Code", + "skills_click_to_view_code": "Click tool name to view code", + "skills_config": "Config", + "skills_config_saved": "Config saved", + "skills_check_updates": "Check Updates", + "skills_no_updates": "All skills are up to date", + "skills_updates_available": "update(s) available", + "skills_update": "Update", + "skills_docs": "Tài liệu", + "skills_count": "Đã cài đặt {{count}} Skill", + "skills_update_count": "{{count}} bản cập nhật có sẵn", + "skills_update_available": "Cập nhật {{version}}", + "skills_references_short": "Tham khảo", + "skills_configurable": "Có thể cấu hình", + "skills_open_config": "Mở cấu hình", + "skills_update_success": "Updated successfully", + "skills_url_placeholder": "Enter SKILL.cat.md URL", + "chat_new": "New Chat", + "chat_delete": "Delete Chat", + "chat_delete_confirm": "Delete this conversation?", + "chat_no_conversations": "No conversations", + "chat_search_placeholder": "Tìm cuộc trò chuyện…", + "chat_search_no_results": "Không có cuộc trò chuyện phù hợp", + "chat_export": "Xuất", + "chat_input_placeholder": "Type a message...", + "chat_send": "Send", + "chat_stop": "Stop", + "chat_thinking": "Thinking", + "chat_tool_call": "Tool Call", + "chat_tool_arguments": "Tham số", + "chat_tool_result": "Kết quả", + "chat_error": "Error occurred", + "chat_no_model": "No model configured. Please add one in Model Service first.", + "chat_model_select": "Select Model", + "chat_rename": "Rename", + "chat_copy": "Copy", + "chat_copy_success": "Copied", + "chat_regenerate": "Regenerate", + "chat_streaming": "Generating...", + "chat_retrying": "Đang thử lại ({{attempt}}/{{max}})...", + "chat_attach_image": "Đính kèm hình ảnh", + "chat_attach_file": "Đính kèm tệp", + "chat_welcome_hint": "Ask me anything about your scripts", + "chat_welcome_start": "Create a conversation to get started", + "chat_tokens": "tokens", + "chat_first_token": "TTFT", + "chat_tools_count": "{{count}} tools", + "chat_tools_enabled": "Tools enabled", + "chat_tools_disabled": "Tools disabled", + "chat_tools_enabled_tip": "Tools enabled — click to disable", + "chat_tools_disabled_tip": "Tools disabled — click to enable", + "chat_background_enabled_tip": "Chế độ nền BẬT — cuộc hội thoại tiếp tục chạy sau khi đóng trang, nhấn để tắt", + "chat_background_disabled_tip": "Chế độ nền TẮT — cuộc hội thoại dừng khi đóng trang, nhấn để bật", + "chat_delete_round": "Delete", + "chat_copy_message": "Copy", + "chat_edit_message": "Chỉnh sửa", + "chat_save_and_send": "Lưu & Gửi", + "chat_cancel_edit": "Hủy", + "chat_message_queued": "Đang chờ", + "chat_cancel_message": "Hủy gửi", + "permission_title": "Script yêu cầu sử dụng cuộc trò chuyện Agent", + "permission_describe": "Script này yêu cầu quyền truy cập chức năng cuộc trò chuyện Agent, sẽ tiêu thụ API token. Chỉ cấp quyền cho các script đáng tin cậy.", + "permission_content": "Cuộc trò chuyện Agent", + "opfs": "OPFS", + "opfs_title": "Trình duyệt tệp OPFS", + "opfs_empty": "Thư mục trống", + "opfs_name": "Tên", + "opfs_size": "Kích thước", + "opfs_type": "Loại", + "opfs_modified": "Sửa đổi lần cuối", + "opfs_delete_confirm": "Bạn có chắc chắn muốn xóa không?", + "opfs_delete_success": "Đã xóa", + "opfs_file": "Tệp", + "opfs_directory": "Thư mục", + "opfs_preview": "Xem trước", + "opfs_subtitle": "Origin Private File System · Lưu trữ riêng của Agent", + "opfs_refresh": "Làm mới", + "opfs_upload": "Tải lên", + "opfs_actions": "Thao tác", + "opfs_item_count": "{{count}} mục", + "opfs_empty_desc": "Chưa có tệp nào trong thư mục này", + "opfs_upload_success": "Đã tải lên", + "opfs_upload_failed": "Tải lên thất bại", + "opfs_type_directory": "Thư mục", + "opfs_type_image": "Hình ảnh", + "opfs_type_text": "Văn bản", + "opfs_type_binary": "Nhị phân", + "opfs_root": "Gốc", + "dom_permission_title": "Script yêu cầu quyền thao tác DOM", + "dom_permission_describe": "Script này yêu cầu khả năng đọc và thao tác DOM trang web (nhấp chuột, điền biểu mẫu, điều hướng, chụp ảnh màn hình, v.v.). Chỉ cấp quyền cho các script đáng tin cậy.", + "dom_permission_content": "Agent DOM thao tác", + "mcp_title": "Máy chủ MCP", + "mcp_subtitle": "Connect MCP servers to extend agent tools, resources and prompts", + "mcp_add_server": "Thêm máy chủ", + "mcp_no_servers": "Chưa cấu hình máy chủ MCP", + "mcp_no_servers_desc": "Add your first MCP server to connect external tools and data", + "mcp_status_connected": "Connected", + "mcp_status_failed": "Connection failed", + "mcp_status_untested": "Not tested", + "mcp_has_key": "Khóa bí mật", + "mcp_headers_count": "{{count}} tiêu đề", + "mcp_count_servers": "{{count}} máy chủ", + "mcp_count_connected": "{{count}} đã kết nối", + "mcp_count_tools": "{{count}} công cụ khả dụng", + "mcp_docs": "Tài liệu", + "mcp_test_connection": "Kiểm tra", + "mcp_name_url_required": "Tên và URL là bắt buộc", + "mcp_optional": "tùy chọn", + "mcp_custom_headers": "Header tùy chỉnh", + "mcp_enabled": "Đã bật", + "mcp_detail": "Chi tiết", + "mcp_tools": "Công cụ", + "mcp_resources": "Tài nguyên", + "mcp_prompts": "Lời nhắc", + "mcp_no_tools": "Không có công cụ nào", + "mcp_no_resources": "Không có tài nguyên nào", + "mcp_no_prompts": "Không có lời nhắc nào", + "mcp_loading": "Đang tải...", + "mcp_parameters": "Tham số", + "tasks": "Tác vụ định kỳ", + "tasks_title": "Quản lý tác vụ định kỳ", + "tasks_subtitle": "Tự động chạy tác vụ Agent theo lịch cron", + "settings": "Cài đặt", + "settings_title": "Cài đặt Agent", + "settings_subtitle": "Mô hình, tìm kiếm và tùy chọn chung · thay đổi áp dụng tức thì", + "settings_cat_model": "Mô hình", + "settings_cat_search": "Tìm kiếm", + "model_settings": "Cài đặt mô hình", + "summary_model": "Mô hình tóm tắt", + "summary_model_desc": "Dùng cho tóm tắt trang web. Nếu chưa đặt sẽ dùng mô hình mặc định", + "summary_model_placeholder": "Dùng mô hình mặc định", + "search_settings": "Cài đặt tìm kiếm", + "search_engine": "Công cụ tìm kiếm", + "search_engine_desc": "Nguồn truy xuất được công cụ web_search sử dụng.", + "search_engine_baidu": "Baidu", + "search_google_api_key": "Google API Key", + "search_google_api_key_desc": "Dùng để gọi Custom Search JSON API.", + "search_google_cse_id": "ID công cụ tìm kiếm tùy chỉnh", + "search_google_cse_id_desc": "Tham số cx của Programmable Search Engine.", + "settings_saved": "Đã lưu cài đặt", + "settings_save_failed": "Lưu cài đặt thất bại", + "search_engine_tip_bing": "Công cụ tìm kiếm mặc định, phạm vi toàn cầu rộng, không cần cấu hình thêm.", + "search_engine_tip_duckduckgo": "Công cụ tìm kiếm chú trọng quyền riêng tư, không cần API Key.", + "search_engine_tip_baidu": "Tối ưu cho nội dung tiếng Trung, kết quả tốt hơn cho truy vấn tiếng Trung.", + "search_engine_tip_google": "Kết quả tìm kiếm chất lượng cao, cần cấu hình Google API Key và ID công cụ tìm kiếm tùy chỉnh.", + "tasks_docs": "Tài liệu", + "tasks_count_total": "{{count}} tác vụ", + "tasks_count_enabled": "{{count}} đã bật", + "tasks_create": "Tạo tác vụ", + "tasks_edit": "Chỉnh sửa tác vụ", + "tasks_mode": "Chế độ", + "tasks_mode_internal": "Chạy nội bộ", + "tasks_mode_event": "Theo sự kiện", + "tasks_mode_internal_short": "Nội bộ", + "tasks_mode_event_short": "Sự kiện", + "tasks_cron": "Biểu thức cron", + "tasks_next_run": "Lần chạy tiếp theo", + "tasks_last_status": "Trạng thái gần nhất", + "tasks_run_now": "Chạy ngay", + "tasks_history": "Lịch sử", + "tasks_prompt": "Câu lệnh", + "tasks_max_iterations": "Số lần lặp tối đa", + "tasks_notify": "Thông báo khi hoàn tất", + "tasks_notify_desc": "Gửi thông báo trình duyệt khi tác vụ hoàn tất", + "tasks_no_tasks": "Chưa có tác vụ định kỳ", + "tasks_no_tasks_desc": "Tạo tác vụ định kỳ đầu tiên để Agent tự chạy theo lịch", + "tasks_no_runs": "Chưa có lịch sử chạy", + "tasks_delete_confirm": "Bạn có chắc muốn xóa tác vụ định kỳ này không?", + "tasks_clear_runs": "Xóa lịch sử", + "tasks_clear_runs_confirm": "Bạn có chắc muốn xóa lịch sử chạy của tác vụ này không?", + "tasks_event_hint": "Khi được kích hoạt, script đã tạo tác vụ này sẽ được thông báo", + "tasks_event_trigger": "Kích hoạt sự kiện", + "tasks_name_cron_required": "Tên và biểu thức cron không được để trống", + "tasks_model_select": "Chọn mô hình", + "tasks_skills": "Skills", + "tasks_skills_auto": "Tự động tải tất cả", + "tasks_conversation_id": "ID hội thoại tiếp nối (tùy chọn)", + "tasks_run_status_success": "Thành công", + "tasks_run_status_error": "Lỗi", + "tasks_run_status_running": "Đang chạy", + "tasks_run_duration": "Thời lượng", + "tasks_run_usage": "Mức dùng", + "tasks_run_conversation": "Xem hội thoại", + "tasks_run_time": "Thời gian", + "tasks_run_status": "Trạng thái", + "tasks_never_run": "Chưa chạy" +} diff --git a/src/locales/vi-VN/common.json b/src/locales/vi-VN/common.json new file mode 100644 index 000000000..ce64c180d --- /dev/null +++ b/src/locales/vi-VN/common.json @@ -0,0 +1,131 @@ +{ + "user_guide": "Hướng dẫn sử dụng", + "api_docs": "Tài liệu api", + "development_guide": "Hướng dẫn phát triển", + "script_gallery": "Thư viện script", + "community_forum": "Diễn đàn cộng đồng", + "external_links": "Liên kết bên ngoài", + "system_follow": "Theo hệ thống", + "no_data": "Không có dữ liệu", + "logs": "Nhật ký", + "tools": "Công cụ", + "find": "Tìm kiếm", + "replace": "Thay thế", + "settings": "Cài đặt", + "change_theme": "Change Theme", + "hide_main_sidebar": "Thu gọn thanh bên", + "show_main_sidebar": "Mở rộng thanh bên", + "menu": "Menu", + "guide": "Hướng dẫn", + "helpcenter": "Trung tâm trợ giúp", + "save": "Lưu", + "file": "Tệp", + "save_success": "Lưu thành công", + "reset_success": "Đặt lại thành công", + "update": "Cập nhật", + "check_update": "Kiểm tra cập nhật", + "confirm_delete": "Xác nhận xóa", + "confirm_update": "Xác nhận cập nhật", + "delete_success": "Xóa thành công", + "deleting": "Đang xóa", + "enable": "Bật", + "script_list_enable_width": 100, + "script_list_last_updated_width": 150, + "script_list_apply_to_run_status_width": 110, + "subscribe_list_enable_width": 120, + "disable": "Tắt", + "name": "Tên", + "version": "Phiên bản", + "source": "Nguồn", + "home": "Trang chủ", + "action": "Hành động", + "export": "Xuất", + "delete": "Xóa", + "pin_to_top": "Ghim lên đầu", + "confirm": "Xác nhận", + "close": "Đóng", + "config": "Cấu hình", + "key": "Khóa", + "value": "Giá trị", + "add": "Thêm", + "type": "Loại", + "size": "Kích thước", + "download": "Tải xuống", + "edit_value": "Sửa giá trị", + "add_value": "Thêm giá trị", + "update_success": "Cập nhật thành công", + "add_success": "Thêm thành công", + "clear": "Xóa", + "type_string": "Chuỗi", + "type_number": "Số", + "type_boolean": "Boolean", + "type_object": "Đối tượng", + "confirm_delete_resource": "Bạn có chắc chắn muốn xóa tài nguyên này không? Tài nguyên này sẽ tải lại vào lần khởi động tiếp theo.", + "confirm_clear_resource": "Bạn có chắc chắn muốn xóa các tài nguyên này không? Các tài nguyên sẽ tải lại vào lần khởi động tiếp theo.", + "yes": "Có", + "no": "Không", + "confirm_delete_permission": "Bạn có chắc muốn xóa quyền này không?", + "reset": "Đặt lại", + "run_once": "Chạy một lần", + "stop": "Dừng", + "edit": "Sửa", + "copy": "Sao chép", + "exclude_on": "Cho phép chạy lại $0", + "exclude_off": "Loại trừ chạy $0", + "get_confirm_error": "Lấy thông tin xác nhận thất bại", + "confirm_error": "Xác nhận thất bại", + "ignore": "Bỏ qua", + "temporary_allow": "Tạm thời cho phép {{permissionContent}} này", + "temporary_allow_all": "Tạm thời cho phép tất cả {{permissionContent}}", + "permanent_allow": "Cho phép vĩnh viễn {{permissionContent}} này", + "permanent_allow_all": "Cho phép vĩnh viễn tất cả {{permissionContent}}", + "temporary_deny": "Tạm thời từ chối {{permissionContent}} này", + "temporary_deny_all": "Tạm thời từ chối tất cả {{permissionContent}}", + "permanent_deny": "Từ chối vĩnh viễn {{permissionContent}} này", + "permanent_deny_all": "Từ chối vĩnh viễn tất cả {{permissionContent}}", + "import": "Nhập", + "author": "Tác giả", + "description": "Mô tả", + "operation": "Thao tác", + "error": "Lỗi", + "unknown": "Không xác định", + "add_new": "Thêm mới", + "no_operation": "Không có thao tác nào", + "enable_script": "Bật script", + "local_creation": "Tạo cục bộ", + "import_success": "Nhập thành công", + "get_script": "Lấy script", + "report_issue": "Báo cáo sự cố", + "project_docs": "Tài liệu dự án", + "community": "Cộng đồng", + "domain": "Miền", + "script_name": "Tên script", + "skip": "Bỏ qua", + "next": "Tiếp theo", + "next_with_progress": "Tiếp theo (bước {step} trên {steps})", + "back": "Quay lại", + "last": "Hoàn thành", + "auto": "Tự động", + "hide": "Ẩn", + "custom": "Tùy chỉnh", + "resize_column_width": "Thay đổi kích thước cột", + "collapse": "Thu gọn", + "expand": "Mở rộng", + "import_script_placeholder": "Hỗ trợ liên kết tuyệt đối kết thúc bằng .user.js hoặc liên kết đến trang cài đặt scriptcat\ncó thể điền nhiều dòng, mỗi mục\nví dụ:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/en/script-show-page/1234", + "light": "Sáng", + "dark": "Tối", + "enter_search_value": "Nhập {{search}} để tìm kiếm", + "no_message_content": "Không có nội dung tin nhắn", + "loading": "Đang tải...", + "auth_type": "Loại xác thực", + "url": "Url", + "username": "Tên người dùng", + "password": "Mật khẩu", + "access_token_bearer": "Mã truy cập (Bearer)", + "s3_bucket_name": "Tên Bucket", + "s3_region": "Vùng", + "s3_access_key_id": "ID Khóa Truy Cập", + "s3_secret_access_key": "Khóa Truy Cập Bí Mật", + "s3_custom_endpoint": "Điểm Cuối Tùy Chỉnh (Tùy Chọn)", + "cancel": "Hủy" +} diff --git a/src/locales/vi-VN/editor.json b/src/locales/vi-VN/editor.json new file mode 100644 index 000000000..819cb4bd5 --- /dev/null +++ b/src/locales/vi-VN/editor.json @@ -0,0 +1,132 @@ +{ + "save": "Lưu", + "save_success": "Đã lưu thành công", + "copy": "Sao chép", + "find": "Tìm", + "replace": "Thay thế", + "select_all": "Chọn tất cả", + "format": "Định dạng", + "back": "Quay lại", + "more": "Thêm", + "file": "Tệp", + "edit": "Sửa", + "settings": "Cài đặt", + "run_settings": "Cài đặt chạy", + "code": "Mã", + "script_setting": "Cài đặt tập lệnh", + "storage": "Lưu trữ", + "resource": "Tài nguyên", + "search_resource": "Tìm tài nguyên", + "search_storage": "Tìm Key", + "record_count": "{{count}} mục", + "resource_count": "{{count}} tài nguyên · {{size}}", + "script_list": "Danh sách tập lệnh", + "search_scripts": "Tìm tập lệnh...", + "new_script": "Tập lệnh mới", + "source": "Nguồn", + "from_user": "Người dùng", + "from_script": "Script", + "last_updated": "Cập nhật gần nhất", + "run_at": "Thời điểm chạy", + "run_in": "Môi trường chạy", + "check_update": "Kiểm tra cập nhật", + "line_col": "Dòng {{line}}, Cột {{col}}", + "script_not_found": "Không tìm thấy tập lệnh", + "confirm_delete_script": "Xóa tập lệnh \"{{name}}\"?", + "delete_success": "Đã xóa thành công", + "delete_failed": "Xóa thất bại", + "cancel": "Hủy", + "confirm": "Xác nhận", + "save_as": "Lưu thành", + "run": "Chạy", + "debug": "Gỡ lỗi", + "script_storage": "Lưu trữ script", + "enter_key": "Vui lòng nhập khóa", + "key_placeholder": "Khóa", + "value_placeholder": "Khi loại là đối tượng, vui lòng nhập dữ liệu có thể phân tích cú pháp json.", + "clear_success": "Xóa thành công", + "confirm_clear": "Bạn có chắc chắn muốn xóa không gian lưu trữ này không?", + "script_resource": "Tài nguyên script", + "basic_info": "Thông tin cơ bản", + "update_url": "Url cập nhật", + "add_permission": "Thêm quyền", + "match": "Khớp", + "user_setting": "Cài đặt người dùng", + "confirm_delete_exclude": "Xác nhận xóa loại trừ này?", + "after_deleting_match_item": "Sau khi các mục khớp của script bị xóa, chúng sẽ tự động được thêm vào các mục khớp", + "confirm_delete_match": "Xác nhận xóa khớp này?", + "after_deleting_exclude_item": "Sau khi xóa mục khớp của script, nó sẽ tự động được thêm vào các mục loại trừ", + "add_match": "Thêm khớp", + "add_exclude": "Thêm loại trừ", + "website_match": "Khớp trang web (@match)", + "website_exclude": "Loại trừ trang web (@exclude)", + "confirm_reset": "Xác nhận đặt lại?", + "undo": "Hoàn tác", + "redo": "Làm lại", + "cut": "Cắt", + "paste": "Dán", + "user_config": "Cấu hình người dùng", + "gm_api": "Api gm", + "storage_api": "Api lưu trữ", + "use_file_system": "Hệ thống tệp đang được sử dụng", + "open_directory": "Mở thư mục", + "account_validation_failed": "Xác thực tài khoản thất bại", + "not_set": "Chưa đặt", + "in_use": "Đang sử dụng", + "storage_error": "Lỗi lưu trữ", + "upload_to_cloud": "Tải lên đám mây", + "save_failed": "Lưu thất bại", + "exporting": "Đang xuất...", + "upload_to": "Tải lên", + "value_export_expression": "Biểu thức xuất giá trị", + "overwrite_original_value_on_import": "Ghi đè giá trị gốc khi nhập", + "cookie_export_expression": "Biểu thức xuất cookie", + "overwrite_original_cookie_on_import": "Ghi đè cookie gốc khi nhập", + "restore_default_values": "Khôi phục giá trị mặc định", + "edit_conflict": "Xung đột chỉnh sửa", + "confirm_override_when_edit_conflict": "Tập lệnh này đã được chỉnh sửa ở một phiên bản khác. Việc thay thế sẽ ghi đè các thay đổi đó. Bạn có muốn giữ phiên bản này không?", + "save_abort_when_edit_conflict": "Tập lệnh đã được chỉnh sửa ở một phiên bản khác. Đã hủy lưu.", + "scriptname_conflict": "Xung đột tên script", + "confirm_save_when_scriptname_conflict": "Tên script này đã được sử dụng bởi một script khác. Bạn vẫn muốn lưu chứ?", + "save_abort_when_scriptname_conflict": "Tên script này đã được sử dụng bởi một script khác. Đã hủy lưu.", + "eslint_config_format_error": "Lỗi định dạng cấu hình eslint", + "script_modified_leave_confirm": "Nội dung hiện chưa được lưu. Nếu rời khỏi trang, các thay đổi sẽ bị mất. Bạn có chắc chắn muốn rời đi không?", + "create_success_note": "Script mới được tạo thành công. Lưu ý rằng script nền sẽ không được bật theo mặc định.", + "save_as_failed": "Lưu thành thất bại", + "save_as_success": "Lưu thành công", + "only_background_scheduled_can_run": "Chỉ script nền/script crontab mới có thể chạy", + "preparing_script_resources": "Đang chuẩn bị tài nguyên script...", + "build_success_message": "Xây dựng thành công, mở công cụ nhà phát triển để xem đầu ra trong bảng điều khiển", + "script_storage_tooltip": "Có thể quản lý dữ liệu script đã lưu trữ (gm_value)", + "script_resource_tooltip": "Quản lý @resource, @required các tài nguyên đã tải xuống", + "script_setting_tooltip": "Thực hiện một số cài đặt tùy chỉnh cho script", + "script_modified_close_confirm": "Script đã được sửa đổi. Các thay đổi sẽ bị mất sau khi đóng, tiếp tục?", + "close_current_tab": "Đóng tab hiện tại", + "close_other_tabs": "Đóng các tab khác", + "close_left_tabs": "Đóng các tab bên trái", + "close_right_tabs": "Đóng các tab bên phải", + "invalid_script_code": "Mã không hợp lệ", + "build_failed": "Xây dựng thất bại", + "drag_script_here_to_upload": "Kéo script vào đây để tải lên", + "watch_file_description": "Theo dõi các thay đổi tệp và tự động cập nhật script. Khi sử dụng, vui lòng đảm bảo đường dẫn tệp script không thay đổi và không thể đóng trang.", + "watch_file": "Theo dõi tệp", + "stop_watch_file": "Dừng theo dõi", + "individual_edit": "Chỉnh sửa cá nhân", + "batch_edit": "Chỉnh sửa hàng loạt", + "script_code": "Mã kịch bản", + "editor_config": "Cấu hình trình soạn thảo", + "editor_config_description": "Bạn có thể tham khảo compilerOptions trong jsconfig.js để cấu hình", + "editor_type_definition": "Định nghĩa kiểu trình soạn thảo", + "editor_type_definition_description": "Bạn có thể tự định nghĩa các kiểu của riêng mình, trình soạn thảo script sẽ tự động tải những định nghĩa kiểu này", + "eslint_rules_reset": "Quy tắc ESLint đã được đặt lại", + "eslint_rules_saved": "Quy tắc ESLint đã được lưu", + "editor_config_reset": "Cấu hình trình soạn thảo đã được đặt lại", + "editor_config_saved": "Cấu hình trình soạn thảo đã được lưu", + "editor_config_format_error": "Lỗi định dạng cấu hình trình soạn thảo", + "editor_type_definition_reset": "Định nghĩa kiểu trình soạn thảo đã được đặt lại", + "editor_type_definition_saved": "Định nghĩa kiểu trình soạn thảo đã được lưu", + "editor": { + "show_script_list": "Hiển thị danh sách script", + "hide_script_list": "Ẩn danh sách script" + } +} diff --git a/src/locales/vi-VN/guide.json b/src/locales/vi-VN/guide.json new file mode 100644 index 000000000..bff1681ba --- /dev/null +++ b/src/locales/vi-VN/guide.json @@ -0,0 +1,25 @@ +{ + "start_title": "Chào mừng bạn đến với tiện ích scriptcat", + "start_content": "Tiếp theo, chúng tôi sẽ giới thiệu cho bạn cách sử dụng cơ bản của scriptcat.", + "installed_scripts": "Các script bạn đã cài đặt sẽ được hiển thị ở đây.", + "script_list_title": "Chợ script", + "script_list_content": "Bạn có thể cài đặt script từ chợ script. Ngoài việc hỗ trợ script người dùng, scriptcat còn hỗ trợ script nền.", + "script_list_enable_title": "Bật script", + "script_list_enable_content": "Script cần được bật để sử dụng. Script trang được bật theo mặc định khi cài đặt, trong khi script nền bị tắt theo mặc định.", + "script_list_apply_to_run_status_title": "Áp dụng cho và trạng thái chạy", + "script_list_apply_to_run_status_content": "Trạng thái chạy của script được hiển thị ở đây. Di chuột qua các thẻ để xem loại script.", + "script_list_sort_title": "Sắp xếp", + "script_list_sort_content": "Bạn có thể kéo nhãn tập lệnh để sắp xếp chúng.", + "script_list_update_title": "Cập nhật lần cuối", + "script_list_update_content": "Nhấp vào nhãn cột \"Cập nhật lần cuối\" sẽ thực hiện kiểm tra các bản cập nhật cho tập lệnh.", + "script_list_action_title": "Thao tác", + "script_list_action_content": "Thanh thao tác cho phép truy cập vào chức năng chỉnh sửa tập lệnh, kiểm soát việc thực thi và kết thúc tập lệnh (tập lệnh nền) và cài đặt (xem UserConfig). Các nút bên phải cho phép truy cập vào chức năng lọc nâng cao và chuyển đổi chế độ xem.", + "tools_title": "Công cụ chung", + "tools_content": "Các công cụ cung cấp tính năng sao lưu và phát triển.", + "tools_backup_title": "Sao lưu", + "tools_backup_content": "Sao lưu cho phép bạn lưu script để tránh mất mát. Bạn có thể xuất tệp script sang máy cục bộ hoặc tải tệp script từ máy cục bộ của mình. Bạn cũng có thể sao lưu lên đám mây để thuận tiện hơn.", + "setting_title": "Cài đặt", + "setting_content": "Các cài đặt chủ yếu bao gồm ngôn ngữ, đồng bộ script, tần suất cập nhật và các tùy chọn chung khác.", + "setting_sync_title": "Cập nhật và đồng bộ", + "setting_sync_content": "Tính năng đồng bộ script cho phép bạn dễ dàng đồng bộ nội dung script của thiết bị này lên đám mây. Nếu bạn chọn tùy chọn đồng bộ xóa, khi một script bị xóa trên thiết bị này, nó cũng sẽ bị xóa khỏi đám mây. Bạn cũng có thể cập nhật phiên bản script ở đây để có được các tính năng mạnh mẽ hơn." +} diff --git a/src/locales/vi-VN/index.ts b/src/locales/vi-VN/index.ts new file mode 100644 index 000000000..bacd6d096 --- /dev/null +++ b/src/locales/vi-VN/index.ts @@ -0,0 +1,11 @@ +export { default as agent } from "./agent.json"; +export { default as common } from "./common.json"; +export { default as editor } from "./editor.json"; +export { default as guide } from "./guide.json"; +export { default as install } from "./install.json"; +export { default as logs } from "./logs.json"; +export { default as permission } from "./permission.json"; +export { default as popup } from "./popup.json"; +export { default as script } from "./script.json"; +export { default as settings } from "./settings.json"; +export { default as tools } from "./tools.json"; diff --git a/src/locales/vi-VN/install.json b/src/locales/vi-VN/install.json new file mode 100644 index 000000000..452c58917 --- /dev/null +++ b/src/locales/vi-VN/install.json @@ -0,0 +1,207 @@ +{ + "data_import": "Nhập dữ liệu", + "select_scripts_to_import": "Vui lòng chọn các script bạn muốn nhập", + "select_all": "Chọn tất cả", + "script_import_progress": "Tiến độ nhập script", + "select_subscribes_to_import": "Vui lòng chọn các đăng ký bạn muốn nhập", + "subscribe_import_progress": "Tiến độ nhập đăng ký", + "script": "Cài đặt script", + "update_script": "Cập nhật script", + "subscribe": "Cài đặt đăng ký", + "update_subscribe": "Cập nhật đăng ký", + "update_script_no_close": "Cập nhật script mà không đóng cửa sổ", + "script_no_close": "Cài đặt script mà không đóng cửa sổ", + "update_script_no_more_update": "Cập nhật script và tắt kiểm tra cập nhật", + "close_update_script_no_more_update": "Đóng và không kiểm tra cập nhật", + "script_no_more_update": "Cài đặt script và tắt kiểm tra cập nhật", + "invalid_link": "Liên kết không hợp lệ", + "subscribe_install_label": "Đăng ký này sẽ cài đặt các script sau", + "subscribe_scripts_title": "Đăng ký này sẽ cài đặt các script sau", + "subscribe_scripts_empty": "Đăng ký này chưa khai báo script nào", + "script_runs_in": "Script sẽ chạy trên các trang web sau", + "script_has_full_access_to": "Script sẽ có toàn quyền truy cập vào các url sau", + "script_requires": "Script yêu cầu các tài nguyên bên ngoài sau", + "cookie_warning": "Xin lưu ý, script này yêu cầu quyền truy cập cookie, đây là một quyền nguy hiểm. Vui lòng xác minh tính bảo mật của script.", + "perm_card_title": "Script này sẽ được cấp các quyền sau", + "perm_card_hint": "Vui lòng xác nhận trước khi cài đặt", + "perm_card_empty": "Script này không yêu cầu bất kỳ quyền đặc biệt nào", + "perm_match_label": "Chạy trên", + "perm_match_summary": "Script chạy trên các website này và thay đổi trang", + "perm_connect_label": "Truy cập khác nguồn gốc", + "perm_connect_summary": "Có thể gửi yêu cầu đến và đọc dữ liệu từ các tên miền sau", + "perm_grant_label": "Khả năng GM", + "perm_grant_summary": "Có thể gọi các GM API sau", + "perm_require_label": "Tài nguyên bên ngoài", + "perm_require_summary": "Tải các script và tài nguyên của bên thứ ba sau", + "badge_background": "Nền", + "badge_scheduled": "Hẹn giờ", + "enabled_label": "Đã bật", + "schedule_cron_label": "Tác vụ hẹn giờ", + "schedule_next_run": "Lần chạy tiếp theo", + "schedule_background_desc": "Tự động chạy khi trình duyệt đang mở", + "code_lines": "{{count}} dòng", + "code_copy": "Sao chép mã", + "code_collapse": "Thu gọn", + "code_expand": "Mở rộng", + "loading_title": "Đang tải script", + "loading_desc": "Đang tải xuống và phân tích nội dung script từ nguồn", + "error_retry": "Thử lại", + "error_invalid_desc": "Thiếu tham số nguồn cài đặt hợp lệ nên không thể tải script.", + "context_install": "Cài đặt script", + "context_update": "Cập nhật script", + "background_script": "Script nền", + "scheduled_script": "Script hẹn giờ", + "watching_status": "Đang theo dõi thay đổi tệp; sẽ tự động cài đặt lại sau khi lưu", + "watching_chip": "Đang theo dõi", + "watching_title": "Đang theo dõi thay đổi tệp", + "watching_file_desc": "Lưu {{file}} sẽ tự động cài đặt lại và đồng bộ script", + "watching_last_sync": "Đồng bộ lần cuối {{time}}", + "warning_title": "Vui lòng xác nhận script đến từ nguồn đáng tin cậy", + "warning_risk_connect": "nó có thể truy cập mọi tên miền", + "warning_risk_antifeature": "nó khai báo các tính năng đối nghịch", + "warning_risk_join": " và ", + "warning_risk_tail": " — hãy cài đặt thận trọng.", + "action_note_install": "Cài đặt nghĩa là bạn tin tưởng nguồn và tác giả của script này", + "action_note_update": "Cập nhật nghĩa là bạn chấp nhận thay đổi mã và quyền này", + "action_note_subscribe": "Cài đặt nghĩa là bạn tin tưởng đăng ký này và tác giả của nó", + "action_note_watching": "Đang theo dõi — lưu tệp sẽ tự động cập nhật; dừng để thao tác thủ công", + "context_skill_install": "Cài đặt Skill", + "context_skill_update": "Cập nhật Skill", + "skill_kind": "Kỹ năng AI", + "skill_prompt_title": "Lời nhắc", + "skill_prompt_chip": "SKILL.md", + "skill_tools_title": "Công cụ", + "skill_config_title": "Cấu hình", + "skill_references_title": "Tài liệu tham khảo", + "skill_required": "Bắt buộc", + "skill_secret": "Bí mật", + "skill_install": "Cài đặt Skill", + "skill_update": "Cập nhật Skill", + "skill_warning": "Skill sẽ chèn lời nhắc vào AI và cấp cho nó quyền gọi các công cụ, khả năng GM và cấu hình sau. Hãy cài đặt từ nguồn hợp pháp!", + "skill_warning_title": "Vui lòng xác nhận Skill này đến từ nguồn đáng tin cậy", + "skill_warning_desc": "Sau khi cài đặt, nó chèn lời nhắc vào AI và cấp các công cụ được liệt kê (gồm cả quyền GM) cùng quyền đọc/ghi cấu hình — hãy cài đặt thận trọng.", + "success": "Cài đặt thành công", + "install": { + "update_success": "Cập nhật thành công" + }, + "failed": "Cài đặt thất bại", + "subscribe_success": "Đăng ký thành công", + "subscribe_failed": "Đăng ký thất bại", + "current_version": "Phiên bản hiện tại", + "update_version": "Phiên bản cập nhật", + "updatepage": { + "title": "Cập nhật hàng loạt", + "main_header": "Kiểm tra Cập nhật", + "last_check": "Kiểm tra lần cuối {{time}}", + "status_checking_updates": "Đang kiểm tra cập nhật...", + "updates_available": "{{count}} bản cập nhật", + "ignored_count": "{{count}} đã bỏ qua", + "selected_count": "Đã chọn {{selected}} / {{total}}", + "update_selected": "Cập nhật mục đã chọn ({{count}})", + "ignore_selected": "Bỏ qua mục đã chọn", + "update": "Cập nhật", + "ignore": "Bỏ qua", + "restore": "Khôi phục", + "restore_all": "Khôi phục tất cả", + "ignored_section": "Cập nhật đã bỏ qua", + "auto_close": "Tự đóng sau {{count}} giây", + "col_script": "Tập lệnh", + "col_version": "Phiên bản", + "col_change": "Thay đổi", + "col_source": "Nguồn", + "col_action": "Thao tác", + "enabled": "Đã bật", + "disabled": "Đã tắt", + "codechange_major": "Thay đổi lớn", + "codechange_noticeable": "Thay đổi đáng chú ý", + "codechange_tiny": "Thay đổi nhỏ", + "tag_new_connect": "@connect mới", + "empty_title": "Tất cả tập lệnh đều là mới nhất", + "empty_desc": "Đã kiểm tra {{count}} tập lệnh · Không có bản cập nhật", + "similarity": "Độ tương đồng", + "new_connects": "@connect mới", + "toast_found": "Tìm thấy {{count}} tập lệnh có bản cập nhật", + "toast_uptodate": "Tất cả tập lệnh đều là mới nhất" + }, + "importpage": { + "title": "Nhập dữ liệu", + "context_review": "Nhập dữ liệu", + "context_importing": "Đang nhập", + "context_done": "Đã nhập xong", + "selected_count": "Đã chọn {{selected}} / {{total}}", + "unimportable_count": "{{count}} không thể nhập", + "count_scripts": "{{count}} script", + "count_subscribes": "{{count}} đăng ký", + "col_script": "Script", + "col_version": "Phiên bản", + "col_source": "Nguồn", + "col_data": "Dữ liệu", + "col_status": "Trạng thái", + "col_enabled": "Bật", + "op_add": "Mới", + "op_update": "Cập nhật", + "op_error": "Lỗi phân tích", + "source_local": "Tạo cục bộ", + "data_values": "{{count}} mục", + "data_resources": "có tài nguyên", + "enable_after_import": "Bật sau khi nhập", + "row_error": "Tệp bị hỏng, không thể nhập", + "unknown_script": "Script không xác định", + "subscribe_section": "Đăng ký", + "trust_hint": "Chỉ khôi phục các mục bạn chọn; việc nhập không gửi dữ liệu đi đâu cả", + "import_selected": "Nhập mục đã chọn ({{count}})", + "importing_progress": "Đang khôi phục bản sao lưu · {{done}} / {{total}} hoàn tất", + "importing_hint": "Vui lòng giữ trang này mở", + "importing_actionbar_hint": "Đang khôi phục script và dữ liệu, vui lòng không đóng trang này", + "importing_button": "Đang nhập…", + "cancel": "Hủy", + "status_pending": "Đang chờ", + "status_importing": "Đang nhập", + "status_done": "Đã nhập", + "status_skipped": "Đã bỏ qua", + "done_title": "Đã nhập xong", + "done_desc": "Đã khôi phục các script và dữ liệu đã chọn", + "done_stat_scripts": "{{count}} script", + "done_stat_subscribes": "{{count}} đăng ký", + "done_stat_values": "{{count}} mục dữ liệu", + "view_scripts": "Xem danh sách script", + "loading_title": "Đang phân tích tệp sao lưu", + "loading_desc": "Đang đọc và kiểm tra nội dung sao lưu", + "error_title": "Không thể đọc tệp sao lưu", + "error_desc": "Tệp sao lưu có thể bị hỏng hoặc liên kết nhập đã hết hạn", + "invalid_desc": "Liên kết nhập không hợp lệ hoặc đã hết hạn", + "retry": "Thử lại", + "empty_title": "Không có gì để nhập trong bản sao lưu này", + "empty_desc": "Tệp sao lưu này không chứa script hoặc đăng ký nào" + }, + "downloading_status_text": "Đang tải xuống. Đã nhận {{bytes}}.", + "downloading_status_percent": "Đang tải xuống. Đã nhận {{bytes}} / {{total}} ({{percent}}%).", + "page_please_wait": "Vui lòng chờ", + "page_loading": "Đang tải trang cài đặt", + "page_load_failed": "Tải trang cài đặt thất bại", + "invalid_page": "Trang không hợp lệ", + "background_script_tag": "Đây là một script nền", + "scheduled_script_tag": "Đây là một script hẹn giờ", + "from_legitimate_sources_warning": "Vui lòng cài đặt script từ các nguồn hợp pháp! Script không rõ nguồn gốc có thể xâm phạm quyền riêng tư của bạn hoặc thực hiện các hoạt động độc hại.", + "referral_link_title": "Liên kết giới thiệu", + "referral_link_description": "Script này sửa đổi hoặc chuyển hướng đến liên kết giới thiệu của tác giả", + "ads_title": "Quảng cáo", + "ads_description": "Script này chèn quảng cáo vào các trang bạn truy cập", + "payment_title": "Thanh toán", + "payment_description": "Script này yêu cầu thanh toán để sử dụng đúng cách", + "miner_title": "Đào coin", + "miner_description": "Script này tham gia vào các hoạt động đào coin", + "membership_title": "Tính năng thành viên", + "membership_description": "Script này yêu cầu đăng ký làm thành viên để sử dụng đúng cách", + "tracking_title": "Theo dõi", + "tracking_description": "Script này theo dõi thông tin người dùng của bạn", + "script_info_load_failed": "Tải thông tin script thất bại", + "script_import_result": "Kết quả nhập script", + "failure_info": "Thông tin thất bại", + "source": "Nguồn cài đặt", + "skill_prompt": "Prompt", + "skill_tools": "Công cụ", + "skill_config": "Cấu hình", + "skill_references": "Tài liệu tham khảo", + "skill_install_failed": "Cài đặt Skill thất bại" +} diff --git a/src/locales/vi-VN/logs.json b/src/locales/vi-VN/logs.json new file mode 100644 index 000000000..0e123e1a6 --- /dev/null +++ b/src/locales/vi-VN/logs.json @@ -0,0 +1,59 @@ +{ + "log_title": "Nhật ký chạy", + "last_5_minutes": "5 phút trước", + "last_15_minutes": "15 phút trước", + "last_30_minutes": "30 phút trước", + "last_1_hour": "1 giờ trước", + "last_3_hours": "3 giờ trước", + "last_6_hours": "6 giờ trước", + "last_12_hours": "12 giờ trước", + "last_24_hours": "24 giờ trước", + "last_7_days": "7 ngày trước", + "query": "Truy vấn", + "labels": "Nhãn", + "search_regex": "Tìm kiếm (hỗ trợ regex)", + "clean_schedule": "Tự động xóa nhật ký cũ hơn", + "days_ago_logs": "Ngày", + "delete_completed": "Xóa hoàn tất", + "delete_current_logs": "Xóa nhật ký hiện tại", + "clear_completed": "Xóa hoàn tất", + "clear_logs": "Xóa nhật ký", + "now": "Hiện tại", + "total_logs": "Tổng cộng {{length}} nhật ký đã được truy vấn", + "filtered_logs": "{{length}} nhật ký sau khi lọc", + "enter_filter_conditions": "Vui lòng nhập điều kiện lọc để truy vấn", + "last_updated": "Cập nhật lần cuối", + "runtime": "Thời gian chạy", + "advanced": "Nâng cao", + "label_filter": "Lọc nhãn", + "add_label": "Thêm nhãn", + "custom_range": "Tùy chỉnh", + "back_to_top": "Lên đầu trang", + "refresh": "Làm mới", + "all_levels": "Tất cả", + "total_count": "Tổng {{count}}", + "filtered_count": "Đã lọc {{count}}", + "clear_logs_confirm": "Xóa tất cả nhật ký? Không thể hoàn tác.", + "no_logs": "Không có nhật ký", + "refresh_off": "Tắt", + "interval_5s": "5 giây", + "interval_10s": "10 giây", + "interval_30s": "30 giây", + "interval_1m": "1 phút", + "interval_5m": "5 phút", + "quick_range": "Phạm vi nhanh", + "absolute_range": "Phạm vi tuyệt đối", + "from_start": "Từ (Bắt đầu)", + "to_end": "Đến (Kết thúc)", + "apply_range": "Áp dụng phạm vi", + "auto_refresh_hint": "Khi đặt thời điểm kết thúc thành \"Hiện tại\", tự động làm mới sẽ liên tục tải nhật ký mới nhất", + "group_minutes": "Phút", + "group_hours": "Giờ", + "group_days": "Ngày", + "weekdays_short": "CN,T2,T3,T4,T5,T6,T7", + "year_month": "{{month}}/{{year}}", + "time": "Thời gian", + "prev_month": "Tháng trước", + "next_month": "Tháng sau", + "live": "Trực tiếp" +} diff --git a/src/locales/vi-VN/permission.json b/src/locales/vi-VN/permission.json new file mode 100644 index 000000000..b15454bba --- /dev/null +++ b/src/locales/vi-VN/permission.json @@ -0,0 +1,42 @@ +{ + "permission": "Quyền", + "permission_value": "Giá trị quyền", + "allow": "Cho phép", + "permission_management": "Quản lý quyền", + "permission_cors": "Cross-domain (cors)", + "permission_cookie": "Quản lý cookie", + "allow_once": "Cho phép một lần", + "deny_once": "Từ chối một lần", + "script_accessing_cross_origin_resource": "Script đang cố gắng truy cập tài nguyên cross-origin", + "confirm_operation_description": "Vui lòng xác nhận xem bạn có cho phép script thực hiện thao tác này không. Script cũng có thể thêm thẻ @connect để bỏ qua tùy chọn này.", + "request_domain": "Miền yêu cầu", + "request_url": "Url yêu cầu", + "access_cookie_content": "Script đang cố gắng truy cập nội dung cookie của trang web", + "confirm_script_operation": "Vui lòng xác nhận xem bạn có cho phép script thực hiện thao tác này không. Cookie chứa dữ liệu người dùng quan trọng, vì vậy chỉ cấp quyền truy cập cho các script đáng tin cậy.", + "cookie_domain": "Miền cookie", + "script_operation_title": "Script đang cố gắng truy cập bộ nhớ đồng bộ script", + "script_operation_description": "Vui lòng xác nhận xem bạn có cho phép script thực hiện thao tác này không. Nếu được phép, script sẽ có thể truy cập không gian lưu trữ bạn đã thiết lập và tạo một thư mục app/${dir} trong đó.", + "script_permission_content": "Script", + "extension_site_access_title": "ScriptCat cần quyền truy cập trang web", + "extension_site_access_description": "Cấp quyền truy cập trang web của trình duyệt cho nguồn này để ScriptCat có thể thực hiện yêu cầu. Thao tác này thay đổi cài đặt truy cập trang web của tiện ích.", + "extension_site_access_content": "Trang web", + "request_permission": "Yêu cầu quyền", + "allow_user_script_guide": "'Cho phép tập lệnh người dùng' hiện chưa được bật, nên các script không thể hoạt động đúng cách. 👉Nhấn để xem cách bật", + "user_script_type": "Script người dùng", + "auth_duration": "Thời hạn cấp quyền", + "duration_once": "Chỉ lần này", + "duration_temporary": "Tạm thời", + "duration_permanent": "Vĩnh viễn", + "apply_to_all_domains": "Áp dụng cho tất cả miền được yêu cầu", + "apply_to_all_domains_desc": "Có hiệu lực cho mọi yêu cầu của script này (ký tự đại diện)", + "allow_action": "Cho phép", + "deny_action": "Từ chối", + "ignore_action": "Bỏ qua", + "cancel_action": "Hủy", + "loading_confirm": "Đang tải yêu cầu cấp quyền…", + "cookie_warning_title": "Quyền có độ nhạy cảm cao", + "cookie_warning_desc": "Cookie chứa dữ liệu nhạy cảm như trạng thái đăng nhập. Chỉ cấp quyền cho các script đáng tin cậy.", + "confirm_expired_title": "Yêu cầu cấp quyền đã hết hạn", + "confirm_expired_desc": "Yêu cầu cấp quyền này đã hết thời gian chờ hoặc đã được xử lý. Vui lòng quay lại trang và kích hoạt lại thao tác này.", + "auto_close_in": "Cửa sổ sẽ tự động đóng sau {{second}} giây" +} diff --git a/src/locales/vi-VN/popup.json b/src/locales/vi-VN/popup.json new file mode 100644 index 000000000..189ec783f --- /dev/null +++ b/src/locales/vi-VN/popup.json @@ -0,0 +1,27 @@ +{ + "new_version_available": "Phiên bản mới có sẵn", + "current_page_scripts": "Script đang chạy trên trang hiện tại", + "enabled_background_scripts": "Script nền đã bật và đang chạy", + "menu_expand_num_before": "Các mục menu nhiều hơn", + "menu_expand_num_after": "sẽ bị ẩn.", + "develop_mode_guide": "'Chế độ nhà phát triển' hiện chưa được bật, nên các script không thể hoạt động đúng cách. 👉Nhấn để xem cách bật", + "lower_version_browser_guide": "Trình duyệt của bạn quá cũ, nên các script không thể hoạt động đúng cách. 👉Nhấn để xem thêm", + "click_to_reload": "👉Nhấp chuột để tải lại", + "page_in_blacklist": "Trang hiện tại nằm trong danh sách đen, không thể sử dụng script", + "ext_update_notification": "Tiện ích scriptcat đã cập nhật", + "ext_update_notification_desc": "Phiên bản hiện tại: {{version}}, vui lòng xem nhật ký cập nhật để biết chi tiết", + "script_menu_display": "Menu đã đăng ký script", + "badge_type_none": "Không", + "badge_type_run_count": "Số lần chạy", + "badge_type_script_count": "Số script", + "script_menu": "Menu script", + "display_right_click_menu": "Hiển thị menu chuột phải", + "display_right_click_menu_desc": "Hiển thị menu script trong menu chuột phải của trình duyệt", + "expand_count": "Số lượng mở rộng", + "auto_collapse_when_exceeds": "Tự động thu gọn khi vượt quá số này", + "allow_user_script_guide": "'Cho phép tập lệnh người dùng' hiện chưa được bật, nên các script không thể hoạt động đúng cách. 👉Nhấn để xem cách bật", + "request_permission": "Yêu cầu quyền", + "show_more_scripts": "+{{count}} tập lệnh", + "use_on_mobile": "Sử dụng ScriptCat trên điện thoại", + "scan_qr_to_install": "Quét mã QR để cài ScriptCat trên điện thoại" +} diff --git a/src/locales/vi-VN/script.json b/src/locales/vi-VN/script.json new file mode 100644 index 000000000..02fb0e867 --- /dev/null +++ b/src/locales/vi-VN/script.json @@ -0,0 +1,115 @@ +{ + "import_link": "Liên kết nhập", + "import_link_failure": "Nhập liên kết thất bại", + "create_user_script": "Tạo script người dùng", + "create_background_script": "Tạo script nền", + "create_scheduled_script": "Tạo script hẹn giờ", + "import_by_local": "Đã nhập cục bộ", + "import_local_failure": "Nhập cục bộ thất bại", + "import_local_success": "Nhập cục bộ thành công", + "create_script": "Tạo script", + "installed_scripts": "Các tập lệnh đã cài đặt", + "nav_scripts": "Tập lệnh", + "subscribe": "Đăng ký", + "subscribe_scripts_count": "{{count}} tập lệnh", + "enter_subscribe_name": "Vui lòng nhập tên đăng ký", + "subscribe_url": "Url đăng ký", + "confirm_delete_subscription": "Bạn có chắc chắn muốn xóa đăng ký này không? Các script liên quan cũng sẽ bị xóa.", + "list": { + "confirm_delete": "Bạn có chắc chắn muốn xóa không? Xin lưu ý rằng đây là một thao tác không thể đảo ngược.", + "confirm_update": "Bạn có chắc chắn muốn cập nhật không? Xin lưu ý rằng đây là một thao tác không thể đảo ngược." + }, + "apply_to_run_status": "Áp dụng cho / trạng thái chạy", + "sorting": "Sắp xếp", + "foreground_page_script_tooltip": "Script trang nền trước, chạy trên trang được chỉ định", + "background_script_tooltip": "Script nền, sẽ chạy ngầm khi được bật", + "scheduled_script_tooltip": "Script hẹn giờ, thời gian chạy tiếp theo:", + "running": "Đang chạy", + "completed": "Hoàn thành", + "source_subscribe_link": "Liên kết đăng ký", + "source_local_script": "Tập lệnh cục bộ", + "source_script_link": "Liên kết script", + "by_manual_creation": "Được tạo cục bộ thông qua chỉnh sửa mã", + "confirm_delete_script": "Bạn có chắc chắn muốn xóa script này không?", + "confirm_delete_scripts_content": "Bạn có chắc chắn muốn xóa {{count}} script đã chọn không? Hành động này không thể hoàn tác.", + "confirm_delete_script_content": "Bạn có chắc chắn muốn xóa script \"{{name}}\" không? Hành động này không thể hoàn tác.", + "delete_failed": "Xóa thất bại", + "enter_script_name": "Vui lòng nhập tên script", + "update_not_supported": "Script này không hỗ trợ kiểm tra cập nhật", + "checking_for_updates": "Đang kiểm tra cập nhật...", + "new_version_available": "Phiên bản mới có sẵn", + "latest_version": "Phiên bản mới nhất", + "checked_for_all_selected": "Đã kiểm tra cập nhật cho tất cả các mục đã chọn", + "update_check_failed": "Kiểm tra cập nhật thất bại", + "stopping_script": "Đang dừng script", + "script_stopped": "Script đã dừng", + "starting_script": "Đang khởi động script...", + "starting_updates": "Đang khởi động cập nhật hàng loạt...", + "script_started": "Script đã khởi động", + "operation_failed": "Thao tác thất bại", + "batch_operations": "Thao tác hàng loạt", + "scripts_pinned_to_top": "Script đã chọn đã được ghim", + "unknown_operation": "Thao tác không xác định", + "page_script": "Script trang", + "homepage": "Trang chủ", + "script_website": "Trang web script", + "script_source": "Mã nguồn script", + "bug_feedback_script_support": "Phản hồi lỗi/hỗ trợ script", + "script_total_runs": "Script đã chạy {{runNum}} lần, {{runNumByIframe}} lần trong iframes", + "script_total_runs_single": "Script đã chạy {{runNum}} lần", + "script_disabled": "Script chưa được bật", + "cron_oncetype": { + "minute": "{{next}} (chạy mỗi phút)", + "hour": "{{next}} (chạy mỗi giờ)", + "day": "{{next}} (chạy mỗi ngày)", + "month": "{{next}} (chạy mỗi tháng)", + "week": "{{next}} (chạy mỗi tuần)" + }, + "cron_invalid_expr": "Biểu thức cron không hợp lệ", + "scheduled_script_description_title": "Đây là script hẹn giờ, sẽ tự động chạy vào một thời điểm cụ thể sau khi được bật và có thể được điều khiển thủ công trong bảng điều khiển.", + "scheduled_script_description_description_expr": "Biểu thức tác vụ hẹn giờ:", + "scheduled_script_description_description_next": "Thời gian chạy gần nhất:", + "background_script_description": "Đây là script nền, sẽ tự động chạy một lần khi trình duyệt mở sau khi được bật và có thể được điều khiển thủ công trong bảng điều khiển.", + "background_script": "Script nền", + "scheduled_script": "Script hẹn giờ", + "script_status_tooltip": "Bạn có thể kiểm soát trạng thái bật của các tập lệnh. Các tập lệnh Tampermonkey thông thường được bật theo mặc định, còn các tập lệnh Nền và Lập lịch thì bị tắt theo mặc định.", + "subscribe_source_tooltip": "Đây là nguồn đăng ký. Khi bạn mở đăng ký, tập lệnh đăng ký sẽ tự động được cài đặt.", + "script_name_cannot_be_set_to_empty": "Tên script không được để trống", + "search_scripts": "Tìm kiếm script", + "script_list": { + "sidebar": { + "stopped": "Đã dừng", + "all": "Tất cả", + "normal_script": "Script bình thường", + "status": "Trạng thái" + } + }, + "tags": "Thẻ", + "input_tags_placeholder": "Nhập thẻ, nhấn Enter để xác nhận", + "switch_to_card_mode": "Chuyển sang chế độ thẻ", + "switch_to_table_mode": "Chuyển sang chế độ bảng", + "open_sidebar": "Mở thanh bên", + "close_sidebar": "Đóng thanh bên", + "error_metadata_invalid": "MetaData không hợp lệ", + "error_script_name_required": "Tên script là bắt buộc", + "error_script_version_required": "@version của script là bắt buộc", + "error_script_namespace_required": "@namespace của script là bắt buộc", + "error_cron_invalid": "Biểu thức cron không hợp lệ: {{expr}}", + "error_script_type_mismatch": "Loại script không khớp: script thường và nền không thể chuyển đổi cho nhau", + "error_old_script_code_missing": "Không tìm thấy mã script cũ", + "error_subscribe_name_required": "Tên đăng ký là bắt buộc", + "error_grant_conflict": "@grant khai báo đồng thời 'none' và GM API", + "error_metadata_line_duplicated": "Có các khai báo trùng lặp trong metadata.", + "create_group": "Tạo mới", + "import_group": "Nhập", + "import_local_script": "Nhập script cục bộ", + "link_import": "Nhập từ URL", + "import_skill": "Nhập Skill", + "link_import_desc": "Dán URL script / đăng ký, mỗi dòng một URL", + "link_import_placeholder": "https://example.com/script.user.js", + "link_import_hint": "Hỗ trợ URL script người dùng / đăng ký / Skill", + "not_a_valid_script": "Không phải script người dùng hoặc SkillScript hợp lệ", + "import_done": "Nhập hoàn tất: thành công {{success}} · thất bại {{fail}}", + "drop_to_install": "Kéo thả script hoặc Skill vào đây để cài đặt", + "drop_to_install_hint": "Kéo thả script .js / đăng ký · gói Skill .zip" +} diff --git a/src/locales/vi-VN/settings.json b/src/locales/vi-VN/settings.json new file mode 100644 index 000000000..9383c9f70 --- /dev/null +++ b/src/locales/vi-VN/settings.json @@ -0,0 +1,119 @@ +{ + "general": "Chung", + "language": "Ngôn ngữ", + "help_translate": "Giúp chúng tôi dịch", + "script_sync": "Đồng bộ script", + "sync_delete": "Đồng bộ xóa", + "sync_delete_desc": "Khi bật, khi xóa script sẽ đánh dấu là đã xóa, các thiết bị khác phát hiện trạng thái này sẽ xóa script tương ứng. Khi tắt, sẽ xóa trực tiếp script trên bộ nhớ cục bộ và đám mây, nếu có nhiều thiết bị có thể xảy ra tình trạng đồng bộ lặp lại.", + "enable_script_sync_to": "Bật đồng bộ script tới", + "script_subscription_check_interval": "Khoảng thời gian kiểm tra cập nhật script/đăng ký", + "never": "Không bao giờ", + "6_hours": "6 giờ", + "12_hours": "12 giờ", + "every_day": "Mỗi ngày", + "every_week": "Mỗi tuần", + "update_disabled_scripts": "Cập nhật script đã tắt", + "silent_update_non_critical_changes": "Tự động cập nhật các thay đổi không quan trọng", + "enable_eslint": "Bật eslint", + "eslint_rules": "Quy tắc eslint", + "enter_eslint_rules": "Vui lòng nhập các quy tắc eslint. Cấu hình có thể được tải xuống từ https://eslint.org/play/.", + "language_change_tip": "Thay đổi ngôn ngữ thành công", + "backup": "Sao lưu", + "local": "Cục bộ", + "export_file": "Xuất tệp", + "import_file": "Nhập tệp", + "cloud": "Đám mây", + "backup_to": "Sao lưu tới", + "preparing_backup": "Đang chuẩn bị sao lưu lên đám mây", + "backup_success": "Sao lưu thành công", + "backup_failed": "Sao lưu thất bại", + "no_backup_files": "Không có tệp sao lưu nào", + "backup_list": "Danh sách sao lưu", + "open_backup_dir": "Mở thư mục sao lưu", + "confirm_delete_backup_file": "Xác nhận xóa tệp sao lưu", + "backup_strategy": "Chiến lược sao lưu", + "under_construction": "Đang xây dựng", + "sync_system_connect_failed": "Kết nối hệ thống đồng bộ thất bại", + "sync_system_closed": "Đồng bộ đã tắt", + "sync_system_closed_description": "Đồng bộ bị tắt, vui lòng cấu hình lại", + "export_success": "Đổ dữ liệu thành công đã lưu", + "get_backup_dir_url_failed": "Không thể lấy địa chỉ thư mục sao lưu", + "get_backup_files_failed": "Không thể lấy các bản sao lưu", + "baidu_netdisk": "Baidunetdisk", + "netdisk_unbind": "Hủy liên kết {{provider}}", + "netdisk_unbind_confirm": "Hủy liên kết tài khoản {{provider}}?", + "netdisk_unbind_success": "Đã hủy liên kết tài khoản {{provider}}", + "netdisk_unbind_error": "Không thể hủy liên kết tài khoản {{provider}}", + "save_only_current_group": "Chỉ lưu cho nhóm hiện tại", + "security": "Bảo mật", + "blacklist_pages": "Trang danh sách đen", + "blacklist_placeholder": "Tắt scriptcat để chạy script trên các trang như\nhttps://*.example.com", + "expression_format_error": "Lỗi định dạng biểu thức điều kiện", + "migration_confirm_message": "Thử lại công cụ lưu trữ di chuyển để sửa đổi dữ liệu hiện có. Vui lòng xác nhận, xem: https://docs.scriptcat.org/docs/change/v0.17/.", + "retry_migration": "Thử lại công cụ lưu trữ di chuyển", + "sync_status": "Trạng thái đồng bộ", + "interface_settings": "Giao diện", + "select_interface_language": "Chọn ngôn ngữ hiển thị giao diện", + "extension_icon_badge": "Huy hiệu biểu tượng tiện ích", + "display_type": "Loại hiển thị", + "extension_icon_badge_type": "Loại số hiển thị trên biểu tượng tiện ích", + "background_color": "Màu nền", + "badge_background_color_desc": "Màu nền huy hiệu", + "text_color": "Màu văn bản", + "badge_text_color_desc": "Màu văn bản huy hiệu", + "badge_type_none": "Không", + "badge_type_run_count": "Số lần chạy", + "badge_type_script_count": "Số script", + "script_menu": "Menu script", + "display_right_click_menu": "Hiển thị menu chuột phải", + "display_right_click_menu_desc": "Hiển thị menu script trong menu chuột phải của trình duyệt", + "expand_count": "Số lượng mở rộng", + "auto_collapse_when_exceeds": "Tự động thu gọn khi vượt quá số này", + "script_update_check_frequency": "Tần suất kiểm tra cập nhật tập lệnh", + "script_auto_update_frequency": "Tần suất kiểm tra cập nhật script tự động", + "update_options": "Tùy chọn cập nhật", + "control_script_update_behavior": "Kiểm soát hành vi cập nhật script", + "blacklist_pages_desc": "Ngăn script chạy trên các trang được chỉ định, hỗ trợ ký tự đại diện", + "development_tools": "Công cụ phát triển", + "check_script_code_quality": "Kiểm tra chất lượng mã và lỗi script", + "custom_eslint_rules_config": "Cấu hình quy tắc eslint tùy chỉnh (định dạng json)", + "script_run_env": { + "title": "Môi trường chạy", + "all": "Tất cả tab", + "normal-tabs": "Tab thường", + "incognito-tabs": "Tab ẩn danh" + }, + "script_run_at": { + "title": "Thời điểm chạy" + }, + "script_setting": { + "title": "Cài đặt script", + "default": "Mặc định" + }, + "notification": { + "script_sync_delete": "Đồng bộ xóa script", + "script_sync_delete_desc": "Script {{scriptName}} đã bị xóa", + "subscribe_update": "Đăng ký {{subscribeName}} đã được cập nhật", + "subscribe_update_desc": "Script mới: {{newScripts}}\nScript đã xóa: {{deletedScripts}}" + }, + "enable_background": { + "title": "Bật chạy nền", + "description": "Khi bật, trình duyệt sẽ tiếp tục chạy trong nền sau khi bạn đóng tất cả các cửa sổ, thu nhỏ xuống khay hệ thống cho đến khi bạn thoát trình duyệt thủ công. Điều này cho phép các script chạy nền tiếp tục hoạt động.", + "enable_failed": "Bật thất bại", + "disable_failed": "Tắt thất bại", + "prompt_title": "Bật chạy nền?", + "prompt_description": "Đây là {{scriptType}}. Bật chạy nền cho phép script tiếp tục chạy sau khi đóng trình duyệt.", + "enable_now": "Bật ngay", + "maybe_later": "Để sau", + "settings_hint": "Bạn có thể thay đổi tùy chọn này trong cài đặt bất kỳ lúc nào." + }, + "favicon_service": "Dịch vụ Favicon", + "favicon_service_desc": "Chọn dịch vụ để lấy biểu tượng trang web", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_google": "Google", + "favicon_service_duckduckgo": "DuckDuckGo", + "favicon_service_icon-horse": "Icon Horse", + "favicon_service_local": "Lấy cục bộ", + "cloud_sync_account_verification": "Đang xác minh tài khoản đồng bộ đám mây...", + "cloud_sync_verification_failed": "Xác minh tài khoản đồng bộ đám mây thất bại" +} diff --git a/src/locales/vi-VN/tools.json b/src/locales/vi-VN/tools.json new file mode 100644 index 000000000..062201890 --- /dev/null +++ b/src/locales/vi-VN/tools.json @@ -0,0 +1,17 @@ +{ + "development_tool": "Công cụ phát triển", + "vscode_url": "Url vscode", + "auto_connect_vscode_service": "Tự động kết nối dịch vụ vscode", + "connect": "Kết nối", + "connection_success": "Kết nối thành công", + "connection_failed": "Kết nối thất bại", + "select_import_script": "Vui lòng chọn script để nhập trong trang mới", + "import_error": "Lỗi nhập", + "pulling_data_from_cloud": "Đang tải dữ liệu từ đám mây", + "pull_failed": "Tải thất bại", + "restore": "Khôi phục", + "local_backup": "Sao lưu cục bộ", + "cloud_backup": "Sao lưu đám mây", + "auto_backup": "Sao lưu tự động", + "data_migration": "Di chuyển dữ liệu" +} diff --git a/src/locales/vi-VN/translation.json b/src/locales/vi-VN/translation.json deleted file mode 100644 index 97ac60c2e..000000000 --- a/src/locales/vi-VN/translation.json +++ /dev/null @@ -1,770 +0,0 @@ -{ - "sentence-separator": ". ", - "import_link": "Liên kết nhập", - "import_link_failure": "Nhập liên kết thất bại", - "create_user_script": "Tạo script người dùng", - "create_background_script": "Tạo script nền", - "create_scheduled_script": "Tạo script hẹn giờ", - "import_by_local": "Đã nhập cục bộ", - "import_local_failure": "Nhập cục bộ thất bại", - "import_local_success": "Nhập cục bộ thành công", - "create_script": "Tạo script", - "user_guide": "Hướng dẫn sử dụng", - "api_docs": "Tài liệu api", - "development_guide": "Hướng dẫn phát triển", - "script_gallery": "Thư viện script", - "community_forum": "Diễn đàn cộng đồng", - "external_links": "Liên kết bên ngoài", - "system_follow": "Theo hệ thống", - "no_data": "Không có dữ liệu", - "installed_scripts": "Các tập lệnh đã cài đặt", - "subscribe": "Đăng ký", - "logs": "Nhật ký", - "tools": "Công cụ", - "find": "Tìm kiếm", - "replace": "Thay thế", - "settings": "Cài đặt", - "hide_main_sidebar": "Thu gọn thanh bên", - "show_main_sidebar": "Mở rộng thanh bên", - "guide": "Hướng dẫn", - "helpcenter": "Trung tâm trợ giúp", - "general": "Chung", - "language": "Ngôn ngữ", - "help_translate": "Giúp chúng tôi dịch", - "script_sync": "Đồng bộ script", - "sync_delete": "Đồng bộ xóa", - "sync_delete_desc": "Khi bật, khi xóa script sẽ đánh dấu là đã xóa, các thiết bị khác phát hiện trạng thái này sẽ xóa script tương ứng. Khi tắt, sẽ xóa trực tiếp script trên bộ nhớ cục bộ và đám mây, nếu có nhiều thiết bị có thể xảy ra tình trạng đồng bộ lặp lại.", - "enable_script_sync_to": "Bật đồng bộ script tới", - "save": "Lưu", - "save_as": "Lưu thành", - "file": "Tệp", - "run": "Chạy", - "debug": "Gỡ lỗi", - "cloud_sync_account_verification": "Đang xác minh tài khoản đồng bộ đám mây...", - "cloud_sync_verification_failed": "Xác minh tài khoản đồng bộ đám mây thất bại", - "save_success": "Lưu thành công", - "reset_success": "Đặt lại thành công", - "update": "Cập nhật", - "check_update": "Kiểm tra cập nhật", - "script_subscription_check_interval": "Khoảng thời gian kiểm tra cập nhật script/đăng ký", - "never": "Không bao giờ", - "6_hours": "6 giờ", - "12_hours": "12 giờ", - "every_day": "Mỗi ngày", - "every_week": "Mỗi tuần", - "update_disabled_scripts": "Cập nhật script đã tắt", - "silent_update_non_critical_changes": "Tự động cập nhật các thay đổi không quan trọng", - "enable_eslint": "Bật eslint", - "eslint_rules": "Quy tắc eslint", - "enter_eslint_rules": "Vui lòng nhập các quy tắc eslint. Cấu hình có thể được tải xuống từ https://eslint.org/play/.", - "language_change_tip": "Thay đổi ngôn ngữ thành công", - "backup": "Sao lưu", - "local": "Cục bộ", - "export_file": "Xuất tệp", - "import_file": "Nhập tệp", - "cloud": "Đám mây", - "backup_to": "Sao lưu tới", - "preparing_backup": "Đang chuẩn bị sao lưu lên đám mây", - "backup_success": "Sao lưu thành công", - "backup_failed": "Sao lưu thất bại", - "no_backup_files": "Không có tệp sao lưu nào", - "backup_list": "Danh sách sao lưu", - "open_backup_dir": "Mở thư mục sao lưu", - "confirm_delete": "Xác nhận xóa", - "confirm_delete_backup_file": "Xác nhận xóa tệp sao lưu", - "confirm_update": "Xác nhận cập nhật", - "delete_success": "Xóa thành công", - "deleting": "Đang xóa", - "backup_strategy": "Chiến lược sao lưu", - "under_construction": "Đang phát triển", - "development_tool": "Công cụ phát triển", - "vscode_url": "Url vscode", - "auto_connect_vscode_service": "Tự động kết nối dịch vụ vscode", - "connect": "Kết nối", - "connection_success": "Kết nối thành công", - "connection_failed": "Kết nối thất bại", - "select_import_script": "Vui lòng chọn script để nhập trong trang mới", - "import_error": "Lỗi nhập", - "pulling_data_from_cloud": "Đang tải dữ liệu từ đám mây", - "pull_failed": "Tải thất bại", - "restore": "Khôi phục", - "log_title": "Nhật ký chạy", - "last_5_minutes": "5 phút trước", - "last_15_minutes": "15 phút trước", - "last_30_minutes": "30 phút trước", - "last_1_hour": "1 giờ trước", - "last_3_hours": "3 giờ trước", - "last_6_hours": "6 giờ trước", - "last_12_hours": "12 giờ trước", - "last_24_hours": "24 giờ trước", - "last_7_days": "7 ngày trước", - "query": "Truy vấn", - "labels": "Nhãn", - "search_regex": "Tìm kiếm (hỗ trợ regex)", - "clean_schedule": "Tự động xóa nhật ký cũ hơn", - "days_ago_logs": "Ngày", - "delete_completed": "Xóa hoàn tất", - "delete_current_logs": "Xóa nhật ký hiện tại", - "clear_completed": "Xóa hoàn tất", - "clear_logs": "Xóa nhật ký", - "to": " - ", - "now": "Hiện tại", - "total_logs": "Tìm thấy {{length}} nhật ký", - "filtered_logs": "{{length}} nhật ký sau khi lọc", - "enter_filter_conditions": "Vui lòng nhập điều kiện lọc", - "permission": "Quyền", - "enter_subscribe_name": "Vui lòng nhập tên đăng ký", - "subscribe_url": "Url đăng ký", - "confirm_delete_subscription": "Bạn có chắc chắn muốn xóa đăng ký này không? Các script liên quan cũng sẽ bị xóa.", - "list": { - "confirm_delete": "Bạn có chắc chắn muốn xóa không? Xin lưu ý rằng đây là một thao tác không thể đảo ngược.", - "confirm_update": "Bạn có chắc chắn muốn cập nhật không? Xin lưu ý rằng đây là một thao tác không thể đảo ngược." - }, - "enable": "Bật", - "script_list_enable_width": 100, - "script_list_last_updated_width": 150, - "script_list_apply_to_run_status_width": 110, - "subscribe_list_enable_width": 120, - "disable": "Tắt", - "name": "Tên", - "version": "Phiên bản", - "apply_to_run_status": "Áp dụng cho / trạng thái chạy", - "source": "Nguồn", - "home": "Trang chủ", - "sorting": "Sắp xếp", - "last_updated": "Cập nhật lần cuối", - "action": "Hành động", - "foreground_page_script_tooltip": "Script trang nền trước, chạy trên trang được chỉ định", - "background_script_tooltip": "Script nền, sẽ chạy ngầm khi được bật", - "scheduled_script_tooltip": "Script hẹn giờ, thời gian chạy tiếp theo:", - "running": "Đang chạy", - "completed": "Hoàn thành", - "source_subscribe_link": "Liên kết đăng ký", - "source_local_script": "Tập lệnh cục bộ", - "source_script_link": "Liên kết script", - "by_manual_creation": "Được tạo cục bộ thông qua chỉnh sửa mã", - "confirm_delete_script": "Bạn có chắc chắn muốn xóa script này không?", - "confirm_delete_script_content": "Bạn có chắc chắn muốn xóa script \"{{name}}\" không? Hành động này không thể hoàn tác.", - "delete_failed": "Xóa thất bại", - "enter_script_name": "Vui lòng nhập tên script", - "update_not_supported": "Script này không hỗ trợ kiểm tra cập nhật", - "checking_for_updates": "Đang kiểm tra cập nhật...", - "new_version_available": "Phiên bản mới có sẵn", - "latest_version": "Phiên bản mới nhất", - "checked_for_all_selected": "Đã kiểm tra cập nhật cho tất cả các mục đã chọn", - "update_check_failed": "Kiểm tra cập nhật thất bại", - "script_import_failed": "Nhập script thất bại", - "install_page_open_failed": "Không thể mở trang cài đặt", - "stopping_script": "Đang dừng script", - "script_stopped": "Script đã dừng", - "starting_script": "Đang khởi động script...", - "starting_updates": "Đang khởi động cập nhật hàng loạt...", - "script_started": "Script đã khởi động", - "operation_failed": "Thao tác thất bại", - "batch_operations": "Thao tác hàng loạt", - "export": "Xuất", - "delete": "Xóa", - "pin_to_top": "Ghim lên đầu", - "scripts_pinned_to_top": "Script đã chọn đã được ghim", - "unknown_operation": "Thao tác không xác định", - "confirm": "Xác nhận", - "close": "Đóng", - "page_script": "Script trang", - "homepage": "Trang chủ", - "script_website": "Trang web script", - "script_source": "Mã nguồn script", - "bug_feedback_script_support": "Phản hồi lỗi/hỗ trợ script", - "config": "Cấu hình", - "key": "key", - "value": "value", - "add": "Thêm", - "type": "Loại", - "edit_value": "Sửa giá trị", - "add_value": "Thêm giá trị", - "update_success": "Cập nhật thành công", - "add_success": "Thêm thành công", - "script_storage": "Lưu trữ script", - "enter_key": "Vui lòng nhập key", - "key_placeholder": "key", - "value_placeholder": "Khi loại là object, vui lòng nhập dữ liệu có thể phân tích cú pháp JSON.", - "clear": "Xóa", - "clear_success": "Xóa thành công", - "confirm_clear": "Bạn có chắc chắn muốn xóa không gian lưu trữ này không?", - "type_string": "string", - "type_number": "number", - "type_boolean": "boolean", - "type_object": "object", - "confirm_delete_resource": "Bạn có chắc chắn muốn xóa tài nguyên này không? Tài nguyên này sẽ tải lại vào lần khởi động tiếp theo.", - "confirm_clear_resource": "Bạn có chắc chắn muốn xóa các tài nguyên này không? Các tài nguyên sẽ tải lại vào lần khởi động tiếp theo.", - "script_resource": "Tài nguyên script", - "permission_value": "Giá trị quyền", - "allow": "Cho phép", - "yes": "Có", - "no": "Không", - "confirm_delete_permission": "Bạn có chắc muốn xóa quyền này không?", - "basic_info": "Thông tin cơ bản", - "update_url": "Url cập nhật", - "permission_management": "Quản lý quyền", - "add_permission": "Thêm quyền", - "permission_cors": "Liên miền (CORS)", - "permission_cookie": "Quản lý cookie", - "match": "Khớp", - "user_setting": "Cài đặt người dùng", - "confirm_delete_exclude": "Xác nhận xóa loại trừ này?", - "after_deleting_match_item": "Sau khi các mục khớp của script bị xóa, chúng sẽ tự động được thêm vào các mục khớp", - "confirm_delete_match": "Xác nhận xóa khớp này?", - "after_deleting_exclude_item": "Sau khi xóa mục khớp của script, nó sẽ tự động được thêm vào các mục loại trừ", - "add_match": "Thêm khớp", - "add_exclude": "Thêm loại trừ", - "website_match": "Khớp trang web (@match)", - "reset": "Đặt lại", - "website_exclude": "Loại trừ trang web (@exclude)", - "confirm_reset": "Xác nhận đặt lại?", - "script_total_runs": "Script đã chạy {{runNum}} lần, {{runNumByIframe}} lần trong iframes", - "script_total_runs_single": "Script đã chạy {{runNum}} lần", - "script_disabled": "Script chưa được bật", - "run_once": "Chạy một lần", - "stop": "Dừng", - "edit": "Sửa", - "undo": "Hoàn tác", - "redo": "Làm lại", - "cut": "Cắt", - "copy": "Sao chép", - "paste": "Dán", - "format": "Định dạng", - "exclude_on": "Cho phép chạy lại $0", - "exclude_off": "Loại trừ chạy $0", - "user_config": "Cấu hình người dùng", - "gm_api": "GM API", - "storage_api": "API lưu trữ", - "use_file_system": "Hệ thống tệp đang được sử dụng", - "open_directory": "Mở thư mục", - "account_validation_failed": "Xác thực tài khoản thất bại", - "not_set": "Chưa đặt", - "in_use": "Đang sử dụng", - "storage_error": "Lỗi lưu trữ", - "upload_to_cloud": "Tải lên đám mây", - "save_failed": "Lưu thất bại", - "exporting": "Đang xuất...", - "upload_to": "Tải lên", - "value_export_expression": "Biểu thức xuất giá trị", - "overwrite_original_value_on_import": "Ghi đè giá trị gốc khi nhập", - "cookie_export_expression": "Biểu thức xuất cookie", - "overwrite_original_cookie_on_import": "Ghi đè cookie gốc khi nhập", - "restore_default_values": "Khôi phục giá trị mặc định", - "get_confirm_error": "Lấy thông tin xác nhận thất bại", - "confirm_error": "Xác nhận thất bại", - "ignore": "Bỏ qua", - "allow_once": "Cho phép một lần", - "temporary_allow": "Tạm thời cho phép {{permissionContent}} này", - "temporary_allow_all": "Tạm thời cho phép tất cả {{permissionContent}}", - "permanent_allow": "Cho phép vĩnh viễn {{permissionContent}} này", - "permanent_allow_all": "Cho phép vĩnh viễn tất cả {{permissionContent}}", - "deny_once": "Từ chối một lần", - "temporary_deny": "Tạm thời từ chối {{permissionContent}} này", - "temporary_deny_all": "Tạm thời từ chối tất cả {{permissionContent}}", - "permanent_deny": "Từ chối vĩnh viễn {{permissionContent}} này", - "permanent_deny_all": "Từ chối vĩnh viễn tất cả {{permissionContent}}", - "data_import": "Nhập dữ liệu", - "import": "Nhập", - "select_scripts_to_import": "Vui lòng chọn các script bạn muốn nhập", - "select_all": "Chọn tất cả", - "script_import_progress": "Tiến độ nhập script", - "select_subscribes_to_import": "Vui lòng chọn các đăng ký bạn muốn nhập", - "subscribe_import_progress": "Tiến độ nhập đăng ký", - "author": "Tác giả", - "description": "Mô tả", - "operation": "Thao tác", - "error": "Lỗi", - "unknown": "Không xác định", - "add_new": "Thêm mới", - "no_operation": "Không có thao tác nào", - "enable_script": "Bật script", - "local_creation": "Tạo cục bộ", - "import_success": "Nhập thành công", - "install_script": "Cài đặt script", - "update_script": "Cập nhật script", - "install_subscribe": "Cài đặt đăng ký", - "update_subscribe": "Cập nhật đăng ký", - "update_script_no_close": "Cập nhật script mà không đóng cửa sổ", - "install_script_no_close": "Cài đặt script mà không đóng cửa sổ", - "update_script_no_more_update": "Cập nhật script và tắt kiểm tra cập nhật", - "close_update_script_no_more_update": "Đóng và không kiểm tra cập nhật", - "install_script_no_more_update": "Cài đặt script và tắt kiểm tra cập nhật", - "invalid_link": "Liên kết không hợp lệ", - "subscribe_install_label": "Đăng ký này sẽ cài đặt các script sau", - "script_runs_in": "Script sẽ chạy trên các trang web sau", - "script_has_full_access_to": "Script sẽ có toàn quyền truy cập vào các url sau", - "script_requires": "Script yêu cầu các tài nguyên bên ngoài sau", - "cookie_warning": "Xin lưu ý, script này yêu cầu quyền truy cập cookie, đây là một quyền nguy hiểm. Vui lòng xác minh tính bảo mật của script.", - "cron_oncetype": { - "minute": "{{next}} (chạy mỗi phút)", - "hour": "{{next}} (chạy mỗi giờ)", - "day": "{{next}} (chạy mỗi ngày)", - "month": "{{next}} (chạy mỗi tháng)", - "week": "{{next}} (chạy mỗi tuần)" - }, - "cron_invalid_expr": "Biểu thức cron không hợp lệ", - "scheduled_script_description_title": "Đây là script hẹn giờ, sẽ tự động chạy vào một thời điểm cụ thể sau khi được bật và có thể được điều khiển thủ công trong bảng điều khiển.", - "scheduled_script_description_description_expr": "Biểu thức tác vụ hẹn giờ:", - "scheduled_script_description_description_next": "Thời gian chạy gần nhất:", - "background_script_description": "Đây là script nền, sẽ tự động chạy một lần khi trình duyệt mở sau khi được bật và có thể được điều khiển thủ công trong bảng điều khiển.", - "install_success": "Cài đặt thành công", - "install": { - "update_success": "Cập nhật thành công" - }, - "install_failed": "Cài đặt thất bại", - "subscribe_success": "Đăng ký thành công", - "subscribe_failed": "Đăng ký thất bại", - "current_version": "Phiên bản hiện tại", - "update_version": "Phiên bản cập nhật", - "updatepage": { - "main_header": "Kiểm tra Cập nhật", - "header_site_specific": "Có bản cập nhật cho trang web này ($0)", - "header_site_all": "Có bản cập nhật", - "header_ignored": "Có bản cập nhật nhưng đã bỏ qua", - "update_all": "Cập nhật tất cả", - "ignore_all": "Bỏ qua tất cả", - "update": "Cập nhật", - "ignore": "Bỏ qua", - "old_version_": "Phiên bản cũ: ", - "new_version_": "Phiên bản mới: ", - "enabled": "Đã bật", - "tooltip_enabled": "Tập lệnh này đã được bật.", - "disabled": "Đã tắt", - "tooltip_disabled": "Tập lệnh này đã bị tắt.", - "similarity_": "Độ tương đồng: ", - "codechange_major": "Thay đổi lớn", - "codechange_noticeable": "Thay đổi đáng chú ý", - "codechange_tiny": "Thay đổi nhỏ", - "new_connects_": "@connect mới: ", - "tag_new_connect": "Đã thêm @connect", - "status_last_check": "Lần kiểm tra cuối: $0", - "status_checking_updates": "Đang kiểm tra cập nhật...", - "status_no_update": "Không cần cập nhật", - "status_n_update": "Có $0 bản cần cập nhật", - "status_n_ignored": "Có $0 bản cập nhật bị bỏ qua", - "status_autoclose": "Tự đóng trong $0 giây", - "header_other_update": "Các bản cập nhật khác" - }, - "downloading_status_text": "Đang tải xuống. Đã nhận {{bytes}}.", - "install_page_please_wait": "Vui lòng chờ", - "install_page_loading": "Đang tải trang cài đặt", - "install_page_load_failed": "Tải trang cài đặt thất bại", - "invalid_page": "Trang không hợp lệ", - "background_script_tag": "Đây là một script nền", - "scheduled_script_tag": "Đây là một script hẹn giờ", - "background_script": "Script nền", - "scheduled_script": "Script hẹn giờ", - "install_from_legitimate_sources_warning": "Vui lòng cài đặt script từ các nguồn hợp pháp! Script không rõ nguồn gốc có thể xâm phạm quyền riêng tư của bạn hoặc thực hiện các hoạt động độc hại.", - "antifeature_referral_link_title": "Liên kết giới thiệu", - "antifeature_referral_link_description": "Script này sửa đổi hoặc chuyển hướng đến liên kết giới thiệu của tác giả", - "antifeature_ads_title": "Quảng cáo", - "antifeature_ads_description": "Script này chèn quảng cáo vào các trang bạn truy cập", - "antifeature_payment_title": "Thanh toán", - "antifeature_payment_description": "Script này yêu cầu thanh toán để sử dụng đúng cách", - "antifeature_miner_title": "Đào coin", - "antifeature_miner_description": "Script này tham gia vào các hoạt động đào coin", - "antifeature_membership_title": "Tính năng thành viên", - "antifeature_membership_description": "Script này yêu cầu đăng ký làm thành viên để sử dụng đúng cách", - "antifeature_tracking_title": "Theo dõi", - "antifeature_tracking_description": "Script này theo dõi thông tin người dùng của bạn", - "script_info_load_failed": "Tải thông tin script thất bại", - "script_status_tooltip": "Bạn có thể kiểm soát trạng thái bật của các tập lệnh. Các tập lệnh Tampermonkey thông thường được bật theo mặc định, còn các tập lệnh Nền và Lập lịch thì bị tắt theo mặc định.", - "subscribe_source_tooltip": "Đây là nguồn đăng ký. Khi bạn mở đăng ký, tập lệnh đăng ký sẽ tự động được cài đặt.", - "get_script": "Lấy script", - "report_issue": "Phản hồi lỗi / vấn đề", - "project_docs": "Tài liệu dự án", - "community": "Cộng đồng", - "popup": { - "new_version_available": "Phiên bản mới có sẵn" - }, - "current_page_scripts": "Script đang chạy trên trang hiện tại", - "enabled_background_scripts": "Script nền đã bật và đang chạy", - "script_accessing_cross_origin_resource": "Script đang cố gắng truy cập tài nguyên cross-origin", - "confirm_operation_description": "Vui lòng xác nhận xem bạn có cho phép script thực hiện thao tác này không. Script cũng có thể thêm thẻ @connect để bỏ qua tùy chọn này.", - "extension_site_access_title": "ScriptCat cần quyền truy cập trang web", - "extension_site_access_description": "Cấp quyền truy cập trang web của trình duyệt cho nguồn này để ScriptCat có thể thực hiện yêu cầu. Thao tác này thay đổi cài đặt truy cập trang web của tiện ích.", - "extension_site_access_content": "Trang web", - "domain": "Miền", - "script_name": "Tên script", - "request_domain": "Miền yêu cầu", - "request_url": "Url yêu cầu", - "access_cookie_content": "Script đang cố gắng truy cập nội dung cookie của trang web", - "confirm_script_operation": "Vui lòng xác nhận xem bạn có cho phép script thực hiện thao tác này không. Cookie chứa dữ liệu người dùng quan trọng, vì vậy chỉ cấp quyền truy cập cho các script đáng tin cậy.", - "cookie_domain": "Miền cookie", - "script_operation_title": "Script đang cố gắng truy cập không gian lưu trữ", - "script_operation_description": "Vui lòng xác nhận xem bạn có cho phép script thực hiện thao tác này không. Nếu được phép, script sẽ có thể truy cập không gian lưu trữ bạn đã thiết lập và tạo một thư mục app/${dir} trong đó.", - "script_permission_content": "Script", - "sync_system_connect_failed": "Kết nối hệ thống đồng bộ thất bại", - "sync_system_closed": "Đồng bộ đã tắt", - "sync_system_closed_description": "Đồng bộ bị tắt, vui lòng cấu hình lại", - "auth_type": "Loại xác thực", - "url": "Url", - "username": "Tên người dùng", - "password": "Mật khẩu", - "access_token_bearer": "Mã truy cập (Bearer)", - "s3_bucket_name": "Tên Bucket", - "s3_region": "Vùng", - "s3_access_key_id": "ID Khóa Truy Cập", - "s3_secret_access_key": "Khóa Truy Cập Bí Mật", - "s3_custom_endpoint": "Điểm Cuối Tùy Chỉnh (Tùy Chọn)", - "skip": "Bỏ qua", - "next": "Tiếp theo", - "next_with_progress": "Tiếp theo (bước {step} trên {steps})", - "back": "Quay lại", - "last": "Hoàn thành", - "start_guide_title": "Chào mừng bạn đến với tiện ích scriptcat", - "start_guide_content": "Tiếp theo, chúng tôi sẽ giới thiệu cho bạn cách sử dụng cơ bản của scriptcat.", - "guide_installed_scripts": "Các script bạn đã cài đặt sẽ được hiển thị ở đây.", - "guide_script_list_title": "Chợ script", - "guide_script_list_content": "Bạn có thể cài đặt script từ chợ script. Ngoài việc hỗ trợ script người dùng, scriptcat còn hỗ trợ script nền.", - "guide_script_list_enable_title": "Bật script", - "guide_script_list_enable_content": "Script cần được bật để sử dụng. Script trang được bật theo mặc định khi cài đặt, trong khi script nền bị tắt theo mặc định.", - "guide_script_list_apply_to_run_status_title": "Áp dụng cho và trạng thái chạy", - "guide_script_list_apply_to_run_status_content": "Trạng thái chạy của script được hiển thị ở đây. Di chuột qua các thẻ để xem loại script.", - "guide_script_list_sort_title": "Sắp xếp", - "guide_script_list_sort_content": "Bạn có thể kéo nhãn tập lệnh để sắp xếp chúng.", - "guide_script_list_update_title": "Cập nhật lần cuối", - "guide_script_list_update_content": "Nhấp vào nhãn cột \"Cập nhật lần cuối\" sẽ thực hiện kiểm tra các bản cập nhật cho tập lệnh.", - "guide_script_list_action_title": "Thao tác", - "guide_script_list_action_content": "Thanh thao tác cho phép truy cập vào chức năng chỉnh sửa tập lệnh, kiểm soát việc thực thi và kết thúc tập lệnh (tập lệnh nền) và cài đặt (xem UserConfig). Các nút bên phải cho phép truy cập vào chức năng lọc nâng cao và chuyển đổi chế độ xem.", - "guide_tools_title": "Công cụ chung", - "guide_tools_content": "Các công cụ cung cấp tính năng sao lưu và phát triển.", - "guide_tools_backup_title": "Sao lưu", - "guide_tools_backup_content": "Sao lưu cho phép bạn lưu script để tránh mất mát. Bạn có thể xuất tệp script sang máy cục bộ hoặc tải tệp script từ máy cục bộ của mình. Bạn cũng có thể sao lưu lên đám mây để thuận tiện hơn.", - "guide_setting_title": "Cài đặt", - "guide_setting_content": "Các cài đặt chủ yếu bao gồm ngôn ngữ, đồng bộ script, tần suất cập nhật và các tùy chọn chung khác.", - "guide_setting_sync_title": "Cập nhật và đồng bộ", - "guide_setting_sync_content": "Tính năng đồng bộ script cho phép bạn dễ dàng đồng bộ nội dung script của thiết bị này lên đám mây. Nếu bạn chọn tùy chọn đồng bộ xóa, khi một script bị xóa trên thiết bị này, nó cũng sẽ bị xóa khỏi đám mây. Bạn cũng có thể cập nhật phiên bản script ở đây để có được các tính năng mạnh mẽ hơn.", - "auto": "Tự động", - "hide": "Ẩn", - "custom": "Tùy chỉnh", - "resize_column_width": "Thay đổi kích thước cột", - "collapse": "Thu gọn", - "expand": "Mở rộng", - "menu_expand_num_before": "Các mục menu nhiều hơn", - "menu_expand_num_after": "sẽ bị ẩn.", - "script_name_cannot_be_set_to_empty": "Tên script không được để trống", - "edit_conflict": "Xung đột chỉnh sửa", - "confirm_override_when_edit_conflict": "Tập lệnh này đã được chỉnh sửa ở một phiên bản khác. Việc thay thế sẽ ghi đè các thay đổi đó. Bạn có muốn giữ phiên bản này không?", - "save_abort_when_edit_conflict": "Tập lệnh đã được chỉnh sửa ở một phiên bản khác. Đã hủy lưu.", - "scriptname_conflict": "Xung đột tên script", - "confirm_save_when_scriptname_conflict": "Tên script này đã được sử dụng bởi một script khác. Bạn vẫn muốn lưu chứ?", - "save_abort_when_scriptname_conflict": "Tên script này đã được sử dụng bởi một script khác. Đã hủy lưu.", - "eslint_config_format_error": "Lỗi định dạng cấu hình eslint", - "export_success": "Đổ dữ liệu thành công đã lưu", - "get_backup_dir_url_failed": "Không thể lấy địa chỉ thư mục sao lưu", - "get_backup_files_failed": "Không thể lấy các bản sao lưu", - "request_permission": "Yêu cầu quyền", - "develop_mode_guide": "'Chế độ nhà phát triển' hiện chưa được bật, nên các script không thể hoạt động đúng cách. 👉Nhấn để xem cách bật", - "allow_user_script_guide": "'Cho phép tập lệnh người dùng' hiện chưa được bật, nên các script không thể hoạt động đúng cách. 👉Nhấn để xem cách bật", - "lower_version_browser_guide": "Trình duyệt của bạn quá cũ, nên các script không thể hoạt động đúng cách. 👉Nhấn để xem thêm", - "click_to_reload": "👉Nhấp chuột để tải lại", - "page_in_blacklist": "Trang hiện tại nằm trong danh sách đen, không thể sử dụng script", - "baidu_netdisk": "Baidunetdisk", - "netdisk_unbind": "Hủy liên kết {{provider}}", - "netdisk_unbind_confirm": "Hủy liên kết tài khoản {{provider}}?", - "netdisk_unbind_success": "Đã hủy liên kết tài khoản {{provider}}", - "netdisk_unbind_error": "Không thể hủy liên kết tài khoản {{provider}}", - "save_only_current_group": "Chỉ lưu cho nhóm hiện tại", - "script_import_result": "Kết quả nhập script", - "failure_info": "Thông tin thất bại", - "security": "Bảo mật", - "blacklist_pages": "Trang danh sách đen", - "blacklist_placeholder": "Tắt scriptcat để chạy script trên các trang như\nhttps://*.example.com", - "expression_format_error": "Lỗi định dạng biểu thức điều kiện", - "migration_confirm_message": "Thử lại công cụ lưu trữ di chuyển để sửa đổi dữ liệu hiện có. Vui lòng xác nhận, xem: https://docs.scriptcat.org/docs/change/v0.17/.", - "retry_migration": "Thử lại công cụ lưu trữ di chuyển", - "script_modified_leave_confirm": "Nội dung hiện chưa được lưu. Nếu rời khỏi trang, các thay đổi sẽ bị mất. Bạn có chắc chắn muốn rời đi không?", - "create_success_note": "Script mới được tạo thành công. Lưu ý rằng script nền sẽ không được bật theo mặc định.", - "save_as_failed": "Lưu thành thất bại", - "save_as_success": "Lưu thành công", - "only_background_scheduled_can_run": "Chỉ script nền/script crontab mới có thể chạy", - "preparing_script_resources": "Đang chuẩn bị tài nguyên script...", - "build_success_message": "Xây dựng thành công, mở công cụ nhà phát triển để xem đầu ra trong bảng điều khiển", - "script_storage_tooltip": "Có thể quản lý dữ liệu script đã lưu trữ (gm_value)", - "script_resource_tooltip": "Quản lý @resource, @required các tài nguyên đã tải xuống", - "script_setting_tooltip": "Thực hiện một số cài đặt tùy chỉnh cho script", - "script_modified_close_confirm": "Script đã được sửa đổi. Các thay đổi sẽ bị mất sau khi đóng, tiếp tục?", - "close_current_tab": "Đóng tab hiện tại", - "close_other_tabs": "Đóng các tab khác", - "close_left_tabs": "Đóng các tab bên trái", - "close_right_tabs": "Đóng các tab bên phải", - "import_script_placeholder": "Hỗ trợ liên kết tuyệt đối kết thúc bằng .user.js hoặc liên kết đến trang cài đặt scriptcat\ncó thể điền nhiều dòng, mỗi mục\nví dụ:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/en/script-show-page/1234", - "invalid_script_code": "Mã không hợp lệ", - "build_failed": "Xây dựng thất bại", - "drag_script_here_to_upload": "Kéo script vào đây để tải lên", - "sync_status": "Trạng thái đồng bộ", - "search_scripts": "Tìm kiếm script", - "ext_update_notification": "Tiện ích scriptcat đã cập nhật", - "ext_update_notification_desc": "Phiên bản hiện tại: {{version}}, vui lòng xem nhật ký cập nhật để biết chi tiết", - "watch_file_description": "Theo dõi các thay đổi tệp và tự động cập nhật script. Khi sử dụng, vui lòng đảm bảo đường dẫn tệp script không thay đổi và không thể đóng trang.", - "watch_file": "Theo dõi tệp", - "stop_watch_file": "Dừng theo dõi", - "script_menu_display": "Menu đã đăng ký script", - "badge_type_none": "Không", - "badge_type_run_count": "Số lần chạy", - "badge_type_script_count": "Số script", - "interface_settings": "Giao diện", - "select_interface_language": "Chọn ngôn ngữ hiển thị giao diện", - "extension_icon_badge": "Huy hiệu biểu tượng tiện ích", - "display_type": "Loại hiển thị", - "extension_icon_badge_type": "Loại số hiển thị trên biểu tượng tiện ích", - "background_color": "Màu nền", - "badge_background_color_desc": "Màu nền huy hiệu", - "text_color": "Màu văn bản", - "badge_text_color_desc": "Màu văn bản huy hiệu", - "script_menu": "Menu script", - "display_right_click_menu": "Hiển thị menu chuột phải", - "display_right_click_menu_desc": "Hiển thị menu script trong menu chuột phải của trình duyệt", - "expand_count": "Số lượng mở rộng", - "auto_collapse_when_exceeds": "Tự động thu gọn khi vượt quá số này", - "script_update_check_frequency": "Tần suất kiểm tra cập nhật tập lệnh", - "script_auto_update_frequency": "Tần suất kiểm tra cập nhật script tự động", - "update_options": "Tùy chọn cập nhật", - "control_script_update_behavior": "Kiểm soát hành vi cập nhật script", - "blacklist_pages_desc": "Ngăn script chạy trên các trang được chỉ định, hỗ trợ ký tự đại diện", - "development_tools": "Công cụ phát triển", - "check_script_code_quality": "Kiểm tra chất lượng mã và lỗi script", - "custom_eslint_rules_config": "Cấu hình quy tắc eslint tùy chỉnh (định dạng json)", - "light": "Sáng", - "dark": "Tối", - "individual_edit": "Chỉnh sửa cá nhân", - "batch_edit": "Chỉnh sửa hàng loạt", - "script_code": "Mã kịch bản", - "enter_search_value": "Nhập {{search}} để tìm kiếm", - "script_run_env": { - "title": "Môi trường chạy", - "all": "Tất cả tab", - "normal-tabs": "Tab thường", - "incognito-tabs": "Tab ẩn danh" - }, - "script_run_at": { - "title": "Thời điểm chạy" - }, - "script_setting": { - "title": "Cài đặt script", - "default": "Mặc định" - }, - "editor_config": "Cấu hình trình soạn thảo", - "editor_config_description": "Bạn có thể tham khảo compilerOptions trong jsconfig.js để cấu hình", - "editor_type_definition": "Định nghĩa kiểu trình soạn thảo", - "editor_type_definition_description": "Bạn có thể tự định nghĩa các kiểu của riêng mình, trình soạn thảo script sẽ tự động tải những định nghĩa kiểu này", - "eslint_rules_reset": "Quy tắc ESLint đã được đặt lại", - "eslint_rules_saved": "Quy tắc ESLint đã được lưu", - "editor_config_reset": "Cấu hình trình soạn thảo đã được đặt lại", - "editor_config_saved": "Cấu hình trình soạn thảo đã được lưu", - "editor_config_format_error": "Lỗi định dạng cấu hình trình soạn thảo", - "editor_type_definition_reset": "Định nghĩa kiểu trình soạn thảo đã được đặt lại", - "editor_type_definition_saved": "Định nghĩa kiểu trình soạn thảo đã được lưu", - "script_list": { - "sidebar": { - "stopped": "Đã dừng", - "all": "Tất cả", - "normal_script": "Script bình thường", - "status": "Trạng thái" - } - }, - "tags": "Thẻ", - "install_source": "Nguồn cài đặt", - "input_tags_placeholder": "Nhập thẻ, nhấn Enter để xác nhận", - "switch_to_card_mode": "Chuyển sang chế độ thẻ", - "switch_to_table_mode": "Chuyển sang chế độ bảng", - "open_sidebar": "Mở thanh bên", - "close_sidebar": "Đóng thanh bên", - "no_message_content": "Không có nội dung tin nhắn", - "error_metadata_invalid": "MetaData không hợp lệ", - "error_script_name_required": "Tên script là bắt buộc", - "error_script_version_required": "@version của script là bắt buộc", - "error_script_namespace_required": "@namespace của script là bắt buộc", - "error_cron_invalid": "Biểu thức cron không hợp lệ: {{expr}}", - "error_script_type_mismatch": "Loại script không khớp: script thường và nền không thể chuyển đổi cho nhau", - "error_old_script_code_missing": "Không tìm thấy mã script cũ", - "error_subscribe_name_required": "Tên đăng ký là bắt buộc", - "error_grant_conflict": "@grant khai báo đồng thời 'none' và GM API", - "error_metadata_line_duplicated": "Có các khai báo trùng lặp trong metadata.", - "notification": { - "script_sync_delete": "Đồng bộ xóa script", - "script_sync_delete_desc": "Script {{scriptName}} đã bị xóa", - "subscribe_update": "Đăng ký {{subscribeName}} đã được cập nhật", - "subscribe_update_desc": "Script mới: {{newScripts}}\nScript đã xóa: {{deletedScripts}}" - }, - "loading": "Đang tải...", - "runtime": "Thời gian chạy", - "enable_background": { - "title": "Bật chạy nền", - "description": "Khi bật, trình duyệt sẽ tiếp tục chạy trong nền sau khi bạn đóng tất cả các cửa sổ, thu nhỏ xuống khay hệ thống cho đến khi bạn thoát trình duyệt thủ công. Điều này cho phép các script chạy nền tiếp tục hoạt động.", - "enable_failed": "Bật thất bại", - "disable_failed": "Tắt thất bại", - "prompt_title": "Bật chạy nền?", - "prompt_description": "Đây là {{scriptType}}. Bật chạy nền cho phép script tiếp tục chạy sau khi đóng trình duyệt.", - "enable_now": "Bật ngay", - "maybe_later": "Để sau", - "settings_hint": "Bạn có thể thay đổi tùy chọn này trong cài đặt bất kỳ lúc nào." - }, - "favicon_service": "Dịch vụ Favicon", - "favicon_service_desc": "Chọn dịch vụ để lấy biểu tượng trang web", - "favicon_service_scriptcat": "ScriptCat", - "favicon_service_google": "Google", - "favicon_service_duckduckgo": "DuckDuckGo", - "favicon_service_icon-horse": "Icon Horse", - "favicon_service_local": "Lấy cục bộ", - "editor": { - "show_script_list": "Hiển thị danh sách script", - "hide_script_list": "Ẩn danh sách script" - }, - "agent": "AI Agent", - "agent_chat": "Chat", - "agent_provider": "Model Service", - "agent_mcp": "MCP", - "agent_skills": "Skills", - "agent_provider_title": "Model Service", - "agent_provider_select": "AI Provider", - "agent_provider_api_base_url": "API Base URL", - "agent_provider_api_key": "API Key", - "agent_provider_model": "Default Model", - "agent_provider_test_connection": "Test Connection", - "agent_provider_test_success": "Connection Successful", - "agent_provider_test_failed": "Connection Failed", - "agent_model_fetch": "Fetch Models", - "agent_model_fetch_failed": "Failed to fetch models", - "agent_model_name": "Name", - "agent_model_add": "Add Model", - "agent_model_edit": "Edit", - "agent_model_copy": "Copy", - "agent_model_delete": "Delete", - "agent_model_set_default": "Set as Default", - "agent_model_default_label": "Default", - "agent_model_delete_confirm": "Are you sure to delete this model?", - "agent_model_max_tokens": "Max Output Tokens", - "agent_model_no_models": "No models configured", - "agent_model_vision_support": "Supports vision input", - "agent_model_image_output": "Supports image output", - "agent_model_capabilities": "Khả năng", - "agent_model_supports_vision": "Nhập hình ảnh", - "agent_model_supports_image_output": "Xuất hình ảnh", - "agent_coming_soon": "Coming soon...", - "agent_skills_title": "Skills Management", - "agent_skills_add": "Add Skill", - "agent_skills_empty": "No skills installed", - "agent_skills_tools": "Tools", - "agent_skills_references": "References", - "agent_skills_detail": "Skill Details", - "agent_skills_edit_prompt": "Prompt", - "agent_skills_install": "Install Skill", - "agent_skills_install_url": "Import from URL", - "agent_skills_install_paste": "Paste SKILL.md", - "agent_skills_uninstall": "Uninstall", - "agent_skills_uninstall_confirm": "Are you sure to uninstall Skill \"{{name}}\"?", - "agent_skills_save_success": "Saved successfully", - "agent_skills_install_success": "Installed successfully", - "agent_skills_fetch_failed": "Fetch failed", - "agent_skills_add_script": "Add Script", - "agent_skills_add_reference": "Add Reference", - "agent_skills_install_zip": "Upload ZIP", - "agent_skills_install_zip_hint": "Click to select a .zip file", - "agent_skills_prompt": "Prompt", - "agent_skills_installed_at": "Installed at", - "agent_skills_refresh": "Refresh", - "agent_skills_refresh_success": "Refreshed successfully", - "agent_skills_tool_code": "Tool Code", - "agent_skills_click_to_view_code": "Click tool name to view code", - "agent_skills_config": "Config", - "agent_skills_config_saved": "Config saved", - "agent_skills_check_updates": "Check Updates", - "agent_skills_no_updates": "All skills are up to date", - "agent_skills_updates_available": "update(s) available", - "agent_skills_update": "Update", - "agent_skills_update_success": "Updated successfully", - "agent_skills_url_placeholder": "Enter SKILL.cat.md URL", - "agent_chat_new": "New Chat", - "agent_chat_delete": "Delete Chat", - "agent_chat_delete_confirm": "Delete this conversation?", - "agent_chat_no_conversations": "No conversations", - "agent_chat_input_placeholder": "Type a message...", - "agent_chat_send": "Send", - "agent_chat_stop": "Stop", - "agent_chat_thinking": "Thinking", - "agent_chat_tool_call": "Tool Call", - "agent_chat_error": "Error occurred", - "agent_chat_no_model": "No model configured. Please add one in Model Service first.", - "agent_chat_model_select": "Select Model", - "agent_chat_rename": "Rename", - "agent_chat_copy": "Copy", - "agent_chat_copy_success": "Copied", - "agent_chat_regenerate": "Regenerate", - "agent_chat_streaming": "Generating...", - "agent_chat_newline": "for new line", - "agent_chat_welcome_hint": "Ask me anything about your scripts", - "agent_chat_welcome_start": "Create a conversation to get started", - "agent_chat_tokens": "tokens", - "agent_chat_first_token": "TTFT", - "agent_chat_tools_count": "{{count}} tools", - "agent_chat_tools_enabled": "Tools enabled", - "agent_chat_tools_disabled": "Tools disabled", - "agent_chat_tools_enabled_tip": "Tools enabled — click to disable", - "agent_chat_tools_disabled_tip": "Tools disabled — click to enable", - "agent_chat_background_enabled_tip": "Chế độ nền BẬT — cuộc hội thoại tiếp tục chạy sau khi đóng trang, nhấn để tắt", - "agent_chat_background_disabled_tip": "Chế độ nền TẮT — cuộc hội thoại dừng khi đóng trang, nhấn để bật", - "agent_chat_delete_round": "Delete", - "agent_chat_copy_message": "Copy", - "agent_chat_edit_message": "Chỉnh sửa", - "agent_chat_save_and_send": "Lưu & Gửi", - "agent_chat_cancel_edit": "Hủy", - "agent_chat_message_queued": "Đang chờ", - "agent_chat_cancel_message": "Hủy gửi", - "agent_permission_title": "Script yêu cầu sử dụng cuộc trò chuyện Agent", - "agent_permission_describe": "Script này yêu cầu quyền truy cập chức năng cuộc trò chuyện Agent, sẽ tiêu thụ API token. Chỉ cấp quyền cho các script đáng tin cậy.", - "agent_permission_content": "Cuộc trò chuyện Agent", - "agent_opfs": "OPFS", - "agent_opfs_title": "Trình duyệt tệp OPFS", - "agent_opfs_empty": "Thư mục trống", - "agent_opfs_name": "Tên", - "agent_opfs_size": "Kích thước", - "agent_opfs_type": "Loại", - "agent_opfs_modified": "Sửa đổi lần cuối", - "agent_opfs_delete_confirm": "Bạn có chắc chắn muốn xóa không?", - "agent_opfs_delete_success": "Đã xóa", - "agent_opfs_file": "Tệp", - "agent_opfs_directory": "Thư mục", - "agent_opfs_preview": "Xem trước", - "agent_opfs_root": "Gốc", - "agent_dom_permission_title": "Script yêu cầu quyền thao tác DOM", - "agent_dom_permission_describe": "Script này yêu cầu khả năng đọc và thao tác DOM trang web (nhấp chuột, điền biểu mẫu, điều hướng, chụp ảnh màn hình, v.v.). Chỉ cấp quyền cho các script đáng tin cậy.", - "agent_dom_permission_content": "Agent DOM thao tác", - "agent_mcp_title": "Máy chủ MCP", - "agent_mcp_add_server": "Thêm máy chủ", - "agent_mcp_no_servers": "Chưa cấu hình máy chủ MCP", - "agent_mcp_test_connection": "Kiểm tra", - "agent_mcp_name_url_required": "Tên và URL là bắt buộc", - "agent_mcp_optional": "tùy chọn", - "agent_mcp_custom_headers": "Header tùy chỉnh", - "agent_mcp_enabled": "Đã bật", - "agent_mcp_detail": "Chi tiết", - "agent_mcp_tools": "Công cụ", - "agent_mcp_resources": "Tài nguyên", - "agent_mcp_prompts": "Lời nhắc", - "agent_mcp_no_tools": "Không có công cụ nào", - "agent_mcp_no_resources": "Không có tài nguyên nào", - "agent_mcp_no_prompts": "Không có lời nhắc nào", - "agent_mcp_loading": "Đang tải...", - "agent_mcp_parameters": "Tham số", - "agent_settings": "Cài đặt", - "agent_settings_title": "Cài đặt Agent", - "agent_model_settings": "Cài đặt mô hình", - "agent_summary_model": "Mô hình tóm tắt", - "agent_summary_model_desc": "Dùng cho tóm tắt trang web. Nếu chưa đặt sẽ dùng mô hình mặc định", - "agent_summary_model_placeholder": "Dùng mô hình mặc định", - "agent_search_settings": "Cài đặt tìm kiếm", - "agent_search_engine": "Công cụ tìm kiếm", - "agent_search_engine_baidu": "Baidu", - "agent_search_google_api_key": "Google API Key", - "agent_search_google_cse_id": "ID công cụ tìm kiếm tùy chỉnh", - "agent_settings_saved": "Đã lưu cài đặt", - "agent_settings_save_failed": "Lưu cài đặt thất bại", - "agent_search_engine_tip_bing": "Công cụ tìm kiếm mặc định, phạm vi toàn cầu rộng, không cần cấu hình thêm.", - "agent_search_engine_tip_duckduckgo": "Công cụ tìm kiếm chú trọng quyền riêng tư, không cần API Key.", - "agent_search_engine_tip_baidu": "Tối ưu cho nội dung tiếng Trung, kết quả tốt hơn cho truy vấn tiếng Trung.", - "agent_search_engine_tip_google": "Kết quả tìm kiếm chất lượng cao, cần cấu hình Google API Key và ID công cụ tìm kiếm tùy chỉnh." -} \ No newline at end of file diff --git a/src/locales/zh-CN/agent.json b/src/locales/zh-CN/agent.json new file mode 100644 index 000000000..0e505ed71 --- /dev/null +++ b/src/locales/zh-CN/agent.json @@ -0,0 +1,252 @@ +{ + "title": "AI Agent", + "docs": "文档", + "chat": "会话", + "provider": "模型服务", + "mcp": "MCP", + "skills": "Skills", + "provider_title": "模型服务", + "provider_subtitle": "管理 AI 模型服务商与连接配置", + "provider_select": "AI 服务提供商", + "provider_api_base_url": "API 地址", + "provider_api_key": "API 密钥", + "provider_model": "默认模型", + "provider_test_connection": "测试连接", + "provider_test_success": "连接成功", + "provider_test_failed": "连接失败", + "provider_test_hint": "测试会发送一条最小请求以验证配置", + "provider_docs": "文档", + "provider_count_hint": "默认模型用于新会话", + "model_count": "{{count}} 个模型", + "model_fetch": "获取模型", + "model_fetch_failed": "获取模型列表失败", + "model_name": "名称", + "model_add": "添加模型", + "model_edit": "编辑", + "model_copy": "复制", + "model_delete": "删除", + "model_set_default": "设为默认", + "model_default_label": "默认", + "model_delete_confirm": "确定要删除此模型配置吗?", + "model_max_tokens": "最大输出 Token 数", + "model_context_window": "上下文窗口大小", + "model_no_models": "暂无模型配置", + "model_no_models_desc": "添加第一个模型服务,开始使用 AI Agent", + "model_vision_support": "支持图片输入", + "model_image_output": "支持图片输出", + "model_capabilities": "模型能力", + "model_supports_vision": "视觉输入", + "model_supports_image_output": "图片输出", + "coming_soon": "开发中...", + "skills_title": "Skills 管理", + "skills_subtitle": "管理 Agent 技能包(SKILL.md 提示词 + 脚本 + 参考资料)", + "skills_add": "添加 Skill", + "skills_empty": "暂无已安装的 Skill", + "skills_empty_desc": "上传 ZIP 或从 URL 导入第一个 Skill", + "skills_tools": "工具", + "skills_references": "参考资料", + "skills_detail": "Skill 详情", + "skills_edit_prompt": "提示词", + "skills_install": "安装 Skill", + "skills_install_url": "从 URL 导入", + "skills_install_paste": "粘贴 SKILL.md", + "skills_uninstall": "卸载", + "skills_uninstall_confirm": "确定要卸载 Skill「{{name}}」?", + "skills_save_success": "保存成功", + "skills_install_success": "安装成功", + "skills_fetch_failed": "获取失败", + "skills_add_script": "添加脚本", + "skills_add_reference": "添加参考资料", + "skills_install_zip": "上传 ZIP", + "skills_install_zip_hint": "点击选择 .zip 文件", + "skills_prompt": "提示词", + "skills_installed_at": "安装时间", + "skills_refresh": "刷新", + "skills_refresh_success": "刷新成功", + "skills_tool_code": "工具代码", + "skills_click_to_view_code": "点击工具名称查看代码", + "skills_config": "配置", + "skills_config_saved": "配置已保存", + "skills_check_updates": "检查更新", + "skills_no_updates": "所有 Skill 已是最新版本", + "skills_updates_available": "个 Skill 有更新", + "skills_update": "更新", + "skills_update_success": "更新成功", + "skills_url_placeholder": "输入 SKILL.cat.md URL", + "skills_docs": "文档", + "skills_count": "已安装 {{count}} 个 Skill", + "skills_update_count": "{{count}} 个有可用更新", + "skills_update_available": "可更新 {{version}}", + "skills_references_short": "参考", + "skills_configurable": "可配置", + "skills_open_config": "打开配置", + "chat_new": "新建会话", + "chat_delete": "删除会话", + "chat_delete_confirm": "确定要删除此会话吗?", + "chat_no_conversations": "暂无会话", + "chat_search_placeholder": "搜索会话…", + "chat_search_no_results": "没有匹配的会话", + "chat_export": "导出", + "chat_input_placeholder": "输入消息...", + "chat_send": "发送", + "chat_stop": "停止", + "chat_thinking": "思考过程", + "chat_tool_call": "工具调用", + "chat_tool_arguments": "参数", + "chat_tool_result": "结果", + "chat_error": "发生错误", + "chat_no_model": "未配置模型,请先在模型服务中添加", + "chat_model_select": "选择模型", + "chat_rename": "重命名", + "chat_copy": "复制", + "chat_copy_success": "已复制", + "chat_regenerate": "重新生成", + "chat_streaming": "生成中...", + "chat_retrying": "正在重试 ({{attempt}}/{{max}})...", + "chat_attach_image": "添加图片", + "chat_attach_file": "添加文件", + "chat_welcome_hint": "有关脚本的任何问题,尽管问我", + "chat_welcome_start": "创建一个对话开始吧", + "chat_tokens": "令牌", + "chat_first_token": "TTFT", + "chat_tools_count": "{{count}} 次工具调用", + "chat_tools_enabled": "工具已启用", + "chat_tools_disabled": "工具已禁用", + "chat_tools_enabled_tip": "工具已启用 — 点击禁用", + "chat_tools_disabled_tip": "工具已禁用 — 点击启用", + "chat_background_enabled_tip": "后台运行已开启 — 关闭页面后对话将继续执行,点击关闭", + "chat_background_disabled_tip": "后台运行已关闭 — 关闭页面后对话将停止,点击开启", + "chat_delete_round": "删除", + "chat_copy_message": "复制", + "chat_edit_message": "编辑", + "chat_save_and_send": "保存并发送", + "chat_cancel_edit": "取消", + "chat_message_queued": "排队中", + "chat_cancel_message": "取消发送", + "permission_title": "脚本请求使用 Agent 对话", + "permission_describe": "此脚本请求使用 Agent 对话功能,将消耗 API Token。请仅对可信脚本授权。", + "permission_content": "Agent 对话", + "opfs": "OPFS", + "opfs_title": "OPFS 文件浏览器", + "opfs_empty": "空目录", + "opfs_name": "名称", + "opfs_size": "大小", + "opfs_type": "类型", + "opfs_modified": "最后修改", + "opfs_delete_confirm": "确定要删除吗?", + "opfs_delete_success": "已删除", + "opfs_file": "文件", + "opfs_directory": "目录", + "opfs_preview": "预览", + "opfs_root": "根目录", + "opfs_subtitle": "Origin Private File System · Agent 私有存储", + "opfs_refresh": "刷新", + "opfs_upload": "上传", + "opfs_actions": "操作", + "opfs_item_count": "{{count}} 项", + "opfs_empty_desc": "此目录下暂无文件", + "opfs_upload_success": "已上传", + "opfs_upload_failed": "上传失败", + "opfs_type_directory": "文件夹", + "opfs_type_image": "图片", + "opfs_type_text": "文本", + "opfs_type_binary": "二进制", + "dom_permission_title": "脚本请求 DOM 操作权限", + "dom_permission_describe": "此脚本请求读取和操作网页 DOM 的能力(点击、填写表单、导航、截图等)。请仅对可信脚本授权。", + "dom_permission_content": "Agent DOM 操作", + "mcp_title": "MCP 服务器", + "mcp_subtitle": "连接 MCP 服务器,为 Agent 扩展工具、资源与提示词", + "mcp_add_server": "添加服务器", + "mcp_no_servers": "未配置 MCP 服务器", + "mcp_no_servers_desc": "添加第一个 MCP 服务器,接入外部工具与数据", + "mcp_status_connected": "已连接", + "mcp_status_failed": "连接失败", + "mcp_status_untested": "未测试", + "mcp_has_key": "密钥", + "mcp_headers_count": "{{count}} 个请求头", + "mcp_count_servers": "{{count}} 个服务", + "mcp_count_connected": "{{count}} 个已连接", + "mcp_count_tools": "共 {{count}} 个工具可用", + "mcp_docs": "文档", + "mcp_test_connection": "测试", + "mcp_name_url_required": "名称和 URL 不能为空", + "mcp_optional": "可选", + "mcp_custom_headers": "自定义请求头", + "mcp_enabled": "启用", + "mcp_detail": "详情", + "mcp_tools": "工具", + "mcp_resources": "资源", + "mcp_prompts": "提示词", + "mcp_no_tools": "暂无可用工具", + "mcp_no_resources": "暂无可用资源", + "mcp_no_prompts": "暂无可用提示词", + "mcp_loading": "加载中...", + "mcp_parameters": "参数", + "tasks": "定时任务", + "tasks_title": "定时任务管理", + "tasks_subtitle": "按 Cron 周期自动运行 Agent 任务", + "tasks_docs": "文档", + "tasks_count_total": "{{count}} 个任务", + "tasks_count_enabled": "{{count}} 个已启用", + "tasks_create": "创建任务", + "tasks_edit": "编辑任务", + "tasks_mode": "模式", + "tasks_mode_internal": "内部执行", + "tasks_mode_event": "事件驱动", + "tasks_mode_internal_short": "内部", + "tasks_mode_event_short": "事件", + "tasks_cron": "Cron 表达式", + "tasks_next_run": "下次运行", + "tasks_last_status": "上次状态", + "tasks_run_now": "立即运行", + "tasks_history": "运行历史", + "tasks_prompt": "提示词", + "tasks_max_iterations": "最大迭代次数", + "tasks_notify": "完成通知", + "tasks_notify_desc": "任务完成后发送浏览器通知", + "tasks_no_tasks": "暂无定时任务", + "tasks_no_tasks_desc": "创建第一个定时任务,让 Agent 按计划自动执行", + "tasks_no_runs": "暂无运行记录", + "tasks_delete_confirm": "确定要删除此定时任务吗?", + "tasks_clear_runs": "清除历史", + "tasks_clear_runs_confirm": "确定要清除此任务的运行历史吗?", + "tasks_event_hint": "任务触发时将通知创建此任务的脚本", + "tasks_event_trigger": "事件触发", + "tasks_name_cron_required": "名称和 Cron 表达式不能为空", + "tasks_model_select": "选择模型", + "tasks_skills": "Skills", + "tasks_skills_auto": "自动加载全部", + "tasks_conversation_id": "续接对话 ID(可选)", + "tasks_run_status_success": "成功", + "tasks_run_status_error": "失败", + "tasks_run_status_running": "运行中", + "tasks_run_duration": "耗时", + "tasks_run_usage": "用量", + "tasks_run_conversation": "查看对话", + "tasks_run_time": "时间", + "tasks_run_status": "状态", + "tasks_never_run": "未运行", + "settings": "设置", + "settings_title": "Agent 设置", + "settings_subtitle": "模型、搜索与通用偏好 · 修改即时生效", + "settings_cat_model": "模型", + "settings_cat_search": "搜索", + "model_settings": "模型设置", + "summary_model": "摘要模型", + "summary_model_desc": "用于网页摘要等场景,未设置时使用默认模型", + "summary_model_placeholder": "使用默认模型", + "search_settings": "搜索设置", + "search_engine": "搜索引擎", + "search_engine_desc": "web_search 工具使用的检索来源。", + "search_engine_baidu": "百度", + "search_google_api_key": "Google API Key", + "search_google_api_key_desc": "用于调用 Custom Search JSON API。", + "search_google_cse_id": "自定义搜索引擎 ID", + "search_google_cse_id_desc": "Programmable Search Engine 的 cx 参数。", + "settings_saved": "设置已保存", + "settings_save_failed": "保存设置失败", + "search_engine_tip_bing": "默认搜索引擎,全球覆盖广泛,无需额外配置。", + "search_engine_tip_duckduckgo": "注重隐私保护的搜索引擎,无需 API Key。", + "search_engine_tip_baidu": "针对中文内容优化,中文搜索效果更好。", + "search_engine_tip_google": "搜索质量更高,需要配置 Google API Key 和自定义搜索引擎 ID。" +} diff --git a/src/locales/zh-CN/common.json b/src/locales/zh-CN/common.json new file mode 100644 index 000000000..fa1ad72df --- /dev/null +++ b/src/locales/zh-CN/common.json @@ -0,0 +1,131 @@ +{ + "user_guide": "使用指南", + "api_docs": "API文档", + "development_guide": "开发指南", + "script_gallery": "脚本站", + "community_forum": "社区论坛", + "external_links": "外部链接", + "system_follow": "跟随系统", + "no_data": "暂无数据", + "logs": "日志", + "tools": "工具", + "find": "查找", + "replace": "替换", + "settings": "设置", + "change_theme": "主题切换", + "hide_main_sidebar": "收起侧边栏", + "show_main_sidebar": "展开侧边栏", + "menu": "菜单", + "guide": "新手指引", + "helpcenter": "帮助中心", + "save": "保存", + "file": "文件", + "save_success": "保存成功", + "reset_success": "重置成功", + "update": "更新", + "check_update": "检查更新", + "confirm_delete": "确认删除", + "confirm_update": "确认更新", + "delete_success": "删除成功", + "deleting": "删除中", + "enable": "开启", + "script_list_enable_width": 80, + "script_list_last_updated_width": 120, + "script_list_apply_to_run_status_width": 140, + "subscribe_list_enable_width": 100, + "disable": "关闭", + "name": "名称", + "version": "版本", + "source": "来源", + "home": "主页", + "action": "操作", + "export": "导出", + "delete": "删除", + "pin_to_top": "置顶", + "confirm": "确定", + "close": "关闭", + "config": "配置", + "key": "key", + "value": "value", + "add": "新增", + "type": "类型", + "size": "大小", + "download": "下载", + "edit_value": "编辑值", + "add_value": "新增值", + "update_success": "修改成功", + "add_success": "添加成功", + "clear": "清空", + "type_string": "string", + "type_number": "number", + "type_boolean": "boolean", + "type_object": "object", + "confirm_delete_resource": "你确定删除此资源吗?在下次开启时将会重新加载此资源", + "confirm_clear_resource": "你真的要清空这些资源吗?在下次开启时将会重新加载资源", + "yes": "是", + "no": "否", + "confirm_delete_permission": "确定要删除此授权吗?", + "reset": "重置", + "run_once": "运行一次", + "stop": "停止", + "edit": "编辑", + "copy": "复制", + "exclude_on": "恢复在 $0 上执行", + "exclude_off": "排除在 $0 上执行", + "get_confirm_error": "获取确认信息失败", + "confirm_error": "确认失败", + "ignore": "忽略", + "temporary_allow": "临时允许此{{permissionContent}}", + "temporary_allow_all": "临时允许全部{{permissionContent}}", + "permanent_allow": "永久允许此{{permissionContent}}", + "permanent_allow_all": "永久允许全部{{permissionContent}}", + "temporary_deny": "临时拒绝此{{permissionContent}}", + "temporary_deny_all": "临时拒绝全部{{permissionContent}}", + "permanent_deny": "永久拒绝此{{permissionContent}}", + "permanent_deny_all": "永久拒绝全部{{permissionContent}}", + "import": "导入", + "author": "作者", + "description": "描述", + "operation": "操作", + "error": "错误", + "unknown": "未知", + "add_new": "新增", + "no_operation": "不做操作", + "enable_script": "开启脚本", + "local_creation": "本地创建", + "import_success": "导入成功", + "get_script": "获取脚本", + "report_issue": "BUG/问题反馈", + "project_docs": "项目文档", + "community": "交流社区", + "domain": "域名", + "script_name": "脚本名称", + "skip": "跳过", + "next": "下一步", + "next_with_progress": "下一步(第 {step} 步 / 共 {steps} 步)", + "back": "上一步", + "last": "完成", + "auto": "自动", + "hide": "隐藏", + "custom": "自定义", + "resize_column_width": "调整列宽", + "collapse": "收起", + "expand": "展开", + "import_script_placeholder": "支持输入.user.js结尾的脚本绝对链接 或 脚本猫安装页链接\n可多行填写,每行一条\n示例:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/zh-CN/script-show-page/1234", + "light": "亮色模式", + "dark": "暗色模式", + "enter_search_value": "请输入 {{search}} 进行搜索", + "no_message_content": "无消息内容", + "loading": "加载中...", + "auth_type": "鉴权类型", + "url": "URL", + "username": "用户名", + "password": "密码", + "access_token_bearer": "访问令牌(Bearer)", + "s3_bucket_name": "存储桶名称", + "s3_region": "区域", + "s3_access_key_id": "访问密钥 ID", + "s3_secret_access_key": "访问密钥密文", + "s3_custom_endpoint": "自定义端点(可选)", + "cancel": "取消" +} diff --git a/src/locales/zh-CN/editor.json b/src/locales/zh-CN/editor.json new file mode 100644 index 000000000..18c8b6f6f --- /dev/null +++ b/src/locales/zh-CN/editor.json @@ -0,0 +1,132 @@ +{ + "save": "保存", + "save_success": "保存成功", + "copy": "复制", + "find": "查找", + "replace": "替换", + "select_all": "全选", + "format": "格式化", + "back": "返回", + "more": "更多", + "file": "文件", + "edit": "编辑", + "settings": "设置", + "run_settings": "运行设置", + "code": "代码", + "script_setting": "脚本设置", + "storage": "储存", + "resource": "资源", + "script_list": "脚本列表", + "search_scripts": "搜索脚本...", + "search_resource": "搜索资源", + "search_storage": "搜索 Key", + "record_count": "{{count}} 条记录", + "resource_count": "{{count}} 个资源 · 共 {{size}}", + "new_script": "新建脚本", + "source": "来源", + "from_user": "用户", + "from_script": "脚本", + "last_updated": "最近更新", + "run_at": "运行时机", + "run_in": "运行环境", + "check_update": "检查更新", + "line_col": "行 {{line}}, 列 {{col}}", + "script_not_found": "脚本不存在", + "confirm_delete_script": "确定删除脚本「{{name}}」吗?", + "delete_success": "删除成功", + "delete_failed": "删除失败", + "cancel": "取消", + "confirm": "确定", + "save_as": "另存为", + "run": "运行", + "debug": "调试", + "script_storage": "脚本储存", + "enter_key": "请输入key", + "key_placeholder": "key", + "value_placeholder": "当类型为object时,请输入可以JSON解析的数据", + "clear_success": "清空成功", + "confirm_clear": "你真的要清空这个储存空间吗?", + "script_resource": "脚本资源", + "basic_info": "基本信息", + "update_url": "更新URL", + "add_permission": "添加授权", + "match": "匹配", + "user_setting": "用户设定", + "confirm_delete_exclude": "确认删除该排除?", + "after_deleting_match_item": "脚本设定的匹配项删除后会自动添加到匹配项中", + "confirm_delete_match": "确认删除该匹配?", + "after_deleting_exclude_item": "脚本设定的匹配项删除后会自动添加到排除项中", + "add_match": "添加匹配", + "add_exclude": "添加排除", + "website_match": "网站匹配(@match)", + "website_exclude": "网站排除(@exclude)", + "confirm_reset": "确定重置?", + "undo": "撤销", + "redo": "重做", + "cut": "剪切", + "paste": "粘贴", + "user_config": "用户配置", + "gm_api": "GM Api", + "storage_api": "Storage API", + "use_file_system": "使用的文件系统", + "open_directory": "打开目录", + "account_validation_failed": "账号信息验证失败", + "not_set": "未设置", + "in_use": "使用中", + "storage_error": "储存错误", + "upload_to_cloud": "上传到云端", + "save_failed": "保存失败", + "exporting": "导出中...", + "upload_to": "上传至", + "value_export_expression": "值导出表达式", + "overwrite_original_value_on_import": "导入时覆盖原值", + "cookie_export_expression": "cookie导出表达式", + "overwrite_original_cookie_on_import": "导入时覆盖原值", + "restore_default_values": "恢复默认值", + "edit_conflict": "编辑冲突", + "confirm_override_when_edit_conflict": "此脚本已在其他实例中被修改。替换将覆盖这些更改。是否要保留此版本?", + "save_abort_when_edit_conflict": "该脚本已在其他实例中被修改,保存已中止。", + "scriptname_conflict": "脚本名称冲突", + "confirm_save_when_scriptname_conflict": "该脚本名称已被其他脚本使用,是否仍要保存?", + "save_abort_when_scriptname_conflict": "该脚本名称已被其他脚本使用,已取消保存。", + "eslint_config_format_error": "ESLint配置格式错误", + "script_modified_leave_confirm": "当前内容尚未保存,离开后更改将丢失,确定要离开吗?", + "create_success_note": "新建成功,请注意后台脚本不会默认开启", + "save_as_failed": "另存为失败", + "save_as_success": "另存为成功", + "only_background_scheduled_can_run": "只有后台脚本/定时脚本才能运行", + "preparing_script_resources": "正在准备脚本资源...", + "build_success_message": "构建成功, 可以在扩展页打开开发者工具在控制台中查看输出", + "script_storage_tooltip": "可以管理脚本的储存数据(GM_value)", + "script_resource_tooltip": "管理@resource,@require下载的资源", + "script_setting_tooltip": "对脚本进行一些自定义设置", + "script_modified_close_confirm": "脚本已修改, 关闭后会丢失修改, 是否继续?", + "close_current_tab": "关闭当前标签页", + "close_other_tabs": "关闭其他标签页", + "close_left_tabs": "关闭左侧标签页", + "close_right_tabs": "关闭右侧标签页", + "invalid_script_code": "错误的脚本代码", + "build_failed": "构建失败", + "drag_script_here_to_upload": "拖拽脚本到此处上传", + "watch_file_description": "监听文件变动,自动更新脚本,使用时请确保脚本文件路径不变且不能关闭页面", + "watch_file": "监听文件", + "stop_watch_file": "停止监听", + "individual_edit": "单独编辑", + "batch_edit": "批量编辑", + "script_code": "脚本代码", + "editor_config": "编辑器配置", + "editor_config_description": "你可以参考jsconfig.js中的compilerOptions进行配置", + "editor_type_definition": "编辑器类型定义", + "editor_type_definition_description": "你可以自定义自己的类型定义,脚本编辑器会自动加载这些类型定义", + "eslint_rules_reset": "ESLint规则已重置", + "eslint_rules_saved": "ESLint规则已保存", + "editor_config_reset": "编辑器配置已重置", + "editor_config_saved": "编辑器配置已保存", + "editor_config_format_error": "编辑器配置格式错误", + "editor_type_definition_reset": "编辑器类型定义已重置", + "editor_type_definition_saved": "编辑器类型定义已保存", + "editor": { + "show_script_list": "显示脚本列表", + "hide_script_list": "隐藏脚本列表" + } +} diff --git a/src/locales/zh-CN/guide.json b/src/locales/zh-CN/guide.json new file mode 100644 index 000000000..1c5cfe3a7 --- /dev/null +++ b/src/locales/zh-CN/guide.json @@ -0,0 +1,25 @@ +{ + "start_title": "欢迎使用脚本猫扩展", + "start_content": "接下来我们将为您介绍脚本猫的基本使用方法", + "installed_scripts": "你所安装的脚本将会在这里显示", + "script_list_title": "脚本市场", + "script_list_content": "可以从脚本市场中安装脚本,脚本猫除了支持用户脚本以外,还支持后台脚本", + "script_list_enable_title": "脚本开启", + "script_list_enable_content": "脚本需要开启才能使用,页面脚本安装默认开启,后台脚本安装默认关闭", + "script_list_apply_to_run_status_title": "应用至、与运行状态", + "script_list_apply_to_run_status_content": "脚本运行状态展示,鼠标悬浮至标签可以查看脚本类型", + "script_list_sort_title": "排序", + "script_list_sort_content": "你可以拖动脚本的标签进行排序", + "script_list_update_title": "最后更新", + "script_list_update_content": "点击最后更新列标签可以对脚本进行一次检查更新", + "script_list_action_title": "操作", + "script_list_action_content": "操作列可以进入脚本编辑、控制脚本的运行停止(后台脚本)、设置UserConfig,操作右侧按钮可以打开高级筛选与切换视图模式", + "tools_title": "常用工具", + "tools_content": "工具中提供了备份和开发的工具", + "tools_backup_title": "备份", + "tools_backup_content": "备份可以保存脚本,避免丢失。可以导出脚本文件到本地,也可以加载本地的脚本文件。也可以备份到云端,更加方便。", + "setting_title": "设置", + "setting_content": "设置中主要包含了语言、脚本同步、更新频率等常用设置项", + "setting_sync_title": "更新与同步", + "setting_sync_content": "脚本同步功能可以方便的将本设备的脚本内容同步至云端,如果有多台设备请勾选同步删除选项,当本设备脚本删除时,会从云端删除对应的脚本,也会将其它设备的脚本删除。" +} diff --git a/src/locales/zh-CN/index.ts b/src/locales/zh-CN/index.ts new file mode 100644 index 000000000..bacd6d096 --- /dev/null +++ b/src/locales/zh-CN/index.ts @@ -0,0 +1,11 @@ +export { default as agent } from "./agent.json"; +export { default as common } from "./common.json"; +export { default as editor } from "./editor.json"; +export { default as guide } from "./guide.json"; +export { default as install } from "./install.json"; +export { default as logs } from "./logs.json"; +export { default as permission } from "./permission.json"; +export { default as popup } from "./popup.json"; +export { default as script } from "./script.json"; +export { default as settings } from "./settings.json"; +export { default as tools } from "./tools.json"; diff --git a/src/locales/zh-CN/install.json b/src/locales/zh-CN/install.json new file mode 100644 index 000000000..0731a6ff2 --- /dev/null +++ b/src/locales/zh-CN/install.json @@ -0,0 +1,207 @@ +{ + "data_import": "数据导入", + "select_scripts_to_import": "请选择你要导入的脚本", + "select_all": "全选", + "script_import_progress": "脚本导入进度", + "select_subscribes_to_import": "请选择你要导入的订阅", + "subscribe_import_progress": "订阅导入进度", + "script": "安装", + "update_script": "更新", + "subscribe": "安装订阅", + "update_subscribe": "更新订阅", + "update_script_no_close": "更新,不关闭窗口", + "script_no_close": "安装,不关闭窗口", + "update_script_no_more_update": "更新,但不再检查更新", + "close_update_script_no_more_update": "关闭,且不再检查更新", + "script_no_more_update": "安装,但不再检查更新", + "invalid_link": "错误的链接", + "subscribe_install_label": "该订阅将会安装下面的脚本", + "subscribe_scripts_title": "本订阅将安装以下脚本", + "subscribe_scripts_empty": "该订阅暂未声明脚本", + "script_runs_in": "脚本将在下面的网站中运行", + "script_has_full_access_to": "脚本将获得以下地址的完整访问权限", + "script_requires": "脚本引用了下列外部资源", + "cookie_warning": "请注意,本脚本会请求 Cookie 的操作权限,这是一个危险的权限,请确认脚本的安全性。", + "perm_card_title": "此脚本将获得以下权限", + "perm_card_hint": "安装前请确认", + "perm_card_empty": "此脚本不请求任何特殊权限", + "perm_match_label": "运行网站", + "perm_match_summary": "脚本会在这些网站上运行并修改页面", + "perm_connect_label": "跨域访问", + "perm_connect_summary": "可向以下域名发送请求、读取其数据", + "perm_grant_label": "GM 能力", + "perm_grant_summary": "可调用以下油猴 API", + "perm_require_label": "外部资源", + "perm_require_summary": "加载以下第三方脚本与资源", + "badge_background": "后台", + "badge_scheduled": "定时", + "enabled_label": "已启用", + "schedule_cron_label": "定时任务", + "schedule_next_run": "下次运行", + "schedule_background_desc": "浏览器开启时自动运行", + "code_lines": "{{count}} 行", + "code_copy": "复制代码", + "code_collapse": "折叠", + "code_expand": "展开", + "loading_title": "正在加载脚本", + "loading_desc": "正在从来源下载并解析脚本内容", + "error_retry": "重试", + "error_invalid_desc": "缺少有效的安装来源参数,无法加载脚本。", + "context_install": "脚本安装", + "context_update": "脚本更新", + "background_script": "后台脚本", + "scheduled_script": "定时脚本", + "watching_status": "正在监听文件变化,保存后自动重新安装", + "watching_chip": "监听中", + "watching_title": "正在监听文件变化", + "watching_file_desc": "保存 {{file}} 会自动重新安装并同步脚本", + "watching_last_sync": "最后更新 {{time}}", + "warning_title": "请确认脚本来自可信来源", + "warning_risk_connect": "它可以访问所有域名", + "warning_risk_antifeature": "它声明了反功能特性", + "warning_risk_join": " 且 ", + "warning_risk_tail": " — 请谨慎安装。", + "action_note_install": "安装代表您信任该脚本的来源与作者", + "action_note_update": "更新代表您接受此代码与权限变更", + "action_note_subscribe": "安装代表您信任该订阅及其作者", + "action_note_watching": "监听中 — 保存文件会自动更新,停止后可手动操作", + "context_skill_install": "技能安装", + "context_skill_update": "技能更新", + "skill_kind": "AI 技能", + "skill_prompt_title": "提示词", + "skill_prompt_chip": "SKILL.md", + "skill_tools_title": "工具", + "skill_config_title": "配置项", + "skill_references_title": "参考资料", + "skill_required": "必填", + "skill_secret": "私密", + "skill_install": "安装 Skill", + "skill_update": "更新 Skill", + "skill_warning": "技能会向 AI 注入提示词,并授予其调用以下工具、GM 能力与配置的权限,请从合法来源安装!", + "skill_warning_title": "请确认该 Skill 来自可信来源", + "skill_warning_desc": "安装后会向 AI 注入提示词,并授予列出的工具(含 GM 权限)与配置读写 — 请谨慎安装。", + "success": "安装成功", + "install": { + "update_success": "更新成功" + }, + "failed": "安装失败", + "subscribe_success": "订阅成功", + "subscribe_failed": "订阅失败", + "current_version": "当前版本", + "update_version": "更新版本", + "updatepage": { + "title": "批量更新", + "main_header": "检查更新", + "last_check": "上次检查 {{time}}", + "status_checking_updates": "正在检查更新...", + "updates_available": "{{count}} 个可用更新", + "ignored_count": "{{count}} 个已忽略", + "selected_count": "已选 {{selected}} / {{total}} 项", + "update_selected": "更新选中 ({{count}})", + "ignore_selected": "忽略选中", + "update": "更新", + "ignore": "忽略", + "restore": "恢复更新", + "restore_all": "全部恢复", + "ignored_section": "已忽略的更新", + "auto_close": "{{count}} 秒后自动关闭", + "col_script": "脚本", + "col_version": "版本", + "col_change": "变更", + "col_source": "来源", + "col_action": "操作", + "enabled": "已启用", + "disabled": "已停用", + "codechange_major": "重大改动", + "codechange_noticeable": "显著改动", + "codechange_tiny": "轻微改动", + "tag_new_connect": "新增 @connect", + "empty_title": "所有脚本均为最新", + "empty_desc": "已检查 {{count}} 个脚本 · 暂无可用更新", + "similarity": "相似度", + "new_connects": "新增连接", + "toast_found": "发现 {{count}} 个可更新脚本", + "toast_uptodate": "所有脚本均为最新" + }, + "importpage": { + "title": "数据导入", + "context_review": "数据导入", + "context_importing": "正在导入", + "context_done": "导入完成", + "selected_count": "已选 {{selected}} / {{total}} 项", + "unimportable_count": "{{count}} 项无法导入", + "count_scripts": "{{count}} 脚本", + "count_subscribes": "{{count}} 订阅", + "col_script": "脚本", + "col_version": "版本", + "col_source": "来源", + "col_data": "数据", + "col_status": "状态", + "col_enabled": "启用", + "op_add": "新增", + "op_update": "更新", + "op_error": "解析失败", + "source_local": "本地创建", + "data_values": "{{count}} 项", + "data_resources": "含资源", + "enable_after_import": "安装后启用", + "row_error": "文件损坏,无法导入", + "unknown_script": "未知脚本", + "subscribe_section": "订阅", + "trust_hint": "仅恢复你勾选的项目,导入不会向外发送任何数据", + "import_selected": "导入所选 ({{count}})", + "importing_progress": "正在恢复备份 · 已完成 {{done}} / {{total}}", + "importing_hint": "请保持页面打开", + "importing_actionbar_hint": "正在恢复脚本与数据,请勿关闭页面", + "importing_button": "导入中…", + "cancel": "取消", + "status_pending": "待导入", + "status_importing": "导入中", + "status_done": "已导入", + "status_skipped": "已跳过", + "done_title": "导入完成", + "done_desc": "已恢复所选的脚本与数据", + "done_stat_scripts": "{{count}} 个脚本", + "done_stat_subscribes": "{{count}} 个订阅", + "done_stat_values": "{{count}} 项数据", + "view_scripts": "查看脚本列表", + "loading_title": "正在解析备份文件", + "loading_desc": "正在读取并校验备份内容", + "error_title": "无法读取备份文件", + "error_desc": "备份文件可能已损坏,或导入链接已失效", + "invalid_desc": "导入链接无效或已过期", + "retry": "重试", + "empty_title": "备份中没有可导入的项", + "empty_desc": "该备份文件不包含任何脚本或订阅" + }, + "downloading_status_text": "正在下载。已接收 {{bytes}}。", + "downloading_status_percent": "正在下载。已接收 {{bytes}} / {{total}}({{percent}}%)。", + "page_please_wait": "请稍等", + "page_loading": "安装页面加载中", + "page_load_failed": "安装页面加载失败", + "invalid_page": "无效页面", + "background_script_tag": "这是一个后台脚本", + "scheduled_script_tag": "这是一个定时脚本", + "from_legitimate_sources_warning": "请从合法来源安装脚本!未知的脚本可能会侵犯您的隐私或者做出恶意的操作!", + "referral_link_title": "推荐链接", + "referral_link_description": "该脚本会修改或重定向到作者的返佣链接", + "ads_title": "附带广告", + "ads_description": "该脚本会在你访问的页面上插入广告", + "payment_title": "付费脚本", + "payment_description": "该脚本需要你付费才能够正常使用", + "miner_title": "挖矿", + "miner_description": "该脚本存在挖矿行为", + "membership_title": "会员功能", + "membership_description": "该脚本需要注册会员才能正常使用", + "tracking_title": "信息追踪", + "tracking_description": "该脚本会追踪你的用户信息", + "script_info_load_failed": "脚本信息加载失败!", + "script_import_result": "脚本导入结果", + "failure_info": "失败信息", + "source": "安装来源", + "skill_prompt": "提示词", + "skill_tools": "工具", + "skill_config": "配置项", + "skill_references": "参考资料", + "skill_install_failed": "Skill 安装失败" +} diff --git a/src/locales/zh-CN/logs.json b/src/locales/zh-CN/logs.json new file mode 100644 index 000000000..467df82aa --- /dev/null +++ b/src/locales/zh-CN/logs.json @@ -0,0 +1,59 @@ +{ + "log_title": "运行日志", + "last_5_minutes": "最近5分钟", + "last_15_minutes": "最近15分钟", + "last_30_minutes": "最近30分钟", + "last_1_hour": "最近1小时", + "last_3_hours": "最近3小时", + "last_6_hours": "最近6小时", + "last_12_hours": "最近12小时", + "last_24_hours": "最近24小时", + "last_7_days": "最近7天", + "query": "查询", + "labels": "Labels", + "search_regex": "搜索(支持正则)", + "clean_schedule": "定时清理", + "days_ago_logs": "天前的日志", + "delete_completed": "删除完成", + "delete_current_logs": "删除当前日志", + "clear_completed": "清空完成", + "clear_logs": "清空日志", + "now": "至今", + "total_logs": "共查询到 {{length}} 条日志", + "filtered_logs": "筛选后 {{length}} 条日志", + "enter_filter_conditions": "请输入筛选条件进行查询", + "last_updated": "最后更新", + "runtime": "运行时", + "advanced": "高级", + "label_filter": "标签筛选", + "add_label": "添加标签", + "custom_range": "自定义范围", + "back_to_top": "回到顶部", + "refresh": "刷新", + "all_levels": "全部", + "total_count": "共 {{count}} 条", + "filtered_count": "筛选出 {{count}} 条", + "clear_logs_confirm": "确定清空所有日志?此操作不可撤销。", + "no_logs": "暂无日志", + "refresh_off": "关闭", + "interval_5s": "5秒", + "interval_10s": "10秒", + "interval_30s": "30秒", + "interval_1m": "1分钟", + "interval_5m": "5分钟", + "quick_range": "快捷范围", + "absolute_range": "绝对时间范围", + "from_start": "从(开始)", + "to_end": "到(结束)", + "apply_range": "应用范围", + "auto_refresh_hint": "结束设为「现在」时,自动刷新会持续拉取最新日志", + "group_minutes": "分钟", + "group_hours": "小时", + "group_days": "天", + "weekdays_short": "日,一,二,三,四,五,六", + "year_month": "{{year}} 年 {{month}} 月", + "time": "时间", + "prev_month": "上个月", + "next_month": "下个月", + "live": "实时" +} diff --git a/src/locales/zh-CN/permission.json b/src/locales/zh-CN/permission.json new file mode 100644 index 000000000..6aaa7fd95 --- /dev/null +++ b/src/locales/zh-CN/permission.json @@ -0,0 +1,42 @@ +{ + "permission": "权限", + "permission_value": "授权值", + "allow": "是否允许", + "permission_management": "授权管理", + "permission_cors": "跨域(cors)", + "permission_cookie": "管理cookie", + "allow_once": "允许一次", + "deny_once": "拒绝一次", + "script_accessing_cross_origin_resource": "脚本正在试图访问跨域资源", + "confirm_operation_description": "请您确认是否允许脚本进行此操作,脚本也可增加@connect标签跳过此选项", + "request_domain": "请求域名", + "request_url": "请求地址", + "access_cookie_content": "脚本正在试图访问网站cookie内容", + "confirm_script_operation": "请您确认是否允许脚本进行此操作,cookie是一项重要的用户数据,请务必只给信任的脚本授权.", + "cookie_domain": "Cookie域", + "script_operation_title": "脚本正在试图操作脚本同步储存空间", + "script_operation_description": "请您确认是否允许脚本进行此操作,允许后将允许脚本操作你设定的储存空间,脚本会在储存空间下创建一个app/${dir}的目录进行使用", + "script_permission_content": "脚本", + "extension_site_access_title": "ScriptCat 需要站点访问权限", + "extension_site_access_description": "请授予浏览器对此来源的站点访问权限,让 ScriptCat 可以完成本次请求。这会修改扩展的站点访问设置。", + "extension_site_access_content": "站点", + "request_permission": "请求权限", + "allow_user_script_guide": "当前未启用“允许运行用户脚本”,脚本无法正常运行。👉点击查看启用方法", + "user_script_type": "用户脚本", + "auth_duration": "授权时长", + "duration_once": "仅此次", + "duration_temporary": "临时", + "duration_permanent": "永久", + "apply_to_all_domains": "应用到所有请求域名", + "apply_to_all_domains_desc": "对该脚本的全部请求生效(通配)", + "allow_action": "允许", + "deny_action": "拒绝", + "ignore_action": "忽略", + "cancel_action": "取消", + "loading_confirm": "正在读取授权请求…", + "cookie_warning_title": "高敏感权限", + "cookie_warning_desc": "Cookie 包含登录态等敏感数据,请仅授权可信脚本。", + "confirm_expired_title": "授权请求已失效", + "confirm_expired_desc": "此次授权请求已超时或已被处理。请返回页面重新触发该操作。", + "auto_close_in": "{{second}} 秒后自动关闭窗口" +} diff --git a/src/locales/zh-CN/popup.json b/src/locales/zh-CN/popup.json new file mode 100644 index 000000000..66c34737e --- /dev/null +++ b/src/locales/zh-CN/popup.json @@ -0,0 +1,27 @@ +{ + "new_version_available": "有新版本可用", + "current_page_scripts": "当前页运行脚本", + "enabled_background_scripts": "开启和运行的后台脚本", + "menu_expand_num_before": "菜单项超过", + "menu_expand_num_after": "个时,自动隐藏", + "develop_mode_guide": "当前未启用“开发者模式”,脚本无法正常运行。👉点击查看启用方法", + "lower_version_browser_guide": "您的浏览器版本过低,脚本无法正常运行。👉点击了解更多", + "click_to_reload": "👉点击重新加载", + "page_in_blacklist": "当前页面在黑名单中,无法使用脚本", + "ext_update_notification": "脚本猫扩展已更新", + "ext_update_notification_desc": "当前版本:{{version}},详情请查看更新日志", + "script_menu_display": "脚本注册的菜单", + "badge_type_none": "不显示", + "badge_type_run_count": "运行次数", + "badge_type_script_count": "脚本个数", + "script_menu": "脚本菜单", + "display_right_click_menu": "显示右键菜单", + "display_right_click_menu_desc": "在浏览器右键菜单中显示脚本菜单", + "expand_count": "展开数量", + "auto_collapse_when_exceeds": "超过此数量时自动折叠", + "allow_user_script_guide": "当前未启用“允许运行用户脚本”,脚本无法正常运行。👉点击查看启用方法", + "request_permission": "请求权限", + "show_more_scripts": "+{{count}} 个脚本", + "use_on_mobile": "在手机上使用脚本猫", + "scan_qr_to_install": "扫描二维码在手机上安装脚本猫" +} diff --git a/src/locales/zh-CN/script.json b/src/locales/zh-CN/script.json new file mode 100644 index 000000000..32f60ac91 --- /dev/null +++ b/src/locales/zh-CN/script.json @@ -0,0 +1,115 @@ +{ + "import_link": "链接导入", + "import_link_failure": "链接导入失败", + "create_user_script": "新建普通脚本", + "create_background_script": "新建后台脚本", + "create_scheduled_script": "新建定时脚本", + "import_by_local": "本地导入", + "import_local_failure": "本地导入失败", + "import_local_success": "本地导入成功", + "create_script": "新建脚本", + "installed_scripts": "已安装脚本", + "nav_scripts": "脚本", + "subscribe": "订阅", + "subscribe_scripts_count": "{{count}} 个脚本", + "enter_subscribe_name": "请输入订阅名称", + "subscribe_url": "订阅地址", + "confirm_delete_subscription": "确定要删除此订阅吗?相关的脚本也会被删除", + "list": { + "confirm_delete": "确定要删除吗?请注意这个操作无法恢复!", + "confirm_update": "确认要更新吗?请注意此操作不可逆!" + }, + "apply_to_run_status": "应用至/运行状态", + "sorting": "排序", + "foreground_page_script_tooltip": "前台页面脚本,会在指定的页面上运行", + "background_script_tooltip": "后台脚本,启用后将在后台运行", + "scheduled_script_tooltip": "定时脚本,下一次运行时间:", + "running": "运行中", + "completed": "运行完毕", + "source_subscribe_link": "订阅链接", + "source_local_script": "本地脚本", + "source_script_link": "脚本链接", + "by_manual_creation": "在本地通过代码编辑方法建立", + "confirm_delete_script": "确定要删除此脚本吗?", + "confirm_delete_scripts_content": "确定要删除选中的 {{count}} 个脚本吗?此操作无法撤销。", + "confirm_delete_script_content": "确定要删除脚本\"{{name}}\"吗?此操作无法撤销。", + "delete_failed": "删除失败", + "enter_script_name": "请输入脚本名", + "update_not_supported": "该脚本不支持检查更新", + "checking_for_updates": "检查更新中...", + "new_version_available": "存在新版本", + "latest_version": "已是最新版本", + "checked_for_all_selected": "所选脚本皆已检查更新", + "update_check_failed": "检查更新失败", + "stopping_script": "正在停止脚本", + "script_stopped": "脚本已停止", + "starting_script": "正在启动脚本...", + "starting_updates": "正在批量更新...", + "script_started": "脚本已启动", + "operation_failed": "操作失败", + "batch_operations": "批量操作", + "scripts_pinned_to_top": "已将所选脚本置顶", + "unknown_operation": "未知操作", + "page_script": "页面脚本", + "homepage": "脚本主页", + "script_website": "脚本站点", + "script_source": "脚本源码", + "bug_feedback_script_support": "BUG反馈/脚本支持站点", + "script_total_runs": "该脚本总共运行了{{runNum}}次,在iframe上运行了{{runNumByIframe}}次", + "script_total_runs_single": "该脚本运行了{{runNum}}次", + "script_disabled": "该脚本未开启", + "cron_oncetype": { + "minute": "{{next}} (每分钟运行一次)", + "hour": "{{next}} (每小时运行一次)", + "day": "{{next}} (每天运行一次)", + "month": "{{next}} (每月运行一次)", + "week": "{{next}} (每星期运行一次)" + }, + "cron_invalid_expr": "错误的定时表达式", + "scheduled_script_description_title": "这是一个定时脚本,启用后将在特定时间自动运行,并可在面板中手动控制。", + "scheduled_script_description_description_expr": "定时任务表达式:", + "scheduled_script_description_description_next": "最近一次运行时间:", + "background_script_description": "这是一个后台脚本,启用后将在浏览器打开时自动运行一次,并可在面板中手动控制。", + "background_script": "后台脚本", + "scheduled_script": "定时脚本", + "script_status_tooltip": "可以控制脚本开启状态,普通油猴脚本默认开启,后台脚本、定时脚本默认关闭", + "subscribe_source_tooltip": "这是一个订阅源,当你开启订阅后会自动安装订阅的脚本", + "script_name_cannot_be_set_to_empty": "脚本name不可以设置为空", + "search_scripts": "搜索脚本", + "script_list": { + "sidebar": { + "stopped": "停止", + "all": "全部", + "normal_script": "普通脚本", + "status": "状态" + } + }, + "tags": "标签", + "input_tags_placeholder": "输入标签,按回车确认", + "switch_to_card_mode": "切换到卡片模式", + "switch_to_table_mode": "切换到表格模式", + "open_sidebar": "打开侧边栏", + "close_sidebar": "关闭侧边栏", + "error_metadata_invalid": "MetaData 信息错误", + "error_script_name_required": "脚本名不能为空", + "error_script_version_required": "脚本 @version 版本不能为空", + "error_script_namespace_required": "脚本 @namespace 命名空间不能为空", + "error_cron_invalid": "错误的定时表达式,请检查:{{expr}}", + "error_script_type_mismatch": "脚本类型不匹配,普通脚本与后台脚本不能互相转换", + "error_old_script_code_missing": "旧的脚本代码不存在", + "error_subscribe_name_required": "订阅名不能为空", + "error_grant_conflict": "@grant 同时声明了 none 和 GM API", + "error_metadata_line_duplicated": "Metadata 中有重复的声明。", + "create_group": "新建", + "import_group": "导入", + "import_local_script": "导入本地脚本", + "link_import": "链接导入", + "import_skill": "导入 Skill", + "link_import_desc": "粘贴脚本 / 订阅链接,每行一个", + "link_import_placeholder": "https://example.com/script.user.js", + "link_import_hint": "支持用户脚本 / 订阅 / Skill 链接", + "not_a_valid_script": "不是有效的用户脚本或 SkillScript", + "import_done": "导入完成:成功 {{success}} · 失败 {{fail}}", + "drop_to_install": "拖拽脚本或 Skill 到此处安装", + "drop_to_install_hint": "拖拽 .js 用户脚本 / 订阅 · .zip Skill 包" +} diff --git a/src/locales/zh-CN/settings.json b/src/locales/zh-CN/settings.json new file mode 100644 index 000000000..4b2fd97fe --- /dev/null +++ b/src/locales/zh-CN/settings.json @@ -0,0 +1,119 @@ +{ + "general": "通用", + "language": "语言", + "help_translate": "协助翻译", + "script_sync": "脚本同步", + "sync_delete": "同步删除", + "sync_delete_desc": "启用后,脚本删除时,会标记脚本为删除,其他设备检测到删除状态后会将脚本删除;关闭时,会直接删除本地与云端的脚本,如果有多台设备可能出现脚本反复同步的情况。", + "enable_script_sync_to": "启用脚本同步至", + "script_subscription_check_interval": "脚本/订阅检查更新间隔", + "never": "从不", + "6_hours": "6小时", + "12_hours": "12小时", + "every_day": "每天", + "every_week": "每周", + "update_disabled_scripts": "更新已禁用脚本", + "silent_update_non_critical_changes": "静默更新非重大变更", + "enable_eslint": "开启 ESLint", + "eslint_rules": "ESLint规则", + "enter_eslint_rules": "请输入 ESLint 规则,可以从 https://eslint.org/play/ 下载配置", + "language_change_tip": "语言切换成功", + "backup": "备份", + "local": "本地", + "export_file": "导出文件", + "import_file": "导入文件", + "cloud": "云端", + "backup_to": "备份至", + "preparing_backup": "正在准备备份到云端", + "backup_success": "备份成功", + "backup_failed": "备份失败", + "no_backup_files": "没有备份文件", + "backup_list": "备份列表", + "open_backup_dir": "打开备份目录", + "confirm_delete_backup_file": "确认删除备份文件", + "backup_strategy": "备份策略", + "under_construction": "建设中", + "sync_system_connect_failed": "同步系统连接失败", + "sync_system_closed": "已关闭同步", + "sync_system_closed_description": "同步功能已关闭,请重新配置", + "export_success": "导出成功", + "get_backup_dir_url_failed": "获取备份目录地址失败", + "get_backup_files_failed": "获取备份文件失败", + "baidu_netdisk": "百度网盘", + "netdisk_unbind": "解除绑定 {{provider}}", + "netdisk_unbind_confirm": "确定解除 {{provider}} 账号绑定吗?", + "netdisk_unbind_success": "已解除 {{provider}} 账号绑定", + "netdisk_unbind_error": "解除 {{provider}} 账号失败", + "save_only_current_group": "保存只对当前组有效", + "security": "安全", + "blacklist_pages": "黑名单页面", + "blacklist_placeholder": "禁止脚本猫在以下页面运行脚本,多个页面用换行符分隔,例如:\nhttps://*.example.com", + "expression_format_error": "表达式格式错误", + "migration_confirm_message": "重试迁移储存引擎会对现有数据造成修改,请确认,详情请看:https://docs.scriptcat.org/docs/change/v0.17/", + "retry_migration": "重试迁移储存引擎", + "sync_status": "同步状态", + "interface_settings": "界面", + "select_interface_language": "选择界面显示语言", + "extension_icon_badge": "扩展图标徽标", + "display_type": "显示类型", + "extension_icon_badge_type": "扩展图标上显示的数字类型", + "background_color": "背景颜色", + "badge_background_color_desc": "徽标背景色", + "text_color": "文字颜色", + "badge_text_color_desc": "徽标文字色", + "badge_type_none": "不显示", + "badge_type_run_count": "运行次数", + "badge_type_script_count": "脚本个数", + "script_menu": "脚本菜单", + "display_right_click_menu": "显示右键菜单", + "display_right_click_menu_desc": "在浏览器右键菜单中显示脚本菜单", + "expand_count": "展开数量", + "auto_collapse_when_exceeds": "超过此数量时自动折叠", + "script_update_check_frequency": "脚本更新检查频率", + "script_auto_update_frequency": "脚本自动检查更新的频率", + "update_options": "更新选项", + "control_script_update_behavior": "控制脚本更新的行为", + "blacklist_pages_desc": "禁止脚本在指定页面运行,支持通配符", + "development_tools": "开发工具", + "check_script_code_quality": "检查脚本代码质量和错误", + "custom_eslint_rules_config": "自定义 ESLint 规则配置(JSON 格式)", + "script_run_env": { + "title": "运行环境", + "all": "所有标签", + "normal-tabs": "普通标签", + "incognito-tabs": "隐身标签" + }, + "script_run_at": { + "title": "运行时机" + }, + "script_setting": { + "title": "脚本设置", + "default": "默认" + }, + "notification": { + "script_sync_delete": "脚本删除同步", + "script_sync_delete_desc": "脚本 {{scriptName}} 已被删除", + "subscribe_update": "订阅 {{subscribeName}} 已更新", + "subscribe_update_desc": "新增脚本:{{newScripts}}\n删除脚本:{{deletedScripts}}" + }, + "enable_background": { + "title": "启用后台运行", + "description": "启用后,即使关闭所有窗口,浏览器仍会在后台运行,并最小化到系统托盘,直到您手动退出浏览器。这使后台脚本能够继续运行。", + "enable_failed": "启用失败", + "disable_failed": "禁用失败", + "prompt_title": "是否开启后台运行?", + "prompt_description": "此脚本是{{scriptType}},启用后台运行功能可以让脚本在浏览器关闭后继续运行。", + "enable_now": "立即启用", + "maybe_later": "暂不启用", + "settings_hint": "你可以随时在设置中修改此选项。" + }, + "favicon_service": "图标服务", + "favicon_service_desc": "选择获取网站图标的服务", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_google": "Google", + "favicon_service_duckduckgo": "DuckDuckGo", + "favicon_service_icon-horse": "Icon Horse", + "favicon_service_local": "本地获取", + "cloud_sync_account_verification": "云同步账号信息验证中...", + "cloud_sync_verification_failed": "云同步账号信息验证失败" +} diff --git a/src/locales/zh-CN/tools.json b/src/locales/zh-CN/tools.json new file mode 100644 index 000000000..cb4a3ba4f --- /dev/null +++ b/src/locales/zh-CN/tools.json @@ -0,0 +1,17 @@ +{ + "development_tool": "开发工具", + "vscode_url": "VSCode地址", + "auto_connect_vscode_service": "自动连接vscode服务", + "connect": "连接", + "connection_success": "连接成功", + "connection_failed": "连接失败", + "select_import_script": "请在新页面中选择要导入的脚本", + "import_error": "导入错误", + "pulling_data_from_cloud": "正在从云端拉取数据", + "pull_failed": "拉取失败", + "restore": "恢复", + "local_backup": "本地备份", + "cloud_backup": "云端备份", + "auto_backup": "自动备份", + "data_migration": "数据迁移" +} diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json deleted file mode 100644 index ceecc9a98..000000000 --- a/src/locales/zh-CN/translation.json +++ /dev/null @@ -1,808 +0,0 @@ -{ - "sentence-separator": "。", - "import_link": "链接导入", - "import_link_failure": "链接导入失败", - "create_user_script": "新建普通脚本", - "create_background_script": "新建后台脚本", - "create_scheduled_script": "新建定时脚本", - "import_by_local": "本地导入", - "import_local_failure": "本地导入失败", - "import_local_success": "本地导入成功", - "create_script": "新建脚本", - "user_guide": "使用指南", - "api_docs": "API文档", - "development_guide": "开发指南", - "script_gallery": "脚本站", - "community_forum": "社区论坛", - "external_links": "外部链接", - "system_follow": "跟随系统", - "no_data": "暂无数据", - "installed_scripts": "已安装脚本", - "subscribe": "订阅", - "logs": "日志", - "tools": "工具", - "find": "查找", - "replace": "替换", - "settings": "设置", - "hide_main_sidebar": "收起侧边栏", - "show_main_sidebar": "展开侧边栏", - "guide": "新手指引", - "helpcenter": "帮助中心", - "general": "通用", - "language": "语言", - "help_translate": "协助翻译", - "script_sync": "脚本同步", - "sync_delete": "同步删除", - "sync_delete_desc": "启用后,脚本删除时会标记脚本为删除,其他设备检测到删除状态后会将脚本删除;关闭时,会直接删除本地与云端的脚本,如果有多台设备可能出现脚本反复同步的情况。", - "enable_script_sync_to": "启用脚本同步至", - "save": "保存", - "save_as": "另存为", - "file": "文件", - "run": "运行", - "debug": "调试", - "cloud_sync_account_verification": "云同步账号信息验证中...", - "cloud_sync_verification_failed": "云同步账号信息验证失败", - "save_success": "保存成功", - "reset_success": "重置成功", - "update": "更新", - "check_update": "检查更新", - "script_subscription_check_interval": "脚本/订阅检查更新间隔", - "never": "从不", - "6_hours": "6小时", - "12_hours": "12小时", - "every_day": "每天", - "every_week": "每周", - "update_disabled_scripts": "更新已禁用脚本", - "silent_update_non_critical_changes": "静默更新非重大变更", - "enable_eslint": "开启 ESLint", - "eslint_rules": "ESLint规则", - "enter_eslint_rules": "请输入 ESLint 规则,可以从 https://eslint.org/play/ 下载配置", - "language_change_tip": "语言切换成功", - "backup": "备份", - "local": "本地", - "export_file": "导出文件", - "import_file": "导入文件", - "cloud": "云端", - "backup_to": "备份至", - "preparing_backup": "正在准备备份到云端", - "backup_success": "备份成功", - "backup_failed": "备份失败", - "no_backup_files": "没有备份文件", - "backup_list": "备份列表", - "open_backup_dir": "打开备份目录", - "confirm_delete": "确认删除", - "confirm_delete_backup_file": "确认删除备份文件", - "confirm_update": "确认更新", - "delete_success": "删除成功", - "deleting": "删除中", - "backup_strategy": "备份策略", - "under_construction": "开发中", - "development_tool": "开发工具", - "vscode_url": "VSCode地址", - "auto_connect_vscode_service": "自动连接 VSCode 服务", - "connect": "连接", - "connection_success": "连接成功", - "connection_failed": "连接失败", - "select_import_script": "请在新页面中选择要导入的脚本", - "import_error": "导入错误", - "pulling_data_from_cloud": "正在从云端拉取数据", - "pull_failed": "拉取失败", - "restore": "恢复", - "log_title": "运行日志", - "last_5_minutes": "最近5分钟", - "last_15_minutes": "最近15分钟", - "last_30_minutes": "最近30分钟", - "last_1_hour": "最近1小时", - "last_3_hours": "最近3小时", - "last_6_hours": "最近6小时", - "last_12_hours": "最近12小时", - "last_24_hours": "最近24小时", - "last_7_days": "最近7天", - "query": "查询", - "labels": "标签", - "search_regex": "搜索(支持正则)", - "clean_schedule": "定时清理", - "days_ago_logs": "天前的日志", - "delete_completed": "删除完成", - "delete_current_logs": "删除当前日志", - "clear_completed": "清空完成", - "clear_logs": "清空日志", - "to": " 至 ", - "now": "当前", - "total_logs": "找到 {{length}} 条日志", - "filtered_logs": "筛选后 {{length}} 条日志", - "enter_filter_conditions": "请输入筛选条件", - "permission": "权限", - "enter_subscribe_name": "请输入订阅名称", - "subscribe_url": "订阅地址", - "confirm_delete_subscription": "确定要删除此订阅吗?相关的脚本也会被删除", - "list": { - "confirm_delete": "确定要删除吗?请注意这个操作无法恢复!", - "confirm_update": "确认要更新吗?请注意此操作不可逆!" - }, - "enable": "开启", - "script_list_enable_width": 80, - "script_list_last_updated_width": 120, - "script_list_apply_to_run_status_width": 140, - "subscribe_list_enable_width": 100, - "disable": "关闭", - "name": "名称", - "version": "版本", - "apply_to_run_status": "应用于 / 运行状态", - "source": "来源", - "home": "主页", - "sorting": "排序", - "last_updated": "最后更新", - "action": "操作", - "foreground_page_script_tooltip": "前台页面脚本,会在指定的页面上运行", - "background_script_tooltip": "后台脚本,启用后将在后台运行", - "scheduled_script_tooltip": "定时脚本,下一次运行时间:", - "running": "运行中", - "completed": "运行完毕", - "source_subscribe_link": "订阅链接", - "source_local_script": "本地脚本", - "source_script_link": "脚本链接", - "by_manual_creation": "在本地通过代码编辑方法建立", - "confirm_delete_script": "确定要删除此脚本吗?", - "confirm_delete_script_content": "确定要删除脚本\"{{name}}\"吗?此操作无法撤销。", - "delete_failed": "删除失败", - "enter_script_name": "请输入脚本名", - "update_not_supported": "该脚本不支持检查更新", - "checking_for_updates": "检查更新中...", - "new_version_available": "存在新版本", - "latest_version": "已是最新版本", - "checked_for_all_selected": "所选脚本皆已检查更新", - "update_check_failed": "检查更新失败", - "script_import_failed": "脚本导入失败", - "install_page_open_failed": "安装页面打开失败", - "stopping_script": "正在停止脚本", - "script_stopped": "脚本已停止", - "starting_script": "正在启动脚本...", - "starting_updates": "正在批量更新...", - "script_started": "脚本已启动", - "operation_failed": "操作失败", - "batch_operations": "批量操作", - "export": "导出", - "delete": "删除", - "pin_to_top": "置顶", - "scripts_pinned_to_top": "已将所选脚本置顶", - "unknown_operation": "未知操作", - "confirm": "确定", - "close": "关闭", - "page_script": "页面脚本", - "homepage": "脚本主页", - "script_website": "脚本站点", - "script_source": "脚本源码", - "bug_feedback_script_support": "BUG 反馈/脚本支持站点", - "config": "配置", - "key": "key", - "value": "value", - "add": "新增", - "type": "类型", - "edit_value": "编辑值", - "add_value": "新增值", - "update_success": "修改成功", - "add_success": "添加成功", - "script_storage": "脚本储存", - "enter_key": "请输入 key", - "key_placeholder": "key", - "value_placeholder": "当类型为 object 时,请输入可被 JSON 解析的数据", - "clear": "清空", - "clear_success": "清空成功", - "confirm_clear": "您确定要清除此储存空间吗?", - "type_string": "string", - "type_number": "number", - "type_boolean": "boolean", - "type_object": "object", - "confirm_delete_resource": "你确定删除此资源吗?在下次开启时将会重新加载此资源", - "confirm_clear_resource": "你真的要清空这些资源吗?在下次开启时将会重新加载资源", - "script_resource": "脚本资源", - "permission_value": "授权值", - "allow": "是否允许", - "yes": "是", - "no": "否", - "confirm_delete_permission": "确定要删除此授权吗?", - "basic_info": "基本信息", - "update_url": "更新URL", - "permission_management": "授权管理", - "add_permission": "添加授权", - "permission_cors": "跨域(CORS)", - "permission_cookie": "管理 Cookie", - "match": "匹配", - "user_setting": "用户设定", - "confirm_delete_exclude": "确认删除该排除?", - "after_deleting_match_item": "脚本设定的匹配项删除后会自动添加到匹配项中", - "confirm_delete_match": "确认删除该匹配?", - "after_deleting_exclude_item": "脚本设定的匹配项删除后会自动添加到排除项中", - "add_match": "添加匹配", - "add_exclude": "添加排除", - "website_match": "网站匹配(@match)", - "reset": "重置", - "website_exclude": "网站排除(@exclude)", - "confirm_reset": "确定重置?", - "script_total_runs": "该脚本总共运行了{{runNum}}次,在iframe上运行了{{runNumByIframe}}次", - "script_total_runs_single": "该脚本运行了{{runNum}}次", - "script_disabled": "该脚本未开启", - "run_once": "运行一次", - "stop": "停止", - "edit": "编辑", - "undo": "撤销", - "redo": "重做", - "cut": "剪切", - "copy": "复制", - "paste": "粘贴", - "format": "格式化", - "exclude_on": "恢复在 $0 上执行", - "exclude_off": "排除在 $0 上执行", - "user_config": "用户配置", - "gm_api": "GM API", - "storage_api": "存储 API", - "use_file_system": "使用的文件系统", - "open_directory": "打开目录", - "account_validation_failed": "账号信息验证失败", - "not_set": "未设置", - "in_use": "使用中", - "storage_error": "储存错误", - "upload_to_cloud": "上传至云", - "save_failed": "保存失败", - "exporting": "导出中...", - "upload_to": "上传至", - "value_export_expression": "值导出表达式", - "overwrite_original_value_on_import": "导入时覆盖原值", - "cookie_export_expression": "Cookie 导出表达式", - "overwrite_original_cookie_on_import": "导入时覆盖原值", - "restore_default_values": "恢复默认值", - "get_confirm_error": "获取确认信息失败", - "confirm_error": "确认失败", - "ignore": "忽略", - "allow_once": "允许一次", - "temporary_allow": "临时允许此{{permissionContent}}", - "temporary_allow_all": "临时允许全部{{permissionContent}}", - "permanent_allow": "永久允许此{{permissionContent}}", - "permanent_allow_all": "永久允许全部{{permissionContent}}", - "deny_once": "拒绝一次", - "temporary_deny": "临时拒绝此{{permissionContent}}", - "temporary_deny_all": "临时拒绝全部{{permissionContent}}", - "permanent_deny": "永久拒绝此{{permissionContent}}", - "permanent_deny_all": "永久拒绝全部{{permissionContent}}", - "data_import": "数据导入", - "import": "导入", - "select_scripts_to_import": "请选择你要导入的脚本", - "select_all": "全选", - "script_import_progress": "脚本导入进度", - "select_subscribes_to_import": "请选择你要导入的订阅", - "subscribe_import_progress": "订阅导入进度", - "author": "作者", - "description": "描述", - "operation": "操作", - "error": "错误", - "unknown": "未知", - "add_new": "新增", - "no_operation": "不做操作", - "enable_script": "开启脚本", - "local_creation": "本地创建", - "import_success": "导入成功", - "install_script": "安装", - "update_script": "更新", - "install_subscribe": "安装订阅", - "update_subscribe": "更新订阅", - "update_script_no_close": "更新,不关闭窗口", - "install_script_no_close": "安装,不关闭窗口", - "update_script_no_more_update": "更新,但不再检查更新", - "close_update_script_no_more_update": "关闭,且不再检查更新", - "install_script_no_more_update": "安装,但不再检查更新", - "invalid_link": "错误的链接", - "subscribe_install_label": "该订阅将会安装下面的脚本", - "script_runs_in": "脚本将在下面的网站中运行", - "script_has_full_access_to": "脚本将获得以下地址的完整访问权限", - "script_requires": "脚本引用了下列外部资源", - "cookie_warning": "请注意,本脚本会请求 Cookie 的操作权限,这是一个危险的权限,请确认脚本的安全性。", - "cron_oncetype": { - "minute": "{{next}} (每分钟运行一次)", - "hour": "{{next}} (每小时运行一次)", - "day": "{{next}} (每天运行一次)", - "month": "{{next}} (每月运行一次)", - "week": "{{next}} (每星期运行一次)" - }, - "cron_invalid_expr": "错误的定时表达式", - "scheduled_script_description_title": "这是一个定时脚本,启用后将在特定时间自动运行,并可在面板中手动控制。", - "scheduled_script_description_description_expr": "定时任务表达式:", - "scheduled_script_description_description_next": "最近一次运行时间:", - "background_script_description": "这是一个后台脚本,启用后将在浏览器打开时自动运行一次,并可在面板中手动控制。", - "install_success": "安装成功", - "install": { - "update_success": "更新成功" - }, - "install_failed": "安装失败", - "subscribe_success": "订阅成功", - "subscribe_failed": "订阅失败", - "current_version": "当前版本", - "update_version": "更新版本", - "updatepage": { - "main_header": "检查更新", - "header_site_specific": "此网站有可用更新($0)", - "header_site_all": "有可用更新", - "header_ignored": "有可用更新但已忽略", - "update_all": "全部更新", - "ignore_all": "全部忽略", - "update": "更新", - "ignore": "忽略", - "old_version_": "旧版本:", - "new_version_": "新版本:", - "enabled": "已启用", - "tooltip_enabled": "此脚本已启用。", - "disabled": "已停用", - "tooltip_disabled": "此脚本已停用。", - "similarity_": "相似度:", - "codechange_major": "重大改动", - "codechange_noticeable": "显著改动", - "codechange_tiny": "轻微改动", - "new_connects_": "新 @connect:", - "tag_new_connect": "已添加 @connect", - "status_last_check": "上次检查:$0", - "status_checking_updates": "正在检查更新...", - "status_no_update": "没有需要更新的内容", - "status_n_update": "有 $0 个需要更新的内容", - "status_n_ignored": "有 $0 个已忽略的更新", - "status_autoclose": "$0 秒后自动关闭", - "header_other_update": "其他可用更新" - }, - "downloading_status_text": "正在下载。已接收 {{bytes}}。", - "install_page_please_wait": "请稍等", - "install_page_loading": "安装页面加载中", - "install_page_load_failed": "安装页面加载失败", - "invalid_page": "无效页面", - "background_script_tag": "这是一个后台脚本", - "scheduled_script_tag": "这是一个定时脚本", - "background_script": "后台脚本", - "scheduled_script": "定时脚本", - "install_from_legitimate_sources_warning": "请从合法来源安装脚本!未知的脚本可能会侵犯您的隐私或者做出恶意的操作!", - "antifeature_referral_link_title": "推荐链接", - "antifeature_referral_link_description": "该脚本会修改或重定向到作者的返佣链接", - "antifeature_ads_title": "附带广告", - "antifeature_ads_description": "该脚本会在你访问的页面上插入广告", - "antifeature_payment_title": "付费脚本", - "antifeature_payment_description": "该脚本需要你付费才能够正常使用", - "antifeature_miner_title": "挖矿", - "antifeature_miner_description": "该脚本存在挖矿行为", - "antifeature_membership_title": "会员功能", - "antifeature_membership_description": "该脚本需要注册会员才能正常使用", - "antifeature_tracking_title": "信息追踪", - "antifeature_tracking_description": "该脚本会追踪你的用户信息", - "script_info_load_failed": "脚本信息加载失败!", - "script_status_tooltip": "可以控制脚本开启状态,普通油猴脚本默认开启,后台脚本、定时脚本默认关闭", - "subscribe_source_tooltip": "这是一个订阅源,当你开启订阅后会自动安装订阅的脚本", - "get_script": "获取脚本", - "report_issue": "BUG / 问题反馈", - "project_docs": "项目文档", - "community": "交流社区", - "popup": { - "new_version_available": "有新版本可用" - }, - "current_page_scripts": "当前页运行脚本", - "enabled_background_scripts": "开启和运行的后台脚本", - "script_accessing_cross_origin_resource": "脚本正在试图访问跨域资源", - "confirm_operation_description": "请您确认是否允许脚本进行此操作,脚本也可增加@connect标签跳过此选项", - "extension_site_access_title": "ScriptCat 需要站点访问权限", - "extension_site_access_description": "请授予浏览器对此来源的站点访问权限,让 ScriptCat 可以完成本次请求。这会修改扩展的站点访问设置。", - "extension_site_access_content": "站点", - "domain": "域名", - "script_name": "脚本名称", - "request_domain": "请求域名", - "request_url": "请求地址", - "access_cookie_content": "脚本正在尝试访问网站 Cookie 内容", - "confirm_script_operation": "请您确认是否允许脚本进行此操作,Cookie 是一项重要的用户数据,请务必只给信任的脚本授权。", - "cookie_domain": "Cookie域", - "script_operation_title": "脚本正在尝试访问存储空间", - "script_operation_description": "请您确认是否允许脚本进行此操作,允许后将允许脚本操作你设定的储存空间,脚本会在储存空间下创建一个app/${dir}的目录进行使用", - "script_permission_content": "脚本", - "sync_system_connect_failed": "同步系统连接失败", - "sync_system_closed": "同步已禁用", - "sync_system_closed_description": "同步功能已禁用,请重新配置", - "auth_type": "鉴权类型", - "url": "URL", - "username": "用户名", - "password": "密码", - "access_token_bearer": "访问令牌(Bearer)", - "s3_bucket_name": "存储桶名称", - "s3_region": "区域", - "s3_access_key_id": "访问密钥 ID", - "s3_secret_access_key": "访问密钥密文", - "s3_custom_endpoint": "自定义端点(可选)", - "skip": "跳过", - "next": "下一步", - "next_with_progress": "下一步(第 {step} 步 / 共 {steps} 步)", - "back": "上一步", - "last": "完成", - "start_guide_title": "欢迎使用脚本猫扩展", - "start_guide_content": "接下来我们将为您介绍脚本猫的基本使用方法", - "guide_installed_scripts": "你所安装的脚本将会在这里显示", - "guide_script_list_title": "脚本市场", - "guide_script_list_content": "可以从脚本市场中安装脚本,脚本猫除了支持用户脚本以外,还支持后台脚本", - "guide_script_list_enable_title": "脚本开启", - "guide_script_list_enable_content": "脚本需要开启才能使用,页面脚本安装默认开启,后台脚本安装默认关闭", - "guide_script_list_apply_to_run_status_title": "应用于 / 运行状态", - "guide_script_list_apply_to_run_status_content": "脚本运行状态展示,鼠标悬浮至标签可以查看脚本类型", - "guide_script_list_sort_title": "排序", - "guide_script_list_sort_content": "你可以拖动脚本的标签进行排序", - "guide_script_list_update_title": "最后更新", - "guide_script_list_update_content": "点击最后更新列标签可以对脚本进行一次检查更新", - "guide_script_list_action_title": "操作", - "guide_script_list_action_content": "操作列可以进入脚本编辑、控制脚本的运行与停止(后台脚本)、设置UserConfig,操作右侧按钮可以打开高级筛选与切换视图模式", - "guide_tools_title": "常用工具", - "guide_tools_content": "工具中提供了备份和开发的工具", - "guide_tools_backup_title": "备份", - "guide_tools_backup_content": "备份可以保存脚本,避免丢失。可以导出脚本文件到本地,也可以加载本地的脚本文件。也可以备份到云端,更加方便。", - "guide_setting_title": "设置", - "guide_setting_content": "设置中主要包含了语言、脚本同步、更新频率等常用设置项", - "guide_setting_sync_title": "更新与同步", - "guide_setting_sync_content": "脚本同步功能可以方便地将本设备的脚本内容同步至云端,如果有多台设备请勾选同步删除选项,当本设备脚本删除时,会从云端删除对应的脚本,也会将其他设备的脚本删除。", - "auto": "自动", - "hide": "隐藏", - "custom": "自定义", - "resize_column_width": "调整列宽", - "collapse": "收起", - "expand": "展开", - "menu_expand_num_before": "菜单项超过", - "menu_expand_num_after": "个时,自动隐藏", - "edit_conflict": "编辑冲突", - "confirm_override_when_edit_conflict": "此脚本已在其他实例中被修改。替换将覆盖这些更改。是否要保留此版本?", - "save_abort_when_edit_conflict": "该脚本已在其他实例中被修改,保存已中止。", - "scriptname_conflict": "脚本名称冲突", - "confirm_save_when_scriptname_conflict": "该脚本名称已被其他脚本使用,是否仍要保存?", - "save_abort_when_scriptname_conflict": "该脚本名称已被其他脚本使用,已取消保存。", - "script_name_cannot_be_set_to_empty": "脚本名称不可设置为空", - "eslint_config_format_error": "ESLint配置格式错误", - "export_success": "导出成功", - "get_backup_dir_url_failed": "获取备份目录地址失败", - "get_backup_files_failed": "获取备份文件失败", - "request_permission": "请求权限", - "develop_mode_guide": "当前未启用“开发者模式”,脚本无法正常运行。👉点击查看启用方法", - "allow_user_script_guide": "当前未启用“允许运行用户脚本”,脚本无法正常运行。👉点击查看启用方法", - "lower_version_browser_guide": "您的浏览器版本过低,脚本无法正常运行。👉点击了解更多", - "click_to_reload": "👉点击重新加载", - "page_in_blacklist": "当前页面在黑名单中,无法使用脚本", - "baidu_netdisk": "百度网盘", - "netdisk_unbind": "解除绑定 {{provider}}", - "netdisk_unbind_confirm": "确定解除 {{provider}} 账号绑定吗?", - "netdisk_unbind_success": "已解除 {{provider}} 账号绑定", - "netdisk_unbind_error": "解除 {{provider}} 账号失败", - "save_only_current_group": "保存只对当前组有效", - "script_import_result": "脚本导入结果", - "failure_info": "失败信息", - "security": "安全", - "blacklist_pages": "黑名单页面", - "blacklist_placeholder": "禁止脚本猫在以下页面运行脚本,多个页面用换行符分隔,例如:\nhttps://*.example.com", - "expression_format_error": "表达式格式错误", - "migration_confirm_message": "重试迁移储存引擎会对现有数据造成修改,请确认,详情请看:https://docs.scriptcat.org/docs/change/v0.17/", - "retry_migration": "重试迁移储存引擎", - "script_modified_leave_confirm": "脚本已修改,离开后会丢失修改,是否继续?", - "create_success_note": "新建成功,请注意后台脚本不会默认开启", - "save_as_failed": "另存为失败", - "save_as_success": "另存为成功", - "only_background_scheduled_can_run": "只有后台脚本/定时脚本才能运行", - "preparing_script_resources": "正在准备脚本资源...", - "build_success_message": "构建成功,可以在扩展页打开开发者工具在控制台中查看输出", - "script_storage_tooltip": "可以管理脚本的储存数据(GM_value)", - "script_resource_tooltip": "管理@resource,@require下载的资源", - "script_setting_tooltip": "对脚本进行一些自定义设置", - "script_modified_close_confirm": "脚本已修改,关闭后会丢失修改,是否继续?", - "close_current_tab": "关闭当前标签页", - "close_other_tabs": "关闭其他标签页", - "close_left_tabs": "关闭左侧标签页", - "close_right_tabs": "关闭右侧标签页", - "import_script_placeholder": "支持输入.user.js结尾的脚本绝对链接 或 脚本猫安装页链接\n可多行填写,每行一条\n示例:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/zh-CN/script-show-page/1234", - "invalid_script_code": "错误的脚本代码", - "build_failed": "构建失败", - "drag_script_here_to_upload": "拖拽脚本到此处上传", - "sync_status": "同步状态", - "search_scripts": "搜索脚本", - "ext_update_notification": "脚本猫扩展已更新", - "ext_update_notification_desc": "当前版本:{{version}},详情请查看更新日志", - "watch_file_description": "监听文件变动,自动更新脚本,使用时请确保脚本文件路径不变且不能关闭页面", - "watch_file": "监听文件", - "stop_watch_file": "停止监听", - "script_menu_display": "脚本注册的菜单", - "badge_type_none": "不显示", - "badge_type_run_count": "运行次数", - "badge_type_script_count": "脚本个数", - "interface_settings": "界面", - "select_interface_language": "选择界面显示语言", - "extension_icon_badge": "扩展图标徽标", - "display_type": "显示类型", - "extension_icon_badge_type": "扩展图标上显示的数字类型", - "background_color": "背景颜色", - "badge_background_color_desc": "徽标背景色", - "text_color": "文字颜色", - "badge_text_color_desc": "徽标文字色", - "script_menu": "脚本菜单", - "display_right_click_menu": "显示右键菜单", - "display_right_click_menu_desc": "在浏览器右键菜单中显示脚本菜单", - "expand_count": "展开数量", - "auto_collapse_when_exceeds": "超过此数量时自动折叠", - "script_update_check_frequency": "脚本更新检查频率", - "script_auto_update_frequency": "脚本自动检查更新的频率", - "update_options": "更新选项", - "control_script_update_behavior": "控制脚本更新的行为", - "blacklist_pages_desc": "禁止脚本在指定页面运行,支持通配符", - "development_tools": "开发工具", - "check_script_code_quality": "检查脚本代码质量和错误", - "custom_eslint_rules_config": "自定义 ESLint 规则配置(JSON 格式)", - "light": "亮色模式", - "dark": "暗色模式", - "individual_edit": "单独编辑", - "batch_edit": "批量编辑", - "script_code": "脚本代码", - "enter_search_value": "请输入 {{search}} 进行搜索", - "script_run_env": { - "title": "运行环境", - "all": "所有标签", - "normal-tabs": "普通标签", - "incognito-tabs": "隐身标签" - }, - "script_run_at": { - "title": "运行时机" - }, - "script_setting": { - "title": "脚本设置", - "default": "默认" - }, - "editor_config": "编辑器配置", - "editor_config_description": "你可以参考jsconfig.js中的compilerOptions进行配置", - "editor_type_definition": "编辑器类型定义", - "editor_type_definition_description": "你可以自定义自己的类型定义,脚本编辑器会自动加载这些类型定义", - "eslint_rules_reset": "ESLint规则已重置", - "eslint_rules_saved": "ESLint规则已保存", - "editor_config_reset": "编辑器配置已重置", - "editor_config_saved": "编辑器配置已保存", - "editor_config_format_error": "编辑器配置格式错误", - "editor_type_definition_reset": "编辑器类型定义已重置", - "editor_type_definition_saved": "编辑器类型定义已保存", - "script_list": { - "sidebar": { - "stopped": "停止", - "all": "全部", - "normal_script": "普通脚本", - "status": "状态" - } - }, - "tags": "标签", - "install_source": "安装来源", - "input_tags_placeholder": "输入标签,按回车确认", - "switch_to_card_mode": "切换到卡片模式", - "switch_to_table_mode": "切换到表格模式", - "open_sidebar": "打开侧边栏", - "close_sidebar": "关闭侧边栏", - "no_message_content": "无消息内容", - "error_metadata_invalid": "MetaData 信息错误", - "error_script_name_required": "脚本名不能为空", - "error_script_version_required": "脚本 @version 版本不能为空", - "error_script_namespace_required": "脚本 @namespace 命名空间不能为空", - "error_cron_invalid": "错误的定时表达式,请检查:{{expr}}", - "error_script_type_mismatch": "脚本类型不匹配,普通脚本与后台脚本不能互相转换", - "error_old_script_code_missing": "旧的脚本代码不存在", - "error_subscribe_name_required": "订阅名不能为空", - "error_grant_conflict": "@grant 同时声明了 none 和 GM API", - "error_metadata_line_duplicated": "Metadata 中有重复的声明。", - "notification": { - "script_sync_delete": "同步脚本删除", - "script_sync_delete_desc": "脚本 {{scriptName}} 已被删除", - "subscribe_update": "订阅 {{subscribeName}} 已更新", - "subscribe_update_desc": "新增脚本:{{newScripts}}\n删除脚本:{{deletedScripts}}" - }, - "loading": "加载中...", - "runtime": "运行时", - "enable_background": { - "title": "启用后台运行", - "description": "启用后,即使关闭所有窗口,浏览器仍会在后台运行,并最小化到系统托盘,直到您手动退出浏览器。这使后台脚本能够继续运行。", - "enable_failed": "启用失败", - "disable_failed": "禁用失败", - "prompt_title": "是否开启后台运行?", - "prompt_description": "此脚本是{{scriptType}},启用后台运行功能可以让脚本在浏览器关闭后继续运行。", - "enable_now": "立即启用", - "maybe_later": "暂不启用", - "settings_hint": "你可以随时在设置中修改此选项。" - }, - "favicon_service": "图标服务", - "favicon_service_desc": "选择获取网站图标的服务", - "favicon_service_scriptcat": "ScriptCat", - "favicon_service_google": "Google", - "favicon_service_duckduckgo": "DuckDuckGo", - "favicon_service_icon-horse": "Icon Horse", - "favicon_service_local": "本地获取", - "editor": { - "show_script_list": "显示脚本列表", - "hide_script_list": "隐藏脚本列表" - }, - "agent": "AI Agent", - "agent_chat": "会话", - "agent_provider": "模型服务", - "agent_mcp": "MCP", - "agent_skills": "Skills", - "agent_provider_title": "模型服务", - "agent_provider_select": "AI 服务提供商", - "agent_provider_api_base_url": "API 地址", - "agent_provider_api_key": "API 密钥", - "agent_provider_model": "默认模型", - "agent_provider_test_connection": "测试连接", - "agent_provider_test_success": "连接成功", - "agent_provider_test_failed": "连接失败", - "agent_model_fetch": "获取模型", - "agent_model_fetch_failed": "获取模型列表失败", - "agent_model_name": "名称", - "agent_model_add": "添加模型", - "agent_model_edit": "编辑", - "agent_model_copy": "复制", - "agent_model_delete": "删除", - "agent_model_set_default": "设为默认", - "agent_model_default_label": "默认", - "agent_model_delete_confirm": "确定要删除此模型配置吗?", - "agent_model_max_tokens": "最大输出 Token 数", - "agent_model_context_window": "上下文窗口大小", - "agent_model_no_models": "暂无模型配置", - "agent_model_vision_support": "支持图片输入", - "agent_model_image_output": "支持图片输出", - "agent_model_capabilities": "模型能力", - "agent_model_supports_vision": "视觉输入", - "agent_model_supports_image_output": "图片输出", - "agent_coming_soon": "开发中...", - "agent_skills_title": "Skills 管理", - "agent_skills_add": "添加 Skill", - "agent_skills_empty": "暂无已安装的 Skill", - "agent_skills_tools": "工具", - "agent_skills_references": "参考资料", - "agent_skills_detail": "Skill 详情", - "agent_skills_edit_prompt": "提示词", - "agent_skills_install": "安装 Skill", - "agent_skills_install_url": "从 URL 导入", - "agent_skills_install_paste": "粘贴 SKILL.md", - "agent_skills_uninstall": "卸载", - "agent_skills_uninstall_confirm": "确定要卸载 Skill「{{name}}」?", - "agent_skills_save_success": "保存成功", - "agent_skills_install_success": "安装成功", - "agent_skills_fetch_failed": "获取失败", - "agent_skills_add_script": "添加脚本", - "agent_skills_add_reference": "添加参考资料", - "agent_skills_install_zip": "上传 ZIP", - "agent_skills_install_zip_hint": "点击选择 .zip 文件", - "agent_skills_prompt": "提示词", - "agent_skills_installed_at": "安装时间", - "agent_skills_refresh": "刷新", - "agent_skills_refresh_success": "刷新成功", - "agent_skills_tool_code": "工具代码", - "agent_skills_click_to_view_code": "点击工具名称查看代码", - "agent_skills_config": "配置", - "agent_skills_config_saved": "配置已保存", - "agent_skills_check_updates": "检查更新", - "agent_skills_no_updates": "所有 Skill 已是最新版本", - "agent_skills_updates_available": "个 Skill 有更新", - "agent_skills_update": "更新", - "agent_skills_update_success": "更新成功", - "agent_skills_url_placeholder": "输入 SKILL.cat.md URL", - "agent_chat_new": "新建会话", - "agent_chat_delete": "删除会话", - "agent_chat_delete_confirm": "确定要删除此会话吗?", - "agent_chat_no_conversations": "暂无会话", - "agent_chat_input_placeholder": "输入消息...", - "agent_chat_send": "发送", - "agent_chat_stop": "停止", - "agent_chat_thinking": "思考过程", - "agent_chat_tool_call": "工具调用", - "agent_chat_error": "发生错误", - "agent_chat_no_model": "未配置模型,请先在模型服务中添加", - "agent_chat_model_select": "选择模型", - "agent_chat_rename": "重命名", - "agent_chat_copy": "复制", - "agent_chat_copy_success": "已复制", - "agent_chat_regenerate": "重新生成", - "agent_chat_streaming": "生成中...", - "agent_chat_retrying": "正在重试 ({{attempt}}/{{max}})...", - "agent_chat_newline": "换行", - "agent_chat_attach_image": "添加图片", - "agent_chat_attach_file": "添加文件", - "agent_chat_welcome_hint": "有关脚本的任何问题,尽管问我", - "agent_chat_welcome_start": "创建一个对话开始吧", - "agent_chat_tokens": "令牌", - "agent_chat_first_token": "TTFT", - "agent_chat_tools_count": "{{count}} 次工具调用", - "agent_chat_tools_enabled": "工具已启用", - "agent_chat_tools_disabled": "工具已禁用", - "agent_chat_tools_enabled_tip": "工具已启用 — 点击禁用", - "agent_chat_tools_disabled_tip": "工具已禁用 — 点击启用", - "agent_chat_background_enabled_tip": "后台运行已开启 — 关闭页面后对话将继续执行,点击关闭", - "agent_chat_background_disabled_tip": "后台运行已关闭 — 关闭页面后对话将停止,点击开启", - "agent_chat_delete_round": "删除", - "agent_chat_copy_message": "复制", - "agent_chat_edit_message": "编辑", - "agent_chat_save_and_send": "保存并发送", - "agent_chat_cancel_edit": "取消", - "agent_chat_message_queued": "排队中", - "agent_chat_cancel_message": "取消发送", - "agent_permission_title": "脚本请求使用 Agent 对话", - "agent_permission_describe": "此脚本请求使用 Agent 对话功能,将消耗 API Token。请仅对可信脚本授权。", - "agent_permission_content": "Agent 对话", - "agent_opfs": "OPFS", - "agent_opfs_title": "OPFS 文件浏览器", - "agent_opfs_empty": "空目录", - "agent_opfs_name": "名称", - "agent_opfs_size": "大小", - "agent_opfs_type": "类型", - "agent_opfs_modified": "最后修改", - "agent_opfs_delete_confirm": "确定要删除吗?", - "agent_opfs_delete_success": "已删除", - "agent_opfs_file": "文件", - "agent_opfs_directory": "目录", - "agent_opfs_preview": "预览", - "agent_opfs_root": "根目录", - "agent_dom_permission_title": "脚本请求 DOM 操作权限", - "agent_dom_permission_describe": "此脚本请求读取和操作网页 DOM 的能力(点击、填写表单、导航、截图等)。请仅对可信脚本授权。", - "agent_dom_permission_content": "Agent DOM 操作", - "agent_mcp_title": "MCP 服务器", - "agent_mcp_add_server": "添加服务器", - "agent_mcp_no_servers": "未配置 MCP 服务器", - "agent_mcp_test_connection": "测试", - "agent_mcp_name_url_required": "名称和 URL 不能为空", - "agent_mcp_optional": "可选", - "agent_mcp_custom_headers": "自定义请求头", - "agent_mcp_enabled": "启用", - "agent_mcp_detail": "详情", - "agent_mcp_tools": "工具", - "agent_mcp_resources": "资源", - "agent_mcp_prompts": "提示词", - "agent_mcp_no_tools": "暂无可用工具", - "agent_mcp_no_resources": "暂无可用资源", - "agent_mcp_no_prompts": "暂无可用提示词", - "agent_mcp_loading": "加载中...", - "agent_mcp_parameters": "参数", - "agent_tasks": "定时任务", - "agent_tasks_title": "定时任务管理", - "agent_tasks_create": "创建任务", - "agent_tasks_edit": "编辑任务", - "agent_tasks_mode_internal": "内部执行", - "agent_tasks_mode_event": "事件驱动", - "agent_tasks_cron": "Cron 表达式", - "agent_tasks_next_run": "下次运行", - "agent_tasks_last_status": "上次状态", - "agent_tasks_run_now": "立即运行", - "agent_tasks_history": "运行历史", - "agent_tasks_prompt": "提示词", - "agent_tasks_max_iterations": "最大迭代次数", - "agent_tasks_notify": "完成通知", - "agent_tasks_no_tasks": "暂无定时任务", - "agent_tasks_delete_confirm": "确定要删除此定时任务吗?", - "agent_tasks_clear_runs": "清除历史", - "agent_tasks_clear_runs_confirm": "确定要清除此任务的运行历史吗?", - "agent_tasks_event_hint": "任务触发时将通知创建此任务的脚本", - "agent_tasks_name_cron_required": "名称和 Cron 表达式不能为空", - "agent_tasks_model_select": "选择模型", - "agent_tasks_skills": "Skills", - "agent_tasks_skills_auto": "自动加载全部", - "agent_tasks_conversation_id": "续接对话 ID(可选)", - "agent_tasks_run_status_success": "成功", - "agent_tasks_run_status_error": "失败", - "agent_tasks_run_status_running": "运行中", - "agent_tasks_run_duration": "耗时", - "agent_tasks_run_usage": "用量", - "agent_tasks_run_conversation": "查看对话", - "agent_tasks_run_time": "时间", - "agent_tasks_run_status": "状态", - "agent_tasks_never_run": "未运行", - "agent_settings": "设置", - "agent_settings_title": "Agent 设置", - "agent_doc_link": "查看文档", - "agent_model_settings": "模型设置", - "agent_summary_model": "摘要模型", - "agent_summary_model_desc": "用于网页摘要等场景,未设置时使用默认模型", - "agent_summary_model_placeholder": "使用默认模型", - "agent_search_settings": "搜索设置", - "agent_search_engine": "搜索引擎", - "agent_search_engine_baidu": "百度", - "agent_search_google_api_key": "Google API Key", - "agent_search_google_cse_id": "自定义搜索引擎 ID", - "agent_settings_saved": "设置已保存", - "agent_settings_save_failed": "保存设置失败", - "agent_search_engine_tip_bing": "默认搜索引擎,全球覆盖广泛,无需额外配置。", - "agent_search_engine_tip_duckduckgo": "注重隐私保护的搜索引擎,无需 API Key。", - "agent_search_engine_tip_baidu": "针对中文内容优化,中文搜索效果更好。", - "agent_search_engine_tip_google": "搜索质量更高,需要配置 Google API Key 和自定义搜索引擎 ID。" -} \ No newline at end of file diff --git a/src/locales/zh-TW/agent.json b/src/locales/zh-TW/agent.json new file mode 100644 index 000000000..b68783054 --- /dev/null +++ b/src/locales/zh-TW/agent.json @@ -0,0 +1,252 @@ +{ + "title": "AI Agent", + "docs": "文件", + "chat": "會話", + "provider": "模型服務", + "mcp": "MCP", + "skills": "Skills", + "provider_title": "模型服務", + "provider_subtitle": "管理 AI 模型服務商與連線設定", + "provider_select": "AI 服務提供商", + "provider_api_base_url": "API 地址", + "provider_api_key": "API 密鑰", + "provider_model": "預設模型", + "provider_test_connection": "測試連接", + "provider_test_success": "連接成功", + "provider_test_failed": "連接失敗", + "provider_test_hint": "測試會傳送一條最小請求以驗證設定", + "provider_docs": "文件", + "provider_count_hint": "預設模型用於新會話", + "model_count": "{{count}} 個模型", + "model_fetch": "取得模型", + "model_fetch_failed": "取得模型列表失敗", + "model_name": "名稱", + "model_add": "新增模型", + "model_edit": "編輯", + "model_copy": "複製", + "model_delete": "刪除", + "model_set_default": "設為預設", + "model_default_label": "預設", + "model_delete_confirm": "確定要刪除此模型配置嗎?", + "model_max_tokens": "最大輸出 Token 數", + "model_context_window": "上下文視窗大小", + "model_no_models": "暫無模型配置", + "model_no_models_desc": "新增第一個模型服務,開始使用 AI Agent", + "model_vision_support": "支援圖片輸入", + "model_image_output": "支援圖片輸出", + "model_capabilities": "模型能力", + "model_supports_vision": "視覺輸入", + "model_supports_image_output": "圖片輸出", + "coming_soon": "開發中...", + "skills_title": "Skills 管理", + "skills_subtitle": "管理 Agent 技能包(SKILL.md 提示詞 + 指令稿 + 參考資料)", + "skills_add": "新增 Skill", + "skills_empty": "尚無已安裝的 Skill", + "skills_empty_desc": "上傳 ZIP 或從 URL 匯入第一個 Skill", + "skills_tools": "工具", + "skills_references": "參考資料", + "skills_detail": "Skill 詳情", + "skills_edit_prompt": "提示詞", + "skills_install": "安裝 Skill", + "skills_install_url": "從 URL 匯入", + "skills_install_paste": "貼上 SKILL.md", + "skills_uninstall": "解除安裝", + "skills_uninstall_confirm": "確定要解除安裝 Skill「{{name}}」?", + "skills_save_success": "儲存成功", + "skills_install_success": "安裝成功", + "skills_fetch_failed": "取得失敗", + "skills_add_script": "新增腳本", + "skills_add_reference": "新增參考資料", + "skills_install_zip": "上傳 ZIP", + "skills_install_zip_hint": "點擊選擇 .zip 檔案", + "skills_prompt": "提示詞", + "skills_installed_at": "安裝時間", + "skills_refresh": "重新整理", + "skills_refresh_success": "重新整理成功", + "skills_tool_code": "工具程式碼", + "skills_click_to_view_code": "點擊工具名稱查看程式碼", + "skills_config": "設定", + "skills_config_saved": "設定已儲存", + "skills_check_updates": "檢查更新", + "skills_no_updates": "所有 Skill 已是最新版本", + "skills_updates_available": "個 Skill 有更新", + "skills_update": "更新", + "skills_docs": "文件", + "skills_count": "已安裝 {{count}} 個 Skill", + "skills_update_count": "{{count}} 個有可用更新", + "skills_update_available": "可更新 {{version}}", + "skills_references_short": "參考", + "skills_configurable": "可設定", + "skills_open_config": "開啟設定", + "skills_update_success": "更新成功", + "skills_url_placeholder": "輸入 SKILL.cat.md URL", + "chat_new": "新建會話", + "chat_delete": "刪除會話", + "chat_delete_confirm": "確定要刪除此會話嗎?", + "chat_no_conversations": "暫無會話", + "chat_search_placeholder": "搜尋會話…", + "chat_search_no_results": "沒有相符的會話", + "chat_export": "匯出", + "chat_input_placeholder": "輸入訊息...", + "chat_send": "發送", + "chat_stop": "停止", + "chat_thinking": "思考過程", + "chat_tool_call": "工具調用", + "chat_tool_arguments": "參數", + "chat_tool_result": "結果", + "chat_error": "發生錯誤", + "chat_no_model": "未配置模型,請先在模型服務中新增", + "chat_model_select": "選擇模型", + "chat_rename": "重新命名", + "chat_copy": "複製", + "chat_copy_success": "已複製", + "chat_regenerate": "重新生成", + "chat_streaming": "生成中...", + "chat_retrying": "正在重試 ({{attempt}}/{{max}})...", + "chat_attach_image": "新增圖片", + "chat_attach_file": "新增檔案", + "chat_welcome_hint": "有關腳本的任何問題,儘管問我", + "chat_welcome_start": "建立一個對話開始吧", + "chat_tokens": "令牌", + "chat_first_token": "TTFT", + "chat_tools_count": "{{count}} 次工具調用", + "chat_tools_enabled": "工具已啟用", + "chat_tools_disabled": "工具已停用", + "chat_tools_enabled_tip": "工具已啟用 — 點擊停用", + "chat_tools_disabled_tip": "工具已停用 — 點擊啟用", + "chat_background_enabled_tip": "背景運行已開啟 — 關閉頁面後對話將繼續執行,點擊關閉", + "chat_background_disabled_tip": "背景運行已關閉 — 關閉頁面後對話將停止,點擊開啟", + "chat_delete_round": "刪除", + "chat_copy_message": "複製", + "chat_edit_message": "編輯", + "chat_save_and_send": "儲存並傳送", + "chat_cancel_edit": "取消", + "chat_message_queued": "排隊中", + "chat_cancel_message": "取消傳送", + "permission_title": "腳本請求使用 Agent 對話", + "permission_describe": "此腳本請求使用 Agent 對話功能,將消耗 API Token。請僅對可信腳本授權。", + "permission_content": "Agent 對話", + "opfs": "OPFS", + "opfs_title": "OPFS 檔案瀏覽器", + "opfs_empty": "空目錄", + "opfs_name": "名稱", + "opfs_size": "大小", + "opfs_type": "類型", + "opfs_modified": "最後修改", + "opfs_delete_confirm": "確定要刪除嗎?", + "opfs_delete_success": "已刪除", + "opfs_file": "檔案", + "opfs_directory": "目錄", + "opfs_preview": "預覽", + "opfs_subtitle": "Origin Private File System · Agent 私有儲存空間", + "opfs_refresh": "重新整理", + "opfs_upload": "上傳", + "opfs_actions": "操作", + "opfs_item_count": "{{count}} 項", + "opfs_empty_desc": "此目錄下尚無檔案", + "opfs_upload_success": "已上傳", + "opfs_upload_failed": "上傳失敗", + "opfs_type_directory": "資料夾", + "opfs_type_image": "圖片", + "opfs_type_text": "文字", + "opfs_type_binary": "二進位", + "opfs_root": "根目錄", + "dom_permission_title": "腳本請求 DOM 操作權限", + "dom_permission_describe": "此腳本請求讀取和操作網頁 DOM 的能力(點擊、填寫表單、導航、截圖等)。請僅對可信腳本授權。", + "dom_permission_content": "Agent DOM 操作", + "mcp_title": "MCP 伺服器", + "mcp_subtitle": "連接 MCP 伺服器,為 Agent 擴充工具、資源與提示詞", + "mcp_add_server": "新增伺服器", + "mcp_no_servers": "未配置 MCP 伺服器", + "mcp_no_servers_desc": "新增第一個 MCP 伺服器,接入外部工具與資料", + "mcp_status_connected": "已連接", + "mcp_status_failed": "連接失敗", + "mcp_status_untested": "未測試", + "mcp_has_key": "密鑰", + "mcp_headers_count": "{{count}} 個請求標頭", + "mcp_count_servers": "{{count}} 個服務", + "mcp_count_connected": "{{count}} 個已連線", + "mcp_count_tools": "共 {{count}} 個工具可用", + "mcp_docs": "文件", + "mcp_test_connection": "測試", + "mcp_name_url_required": "名稱和 URL 不能為空", + "mcp_optional": "可選", + "mcp_custom_headers": "自訂請求標頭", + "mcp_enabled": "啟用", + "mcp_detail": "詳情", + "mcp_tools": "工具", + "mcp_resources": "資源", + "mcp_prompts": "提示詞", + "mcp_no_tools": "暫無可用工具", + "mcp_no_resources": "暫無可用資源", + "mcp_no_prompts": "暫無可用提示詞", + "mcp_loading": "載入中...", + "mcp_parameters": "參數", + "tasks": "定時任務", + "tasks_title": "定時任務管理", + "tasks_subtitle": "依 Cron 週期自動執行 Agent 任務", + "settings": "設定", + "settings_title": "Agent 設定", + "settings_subtitle": "模型、搜尋與一般偏好 · 修改即時生效", + "settings_cat_model": "模型", + "settings_cat_search": "搜尋", + "model_settings": "模型設定", + "summary_model": "摘要模型", + "summary_model_desc": "用於網頁摘要等場景,未設定時使用預設模型", + "summary_model_placeholder": "使用預設模型", + "search_settings": "搜尋設定", + "search_engine": "搜尋引擎", + "search_engine_desc": "web_search 工具使用的檢索來源。", + "search_engine_baidu": "百度", + "search_google_api_key": "Google API Key", + "search_google_api_key_desc": "用於呼叫 Custom Search JSON API。", + "search_google_cse_id": "自訂搜尋引擎 ID", + "search_google_cse_id_desc": "Programmable Search Engine 的 cx 參數。", + "settings_saved": "設定已儲存", + "settings_save_failed": "儲存設定失敗", + "search_engine_tip_bing": "預設搜尋引擎,全球覆蓋廣泛,無需額外設定。", + "search_engine_tip_duckduckgo": "注重隱私保護的搜尋引擎,無需 API Key。", + "search_engine_tip_baidu": "針對中文內容最佳化,中文搜尋效果更好。", + "search_engine_tip_google": "搜尋品質更高,需要設定 Google API Key 和自訂搜尋引擎 ID。", + "tasks_docs": "文件", + "tasks_count_total": "{{count}} 個任務", + "tasks_count_enabled": "{{count}} 個已啟用", + "tasks_create": "建立任務", + "tasks_edit": "編輯任務", + "tasks_mode": "模式", + "tasks_mode_internal": "內部執行", + "tasks_mode_event": "事件驅動", + "tasks_mode_internal_short": "內部", + "tasks_mode_event_short": "事件", + "tasks_cron": "Cron 表達式", + "tasks_next_run": "下次執行", + "tasks_last_status": "上次狀態", + "tasks_run_now": "立即執行", + "tasks_history": "執行歷史", + "tasks_prompt": "提示詞", + "tasks_max_iterations": "最大迭代次數", + "tasks_notify": "完成通知", + "tasks_notify_desc": "任務完成後發送瀏覽器通知", + "tasks_no_tasks": "尚無定時任務", + "tasks_no_tasks_desc": "建立第一個定時任務,讓 Agent 按計畫自動執行", + "tasks_no_runs": "尚無執行記錄", + "tasks_delete_confirm": "確定要刪除此定時任務嗎?", + "tasks_clear_runs": "清除歷史", + "tasks_clear_runs_confirm": "確定要清除此任務的執行歷史嗎?", + "tasks_event_hint": "任務觸發時將通知建立此任務的腳本", + "tasks_event_trigger": "事件觸發", + "tasks_name_cron_required": "名稱和 Cron 表達式不能為空", + "tasks_model_select": "選擇模型", + "tasks_skills": "Skills", + "tasks_skills_auto": "自動載入全部", + "tasks_conversation_id": "續接對話 ID(可選)", + "tasks_run_status_success": "成功", + "tasks_run_status_error": "失敗", + "tasks_run_status_running": "執行中", + "tasks_run_duration": "耗時", + "tasks_run_usage": "用量", + "tasks_run_conversation": "查看對話", + "tasks_run_time": "時間", + "tasks_run_status": "狀態", + "tasks_never_run": "未執行" +} diff --git a/src/locales/zh-TW/common.json b/src/locales/zh-TW/common.json new file mode 100644 index 000000000..b9c719191 --- /dev/null +++ b/src/locales/zh-TW/common.json @@ -0,0 +1,131 @@ +{ + "user_guide": "使用手冊", + "api_docs": "API文件", + "development_guide": "開發手冊", + "script_gallery": "腳本網站", + "community_forum": "社群討論區", + "external_links": "外部連結", + "system_follow": "跟隨系統", + "no_data": "暫無資料", + "logs": "紀錄", + "tools": "工具", + "find": "尋找", + "replace": "取代", + "settings": "設定", + "change_theme": "主題切換", + "hide_main_sidebar": "摺疊側邊欄", + "show_main_sidebar": "展開側邊欄", + "menu": "選單", + "guide": "新手指南", + "helpcenter": "幫助中心", + "save": "儲存", + "file": "檔案", + "save_success": "儲存成功", + "reset_success": "重設成功", + "update": "更新", + "check_update": "檢查更新", + "confirm_delete": "確定刪除", + "confirm_update": "確定更新", + "delete_success": "刪除成功", + "deleting": "刪除中", + "enable": "開啟", + "script_list_enable_width": 80, + "script_list_last_updated_width": 120, + "script_list_apply_to_run_status_width": 140, + "subscribe_list_enable_width": 100, + "disable": "停用", + "name": "名稱", + "version": "版本", + "source": "來源", + "home": "首頁", + "action": "操作", + "export": "匯出", + "delete": "刪除", + "pin_to_top": "置頂", + "confirm": "確定", + "close": "關閉", + "config": "設定", + "key": "鍵", + "value": "值", + "add": "新增", + "type": "類型", + "size": "大小", + "download": "下載", + "edit_value": "編輯值", + "add_value": "新增值", + "update_success": "修改成功", + "add_success": "新增成功", + "clear": "清除", + "type_string": "字串", + "type_number": "數字", + "type_boolean": "布林值", + "type_object": "物件", + "confirm_delete_resource": "您確定要刪除此資源嗎?下次開啟時將會重新載入此資源", + "confirm_clear_resource": "您確定要清除這些資源嗎?下次開啟時將會重新載入資源", + "yes": "是", + "no": "否", + "confirm_delete_permission": "您確定要刪除此權限嗎?", + "reset": "重設", + "run_once": "執行一次", + "stop": "停止", + "edit": "編輯", + "copy": "複製", + "exclude_on": "恢復 $0 的執行", + "exclude_off": "排除 $0 的執行", + "get_confirm_error": "取得確認資訊失敗", + "confirm_error": "確認失敗", + "ignore": "忽略", + "temporary_allow": "暫時允許此{{permissionContent}}", + "temporary_allow_all": "暫時允許全部{{permissionContent}}", + "permanent_allow": "永久允許此{{permissionContent}}", + "permanent_allow_all": "永久允許全部{{permissionContent}}", + "temporary_deny": "暫時拒絕此{{permissionContent}}", + "temporary_deny_all": "暫時拒絕全部{{permissionContent}}", + "permanent_deny": "永久拒絕此{{permissionContent}}", + "permanent_deny_all": "永久拒絕全部{{permissionContent}}", + "import": "匯入", + "author": "作者", + "description": "描述", + "operation": "操作", + "error": "錯誤", + "unknown": "未知", + "add_new": "新增", + "no_operation": "不做操作", + "enable_script": "開啟腳本", + "local_creation": "本機建立", + "import_success": "匯入成功", + "get_script": "取得腳本", + "report_issue": "錯誤/問題回報", + "project_docs": "專案文件", + "community": "交流社群", + "domain": "網域", + "script_name": "腳本名稱", + "skip": "跳過", + "next": "下一步", + "next_with_progress": "下一步(第 {step} 步 / 共 {steps} 步)", + "back": "上一步", + "last": "完成", + "auto": "自動", + "hide": "隱藏", + "custom": "自訂", + "resize_column_width": "調整欄位寬度", + "collapse": "摺疊", + "expand": "展開", + "import_script_placeholder": "支援輸入.user.js結尾的腳本絕對連結 或 腳本貓安裝頁連結\n可多行填寫,每行一條\n範例:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/zh-TW/script-show-page/1234", + "light": "淺色模式", + "dark": "暗色模式", + "enter_search_value": "請輸入 {{search}} 進行搜尋", + "no_message_content": "無訊息內容", + "loading": "加載中...", + "auth_type": "驗證類型", + "url": "網址", + "username": "使用者名稱", + "password": "密碼", + "access_token_bearer": "存取權杖(Bearer)", + "s3_bucket_name": "儲存貯體名稱", + "s3_region": "區域", + "s3_access_key_id": "存取金鑰 ID", + "s3_secret_access_key": "私密存取金鑰", + "s3_custom_endpoint": "自訂端點(選用)", + "cancel": "取消" +} diff --git a/src/locales/zh-TW/editor.json b/src/locales/zh-TW/editor.json new file mode 100644 index 000000000..019c3ef9b --- /dev/null +++ b/src/locales/zh-TW/editor.json @@ -0,0 +1,132 @@ +{ + "save": "儲存", + "save_success": "儲存成功", + "copy": "複製", + "find": "尋找", + "replace": "取代", + "select_all": "全選", + "format": "格式化", + "back": "返回", + "more": "更多", + "file": "檔案", + "edit": "編輯", + "settings": "設定", + "run_settings": "執行設定", + "code": "程式碼", + "script_setting": "腳本設定", + "storage": "儲存", + "resource": "資源", + "search_resource": "搜尋資源", + "search_storage": "搜尋 Key", + "record_count": "{{count}} 筆記錄", + "resource_count": "{{count}} 個資源 · 共 {{size}}", + "script_list": "腳本列表", + "search_scripts": "搜尋腳本...", + "new_script": "新增腳本", + "source": "來源", + "from_user": "使用者", + "from_script": "腳本", + "last_updated": "最近更新", + "run_at": "執行時機", + "run_in": "執行環境", + "check_update": "檢查更新", + "line_col": "行 {{line}}, 列 {{col}}", + "script_not_found": "腳本不存在", + "confirm_delete_script": "確定刪除腳本「{{name}}」嗎?", + "delete_success": "刪除成功", + "delete_failed": "刪除失敗", + "cancel": "取消", + "confirm": "確定", + "save_as": "另存新檔", + "run": "執行", + "debug": "除錯", + "script_storage": "腳本儲存", + "enter_key": "請輸入鍵", + "key_placeholder": "鍵", + "value_placeholder": "當類型為object時,請輸入可以JSON解析的資料", + "clear_success": "清除成功", + "confirm_clear": "您確定要清除此儲存空間嗎?", + "script_resource": "腳本資源", + "basic_info": "基本資訊", + "update_url": "更新URL", + "add_permission": "新增權限", + "match": "符合", + "user_setting": "使用者設定", + "confirm_delete_exclude": "確定要刪除此排除項目嗎?", + "after_deleting_match_item": "腳本設定的符合項目刪除後會自動新增至符合項目中", + "confirm_delete_match": "確定要刪除此符合項目嗎?", + "after_deleting_exclude_item": "腳本設定的符合項目刪除後會自動新增至排除項目中", + "add_match": "新增符合項目", + "add_exclude": "新增排除項目", + "website_match": "網站符合(@match)", + "website_exclude": "網站排除(@exclude)", + "confirm_reset": "確定要重設嗎?", + "undo": "復原", + "redo": "重做", + "cut": "剪下", + "paste": "貼上", + "user_config": "使用者設定", + "gm_api": "GM API", + "storage_api": "儲存API", + "use_file_system": "使用的檔案系統", + "open_directory": "開啟目錄", + "account_validation_failed": "帳號資訊驗證失敗", + "not_set": "未設定", + "in_use": "使用中", + "storage_error": "儲存錯誤", + "upload_to_cloud": "上傳至雲端", + "save_failed": "儲存失敗", + "exporting": "匯出中...", + "upload_to": "上傳至", + "value_export_expression": "值匯出表達式", + "overwrite_original_value_on_import": "匯入時覆蓋原值", + "cookie_export_expression": "Cookie匯出表達式", + "overwrite_original_cookie_on_import": "匯入時覆蓋原值", + "restore_default_values": "恢復預設值", + "edit_conflict": "編輯衝突", + "confirm_override_when_edit_conflict": "此腳本已在其他實例中被修改。替換將覆蓋這些變更。是否要保留此版本?", + "save_abort_when_edit_conflict": "此腳本已在其他實例中被修改,已中止儲存。", + "scriptname_conflict": "腳本名稱衝突", + "confirm_save_when_scriptname_conflict": "此腳本名稱已被其他腳本使用,是否仍要儲存?", + "save_abort_when_scriptname_conflict": "此腳本名稱已被其他腳本使用,已取消儲存。", + "eslint_config_format_error": "ESLint設定格式錯誤", + "script_modified_leave_confirm": "目前內容尚未儲存,離開後變更將會遺失,確定要離開嗎?", + "create_success_note": "新建成功,請注意背景腳本不會預設開啟", + "save_as_failed": "另存新檔失敗", + "save_as_success": "另存新檔成功", + "only_background_scheduled_can_run": "只有背景腳本/排程腳本才能執行", + "preparing_script_resources": "正在準備腳本資源...", + "build_success_message": "建置成功,可以在擴充功能頁開啟開發者工具在主控台中查看輸出", + "script_storage_tooltip": "可以管理腳本的儲存資料(GM_value)", + "script_resource_tooltip": "管理@resource,@require下載的資源", + "script_setting_tooltip": "對腳本進行一些自訂設定", + "script_modified_close_confirm": "腳本已修改,關閉後會遺失修改,是否繼續?", + "close_current_tab": "關閉目前分頁", + "close_other_tabs": "關閉其他分頁", + "close_left_tabs": "關閉左側分頁", + "close_right_tabs": "關閉右側分頁", + "invalid_script_code": "錯誤的腳本程式碼", + "build_failed": "建置失敗", + "drag_script_here_to_upload": "拖曳腳本到此處上傳", + "watch_file_description": "監聽檔案變動,自動更新腳本,使用時請確保腳本檔案路徑不變且不能關閉頁面", + "watch_file": "監聽檔案", + "stop_watch_file": "停止監聽", + "individual_edit": "單獨編輯", + "batch_edit": "批量編輯", + "script_code": "腳本代碼", + "editor_config": "編輯器設定", + "editor_config_description": "你可以參考jsconfig.js中的compilerOptions進行配置", + "editor_type_definition": "編輯器類型定義", + "editor_type_definition_description": "你可以自訂自己的類型定義,腳本編輯器會自動載入這些類型定義", + "eslint_rules_reset": "ESLint規則已重置", + "eslint_rules_saved": "ESLint規則已儲存", + "editor_config_reset": "編輯器配置已重置", + "editor_config_saved": "編輯器配置已儲存", + "editor_config_format_error": "編輯器配置格式錯誤", + "editor_type_definition_reset": "編輯器類型定義已重設", + "editor_type_definition_saved": "編輯器類型定義已儲存", + "editor": { + "show_script_list": "顯示腳本列表", + "hide_script_list": "隱藏腳本列表" + } +} diff --git a/src/locales/zh-TW/guide.json b/src/locales/zh-TW/guide.json new file mode 100644 index 000000000..645f2b9f8 --- /dev/null +++ b/src/locales/zh-TW/guide.json @@ -0,0 +1,25 @@ +{ + "start_title": "歡迎使用腳本貓擴充功能", + "start_content": "接下來我們將為您介紹腳本貓的基本使用方法", + "installed_scripts": "您所安裝的腳本將會在這裡顯示", + "script_list_title": "腳本中心", + "script_list_content": "可以從腳本中心中安裝腳本,腳本貓除了支援使用者腳本外,還支援背景腳本", + "script_list_enable_title": "腳本開啟", + "script_list_enable_content": "腳本需要開啟才能使用,頁面腳本安裝預設開啟,背景腳本安裝預設關閉", + "script_list_apply_to_run_status_title": "應用至、與執行狀態", + "script_list_apply_to_run_status_content": "腳本執行狀態展示,滑鼠懸停至標籤可以查看腳本類型", + "script_list_sort_title": "排序", + "script_list_sort_content": "你可以拖曳腳本的標籤進行排序", + "script_list_update_title": "最後更新", + "script_list_update_content": "點擊最後更新列標籤可以對腳本進行一次檢查更新", + "script_list_action_title": "操作", + "script_list_action_content": "操作列可以進入腳本編輯、控制腳本的運行停止(背景腳本)、設定UserConfig,操作右側按鈕可開啟進階篩選與切換視圖模式", + "tools_title": "常用工具", + "tools_content": "工具中提供了備份和開發的工具", + "tools_backup_title": "備份", + "tools_backup_content": "備份可以保存腳本,避免遺失。可以匯出腳本檔案到本機,也可以載入本機的腳本檔案。也可以備份到雲端,更加方便。", + "setting_title": "設定", + "setting_content": "設定中主要包含了語言、腳本同步、更新頻率等常用設定項目", + "setting_sync_title": "更新與同步", + "setting_sync_content": "腳本同步功能可以方便的將本設備的腳本內容同步至雲端,如果有多台設備請勾選同步刪除選項,當本設備腳本刪除時,會從雲端刪除對應的腳本,也會將其它設備的腳本刪除。" +} diff --git a/src/locales/zh-TW/index.ts b/src/locales/zh-TW/index.ts new file mode 100644 index 000000000..bacd6d096 --- /dev/null +++ b/src/locales/zh-TW/index.ts @@ -0,0 +1,11 @@ +export { default as agent } from "./agent.json"; +export { default as common } from "./common.json"; +export { default as editor } from "./editor.json"; +export { default as guide } from "./guide.json"; +export { default as install } from "./install.json"; +export { default as logs } from "./logs.json"; +export { default as permission } from "./permission.json"; +export { default as popup } from "./popup.json"; +export { default as script } from "./script.json"; +export { default as settings } from "./settings.json"; +export { default as tools } from "./tools.json"; diff --git a/src/locales/zh-TW/install.json b/src/locales/zh-TW/install.json new file mode 100644 index 000000000..a9462f587 --- /dev/null +++ b/src/locales/zh-TW/install.json @@ -0,0 +1,207 @@ +{ + "data_import": "資料匯入", + "select_scripts_to_import": "請選擇您要匯入的腳本", + "select_all": "全選", + "script_import_progress": "腳本匯入進度", + "select_subscribes_to_import": "請選擇您要匯入的訂閱", + "subscribe_import_progress": "訂閱匯入進度", + "script": "安裝", + "update_script": "更新", + "subscribe": "安裝訂閱", + "update_subscribe": "更新訂閱", + "update_script_no_close": "更新,不关闭窗口", + "script_no_close": "安装,不关闭窗口", + "update_script_no_more_update": "更新,但不再检查更新", + "close_update_script_no_more_update": "關閉,且不再檢查更新", + "script_no_more_update": "安装,但不再检查更新", + "invalid_link": "錯誤的連結", + "subscribe_install_label": "此訂閱將會安裝以下的腳本", + "subscribe_scripts_title": "本訂閱將安裝以下腳本", + "subscribe_scripts_empty": "該訂閱尚未宣告腳本", + "script_runs_in": "腳本將在以下的網站中執行", + "script_has_full_access_to": "腳本將獲得以下網址的完整存取權限", + "script_requires": "腳本引用了以下外部資源", + "cookie_warning": "請注意,此腳本會要求 Cookie 的操作權限,這是一項危險的權限,請確認腳本的安全性。", + "perm_card_title": "此腳本將取得以下權限", + "perm_card_hint": "安裝前請確認", + "perm_card_empty": "此腳本不要求任何特殊權限", + "perm_match_label": "執行網站", + "perm_match_summary": "腳本會在這些網站上執行並修改頁面", + "perm_connect_label": "跨域存取", + "perm_connect_summary": "可向以下網域傳送請求並讀取其資料", + "perm_grant_label": "GM 能力", + "perm_grant_summary": "可呼叫以下油猴 API", + "perm_require_label": "外部資源", + "perm_require_summary": "載入以下第三方腳本與資源", + "badge_background": "背景", + "badge_scheduled": "定時", + "enabled_label": "已啟用", + "schedule_cron_label": "定時任務", + "schedule_next_run": "下次執行", + "schedule_background_desc": "瀏覽器開啟時自動執行", + "code_lines": "{{count}} 行", + "code_copy": "複製程式碼", + "code_collapse": "收合", + "code_expand": "展開", + "loading_title": "正在載入腳本", + "loading_desc": "正在從來源下載並解析腳本內容", + "error_retry": "重試", + "error_invalid_desc": "缺少有效的安裝來源參數,無法載入腳本。", + "context_install": "腳本安裝", + "context_update": "腳本更新", + "background_script": "背景腳本", + "scheduled_script": "定時腳本", + "watching_status": "正在監聽檔案變化,儲存後自動重新安裝", + "watching_chip": "監聽中", + "watching_title": "正在監聽檔案變化", + "watching_file_desc": "儲存 {{file}} 會自動重新安裝並同步腳本", + "watching_last_sync": "最後更新 {{time}}", + "warning_title": "請確認腳本來自可信來源", + "warning_risk_connect": "它可以存取所有網域", + "warning_risk_antifeature": "它宣告了反功能特性", + "warning_risk_join": " 且 ", + "warning_risk_tail": " — 請謹慎安裝。", + "action_note_install": "安裝代表您信任該腳本的來源與作者", + "action_note_update": "更新代表您接受此程式碼與權限變更", + "action_note_subscribe": "安裝代表您信任該訂閱及其作者", + "action_note_watching": "監聽中 — 儲存檔案會自動更新,停止後可手動操作", + "context_skill_install": "技能安裝", + "context_skill_update": "技能更新", + "skill_kind": "AI 技能", + "skill_prompt_title": "提示詞", + "skill_prompt_chip": "SKILL.md", + "skill_tools_title": "工具", + "skill_config_title": "設定項", + "skill_references_title": "參考資料", + "skill_required": "必填", + "skill_secret": "私密", + "skill_install": "安裝 Skill", + "skill_update": "更新 Skill", + "skill_warning": "技能會向 AI 注入提示詞,並授予其呼叫以下工具、GM 能力與設定的權限,請從合法來源安裝!", + "skill_warning_title": "請確認該 Skill 來自可信來源", + "skill_warning_desc": "安裝後會向 AI 注入提示詞,並授予列出的工具(含 GM 權限)與設定讀寫 — 請謹慎安裝。", + "success": "安裝成功", + "install": { + "update_success": "更新成功" + }, + "failed": "安裝失敗", + "subscribe_success": "訂閱成功", + "subscribe_failed": "訂閱失敗", + "current_version": "目前版本", + "update_version": "更新版本", + "updatepage": { + "title": "批次更新", + "main_header": "檢查更新", + "last_check": "上次檢查 {{time}}", + "status_checking_updates": "正在檢查更新...", + "updates_available": "{{count}} 個可用更新", + "ignored_count": "{{count}} 個已忽略", + "selected_count": "已選 {{selected}} / {{total}} 項", + "update_selected": "更新選取 ({{count}})", + "ignore_selected": "忽略選取", + "update": "更新", + "ignore": "忽略", + "restore": "恢復更新", + "restore_all": "全部恢復", + "ignored_section": "已忽略的更新", + "auto_close": "{{count}} 秒後自動關閉", + "col_script": "腳本", + "col_version": "版本", + "col_change": "變更", + "col_source": "來源", + "col_action": "操作", + "enabled": "已啟用", + "disabled": "已停用", + "codechange_major": "重大變更", + "codechange_noticeable": "明顯變更", + "codechange_tiny": "輕微變更", + "tag_new_connect": "新增 @connect", + "empty_title": "所有腳本皆為最新", + "empty_desc": "已檢查 {{count}} 個腳本 · 暫無可用更新", + "similarity": "相似度", + "new_connects": "新增連線", + "toast_found": "發現 {{count}} 個可更新腳本", + "toast_uptodate": "所有腳本皆為最新" + }, + "importpage": { + "title": "資料匯入", + "context_review": "資料匯入", + "context_importing": "正在匯入", + "context_done": "匯入完成", + "selected_count": "已選 {{selected}} / {{total}} 項", + "unimportable_count": "{{count}} 項無法匯入", + "count_scripts": "{{count}} 腳本", + "count_subscribes": "{{count}} 訂閱", + "col_script": "腳本", + "col_version": "版本", + "col_source": "來源", + "col_data": "資料", + "col_status": "狀態", + "col_enabled": "啟用", + "op_add": "新增", + "op_update": "更新", + "op_error": "解析失敗", + "source_local": "本地建立", + "data_values": "{{count}} 項", + "data_resources": "含資源", + "enable_after_import": "匯入後啟用", + "row_error": "檔案損毀,無法匯入", + "unknown_script": "未知腳本", + "subscribe_section": "訂閱", + "trust_hint": "僅還原你勾選的項目,匯入不會向外傳送任何資料", + "import_selected": "匯入所選 ({{count}})", + "importing_progress": "正在還原備份 · 已完成 {{done}} / {{total}}", + "importing_hint": "請保持頁面開啟", + "importing_actionbar_hint": "正在還原腳本與資料,請勿關閉頁面", + "importing_button": "匯入中…", + "cancel": "取消", + "status_pending": "待匯入", + "status_importing": "匯入中", + "status_done": "已匯入", + "status_skipped": "已略過", + "done_title": "匯入完成", + "done_desc": "已還原所選的腳本與資料", + "done_stat_scripts": "{{count}} 個腳本", + "done_stat_subscribes": "{{count}} 個訂閱", + "done_stat_values": "{{count}} 項資料", + "view_scripts": "檢視腳本清單", + "loading_title": "正在解析備份檔案", + "loading_desc": "正在讀取並驗證備份內容", + "error_title": "無法讀取備份檔案", + "error_desc": "備份檔案可能已損毀,或匯入連結已失效", + "invalid_desc": "匯入連結無效或已過期", + "retry": "重試", + "empty_title": "備份中沒有可匯入的項目", + "empty_desc": "此備份檔案不包含任何腳本或訂閱" + }, + "downloading_status_text": "正在下載。已接收 {{bytes}}。", + "downloading_status_percent": "正在下載。已接收 {{bytes}} / {{total}}({{percent}}%)。", + "page_please_wait": "請稍等", + "page_loading": "安裝頁面載入中", + "page_load_failed": "安裝頁面載入失敗", + "invalid_page": "無效頁面", + "background_script_tag": "這是一個背景腳本", + "scheduled_script_tag": "這是一個排程腳本", + "from_legitimate_sources_warning": "請從合法來源安裝腳本!未知的腳本可能會侵犯您的隱私或進行惡意操作!", + "referral_link_title": "推薦連結", + "referral_link_description": "此腳本會修改或重新導向至作者的返傭連結", + "ads_title": "附帶廣告", + "ads_description": "此腳本會在您存取的頁面上插入廣告", + "payment_title": "付費腳本", + "payment_description": "此腳本需要您付費才能正常使用", + "miner_title": "挖礦", + "miner_description": "此腳本存在挖礦行為", + "membership_title": "會員功能", + "membership_description": "此腳本需要註冊會員才能正常使用", + "tracking_title": "資訊追蹤", + "tracking_description": "此腳本會追蹤您的使用者資訊", + "script_info_load_failed": "腳本資訊載入失敗!", + "script_import_result": "腳本匯入結果", + "failure_info": "失敗資訊", + "source": "安裝來源", + "skill_prompt": "提示詞", + "skill_tools": "工具", + "skill_config": "設定項目", + "skill_references": "參考資料", + "skill_install_failed": "Skill 安裝失敗" +} diff --git a/src/locales/zh-TW/logs.json b/src/locales/zh-TW/logs.json new file mode 100644 index 000000000..901b565e9 --- /dev/null +++ b/src/locales/zh-TW/logs.json @@ -0,0 +1,59 @@ +{ + "log_title": "執行紀錄", + "last_5_minutes": "最近5分鐘", + "last_15_minutes": "最近15分鐘", + "last_30_minutes": "最近30分鐘", + "last_1_hour": "最近1小時", + "last_3_hours": "最近3小時", + "last_6_hours": "最近6小時", + "last_12_hours": "最近12小時", + "last_24_hours": "最近24小時", + "last_7_days": "最近7天", + "query": "查詢", + "labels": "標籤", + "search_regex": "搜尋(支援正規表達式)", + "clean_schedule": "排程清理", + "days_ago_logs": "天前的紀錄", + "delete_completed": "刪除完成", + "delete_current_logs": "刪除目前紀錄", + "clear_completed": "清除完成", + "clear_logs": "清除紀錄", + "now": "至今", + "total_logs": "共查詢到 {{length}} 筆紀錄", + "filtered_logs": "篩選後 {{length}} 條紀錄", + "enter_filter_conditions": "請輸入篩選條件進行查詢", + "last_updated": "最近更新", + "runtime": "運行時", + "advanced": "進階", + "label_filter": "標籤篩選", + "add_label": "新增標籤", + "custom_range": "自訂範圍", + "back_to_top": "回到頂部", + "refresh": "重新整理", + "all_levels": "全部", + "total_count": "共 {{count}} 筆", + "filtered_count": "篩選出 {{count}} 筆", + "clear_logs_confirm": "確定清除所有紀錄?此操作無法復原。", + "no_logs": "暫無紀錄", + "refresh_off": "關閉", + "interval_5s": "5秒", + "interval_10s": "10秒", + "interval_30s": "30秒", + "interval_1m": "1分鐘", + "interval_5m": "5分鐘", + "quick_range": "快捷範圍", + "absolute_range": "絕對時間範圍", + "from_start": "從(開始)", + "to_end": "到(結束)", + "apply_range": "套用範圍", + "auto_refresh_hint": "結束設為「現在」時,自動重新整理會持續拉取最新紀錄", + "group_minutes": "分鐘", + "group_hours": "小時", + "group_days": "天", + "weekdays_short": "日,一,二,三,四,五,六", + "year_month": "{{year}} 年 {{month}} 月", + "time": "時間", + "prev_month": "上個月", + "next_month": "下個月", + "live": "即時" +} diff --git a/src/locales/zh-TW/permission.json b/src/locales/zh-TW/permission.json new file mode 100644 index 000000000..3d4c69fe7 --- /dev/null +++ b/src/locales/zh-TW/permission.json @@ -0,0 +1,42 @@ +{ + "permission": "權限", + "permission_value": "權限值", + "allow": "是否允許", + "permission_management": "權限管理", + "permission_cors": "跨網域(CORS)", + "permission_cookie": "管理Cookie", + "allow_once": "允許一次", + "deny_once": "拒絕一次", + "script_accessing_cross_origin_resource": "腳本正在嘗試存取跨網域資源", + "confirm_operation_description": "請您確認是否允許腳本進行此操作,腳本也可增加@connect標籤跳過此選項", + "request_domain": "請求網域", + "request_url": "請求網址", + "access_cookie_content": "腳本正在嘗試存取網站Cookie內容", + "confirm_script_operation": "請您確認是否允許腳本進行此操作,Cookie是一項重要的使用者資料,請務必只給信任的腳本權限。", + "cookie_domain": "Cookie網域", + "script_operation_title": "腳本正在嘗試操作腳本同步儲存空間", + "script_operation_description": "請您確認是否允許腳本進行此操作,允許後將允許腳本操作您設定的儲存空間,腳本會在儲存空間下建立一個app/${dir}的目錄進行使用", + "script_permission_content": "腳本", + "extension_site_access_title": "ScriptCat 需要網站存取權限", + "extension_site_access_description": "請授予瀏覽器對此來源的網站存取權限,讓 ScriptCat 可以完成本次請求。這會修改擴充功能的網站存取設定。", + "extension_site_access_content": "網站", + "request_permission": "要求權限", + "allow_user_script_guide": "目前尚未啟用「允許使用者指令碼」,腳本無法正常執行。👉點此查看啟用方式", + "user_script_type": "使用者腳本", + "auth_duration": "授權時長", + "duration_once": "僅此次", + "duration_temporary": "暫時", + "duration_permanent": "永久", + "apply_to_all_domains": "套用至所有請求網域", + "apply_to_all_domains_desc": "對此腳本的全部請求生效(萬用字元)", + "allow_action": "允許", + "deny_action": "拒絕", + "ignore_action": "忽略", + "cancel_action": "取消", + "loading_confirm": "正在讀取授權請求…", + "cookie_warning_title": "高敏感權限", + "cookie_warning_desc": "Cookie 包含登入狀態等敏感資料,請僅授權可信任的腳本。", + "confirm_expired_title": "授權請求已失效", + "confirm_expired_desc": "此次授權請求已逾時或已被處理。請返回頁面重新觸發該操作。", + "auto_close_in": "{{second}} 秒後自動關閉視窗" +} diff --git a/src/locales/zh-TW/popup.json b/src/locales/zh-TW/popup.json new file mode 100644 index 000000000..73938897b --- /dev/null +++ b/src/locales/zh-TW/popup.json @@ -0,0 +1,27 @@ +{ + "new_version_available": "有新版本可用", + "current_page_scripts": "目前頁面執行腳本", + "enabled_background_scripts": "開啟和執行的背景腳本", + "menu_expand_num_before": "選單項目超過", + "menu_expand_num_after": "個時,自動隱藏", + "develop_mode_guide": "目前尚未啟用「開發者模式」,腳本無法正常執行。👉點此查看啟用方式", + "lower_version_browser_guide": "您的瀏覽器版本過舊,腳本無法正常執行。👉點擊了解更多", + "click_to_reload": "👉點擊重新載入", + "page_in_blacklist": "目前頁面在黑名單中,無法使用腳本", + "ext_update_notification": "腳本貓擴充功能已更新", + "ext_update_notification_desc": "目前版本:{{version}},詳情請查看更新日誌", + "script_menu_display": "腳本註冊的選單", + "badge_type_none": "不顯示", + "badge_type_run_count": "執行次數", + "badge_type_script_count": "腳本個數", + "script_menu": "腳本選單", + "display_right_click_menu": "顯示右鍵選單", + "display_right_click_menu_desc": "在瀏覽器右鍵選單中顯示腳本選單", + "expand_count": "展開數量", + "auto_collapse_when_exceeds": "超過此數量時自動摺疊", + "allow_user_script_guide": "目前尚未啟用「允許使用者指令碼」,腳本無法正常執行。👉點此了解啟用方式", + "request_permission": "要求權限", + "show_more_scripts": "+{{count}} 個腳本", + "use_on_mobile": "在手機上使用腳本貓", + "scan_qr_to_install": "掃描 QR Code 在手機上安裝腳本貓" +} diff --git a/src/locales/zh-TW/script.json b/src/locales/zh-TW/script.json new file mode 100644 index 000000000..47a5c27d3 --- /dev/null +++ b/src/locales/zh-TW/script.json @@ -0,0 +1,115 @@ +{ + "import_link": "連結匯入", + "import_link_failure": "連結匯入失敗", + "create_user_script": "新建普通腳本", + "create_background_script": "新建背景腳本", + "create_scheduled_script": "新建排程腳本", + "import_by_local": "本機匯入", + "import_local_failure": "本機匯入失敗", + "import_local_success": "本機匯入成功", + "create_script": "新建腳本", + "installed_scripts": "已安裝腳本", + "nav_scripts": "腳本", + "subscribe": "訂閱", + "subscribe_scripts_count": "{{count}} 個腳本", + "enter_subscribe_name": "請輸入訂閱名稱", + "subscribe_url": "訂閱網址", + "confirm_delete_subscription": "確定要刪除此訂閱嗎?相關的腳本也會被刪除", + "list": { + "confirm_delete": "確定要刪除嗎?請注意這個操作無法復原!", + "confirm_update": "確定要更新嗎?請注意此操作不可逆!" + }, + "apply_to_run_status": "應用至/執行狀態", + "sorting": "排序", + "foreground_page_script_tooltip": "前景頁面腳本,會在指定的頁面上執行", + "background_script_tooltip": "背景腳本,啟用後將在背景執行", + "scheduled_script_tooltip": "排程腳本,下一次執行時間:", + "running": "執行中", + "completed": "執行完畢", + "source_subscribe_link": "訂閱連結", + "source_local_script": "本地腳本", + "source_script_link": "腳本連結", + "by_manual_creation": "在本地透過程式碼編輯方法建立", + "confirm_delete_script": "確定要刪除此腳本嗎?", + "confirm_delete_scripts_content": "確定要刪除選取的 {{count}} 個腳本嗎?此操作無法復原。", + "confirm_delete_script_content": "確定要刪除腳本\"{{name}}\"嗎?此操作無法復原。", + "delete_failed": "刪除失敗", + "enter_script_name": "請輸入腳本名稱", + "update_not_supported": "此腳本不支援檢查更新", + "checking_for_updates": "正在檢查更新...", + "new_version_available": "有新版本", + "latest_version": "已經是最新版本", + "checked_for_all_selected": "所選腳本皆已檢查更新", + "update_check_failed": "檢查更新失敗", + "stopping_script": "正在停止腳本", + "script_stopped": "腳本已停止", + "starting_script": "正在啟動腳本...", + "starting_updates": "正在批次更新...", + "script_started": "腳本已啟動", + "operation_failed": "操作失敗", + "batch_operations": "批次操作", + "scripts_pinned_to_top": "已將所選腳本置頂", + "unknown_operation": "未知操作", + "page_script": "頁面腳本", + "homepage": "腳本首頁", + "script_website": "腳本網站", + "script_source": "腳本原始碼", + "bug_feedback_script_support": "錯誤回報/腳本支援網站", + "script_total_runs": "此腳本總共執行了{{runNum}}次,在iframe上執行了{{runNumByIframe}}次", + "script_total_runs_single": "此腳本執行了{{runNum}}次", + "script_disabled": "此腳本未開啟", + "cron_oncetype": { + "minute": "{{next}} (每分鐘執行一次)", + "hour": "{{next}} (每小時執行一次)", + "day": "{{next}} (每天執行一次)", + "month": "{{next}} (每月執行一次)", + "week": "{{next}} (每星期執行一次)" + }, + "cron_invalid_expr": "錯誤的排程表達式", + "scheduled_script_description_title": "這是一個排程腳本,啟用後將在特定時間自動執行,並可在控制面板中手動控制。", + "scheduled_script_description_description_expr": "排程任務表達式:", + "scheduled_script_description_description_next": "最近一次執行時間:", + "background_script_description": "這是一個背景腳本,啟用後將在瀏覽器開啟時自動執行一次,並可在控制面板中手動控制。", + "background_script": "背景腳本", + "scheduled_script": "排程腳本", + "script_status_tooltip": "可以控制腳本開啟狀態,普通油猴腳本預設開啟,背景腳本、排程腳本預設關閉", + "subscribe_source_tooltip": "這是一個訂閱源,當你開啟訂閱後會自動安裝訂閱的腳本", + "script_name_cannot_be_set_to_empty": "腳本名稱不可設定為空", + "search_scripts": "搜尋腳本", + "script_list": { + "sidebar": { + "stopped": "停止", + "all": "全部", + "normal_script": "普通腳本", + "status": "狀態" + } + }, + "tags": "標籤", + "input_tags_placeholder": "輸入標籤,按回車確認", + "switch_to_card_mode": "切換到卡片模式", + "switch_to_table_mode": "切換到表格模式", + "open_sidebar": "打開側邊欄", + "close_sidebar": "關閉側邊欄", + "error_metadata_invalid": "MetaData 資訊錯誤", + "error_script_name_required": "腳本名稱不可為空", + "error_script_version_required": "腳本 @version 版本不可為空", + "error_script_namespace_required": "腳本 @namespace 命名空間不可為空", + "error_cron_invalid": "錯誤的排程表示式,請檢查:{{expr}}", + "error_script_type_mismatch": "腳本類型不相容,前臺腳本與後臺腳本不可互轉", + "error_old_script_code_missing": "舊的腳本程式碼不存在", + "error_subscribe_name_required": "訂閱名稱不可為空", + "error_grant_conflict": "@grant 同時宣告了 none 與 GM API", + "error_metadata_line_duplicated": "Metadata 裡有重覆的聲明。", + "create_group": "新建", + "import_group": "匯入", + "import_local_script": "匯入本機腳本", + "link_import": "連結匯入", + "import_skill": "匯入 Skill", + "link_import_desc": "貼上腳本 / 訂閱連結,每行一個", + "link_import_placeholder": "https://example.com/script.user.js", + "link_import_hint": "支援使用者腳本 / 訂閱 / Skill 連結", + "not_a_valid_script": "不是有效的使用者腳本或 SkillScript", + "import_done": "匯入完成:成功 {{success}} · 失敗 {{fail}}", + "drop_to_install": "拖曳腳本或 Skill 到此處安裝", + "drop_to_install_hint": "拖曳 .js 使用者腳本 / 訂閱 · .zip Skill 包" +} diff --git a/src/locales/zh-TW/settings.json b/src/locales/zh-TW/settings.json new file mode 100644 index 000000000..afc981396 --- /dev/null +++ b/src/locales/zh-TW/settings.json @@ -0,0 +1,119 @@ +{ + "general": "通用", + "language": "語言", + "help_translate": "幫助翻譯", + "script_sync": "腳本同步", + "sync_delete": "同步刪除", + "sync_delete_desc": "啟用後,腳本刪除時會標記腳本為刪除,其他裝置檢測到刪除狀態後會將腳本刪除;關閉時,會直接刪除本機與雲端的腳本,如果有多台裝置可能出現腳本反覆同步的情況。", + "enable_script_sync_to": "啟用腳本同步至", + "script_subscription_check_interval": "腳本/訂閱檢查更新間隔", + "never": "從不", + "6_hours": "6小時", + "12_hours": "12小時", + "every_day": "每天", + "every_week": "每週", + "update_disabled_scripts": "更新已停用腳本", + "silent_update_non_critical_changes": "悄悄更新非重大變更", + "enable_eslint": "開啟 ESLint", + "eslint_rules": "ESLint規則", + "enter_eslint_rules": "請輸入 ESLint 規則,可以從 https://eslint.org/play/ 下載設定", + "language_change_tip": "語言切換成功", + "backup": "備份", + "local": "本機", + "export_file": "匯出檔案", + "import_file": "匯入檔案", + "cloud": "雲端", + "backup_to": "備份至", + "preparing_backup": "正在準備備份到雲端", + "backup_success": "備份成功", + "backup_failed": "備份失敗", + "no_backup_files": "沒有備份檔案", + "backup_list": "備份列表", + "open_backup_dir": "開啟備份目錄", + "confirm_delete_backup_file": "確定刪除備份檔案", + "backup_strategy": "備份策略", + "under_construction": "建設中", + "sync_system_connect_failed": "同步系統連接失敗", + "sync_system_closed": "已關閉同步", + "sync_system_closed_description": "同步功能已關閉,請重新設定", + "export_success": "匯出成功", + "get_backup_dir_url_failed": "取得備份目錄網址失敗", + "get_backup_files_failed": "取得備份檔案失敗", + "baidu_netdisk": "百度網盤", + "netdisk_unbind": "解除綁定 {{provider}}", + "netdisk_unbind_confirm": "確定解除 {{provider}} 帳號綁定嗎?", + "netdisk_unbind_success": "已解除 {{provider}} 帳號綁定", + "netdisk_unbind_error": "解除 {{provider}} 帳號失敗", + "save_only_current_group": "儲存僅對目前群組有效", + "security": "安全性", + "blacklist_pages": "黑名單頁面", + "blacklist_placeholder": "禁止腳本貓在以下頁面執行腳本,多個頁面用換行符號分隔,例如:\nhttps://*.example.com", + "expression_format_error": "表達式格式錯誤", + "migration_confirm_message": "重試遷移儲存引擎會對現有資料造成修改,請確認,詳情請參閱:https://docs.scriptcat.org/docs/change/v0.17/", + "retry_migration": "重試遷移儲存引擎", + "sync_status": "同步狀態", + "interface_settings": "介面", + "select_interface_language": "選擇介面顯示語言", + "extension_icon_badge": "擴充功能圖示徽章", + "display_type": "顯示類型", + "extension_icon_badge_type": "擴充功能圖示上顯示的數字類型", + "background_color": "背景顏色", + "badge_background_color_desc": "徽章背景色", + "text_color": "文字顏色", + "badge_text_color_desc": "徽章文字色", + "badge_type_none": "不顯示", + "badge_type_run_count": "執行次數", + "badge_type_script_count": "腳本數量", + "script_menu": "腳本選單", + "display_right_click_menu": "顯示右鍵選單", + "display_right_click_menu_desc": "在瀏覽器右鍵選單中顯示腳本選單", + "expand_count": "展開數量", + "auto_collapse_when_exceeds": "超過此數量時自動摺疊", + "script_update_check_frequency": "腳本更新檢查頻率", + "script_auto_update_frequency": "腳本自動檢查更新的頻率", + "update_options": "更新選項", + "control_script_update_behavior": "控制腳本更新的行為", + "blacklist_pages_desc": "禁止腳本在指定頁面執行,支援萬用字元", + "development_tools": "開發工具", + "check_script_code_quality": "檢查腳本程式碼品質和錯誤", + "custom_eslint_rules_config": "自訂 ESLint 規則設定(JSON 格式)", + "script_run_env": { + "title": "運作環境", + "all": "所有標籤", + "normal-tabs": "普通標籤", + "incognito-tabs": "隱身標籤" + }, + "script_run_at": { + "title": "運行時機" + }, + "script_setting": { + "title": "腳本設定", + "default": "預設" + }, + "notification": { + "script_sync_delete": "腳本刪除同步", + "script_sync_delete_desc": "腳本 {{scriptName}} 已被刪除", + "subscribe_update": "訂閱 {{subscribeName}} 已更新", + "subscribe_update_desc": "新增腳本:{{newScripts}}\n刪除腳本:{{deletedScripts}}" + }, + "enable_background": { + "title": "啟用背景運行", + "description": "啟用後,即使關閉所有視窗,瀏覽器仍會在背景執行,並最小化到系統匣中,直到您手動退出瀏覽器。這能讓背景腳本繼續運作。", + "enable_failed": "啟用失敗", + "disable_failed": "禁用失敗", + "prompt_title": "是否開啟背景運行?", + "prompt_description": "此腳本是{{scriptType}},啟用背景運行功能可以讓腳本在瀏覽器關閉後繼續運行。", + "enable_now": "立即啟用", + "maybe_later": "暫不啟用", + "settings_hint": "你可以隨時在設定中修改此選項。" + }, + "favicon_service": "圖示服務", + "favicon_service_desc": "選擇取得網站圖示的服務", + "favicon_service_scriptcat": "ScriptCat", + "favicon_service_google": "Google", + "favicon_service_duckduckgo": "DuckDuckGo", + "favicon_service_icon-horse": "Icon Horse", + "favicon_service_local": "本地取得", + "cloud_sync_account_verification": "雲端同步帳號資訊驗證中...", + "cloud_sync_verification_failed": "雲端同步帳號資訊驗證失敗" +} diff --git a/src/locales/zh-TW/tools.json b/src/locales/zh-TW/tools.json new file mode 100644 index 000000000..dfe274e42 --- /dev/null +++ b/src/locales/zh-TW/tools.json @@ -0,0 +1,17 @@ +{ + "development_tool": "開發工具", + "vscode_url": "VSCode網址", + "auto_connect_vscode_service": "自動連接VSCode服務", + "connect": "連接", + "connection_success": "連接成功", + "connection_failed": "連接失敗", + "select_import_script": "請在新頁面中選擇要匯入的腳本", + "import_error": "匯入錯誤", + "pulling_data_from_cloud": "正在從雲端拉取資料", + "pull_failed": "拉取失敗", + "restore": "還原", + "local_backup": "本機備份", + "cloud_backup": "雲端備份", + "auto_backup": "自動備份", + "data_migration": "資料遷移" +} diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json deleted file mode 100644 index efc1ffe20..000000000 --- a/src/locales/zh-TW/translation.json +++ /dev/null @@ -1,770 +0,0 @@ -{ - "sentence-separator": "。", - "import_link": "連結匯入", - "import_link_failure": "連結匯入失敗", - "create_user_script": "新增一般腳本", - "create_background_script": "新增背景腳本", - "create_scheduled_script": "新增排程腳本", - "import_by_local": "本機匯入", - "import_local_failure": "本機匯入失敗", - "import_local_success": "本機匯入成功", - "create_script": "新增腳本", - "user_guide": "使用手冊", - "api_docs": "API文件", - "development_guide": "開發手冊", - "script_gallery": "腳本網站", - "community_forum": "社群討論區", - "external_links": "外部連結", - "system_follow": "跟隨系統", - "no_data": "尚無資料", - "installed_scripts": "已安裝腳本", - "subscribe": "訂閱", - "logs": "紀錄", - "tools": "工具", - "find": "尋找", - "replace": "取代", - "settings": "設定", - "hide_main_sidebar": "摺疊側邊欄", - "show_main_sidebar": "展開側邊欄", - "guide": "新手指南", - "helpcenter": "說明中心", - "general": "一般", - "language": "語言", - "help_translate": "協助翻譯", - "script_sync": "腳本同步", - "sync_delete": "同步刪除", - "sync_delete_desc": "啟用後,腳本刪除時會標記腳本為刪除,其他裝置檢測到刪除狀態後會將腳本刪除;關閉時,會直接刪除本機與雲端的腳本,如果有多台裝置可能出現腳本反覆同步的情況。", - "enable_script_sync_to": "啟用腳本同步至", - "save": "儲存", - "save_as": "另存新檔", - "file": "檔案", - "run": "執行", - "debug": "除錯", - "cloud_sync_account_verification": "雲端同步帳號資訊驗證中...", - "cloud_sync_verification_failed": "雲端同步帳號資訊驗證失敗", - "save_success": "儲存成功", - "reset_success": "重設成功", - "update": "更新", - "check_update": "檢查更新", - "script_subscription_check_interval": "腳本/訂閱檢查更新間隔", - "never": "從不", - "6_hours": "6小時", - "12_hours": "12小時", - "every_day": "每天", - "every_week": "每週", - "update_disabled_scripts": "更新已停用腳本", - "silent_update_non_critical_changes": "悄悄更新非重大變更", - "enable_eslint": "開啟 ESLint", - "eslint_rules": "ESLint規則", - "enter_eslint_rules": "請輸入 ESLint 規則,可以從 https://eslint.org/play/ 下載設定", - "language_change_tip": "語言切換成功", - "backup": "備份", - "local": "本機", - "export_file": "匯出檔案", - "import_file": "匯入檔案", - "cloud": "雲端", - "backup_to": "備份至", - "preparing_backup": "正在準備備份到雲端", - "backup_success": "備份成功", - "backup_failed": "備份失敗", - "no_backup_files": "沒有備份檔案", - "backup_list": "備份清單", - "open_backup_dir": "開啟備份資料夾", - "confirm_delete": "確定刪除", - "confirm_delete_backup_file": "確定刪除備份檔案", - "confirm_update": "確定更新", - "delete_success": "刪除成功", - "deleting": "刪除中", - "backup_strategy": "備份策略", - "under_construction": "開發中", - "development_tool": "開發工具", - "vscode_url": "VSCode網址", - "auto_connect_vscode_service": "自動連線 VSCode 服務", - "connect": "連線", - "connection_success": "連線成功", - "connection_failed": "連線失敗", - "select_import_script": "請在新頁面中選擇要匯入的腳本", - "import_error": "匯入錯誤", - "pulling_data_from_cloud": "正在從雲端下載資料", - "pull_failed": "下載失敗", - "restore": "還原", - "log_title": "執行紀錄", - "last_5_minutes": "最近5分鐘", - "last_15_minutes": "最近15分鐘", - "last_30_minutes": "最近30分鐘", - "last_1_hour": "最近1小時", - "last_3_hours": "最近3小時", - "last_6_hours": "最近6小時", - "last_12_hours": "最近12小時", - "last_24_hours": "最近24小時", - "last_7_days": "最近7天", - "query": "查詢", - "labels": "標籤", - "search_regex": "搜尋(支援正規表達式)", - "clean_schedule": "排程清理", - "days_ago_logs": "天前的紀錄", - "delete_completed": "刪除完成", - "delete_current_logs": "刪除目前紀錄", - "clear_completed": "清除完成", - "clear_logs": "清除紀錄", - "to": " 至 ", - "now": "現在", - "total_logs": "找到 {{length}} 筆紀錄", - "filtered_logs": "篩選後 {{length}} 筆紀錄", - "enter_filter_conditions": "請輸入篩選條件", - "permission": "權限", - "enter_subscribe_name": "請輸入訂閱名稱", - "subscribe_url": "訂閱網址", - "confirm_delete_subscription": "確定要刪除此訂閱嗎?相關的腳本也會被刪除", - "list": { - "confirm_delete": "確定要刪除嗎?請注意這個操作無法復原!", - "confirm_update": "確定要更新嗎?請注意此操作不可逆!" - }, - "enable": "開啟", - "script_list_enable_width": 80, - "script_list_last_updated_width": 120, - "script_list_apply_to_run_status_width": 140, - "subscribe_list_enable_width": 100, - "disable": "停用", - "name": "名稱", - "version": "版本", - "apply_to_run_status": "套用至 / 執行狀態", - "source": "來源", - "home": "首頁", - "sorting": "排序", - "last_updated": "最近更新", - "action": "操作", - "foreground_page_script_tooltip": "前景頁面腳本,會在指定的頁面上執行", - "background_script_tooltip": "背景腳本,啟用後將在背景執行", - "scheduled_script_tooltip": "排程腳本,下一次執行時間:", - "running": "執行中", - "completed": "執行完畢", - "source_subscribe_link": "訂閱連結", - "source_local_script": "本地腳本", - "source_script_link": "腳本連結", - "by_manual_creation": "在本地透過程式碼編輯方法建立", - "confirm_delete_script": "確定要刪除此腳本嗎?", - "confirm_delete_script_content": "確定要刪除腳本\"{{name}}\"嗎?此操作無法復原。", - "delete_failed": "刪除失敗", - "enter_script_name": "請輸入腳本名稱", - "update_not_supported": "此腳本不支援檢查更新", - "checking_for_updates": "正在檢查更新...", - "new_version_available": "有新版本", - "latest_version": "已經是最新版本", - "checked_for_all_selected": "所選腳本皆已檢查更新", - "update_check_failed": "檢查更新失敗", - "script_import_failed": "腳本匯入失敗", - "install_page_open_failed": "安裝頁面開啟失敗", - "stopping_script": "正在停止腳本", - "script_stopped": "腳本已停止", - "starting_script": "正在啟動腳本...", - "starting_updates": "正在批次更新...", - "script_started": "腳本已啟動", - "operation_failed": "操作失敗", - "batch_operations": "批次操作", - "export": "匯出", - "delete": "刪除", - "pin_to_top": "置頂", - "scripts_pinned_to_top": "已將所選腳本置頂", - "unknown_operation": "未知操作", - "confirm": "確定", - "close": "關閉", - "page_script": "頁面腳本", - "homepage": "腳本首頁", - "script_website": "腳本網站", - "script_source": "腳本原始碼", - "bug_feedback_script_support": "BUG 回報/腳本支援網站", - "config": "設定", - "key": "key", - "value": "value", - "add": "新增", - "type": "類型", - "edit_value": "編輯值", - "add_value": "新增值", - "update_success": "修改成功", - "add_success": "新增成功", - "script_storage": "腳本儲存", - "enter_key": "請輸入 key", - "key_placeholder": "key", - "value_placeholder": "當類型為 object 時,請輸入可被 JSON 解析的資料", - "clear": "清除", - "clear_success": "清除成功", - "confirm_clear": "您確定要清除此儲存空間嗎?", - "type_string": "string", - "type_number": "number", - "type_boolean": "boolean", - "type_object": "object", - "confirm_delete_resource": "您確定要刪除此資源嗎?下次開啟時將會重新載入此資源", - "confirm_clear_resource": "您確定要清除這些資源嗎?下次開啟時將會重新載入資源", - "script_resource": "腳本資源", - "permission_value": "權限值", - "allow": "是否允許", - "yes": "是", - "no": "否", - "confirm_delete_permission": "您確定要刪除此權限嗎?", - "basic_info": "基本資訊", - "update_url": "更新URL", - "permission_management": "權限管理", - "add_permission": "新增權限", - "permission_cors": "跨網域(CORS)", - "permission_cookie": "管理 Cookie", - "match": "符合", - "user_setting": "使用者設定", - "confirm_delete_exclude": "確定要刪除此排除項目嗎?", - "after_deleting_match_item": "腳本設定的符合項目刪除後會自動新增至符合項目中", - "confirm_delete_match": "確定要刪除此符合項目嗎?", - "after_deleting_exclude_item": "腳本設定的符合項目刪除後會自動新增至排除項目中", - "add_match": "新增符合項目", - "add_exclude": "新增排除項目", - "website_match": "網站符合(@match)", - "reset": "重設", - "website_exclude": "網站排除(@exclude)", - "confirm_reset": "確定要重設嗎?", - "script_total_runs": "此腳本總共執行了{{runNum}}次,在iframe上執行了{{runNumByIframe}}次", - "script_total_runs_single": "此腳本執行了{{runNum}}次", - "script_disabled": "此腳本未開啟", - "run_once": "執行一次", - "stop": "停止", - "edit": "編輯", - "undo": "復原", - "redo": "重做", - "cut": "剪下", - "copy": "複製", - "paste": "貼上", - "format": "格式化", - "exclude_on": "恢復 $0 的執行", - "exclude_off": "排除 $0 的執行", - "user_config": "使用者設定", - "gm_api": "GM API", - "storage_api": "儲存 API", - "use_file_system": "使用的檔案系統", - "open_directory": "開啟資料夾", - "account_validation_failed": "帳號資訊驗證失敗", - "not_set": "未設定", - "in_use": "使用中", - "storage_error": "儲存錯誤", - "upload_to_cloud": "上傳至雲端", - "save_failed": "儲存失敗", - "exporting": "匯出中...", - "upload_to": "上傳至", - "value_export_expression": "值匯出表達式", - "overwrite_original_value_on_import": "匯入時覆蓋原值", - "cookie_export_expression": "Cookie 匯出表達式", - "overwrite_original_cookie_on_import": "匯入時覆蓋原值", - "restore_default_values": "還原預設值", - "get_confirm_error": "取得確認資訊失敗", - "confirm_error": "確認失敗", - "ignore": "忽略", - "allow_once": "允許一次", - "temporary_allow": "暫時允許此{{permissionContent}}", - "temporary_allow_all": "暫時允許全部{{permissionContent}}", - "permanent_allow": "永久允許此{{permissionContent}}", - "permanent_allow_all": "永久允許全部{{permissionContent}}", - "deny_once": "拒絕一次", - "temporary_deny": "暫時拒絕此{{permissionContent}}", - "temporary_deny_all": "暫時拒絕全部{{permissionContent}}", - "permanent_deny": "永久拒絕此{{permissionContent}}", - "permanent_deny_all": "永久拒絕全部{{permissionContent}}", - "data_import": "資料匯入", - "import": "匯入", - "select_scripts_to_import": "請選擇您要匯入的腳本", - "select_all": "全選", - "script_import_progress": "腳本匯入進度", - "select_subscribes_to_import": "請選擇您要匯入的訂閱", - "subscribe_import_progress": "訂閱匯入進度", - "author": "作者", - "description": "描述", - "operation": "操作", - "error": "錯誤", - "unknown": "未知", - "add_new": "新增", - "no_operation": "不做操作", - "enable_script": "開啟腳本", - "local_creation": "本地建立", - "import_success": "匯入成功", - "install_script": "安裝", - "update_script": "更新", - "install_subscribe": "安裝訂閱", - "update_subscribe": "更新訂閱", - "update_script_no_close": "更新,不關閉視窗", - "install_script_no_close": "安裝,不關閉視窗", - "update_script_no_more_update": "更新,但不再檢查更新", - "close_update_script_no_more_update": "關閉,且不再檢查更新", - "install_script_no_more_update": "安裝,但不再檢查更新", - "invalid_link": "錯誤的連結", - "subscribe_install_label": "此訂閱將會安裝以下的腳本", - "script_runs_in": "腳本將在以下的網站中執行", - "script_has_full_access_to": "腳本將獲得以下網址的完整存取權限", - "script_requires": "腳本引用了以下外部資源", - "cookie_warning": "請注意,此腳本會要求 Cookie 的操作權限,這是一項危險的權限,請確認腳本的安全性。", - "cron_oncetype": { - "minute": "{{next}} (每分鐘執行一次)", - "hour": "{{next}} (每小時執行一次)", - "day": "{{next}} (每天執行一次)", - "month": "{{next}} (每月執行一次)", - "week": "{{next}} (每週執行一次)" - }, - "cron_invalid_expr": "錯誤的排程表達式", - "scheduled_script_description_title": "這是一個排程腳本,啟用後將在特定時間自動執行,並可在面板中手動控制。", - "scheduled_script_description_description_expr": "排程任務表達式:", - "scheduled_script_description_description_next": "最近一次執行時間:", - "background_script_description": "這是一個背景腳本,啟用後將在瀏覽器開啟時自動執行一次,並可在面板中手動控制。", - "install_success": "安裝成功", - "install": { - "update_success": "更新成功" - }, - "install_failed": "安裝失敗", - "subscribe_success": "訂閱成功", - "subscribe_failed": "訂閱失敗", - "current_version": "目前版本", - "update_version": "更新版本", - "updatepage": { - "main_header": "檢查更新", - "header_site_specific": "此網站有可用更新($0)", - "header_site_all": "有可用更新", - "header_ignored": "有可用更新但已忽略", - "update_all": "全部更新", - "ignore_all": "全部忽略", - "update": "更新", - "ignore": "忽略", - "old_version_": "舊版本:", - "new_version_": "新版本:", - "enabled": "已啟用", - "tooltip_enabled": "此腳本已啟用。", - "disabled": "已停用", - "tooltip_disabled": "此腳本已停用。", - "similarity_": "相似度:", - "codechange_major": "重大變更", - "codechange_noticeable": "明顯變更", - "codechange_tiny": "輕微變更", - "new_connects_": "新 @connect:", - "tag_new_connect": "已新增 @connect", - "status_last_check": "上次檢查:$0", - "status_checking_updates": "正在檢查更新...", - "status_no_update": "沒有需要更新的內容", - "status_n_update": "有 $0 個需要更新的內容", - "status_n_ignored": "有 $0 個已忽略的更新", - "status_autoclose": "$0 秒後自動關閉", - "header_other_update": "其他可用更新" - }, - "downloading_status_text": "正在下載。已接收 {{bytes}}。", - "install_page_please_wait": "請稍等", - "install_page_loading": "安裝頁面載入中", - "install_page_load_failed": "安裝頁面載入失敗", - "invalid_page": "無效頁面", - "background_script_tag": "這是一個背景腳本", - "scheduled_script_tag": "這是一個排程腳本", - "background_script": "背景腳本", - "scheduled_script": "排程腳本", - "install_from_legitimate_sources_warning": "請從合法來源安裝腳本!未知的腳本可能會侵犯您的隱私或進行惡意操作!", - "antifeature_referral_link_title": "推薦連結", - "antifeature_referral_link_description": "此腳本會修改或重新導向至作者的分潤連結", - "antifeature_ads_title": "附帶廣告", - "antifeature_ads_description": "此腳本會在您存取的頁面上插入廣告", - "antifeature_payment_title": "付費腳本", - "antifeature_payment_description": "此腳本需要您付費才能正常使用", - "antifeature_miner_title": "挖礦", - "antifeature_miner_description": "此腳本存在挖礦行為", - "antifeature_membership_title": "會員功能", - "antifeature_membership_description": "此腳本需要註冊會員才能正常使用", - "antifeature_tracking_title": "資訊追蹤", - "antifeature_tracking_description": "此腳本會追蹤您的使用者資訊", - "script_info_load_failed": "腳本資訊載入失敗!", - "script_status_tooltip": "可以控制腳本開啟狀態,一般油猴腳本預設開啟,背景腳本、排程腳本預設關閉", - "subscribe_source_tooltip": "這是一個訂閱來源,當你開啟訂閱後會自動安裝訂閱的腳本", - "get_script": "取得腳本", - "report_issue": "BUG / 問題回報", - "project_docs": "專案文件", - "community": "交流社群", - "popup": { - "new_version_available": "有新版本可用" - }, - "current_page_scripts": "目前頁面執行腳本", - "enabled_background_scripts": "開啟和執行的背景腳本", - "script_accessing_cross_origin_resource": "腳本正在嘗試存取跨網域資源", - "confirm_operation_description": "請您確認是否允許腳本進行此操作,腳本也可增加@connect標籤跳過此選項", - "extension_site_access_title": "ScriptCat 需要網站存取權限", - "extension_site_access_description": "請授予瀏覽器對此來源的網站存取權限,讓 ScriptCat 可以完成本次請求。這會修改擴充功能的網站存取設定。", - "extension_site_access_content": "網站", - "domain": "網域", - "script_name": "腳本名稱", - "request_domain": "請求網域", - "request_url": "請求網址", - "access_cookie_content": "腳本正在嘗試存取網站 Cookie 內容", - "confirm_script_operation": "請您確認是否允許腳本進行此操作,Cookie 是一項重要的使用者資料,請務必只給信任的腳本權限。", - "cookie_domain": "Cookie網域", - "script_operation_title": "腳本正在嘗試存取儲存空間", - "script_operation_description": "請您確認是否允許腳本進行此操作,允許後將允許腳本操作您設定的儲存空間,腳本會在儲存空間下建立一個 app/${dir} 的資料夾進行使用", - "script_permission_content": "腳本", - "sync_system_connect_failed": "同步系統連線失敗", - "sync_system_closed": "同步已停用", - "sync_system_closed_description": "同步功能已停用,請重新設定", - "auth_type": "驗證類型", - "url": "網址", - "username": "使用者名稱", - "password": "密碼", - "access_token_bearer": "存取權杖(Bearer)", - "s3_bucket_name": "儲存貯體名稱", - "s3_region": "區域", - "s3_access_key_id": "存取金鑰 ID", - "s3_secret_access_key": "私密存取金鑰", - "s3_custom_endpoint": "自訂端點(選用)", - "skip": "跳過", - "next": "下一步", - "next_with_progress": "下一步(第 {step} 步 / 共 {steps} 步)", - "back": "上一步", - "last": "完成", - "start_guide_title": "歡迎使用腳本貓擴充功能", - "start_guide_content": "接下來我們將為您介紹腳本貓的基本使用方法", - "guide_installed_scripts": "您所安裝的腳本將會在這裡顯示", - "guide_script_list_title": "腳本中心", - "guide_script_list_content": "可以從腳本中心中安裝腳本,腳本貓除了支援使用者腳本外,還支援背景腳本", - "guide_script_list_enable_title": "腳本開啟", - "guide_script_list_enable_content": "腳本需要開啟才能使用,頁面腳本安裝預設開啟,背景腳本安裝預設關閉", - "guide_script_list_apply_to_run_status_title": "套用至 / 執行狀態", - "guide_script_list_apply_to_run_status_content": "腳本執行狀態顯示,將滑鼠移到標籤上方可以檢視腳本類型", - "guide_script_list_sort_title": "排序", - "guide_script_list_sort_content": "你可以拖曳腳本的標籤進行排序", - "guide_script_list_update_title": "最後更新", - "guide_script_list_update_content": "點擊最後更新列標籤可以對腳本進行一次檢查更新", - "guide_script_list_action_title": "操作", - "guide_script_list_action_content": "操作列可以進入腳本編輯、控制腳本的執行與停止(背景腳本)、設定UserConfig,操作右側按鈕可開啟進階篩選與切換檢視模式", - "guide_tools_title": "常用工具", - "guide_tools_content": "工具中提供了備份和開發的工具", - "guide_tools_backup_title": "備份", - "guide_tools_backup_content": "備份可以儲存腳本,避免遺失。可以匯出腳本檔案到本機,也可以載入本機的腳本檔案。也可以備份到雲端,更加方便。", - "guide_setting_title": "設定", - "guide_setting_content": "設定中主要包含了語言、腳本同步、更新頻率等常用設定項目", - "guide_setting_sync_title": "更新與同步", - "guide_setting_sync_content": "腳本同步功能可以方便地將本裝置的腳本內容同步至雲端,如果有多台裝置請勾選同步刪除選項,當本裝置腳本刪除時,會從雲端刪除對應的腳本,也會將其他裝置的腳本刪除。", - "auto": "自動", - "hide": "隱藏", - "custom": "自訂", - "resize_column_width": "調整欄位寬度", - "collapse": "摺疊", - "expand": "展開", - "menu_expand_num_before": "選單項目超過", - "menu_expand_num_after": "個時,自動隱藏", - "script_name_cannot_be_set_to_empty": "腳本名稱不可設定為空", - "edit_conflict": "編輯衝突", - "confirm_override_when_edit_conflict": "此腳本已在其他實例中被修改。替換將覆蓋這些變更。是否要保留此版本?", - "save_abort_when_edit_conflict": "此腳本已在其他實例中被修改,已中止儲存。", - "scriptname_conflict": "腳本名稱衝突", - "confirm_save_when_scriptname_conflict": "此腳本名稱已被其他腳本使用,是否仍要儲存?", - "save_abort_when_scriptname_conflict": "此腳本名稱已被其他腳本使用,已取消儲存。", - "eslint_config_format_error": "ESLint設定格式錯誤", - "export_success": "匯出成功", - "get_backup_dir_url_failed": "取得備份資料夾網址失敗", - "get_backup_files_failed": "取得備份檔案失敗", - "request_permission": "要求權限", - "develop_mode_guide": "目前尚未啟用「開發者模式」,腳本無法正常執行。👉點此了解啟用方式", - "allow_user_script_guide": "目前尚未啟用「允許使用者指令碼」,腳本無法正常執行。👉點此了解啟用方式", - "lower_version_browser_guide": "您的瀏覽器版本過舊,腳本無法正常執行。👉點擊了解更多", - "click_to_reload": "👉點擊重新載入", - "page_in_blacklist": "目前頁面在黑名單中,無法使用腳本", - "baidu_netdisk": "百度網盤", - "netdisk_unbind": "解除綁定 {{provider}}", - "netdisk_unbind_confirm": "確定解除 {{provider}} 帳號綁定嗎?", - "netdisk_unbind_success": "已解除 {{provider}} 帳號綁定", - "netdisk_unbind_error": "解除 {{provider}} 帳號失敗", - "save_only_current_group": "儲存僅對目前群組有效", - "script_import_result": "腳本匯入結果", - "failure_info": "失敗資訊", - "security": "安全性", - "blacklist_pages": "黑名單頁面", - "blacklist_placeholder": "禁止腳本貓在以下頁面執行腳本,多個頁面用換行符號分隔,例如:\nhttps://*.example.com", - "expression_format_error": "表達式格式錯誤", - "migration_confirm_message": "重試遷移儲存引擎會對現有資料造成修改,請確認,詳情請參閱:https://docs.scriptcat.org/docs/change/v0.17/", - "retry_migration": "重試遷移儲存引擎", - "script_modified_leave_confirm": "目前內容尚未儲存,離開後變更將會遺失,確定要離開嗎?", - "create_success_note": "新增成功,請注意背景腳本不會預設開啟", - "save_as_failed": "另存新檔失敗", - "save_as_success": "另存新檔成功", - "only_background_scheduled_can_run": "只有背景腳本/排程腳本才能執行", - "preparing_script_resources": "正在準備腳本資源...", - "build_success_message": "建置成功,可以在擴充功能頁開啟開發者工具在主控台中檢視輸出", - "script_storage_tooltip": "可以管理腳本的儲存資料(GM_value)", - "script_resource_tooltip": "管理@resource,@require下載的資源", - "script_setting_tooltip": "對腳本進行一些自訂設定", - "script_modified_close_confirm": "腳本已修改,關閉後會遺失修改,是否繼續?", - "close_current_tab": "關閉目前分頁", - "close_other_tabs": "關閉其他分頁", - "close_left_tabs": "關閉左側分頁", - "close_right_tabs": "關閉右側分頁", - "import_script_placeholder": "支援輸入.user.js結尾的腳本絕對連結 或 腳本貓安裝頁連結\n可多行填寫,每行一條\n範例:\nhttps://example.com/test.user.js \nhttps://scriptcat.org/zh-TW/script-show-page/1234", - "invalid_script_code": "錯誤的腳本程式碼", - "build_failed": "建置失敗", - "drag_script_here_to_upload": "拖曳腳本到此處上傳", - "sync_status": "同步狀態", - "search_scripts": "搜尋腳本", - "ext_update_notification": "腳本貓擴充功能已更新", - "ext_update_notification_desc": "目前版本:{{version}},詳情請檢視更新紀錄", - "watch_file_description": "監聽檔案變動,自動更新腳本,使用時請確保腳本檔案路徑不變且不能關閉頁面", - "watch_file": "監聽檔案", - "stop_watch_file": "停止監聽", - "script_menu_display": "腳本註冊的選單", - "badge_type_none": "不顯示", - "badge_type_run_count": "執行次數", - "badge_type_script_count": "腳本數量", - "interface_settings": "介面", - "select_interface_language": "選擇介面顯示語言", - "extension_icon_badge": "擴充功能圖示徽章", - "display_type": "顯示類型", - "extension_icon_badge_type": "擴充功能圖示上顯示的數字類型", - "background_color": "背景顏色", - "badge_background_color_desc": "徽章背景色", - "text_color": "文字顏色", - "badge_text_color_desc": "徽章文字色", - "script_menu": "腳本選單", - "display_right_click_menu": "顯示右鍵選單", - "display_right_click_menu_desc": "在瀏覽器右鍵選單中顯示腳本選單", - "expand_count": "展開數量", - "auto_collapse_when_exceeds": "超過此數量時自動摺疊", - "script_update_check_frequency": "腳本更新檢查頻率", - "script_auto_update_frequency": "腳本自動檢查更新的頻率", - "update_options": "更新選項", - "control_script_update_behavior": "控制腳本更新的行為", - "blacklist_pages_desc": "禁止腳本在指定頁面執行,支援萬用字元", - "development_tools": "開發工具", - "check_script_code_quality": "檢查腳本程式碼品質和錯誤", - "custom_eslint_rules_config": "自訂 ESLint 規則設定(JSON 格式)", - "light": "淺色模式", - "dark": "暗色模式", - "individual_edit": "單獨編輯", - "batch_edit": "批次編輯", - "script_code": "腳本程式碼", - "enter_search_value": "請輸入 {{search}} 進行搜尋", - "script_run_env": { - "title": "運作環境", - "all": "所有分頁", - "normal-tabs": "一般分頁", - "incognito-tabs": "無痕分頁" - }, - "script_run_at": { - "title": "執行時機" - }, - "script_setting": { - "title": "腳本設定", - "default": "預設" - }, - "editor_config": "編輯器設定", - "editor_config_description": "你可以參考jsconfig.js中的compilerOptions進行設定", - "editor_type_definition": "編輯器類型定義", - "editor_type_definition_description": "你可以自訂自己的類型定義,腳本編輯器會自動載入這些類型定義", - "eslint_rules_reset": "ESLint規則已重置", - "eslint_rules_saved": "ESLint規則已儲存", - "editor_config_reset": "編輯器設定已重置", - "editor_config_saved": "編輯器設定已儲存", - "editor_config_format_error": "編輯器設定格式錯誤", - "editor_type_definition_reset": "編輯器類型定義已重設", - "editor_type_definition_saved": "編輯器類型定義已儲存", - "script_list": { - "sidebar": { - "stopped": "停止", - "all": "全部", - "normal_script": "一般腳本", - "status": "狀態" - } - }, - "tags": "標籤", - "install_source": "安裝來源", - "input_tags_placeholder": "輸入標籤,按 Enter 鍵確認", - "switch_to_card_mode": "切換到卡片模式", - "switch_to_table_mode": "切換到表格模式", - "open_sidebar": "開啟側邊欄", - "close_sidebar": "關閉側邊欄", - "no_message_content": "無訊息內容", - "error_metadata_invalid": "MetaData 資訊錯誤", - "error_script_name_required": "腳本名稱不可為空", - "error_script_version_required": "腳本 @version 版本不可為空", - "error_script_namespace_required": "腳本 @namespace 命名空間不可為空", - "error_cron_invalid": "錯誤的排程表示式,請檢查:{{expr}}", - "error_script_type_mismatch": "腳本類型不相容,前景腳本與背景腳本不可互轉", - "error_old_script_code_missing": "舊的腳本程式碼不存在", - "error_subscribe_name_required": "訂閱名稱不可為空", - "error_grant_conflict": "@grant 同時宣告了 none 與 GM API", - "error_metadata_line_duplicated": "Metadata 裡有重複的宣告。", - "notification": { - "script_sync_delete": "同步腳本刪除", - "script_sync_delete_desc": "腳本 {{scriptName}} 已被刪除", - "subscribe_update": "訂閱 {{subscribeName}} 已更新", - "subscribe_update_desc": "新增腳本:{{newScripts}}\n刪除腳本:{{deletedScripts}}" - }, - "loading": "載入中...", - "runtime": "執行時", - "enable_background": { - "title": "啟用背景運作", - "description": "啟用後,即使關閉所有視窗,瀏覽器仍會在背景執行,並最小化到系統匣中,直到您手動關閉瀏覽器。這能讓背景腳本繼續運作。", - "enable_failed": "啟用失敗", - "disable_failed": "停用失敗", - "prompt_title": "是否開啟背景運作?", - "prompt_description": "此腳本是{{scriptType}},啟用背景運作功能可以讓腳本在瀏覽器關閉後繼續執行。", - "enable_now": "立即啟用", - "maybe_later": "暫不啟用", - "settings_hint": "你可以隨時在設定中修改此選項。" - }, - "favicon_service": "圖示服務", - "favicon_service_desc": "選擇取得網站圖示的服務", - "favicon_service_scriptcat": "ScriptCat", - "favicon_service_google": "Google", - "favicon_service_duckduckgo": "DuckDuckGo", - "favicon_service_icon-horse": "Icon Horse", - "favicon_service_local": "本地取得", - "editor": { - "show_script_list": "顯示腳本清單", - "hide_script_list": "隱藏腳本清單" - }, - "agent": "AI Agent", - "agent_chat": "會話", - "agent_provider": "模型服務", - "agent_mcp": "MCP", - "agent_skills": "Skills", - "agent_provider_title": "模型服務", - "agent_provider_select": "AI 服務提供商", - "agent_provider_api_base_url": "API 地址", - "agent_provider_api_key": "API 密鑰", - "agent_provider_model": "預設模型", - "agent_provider_test_connection": "測試連接", - "agent_provider_test_success": "連接成功", - "agent_provider_test_failed": "連接失敗", - "agent_model_fetch": "取得模型", - "agent_model_fetch_failed": "取得模型列表失敗", - "agent_model_name": "名稱", - "agent_model_add": "新增模型", - "agent_model_edit": "編輯", - "agent_model_copy": "複製", - "agent_model_delete": "刪除", - "agent_model_set_default": "設為預設", - "agent_model_default_label": "預設", - "agent_model_delete_confirm": "確定要刪除此模型配置嗎?", - "agent_model_max_tokens": "最大輸出 Token 數", - "agent_model_no_models": "暫無模型配置", - "agent_model_vision_support": "支援圖片輸入", - "agent_model_image_output": "支援圖片輸出", - "agent_model_capabilities": "模型能力", - "agent_model_supports_vision": "視覺輸入", - "agent_model_supports_image_output": "圖片輸出", - "agent_coming_soon": "開發中...", - "agent_skills_title": "Skills 管理", - "agent_skills_add": "新增 Skill", - "agent_skills_empty": "尚無已安裝的 Skill", - "agent_skills_tools": "工具", - "agent_skills_references": "參考資料", - "agent_skills_detail": "Skill 詳情", - "agent_skills_edit_prompt": "提示詞", - "agent_skills_install": "安裝 Skill", - "agent_skills_install_url": "從 URL 匯入", - "agent_skills_install_paste": "貼上 SKILL.md", - "agent_skills_uninstall": "解除安裝", - "agent_skills_uninstall_confirm": "確定要解除安裝 Skill「{{name}}」?", - "agent_skills_save_success": "儲存成功", - "agent_skills_install_success": "安裝成功", - "agent_skills_fetch_failed": "取得失敗", - "agent_skills_add_script": "新增腳本", - "agent_skills_add_reference": "新增參考資料", - "agent_skills_install_zip": "上傳 ZIP", - "agent_skills_install_zip_hint": "點擊選擇 .zip 檔案", - "agent_skills_prompt": "提示詞", - "agent_skills_installed_at": "安裝時間", - "agent_skills_refresh": "重新整理", - "agent_skills_refresh_success": "重新整理成功", - "agent_skills_tool_code": "工具程式碼", - "agent_skills_click_to_view_code": "點擊工具名稱查看程式碼", - "agent_skills_config": "設定", - "agent_skills_config_saved": "設定已儲存", - "agent_skills_check_updates": "檢查更新", - "agent_skills_no_updates": "所有 Skill 已是最新版本", - "agent_skills_updates_available": "個 Skill 有更新", - "agent_skills_update": "更新", - "agent_skills_update_success": "更新成功", - "agent_skills_url_placeholder": "輸入 SKILL.cat.md URL", - "agent_chat_new": "新建會話", - "agent_chat_delete": "刪除會話", - "agent_chat_delete_confirm": "確定要刪除此會話嗎?", - "agent_chat_no_conversations": "暫無會話", - "agent_chat_input_placeholder": "輸入訊息...", - "agent_chat_send": "發送", - "agent_chat_stop": "停止", - "agent_chat_thinking": "思考過程", - "agent_chat_tool_call": "工具調用", - "agent_chat_error": "發生錯誤", - "agent_chat_no_model": "未配置模型,請先在模型服務中新增", - "agent_chat_model_select": "選擇模型", - "agent_chat_rename": "重新命名", - "agent_chat_copy": "複製", - "agent_chat_copy_success": "已複製", - "agent_chat_regenerate": "重新生成", - "agent_chat_streaming": "生成中...", - "agent_chat_newline": "換行", - "agent_chat_welcome_hint": "有關腳本的任何問題,儘管問我", - "agent_chat_welcome_start": "建立一個對話開始吧", - "agent_chat_tokens": "令牌", - "agent_chat_first_token": "TTFT", - "agent_chat_tools_count": "{{count}} 次工具調用", - "agent_chat_tools_enabled": "工具已啟用", - "agent_chat_tools_disabled": "工具已停用", - "agent_chat_tools_enabled_tip": "工具已啟用 — 點擊停用", - "agent_chat_tools_disabled_tip": "工具已停用 — 點擊啟用", - "agent_chat_background_enabled_tip": "背景運行已開啟 — 關閉頁面後對話將繼續執行,點擊關閉", - "agent_chat_background_disabled_tip": "背景運行已關閉 — 關閉頁面後對話將停止,點擊開啟", - "agent_chat_delete_round": "刪除", - "agent_chat_copy_message": "複製", - "agent_chat_edit_message": "編輯", - "agent_chat_save_and_send": "儲存並傳送", - "agent_chat_cancel_edit": "取消", - "agent_chat_message_queued": "排隊中", - "agent_chat_cancel_message": "取消傳送", - "agent_permission_title": "腳本請求使用 Agent 對話", - "agent_permission_describe": "此腳本請求使用 Agent 對話功能,將消耗 API Token。請僅對可信腳本授權。", - "agent_permission_content": "Agent 對話", - "agent_opfs": "OPFS", - "agent_opfs_title": "OPFS 檔案瀏覽器", - "agent_opfs_empty": "空目錄", - "agent_opfs_name": "名稱", - "agent_opfs_size": "大小", - "agent_opfs_type": "類型", - "agent_opfs_modified": "最後修改", - "agent_opfs_delete_confirm": "確定要刪除嗎?", - "agent_opfs_delete_success": "已刪除", - "agent_opfs_file": "檔案", - "agent_opfs_directory": "目錄", - "agent_opfs_preview": "預覽", - "agent_opfs_root": "根目錄", - "agent_dom_permission_title": "腳本請求 DOM 操作權限", - "agent_dom_permission_describe": "此腳本請求讀取和操作網頁 DOM 的能力(點擊、填寫表單、導航、截圖等)。請僅對可信腳本授權。", - "agent_dom_permission_content": "Agent DOM 操作", - "agent_mcp_title": "MCP 伺服器", - "agent_mcp_add_server": "新增伺服器", - "agent_mcp_no_servers": "未配置 MCP 伺服器", - "agent_mcp_test_connection": "測試", - "agent_mcp_name_url_required": "名稱和 URL 不能為空", - "agent_mcp_optional": "可選", - "agent_mcp_custom_headers": "自訂請求標頭", - "agent_mcp_enabled": "啟用", - "agent_mcp_detail": "詳情", - "agent_mcp_tools": "工具", - "agent_mcp_resources": "資源", - "agent_mcp_prompts": "提示詞", - "agent_mcp_no_tools": "暫無可用工具", - "agent_mcp_no_resources": "暫無可用資源", - "agent_mcp_no_prompts": "暫無可用提示詞", - "agent_mcp_loading": "載入中...", - "agent_mcp_parameters": "參數", - "agent_settings": "設定", - "agent_settings_title": "Agent 設定", - "agent_model_settings": "模型設定", - "agent_summary_model": "摘要模型", - "agent_summary_model_desc": "用於網頁摘要等場景,未設定時使用預設模型", - "agent_summary_model_placeholder": "使用預設模型", - "agent_search_settings": "搜尋設定", - "agent_search_engine": "搜尋引擎", - "agent_search_engine_baidu": "百度", - "agent_search_google_api_key": "Google API Key", - "agent_search_google_cse_id": "自訂搜尋引擎 ID", - "agent_settings_saved": "設定已儲存", - "agent_settings_save_failed": "儲存設定失敗", - "agent_search_engine_tip_bing": "預設搜尋引擎,全球覆蓋廣泛,無需額外設定。", - "agent_search_engine_tip_duckduckgo": "注重隱私保護的搜尋引擎,無需 API Key。", - "agent_search_engine_tip_baidu": "針對中文內容最佳化,中文搜尋效果更好。", - "agent_search_engine_tip_google": "搜尋品質更高,需要設定 Google API Key 和自訂搜尋引擎 ID。" -} diff --git a/src/manifest.json b/src/manifest.json index 0e02f4510..895349175 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "__MSG_scriptcat__", - "version": "1.4.0.1500", + "version": "1.5.0.1100", "author": "CodFrm", "description": "__MSG_scriptcat_description__", "options_ui": { @@ -66,4 +66,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/src/pages/template.html b/src/pages/batchupdate.html similarity index 80% rename from src/pages/template.html rename to src/pages/batchupdate.html index a79d983dd..53a01f254 100644 --- a/src/pages/template.html +++ b/src/pages/batchupdate.html @@ -1,21 +1,19 @@ - + - <%= htmlRspackPlugin.options.title %> + ScriptCat -
diff --git a/src/pages/batchupdate/App.tsx b/src/pages/batchupdate/App.tsx index 3d02fc5da..67ff4f85a 100644 --- a/src/pages/batchupdate/App.tsx +++ b/src/pages/batchupdate/App.tsx @@ -1,539 +1,10 @@ -import { useEffect, useState } from "react"; -import { - requestBatchUpdateListAction, - requestCheckScriptUpdate, - requestOpenUpdatePageByUUID, - scriptClient, -} from "../store/features/script"; - -import type { CollapseProps } from "@arco-design/web-react"; -import { Collapse, Card, Link, Divider, Grid, Tooltip, Typography, Tag, Space } from "@arco-design/web-react"; -import { useTranslation } from "react-i18next"; -import { - BatchUpdateListActionCode, - UpdateStatusCode, - type TBatchUpdateRecord, - type TBatchUpdateRecordObject, -} from "@App/app/service/service_worker/types"; -import { dayFormat } from "@App/pkg/utils/day_format"; -import { IconSync } from "@arco-design/web-react/icon"; -import { SCRIPT_STATUS_ENABLE } from "@App/app/repo/scripts"; -import { subscribeMessage } from "@App/pages/store/global"; - -const CollapseItem = Collapse.Item; -const { GridItem } = Grid; - -const { Text } = Typography; - -// pageExecute is to store subscribe function(s) globally -const pageExecute: Record void> = {}; - -function App() { - const AUTO_CLOSE_PAGE = 8; // after 8s, auto close - const getUrlParam = (key: string): string => { - return (location.search?.includes(`${key}=`) ? new URLSearchParams(location.search).get(`${key}`) : "") || ""; - }; - // unit: milisecond - const initialTimeForAutoClosePage = parseInt(getUrlParam("autoclose")) || AUTO_CLOSE_PAGE; - const paramSite = getUrlParam("site"); - const { t } = useTranslation(); - - const [mInitial, setInitial] = useState(false); - - const [mRecords, setRecords] = useState<{ - site: TBatchUpdateRecord[]; - other: TBatchUpdateRecord[]; - ignored: TBatchUpdateRecord[]; - } | null>(null); - - const [mStatusText, setStatusText] = useState(""); - - // unit: second - const [mTimeClose, setTimeClose] = useState(initialTimeForAutoClosePage); - useEffect(() => { - if (mTimeClose < 0) return; - if (mTimeClose === 0) { - window.close(); // 会切回到原网页 - return; - } - setTimeout(() => { - // 如 tab 在背景, 不倒数,等用户切回来 - requestAnimationFrame(() => { - setTimeClose((t) => (t >= 1 ? t - 1 : t)); - }); - }, 1000); - }, [mTimeClose]); - - const getBatchUpdateRecord = async (): Promise => { - let resultText = ""; - let r; - let i = 0; - while (true) { - r = await scriptClient.getBatchUpdateRecordLite(i++); - if (!r) break; - const chunk = r.chunk; - if (typeof chunk !== "string") break; - resultText += chunk; - if (r.ended) break; - } - return resultText ? JSON.parse(resultText) : null; - }; - - const updateRecord = () => { - getBatchUpdateRecord().then((batchUpdateRecordObjectLite) => { - const list = batchUpdateRecordObjectLite?.list || []; - const site = [] as TBatchUpdateRecord[]; - const other = [] as TBatchUpdateRecord[]; - const ignored = [] as TBatchUpdateRecord[]; - for (const entry of list) { - if (!entry.checkUpdate) { - site.push(entry); - continue; - } - const newVersion = entry.newMeta?.version?.[0]; - const isIgnored = typeof newVersion === "string" && entry.script.ignoreVersion === newVersion; - const mEntry = { - ...entry, - }; - - if (isIgnored) { - ignored.push(mEntry); - } else { - if (!paramSite || mEntry.sites?.includes(paramSite)) { - site.push(mEntry); - } else { - other.push(mEntry); - } - } - } - setRecords({ site, other, ignored }); - }); - }; - - const onScriptUpdateCheck = (data: any) => { - if ( - mRecords === null && - ((data.status ?? 0) & UpdateStatusCode.CHECKING_UPDATE) === 0 && - ((data.status ?? 0) & UpdateStatusCode.CHECKED_BEFORE) === UpdateStatusCode.CHECKED_BEFORE - ) { - setStatusText( - t("updatepage.status_last_check").replace("$0", data.checktime ? dayFormat(new Date(data.checktime)) : "") - ); - updateRecord(); - setCheckUpdateSpin(false); - } else if (((data.status ?? 0) & UpdateStatusCode.CHECKING_UPDATE) === UpdateStatusCode.CHECKING_UPDATE) { - setStatusText(t("updatepage.status_checking_updates")); - setRecords(null); - setCheckUpdateSpin(true); - } else if (mRecords !== null && data.refreshRecord === true) { - updateRecord(); - } - }; - - // 每次render会重新定义 pageExecute 的 onScriptUpdateCheck - pageExecute.onScriptUpdateCheck = onScriptUpdateCheck; - - // 只在第一次render执行 - const doInitial = () => { - // faster than useEffect - setInitial(true); - subscribeMessage("onScriptUpdateCheck", (msg) => pageExecute.onScriptUpdateCheck!(msg)); - scriptClient.fetchCheckUpdateStatus(); - scriptClient.sendUpdatePageOpened(); - }; - - mInitial === false && doInitial(); - - // const { t } = useTranslation(); - - const onUpdateClick = async (uuid: string) => { - if (!uuid) return; - setTimeClose(-1); // 用户操作,不再倒数,等用户按完用户自行关 - setIsDoingTask(true); - await requestBatchUpdateListAction({ - actionCode: BatchUpdateListActionCode.UPDATE, - actionPayload: [{ uuid }], - }); - setIsDoingTask(false); - }; - - const onIgnoreClick = async (uuid: string, ignoreVersion: string | undefined) => { - if (!ignoreVersion || !uuid) return; - setTimeClose(-1); // 用户操作,不再倒数,等用户按完用户自行关 - setIsDoingTask(true); - await requestBatchUpdateListAction({ - actionCode: BatchUpdateListActionCode.IGNORE, - actionPayload: [{ uuid, ignoreVersion }], - }); - setIsDoingTask(false); - }; - - const onUpdateAllClick = async (s: "site" | "other" | "ignored") => { - const data = (mRecords![s] || null) as TBatchUpdateRecord[] | null; - if (!data) { - console.error("No Data"); - return; - } - if (!data.length) { - console.error("Invalid Array"); - return; - } - const targets = data.filter((entry) => entry.checkUpdate); - const targetUUIDs = targets.map((entry) => entry.uuid); - setTimeClose(-1); // 用户操作,不再倒数,等用户按完用户自行关 - setIsDoingTask(true); - await requestBatchUpdateListAction({ - actionCode: BatchUpdateListActionCode.UPDATE, - actionPayload: targetUUIDs.map((uuid) => ({ uuid })), - }); - setIsDoingTask(false); - }; - - const onIgnoreAllClick = async (s: "site" | "other" | "ignored") => { - const data = (mRecords![s] || null) as TBatchUpdateRecord[] | null; - if (!data) { - console.error("No Data"); - return; - } - if (!data.length) { - console.error("Invalid Array"); - return; - } - const targets = data.filter((entry) => entry.checkUpdate && entry.uuid && entry.newMeta?.version?.[0]); - const payloadScripts = targets.map((entry) => ({ - uuid: entry.uuid, - ignoreVersion: entry.newMeta!.version[0], - })); - setTimeClose(-1); // 用户操作,不再倒数,等用户按完用户自行关 - setIsDoingTask(true); - await requestBatchUpdateListAction({ - actionCode: BatchUpdateListActionCode.IGNORE, - actionPayload: payloadScripts, - }); - setIsDoingTask(false); - }; - - const openUpdatePage = async (uuid: string) => { - setTimeClose(-1); // 用户操作,不再倒数,等用户按完用户自行关 - // this.openUpdatePage(script, "system"); - await requestOpenUpdatePageByUUID(uuid); - }; - - const onCheckUpdateClick = async () => { - if (checkUpdateSpin) return; - setTimeClose(-1); // 用户操作,不再倒数,等用户按完用户自行关 - setCheckUpdateSpin(true); - await requestCheckScriptUpdate({ checkType: "user" }); - setCheckUpdateSpin(false); - }; - - const getNewConnects = (oldConnects: string[] | undefined, newConnects: string[] | undefined) => { - oldConnects = oldConnects || ([] as string[]); - newConnects = newConnects || ([] as string[]); - const oldConnect = new Set(oldConnects || []); - const newConnect = new Set(newConnects || []); - const res = []; - // 老的里面没有新的就需要用户确认了 - for (const key of newConnect) { - if (!oldConnect.has(key)) { - res.push(key); - } - } - return res; - }; - - const makeGrids = (list: TBatchUpdateRecord[] | undefined) => { - return list?.length ? ( - - {list?.map( - (item, _index) => - item?.checkUpdate && ( - - openUpdatePage(item.uuid)} - className="tw-text-clickable tw-text-gray-900 dark:tw-text-gray-100 !hover:tw-text-blue-600 dark:hover:tw-text-blue-400" - > - - {item.script?.name} - - - } - hoverable - extra={ - <> - onUpdateClick(item.uuid)}> - {t("updatepage.update")} - - {typeof item.newMeta?.version?.[0] === "string" && - item.script.ignoreVersion !== item.newMeta.version[0] ? ( - <> - - onIgnoreClick(item.uuid, item.newMeta.version[0])} - > - {t("updatepage.ignore")} - - - ) : ( - <> - )} - - } - > - - - {t("updatepage.old_version_")} - {t("updatepage.new_version_")} - - - - - {item.script?.metadata?.version?.[0] || "N/A"} - - - {item.newMeta?.version?.[0] || "N/A"} - - - - -
- - {item.script.status === 1 ? ( - - - {t("updatepage.enabled")} - - - ) : item.script.status === 2 ? ( - - - {t("updatepage.disabled")} - - - ) : ( - <> - )} - {item.codeSimilarity < 0.75 ? ( - - {t("updatepage.codechange_major")} - - ) : item.codeSimilarity < 0.95 ? ( - - {t("updatepage.codechange_noticeable")} - - ) : ( - - {t("updatepage.codechange_tiny")} - - )} - {item.withNewConnect ? ( - - {t("updatepage.tag_new_connect")} - - ) : ( - <> - )} - -
-
- ) - )} -
- ) : ( - <> - ); - }; - - const [activeKey, setActiveKey] = useState([]); - const [isDoingTask, setIsDoingTask] = useState(false); - const [checkUpdateSpin, setCheckUpdateSpin] = useState(false); - const handleChange: CollapseProps["onChange"] = (_key, keys) => { - // `keys` is the current list of open panels - setActiveKey(keys); - }; - - useEffect(() => { - setActiveKey((prev: string[]) => { - const s = new Set(prev); - if (mRecords?.site?.length) { - s.add("list-current"); - } - if (mRecords?.other?.length) { - s.add("list-other"); - } - return [...s]; - }); - }, [mRecords]); - - return ( - <> - { -
-
- - {t("updatepage.main_header")} - - onCheckUpdateClick()} - className="tw-cursor-pointer tw-text-gray-700 dark:tw-text-gray-300" - /> -
-
- {mStatusText} -
- {mRecords === null ? ( - <> - ) : ( - <> - {mRecords.site.length === 0 && mRecords.other.length === 0 ? ( -
- {t("updatepage.status_no_update")} -
- ) : ( -
- - {t("updatepage.status_n_update").replace("$0", `${mRecords.site.length + mRecords.other.length}`)} - -
- )} - {mRecords.ignored.length === 0 ? ( - //
{"没有已忽略的更新"}
- <> - ) : ( -
- - {t("updatepage.status_n_ignored").replace("$0", `${mRecords.ignored.length}`)} - -
- )} - {mTimeClose >= 0 ? ( -
- - {t("updatepage.status_autoclose").replace("$0", `${mTimeClose}`)} - -
- ) : ( - <> - )} - - )} -
- } - {mRecords === null ? ( -
-
-
- ) : ( -
-
- - {mRecords.site.length === 0 && mRecords.other.length === 0 ? ( - <> - ) : ( - <> - - onUpdateAllClick("site")}> - {t("updatepage.update_all")} - - - onIgnoreAllClick("site")}> - {t("updatepage.ignore_all")} - - - ) : ( - <> - ) - } - > - {makeGrids(mRecords?.site)} - - - onUpdateAllClick("other")}> - {t("updatepage.update_all")} - - - onIgnoreAllClick("other")}> - {t("updatepage.ignore_all")} - - - ) : ( - <> - ) - } - > - {makeGrids(mRecords?.other)} - - - )} - - {mRecords.ignored.length === 0 ? ( - <> - ) : ( - <> - - onUpdateAllClick("ignored")}> - {t("updatepage.update_all")} - - - ) : ( - <> - ) - } - > - {makeGrids(mRecords?.ignored)} - - - )} - -
-
- )} - - ); +import { useIsMobile } from "@App/pages/components/use-is-mobile"; +import { useBatchUpdate } from "./hooks"; +import { DesktopView } from "./components"; +import { MobileView } from "./mobile"; + +export default function App() { + const view = useBatchUpdate(); + const isMobile = useIsMobile(); + return isMobile ? : ; } - -export default App; diff --git a/src/pages/batchupdate/components.test.tsx b/src/pages/batchupdate/components.test.tsx new file mode 100644 index 000000000..9be11b99f --- /dev/null +++ b/src/pages/batchupdate/components.test.tsx @@ -0,0 +1,142 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeAll, afterEach, vi } from "vitest"; +import { cleanup, screen, within, fireEvent } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { renderWithTooltip } from "@Tests/renderWithTooltip"; +import { DesktopView } from "./components"; +import type { BatchUpdateViewProps } from "./components"; +import { MobileView } from "./mobile"; +import type { UpdateItem } from "./logic"; + +function mkItem(p: Partial = {}): UpdateItem { + return { + uuid: "u1", + name: "示例脚本", + enabled: true, + oldVersion: "1.0.0", + newVersion: "1.1.0", + similarity: 0.9, + risk: "noticeable", + withNewConnect: false, + newConnects: [], + source: "example.com", + iconUrl: "", + ignored: false, + siteMatch: false, + ...p, + }; +} + +function mkView(p: Partial = {}): BatchUpdateViewProps { + return { + updates: [], + ignored: [], + totalChecked: 0, + checktime: 0, + checking: false, + loading: false, + selected: new Set(), + autoClose: null, + onToggle: () => {}, + onToggleAll: () => {}, + onUpdate: () => {}, + onIgnore: () => {}, + onRestore: () => {}, + onUpdateSelected: () => {}, + onIgnoreSelected: () => {}, + onRestoreAll: () => {}, + onCheckNow: () => {}, + onOpen: () => {}, + ...p, + }; +} + +const renderDesktop = (p: Partial) => renderWithTooltip(); + +const renderMobile = (p: Partial) => renderWithTooltip(); + +beforeAll(() => initTestLanguage("zh-CN")); +afterEach(cleanup); + +describe("批量更新桌面视图 检查中反馈", () => { + it("检查中时显示顶部进度条", () => { + renderDesktop({ checking: true }); + expect(screen.getByRole("progressbar")).toBeTruthy(); + }); + + it("未检查时不显示顶部进度条", () => { + renderDesktop({ checking: false }); + expect(screen.queryByRole("progressbar")).toBeNull(); + }); + + it("列表为空且检查中时显示骨架而非空状态", () => { + renderDesktop({ checking: true, updates: [], ignored: [] }); + expect(screen.getByTestId("update-skeleton")).toBeTruthy(); + expect(screen.queryByTestId("update-empty")).toBeNull(); + }); + + it("列表为空且未检查时显示空状态", () => { + renderDesktop({ checking: false, updates: [], ignored: [] }); + expect(screen.getByTestId("update-empty")).toBeTruthy(); + expect(screen.queryByTestId("update-skeleton")).toBeNull(); + }); + + it("已有结果且检查中时保留列表不被骨架替换", () => { + renderDesktop({ checking: true, updates: [mkItem({ name: "保留的脚本" })] }); + expect(screen.getByText("保留的脚本")).toBeTruthy(); + expect(screen.queryByTestId("update-skeleton")).toBeNull(); + }); +}); + +describe("批量更新移动视图 检查中反馈", () => { + it("检查中时显示顶部进度条", () => { + renderMobile({ checking: true }); + expect(screen.getByRole("progressbar")).toBeTruthy(); + }); + + it("列表为空且检查中时显示骨架而非空状态", () => { + renderMobile({ checking: true, updates: [], ignored: [] }); + expect(screen.getByTestId("update-skeleton")).toBeTruthy(); + expect(screen.queryByTestId("update-empty")).toBeNull(); + }); +}); + +describe("批量更新空状态 重新检查按钮", () => { + it("桌面空状态展示重新检查按钮并可触发检查", () => { + const onCheckNow = vi.fn(); + renderDesktop({ updates: [], ignored: [], onCheckNow }); + const btn = within(screen.getByTestId("update-empty")).getByTestId("empty-recheck"); + fireEvent.click(btn); + expect(onCheckNow).toHaveBeenCalledTimes(1); + }); + + it("移动空状态展示重新检查按钮并可触发检查", () => { + const onCheckNow = vi.fn(); + renderMobile({ updates: [], ignored: [], onCheckNow }); + const btn = within(screen.getByTestId("update-empty")).getByTestId("empty-recheck"); + fireEvent.click(btn); + expect(onCheckNow).toHaveBeenCalledTimes(1); + }); +}); + +describe("批量更新移动视图 已忽略分组折叠态", () => { + it("折叠时仅显示展开提示而不显示全部恢复按钮", () => { + renderMobile({ updates: [mkItem()], ignored: [mkItem({ uuid: "i1", ignored: true })] }); + expect(screen.getByTestId("ignored-expand-hint")).toBeTruthy(); + expect(screen.queryByTestId("ignored-restore-all")).toBeNull(); + }); + + it("展开后显示全部恢复按钮且可触发恢复", () => { + const onRestoreAll = vi.fn(); + renderMobile({ + updates: [mkItem()], + ignored: [mkItem({ uuid: "i1", ignored: true })], + onRestoreAll, + }); + fireEvent.click(screen.getByTestId("ignored-toggle")); + const restore = screen.getByTestId("ignored-restore-all"); + fireEvent.click(restore); + expect(onRestoreAll).toHaveBeenCalledTimes(1); + expect(screen.queryByTestId("ignored-expand-hint")).toBeNull(); + }); +}); diff --git a/src/pages/batchupdate/components.tsx b/src/pages/batchupdate/components.tsx new file mode 100644 index 000000000..dd2b88079 --- /dev/null +++ b/src/pages/batchupdate/components.tsx @@ -0,0 +1,476 @@ +import { useState, type ReactElement, type ReactNode } from "react"; +import { + ArrowRight, + BellOff, + ChevronDown, + CircleCheckBig, + Download, + Globe, + PackageCheck, + RefreshCw, + ShieldAlert, + Timer, + X, +} from "lucide-react"; +import { cn } from "@App/pkg/utils/cn"; +import { t } from "@App/locales/locales"; +import { formatUnixTime } from "@App/pkg/utils/day_format"; +import { Button } from "@App/pages/components/ui/button"; +import { Checkbox } from "@App/pages/components/ui/checkbox"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@App/pages/components/ui/collapsible"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@App/pages/components/ui/tooltip"; +import type { UpdateItem, UpdateRisk } from "./logic"; + +/** install:updatepage 命名空间下的翻译快捷方法 */ +export const tk = (key: string, opt?: Record): string => t(`install:updatepage.${key}`, opt); + +/** 批量更新视图(桌面/移动共用)所需的数据与回调 */ +export interface BatchUpdateViewProps { + updates: UpdateItem[]; + ignored: UpdateItem[]; + /** 本次检查覆盖的脚本总数(用于空状态文案) */ + totalChecked: number; + checktime: number; + checking: boolean; + loading: boolean; + selected: Set; + /** 自动关闭剩余秒数;为 null 表示不自动关闭 */ + autoClose: number | null; + onToggle: (uuid: string) => void; + onToggleAll: () => void; + onUpdate: (item: UpdateItem) => void; + onIgnore: (item: UpdateItem) => void; + onRestore: (item: UpdateItem) => void; + onUpdateSelected: () => void; + onIgnoreSelected: () => void; + onRestoreAll: () => void; + onCheckNow: () => void; + /** 打开单个脚本的更新详情页 */ + onOpen: (uuid: string) => void; +} + +/** 悬停 tooltip:用于展示过长被截断的内容(脚本名、来源)或附加信息(相似度、新增连接) */ +export function HoverTip({ content, children }: { content: ReactNode; children: ReactElement }) { + return ( + + {children} + {content} + + ); +} + +/** 脚本图标:有 @icon 时显示图片,失败或缺省时回退为首字母方块 */ +export function ScriptAvatar({ name, iconUrl, size = 28 }: { name: string; iconUrl: string; size?: number }) { + const [error, setError] = useState(false); + if (iconUrl && !error) { + return ( + {name} setError(true)} + className="shrink-0 rounded-md object-cover" + style={{ width: size, height: size }} + /> + ); + } + return ( + + {name.charAt(0).toUpperCase()} + + ); +} + +const PILL = "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap"; + +const RISK_CLASS: Record = { + major: "bg-destructive/10 text-destructive", + noticeable: "bg-primary/10 text-primary", + tiny: "bg-success-bg text-success-fg", +}; +const RISK_KEY: Record = { + major: "codechange_major", + noticeable: "codechange_noticeable", + tiny: "codechange_tiny", +}; + +export function RiskBadge({ risk, similarity }: { risk: UpdateRisk; similarity: number }) { + return ( + + {tk(RISK_KEY[risk])} + + ); +} + +export function ConnectBadge({ newConnects }: { newConnects: string[] }) { + const content = newConnects.length > 0 ? `${tk("new_connects")}: ${newConnects.join(", ")}` : tk("tag_new_connect"); + return ( + + + + {tk("tag_new_connect")} + + + ); +} + +export function StatusBadge({ enabled }: { enabled: boolean }) { + return ( + + {enabled ? tk("enabled") : tk("disabled")} + + ); +} + +export function VersionDiff({ oldVersion, newVersion }: { oldVersion: string; newVersion: string }) { + return ( +
+ {`v${oldVersion}`} + + {`v${newVersion}`} +
+ ); +} + +export function SourceCell({ source }: { source: string }) { + if (!source) return {"—"}; + return ( + + + + {source} + + + ); +} + +/** 可点击跳转更新详情页的脚本名(过长时 tooltip 显示全名) */ +export function ScriptName({ name, onClick }: { name: string; onClick: () => void }) { + return ( + + + + ); +} + +/** 行内文字按钮(更新 / 忽略 / 恢复) */ +function LinkAction({ label, onClick, muted }: { label: string; onClick: () => void; muted?: boolean }) { + return ( + + ); +} + +const COL = { + version: "w-[170px] shrink-0", + change: "w-[230px] shrink-0", + source: "w-[160px] shrink-0", + action: "w-[110px] shrink-0", +}; + +/** 桌面端单行(待更新或已忽略) */ +function DesktopRow({ + item, + selected, + onToggle, + onOpen, + onUpdate, + onIgnore, + onRestore, + ignoredRow, +}: { + item: UpdateItem; + selected?: boolean; + onToggle?: (uuid: string) => void; + onOpen: (uuid: string) => void; + onUpdate?: (item: UpdateItem) => void; + onIgnore?: (item: UpdateItem) => void; + onRestore?: (item: UpdateItem) => void; + ignoredRow?: boolean; +}) { + const dim = item.enabled ? "" : "opacity-55"; + return ( +
+
+ {ignoredRow ? ( + + ) : ( + onToggle?.(item.uuid)} /> + )} +
+
+ + onOpen(item.uuid)} /> + +
+
+ +
+
+ + {item.withNewConnect && } +
+
+ +
+
+ {ignoredRow ? ( + onRestore?.(item)} /> + ) : ( + <> + onUpdate?.(item)} /> + + onIgnore?.(item)} muted /> + + )} +
+
+ ); +} + +function DesktopTable({ view }: { view: BatchUpdateViewProps }) { + return ( +
+
+
+
{tk("col_script")}
+
{tk("col_version")}
+
{tk("col_change")}
+
{tk("col_source")}
+
{tk("col_action")}
+
+ {view.updates.map((item) => ( + + ))} +
+ ); +} + +function DesktopIgnored({ view }: { view: BatchUpdateViewProps }) { + return ( + +
+
+ + + {tk("ignored_section")} + + {view.ignored.length} + + + +
+ + {view.ignored.map((item) => ( + + ))} + +
+
+ ); +} + +function DesktopToolbar({ view }: { view: BatchUpdateViewProps }) { + const selectedCount = view.updates.filter((u) => view.selected.has(u.uuid)).length; + const allSelected = view.updates.length > 0 && selectedCount === view.updates.length; + return ( +
+
+ + + {tk("selected_count", { selected: selectedCount, total: view.updates.length })} + + {view.ignored.length > 0 && ( + <> + {"·"} + + {tk("ignored_count", { count: view.ignored.length })} + + + )} +
+
+ + +
+
+ ); +} + +/** 顶部不确定进度条:检查更新进行中时的即时反馈信号(贴在 header 下方,不随内容滚动) */ +export function TopProgressBar() { + return ( +
+
+
+ ); +} + +/** 骨架占位灰条 */ +export function SkeletonBar({ className }: { className?: string }) { + return ; +} + +/** 桌面端检查中的骨架表格:保留表头 + 占位行,取代冻结的空状态/大转圈 */ +export function SkeletonTable() { + return ( +
+
+
+
{tk("col_script")}
+
{tk("col_version")}
+
{tk("col_change")}
+
{tk("col_source")}
+
{tk("col_action")}
+
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ))} +
+ ); +} + +/** 空状态:所有脚本均为最新 */ +export function EmptyState({ totalChecked, onCheckNow }: { totalChecked: number; onCheckNow: () => void }) { + return ( +
+ + + +
+ {tk("empty_title")} + {tk("empty_desc", { count: totalChecked })} +
+ +
+ ); +} + +/** 顶部状态/自动关闭信息条 */ +function HeaderStatus({ view }: { view: BatchUpdateViewProps }) { + const text = view.checking + ? tk("status_checking_updates") + : view.checktime + ? tk("last_check", { time: formatUnixTime(Math.floor(view.checktime / 1000)) }) + : ""; + if (!text) return null; + return ( + <> + {"·"} + {text} + + ); +} + +/** 自动关闭倒计时小药丸 */ +export function AutoCloseChip({ seconds }: { seconds: number }) { + return ( + + + {tk("auto_close", { count: seconds })} + + ); +} + +/** 桌面端整页视图 */ +export function DesktopView({ view }: { view: BatchUpdateViewProps }) { + const empty = view.updates.length === 0 && view.ignored.length === 0; + return ( +
+
+
+ +

{tk("title")}

+ +
+
+ + {view.autoClose !== null && } + +
+
+ {view.checking && } +
+
+ {view.loading || (view.checking && empty) ? ( + + ) : empty ? ( + + ) : ( + <> + {view.updates.length > 0 && ( + <> + + + + )} + {view.ignored.length > 0 && } + + )} +
+
+
+ ); +} diff --git a/src/pages/batchupdate/hooks.test.ts b/src/pages/batchupdate/hooks.test.ts new file mode 100644 index 000000000..1bfe2c4e6 --- /dev/null +++ b/src/pages/batchupdate/hooks.test.ts @@ -0,0 +1,151 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, act, waitFor } from "@testing-library/react"; +import { initLanguage, t } from "@App/locales/locales"; +import type { TBatchUpdateRecord, TBatchUpdateRecordObject } from "@App/app/service/service_worker/types"; + +// useBatchUpdate 通过消息总线订阅检查状态、拉取记录并发起动作;这里整体打桩, +// 只验证「用户主动点检查更新 → 完成后弹 toast」的反馈逻辑。 +const h = vi.hoisted(() => ({ + record: { checktime: 0, list: [] } as TBatchUpdateRecordObject, + handlers: {} as Record void>, + getBatchUpdateRecordLite: vi.fn(), + fetchCheckUpdateStatus: vi.fn(() => Promise.resolve()), + sendUpdatePageOpened: vi.fn(() => Promise.resolve()), + requestCheckScriptUpdate: vi.fn(() => Promise.resolve()), + requestBatchUpdateListAction: vi.fn(() => Promise.resolve()), + requestOpenUpdatePageByUUID: vi.fn(() => Promise.resolve()), + toastSuccess: vi.fn(), +})); + +h.getBatchUpdateRecordLite.mockImplementation((i: number) => + Promise.resolve({ chunk: i === 0 ? JSON.stringify(h.record) : "", ended: true }) +); + +vi.mock("@App/pages/store/features/script", () => ({ + scriptClient: { + getBatchUpdateRecordLite: h.getBatchUpdateRecordLite, + fetchCheckUpdateStatus: h.fetchCheckUpdateStatus, + sendUpdatePageOpened: h.sendUpdatePageOpened, + }, + requestCheckScriptUpdate: h.requestCheckScriptUpdate, + requestBatchUpdateListAction: h.requestBatchUpdateListAction, + requestOpenUpdatePageByUUID: h.requestOpenUpdatePageByUUID, +})); + +vi.mock("@App/pages/store/global", () => ({ + subscribeMessage: (name: string, cb: (msg: unknown) => void) => { + h.handlers[name] = cb; + return () => delete h.handlers[name]; + }, +})); + +vi.mock("sonner", () => ({ toast: { success: h.toastSuccess } })); + +import { useBatchUpdate } from "./hooks"; + +function mkRecord(uuid: string, newVersion = "1.1.0", sites: string[] = []): TBatchUpdateRecord { + return { + uuid, + checkUpdate: true, + oldCode: "", + newCode: "", + codeSimilarity: 0.9, + newMeta: { version: [newVersion], connect: [] }, + script: { + uuid, + name: "脚本", + status: 1, + metadata: { version: ["1.0.0"], connect: [] }, + downloadUrl: "https://example.com/s.user.js", + }, + sites, + withNewConnect: false, + } as unknown as TBatchUpdateRecord; +} + +/** 发起一次检查:emit 检查中状态 → 设置新记录 → emit 完成状态 */ +async function runCheck(records: TBatchUpdateRecord[]) { + act(() => h.handlers.onScriptUpdateCheck({ status: 1 })); + h.record = { checktime: 200, list: records }; + await act(async () => { + h.handlers.onScriptUpdateCheck({ status: 0, checktime: 200 }); + }); +} + +beforeEach(() => { + initLanguage("zh-CN"); + h.record = { checktime: 0, list: [] }; + h.handlers = {}; + vi.clearAllMocks(); +}); + +describe("批量更新 Hook useBatchUpdate 检查完成反馈", () => { + it("用户主动检查后有更新时弹出包含数量的 toast", async () => { + const { result } = renderHook(() => useBatchUpdate()); + await act(async () => {}); + + act(() => result.current.onCheckNow()); + expect(h.requestCheckScriptUpdate).toHaveBeenCalledWith({ checkType: "user" }); + + await runCheck([mkRecord("a")]); + + await waitFor(() => expect(h.toastSuccess).toHaveBeenCalledTimes(1)); + expect(h.toastSuccess.mock.calls[0][0]).toContain("1"); + expect(h.toastSuccess.mock.calls[0][0]).toBe(t("install:updatepage.toast_found", { count: 1 })); + }); + + it("用户主动检查后无更新时弹出「均为最新」toast", async () => { + const { result } = renderHook(() => useBatchUpdate()); + await act(async () => {}); + + act(() => result.current.onCheckNow()); + await runCheck([]); + + await waitFor(() => expect(h.toastSuccess).toHaveBeenCalledTimes(1)); + expect(h.toastSuccess.mock.calls[0][0]).toBe(t("install:updatepage.toast_uptodate")); + }); + + it("非用户发起的后台检查完成时不弹 toast", async () => { + renderHook(() => useBatchUpdate()); + await act(async () => {}); + + await runCheck([mkRecord("a")]); + await waitFor(() => expect(h.toastSuccess).not.toHaveBeenCalled()); + }); +}); + +describe("批量更新 Hook useBatchUpdate 站点优先级(?site=)", () => { + it("URL 带 site 时把命中该站点的更新排到列表最前", async () => { + window.history.replaceState({}, "", "/?site=example.com"); + + const { result } = renderHook(() => useBatchUpdate()); + await act(async () => {}); + + await runCheck([ + mkRecord("a", "1.1.0", ["other.com"]), + mkRecord("b", "1.1.0", ["example.com"]), + mkRecord("c", "1.1.0", []), + ]); + + await waitFor(() => expect(result.current.updates).toHaveLength(3)); + expect(result.current.updates.map((u) => u.uuid)).toEqual(["b", "a", "c"]); + expect(result.current.updates.find((u) => u.uuid === "b")?.siteMatch).toBe(true); + expect(result.current.updates.find((u) => u.uuid === "a")?.siteMatch).toBe(false); + + window.history.replaceState({}, "", "/"); + }); + + it("URL 无 site 时保持记录原有顺序且均不标记 siteMatch", async () => { + window.history.replaceState({}, "", "/"); + + const { result } = renderHook(() => useBatchUpdate()); + await act(async () => {}); + + await runCheck([mkRecord("a", "1.1.0", ["x.com"]), mkRecord("b", "1.1.0", ["y.com"])]); + + await waitFor(() => expect(result.current.updates).toHaveLength(2)); + expect(result.current.updates.map((u) => u.uuid)).toEqual(["a", "b"]); + expect(result.current.updates.every((u) => u.siteMatch === false)).toBe(true); + }); +}); diff --git a/src/pages/batchupdate/hooks.ts b/src/pages/batchupdate/hooks.ts new file mode 100644 index 000000000..842b2e805 --- /dev/null +++ b/src/pages/batchupdate/hooks.ts @@ -0,0 +1,210 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; +import { t } from "@App/locales/locales"; +import type { TBatchUpdateRecord } from "@App/app/service/service_worker/types"; +import { BatchUpdateListActionCode, UpdateStatusCode } from "@App/app/service/service_worker/types"; +import { + requestBatchUpdateListAction, + requestCheckScriptUpdate, + requestOpenUpdatePageByUUID, + scriptClient, +} from "@App/pages/store/features/script"; +import { subscribeMessage } from "@App/pages/store/global"; +import { assembleRecord, categorize, type UpdateItem } from "./logic"; +import type { BatchUpdateViewProps } from "./components"; + +/** 服务端 onScriptUpdateCheck 广播的消息体 */ +interface UpdateCheckMessage { + status?: number; + checktime?: number; + refreshRecord?: boolean; +} + +/** 解析 URL 上的 autoclose 参数;> 0 时返回秒数,否则返回 null(不自动关闭) */ +function parseAutoClose(): number | null { + const raw = new URLSearchParams(window.location.search).get("autoclose"); + const n = raw === null ? NaN : parseInt(raw, 10); + return Number.isFinite(n) && n > 0 ? n : null; +} + +/** 解析 URL 上的 site 参数(触发更新页的当前网址域名);命中该站点的更新会优先靠前 */ +function parseSite(): string { + return new URLSearchParams(window.location.search).get("site") || ""; +} + +/** 批量更新页面的数据与交互逻辑 */ +export function useBatchUpdate(): BatchUpdateViewProps { + const [records, setRecords] = useState([]); + const [checktime, setChecktime] = useState(0); + const [checking, setChecking] = useState(false); + const [loading, setLoading] = useState(true); + const [selected, setSelected] = useState>(() => new Set()); + const [autoClose, setAutoClose] = useState(() => parseAutoClose()); + // 触发本次更新页的当前网址:命中该站点的更新在列表中优先靠前。整页生命周期内不变。 + const siteRef = useRef(parseSite()); + + const loadingRef = useRef(false); + // 标记本次检查由用户主动发起(点击「检查更新」),用于在检查完成后弹出反馈 toast + const userCheckPendingRef = useRef(false); + + const loadRecord = useCallback(async (): Promise => { + if (loadingRef.current) return null; + loadingRef.current = true; + try { + const obj = await assembleRecord((i) => scriptClient.getBatchUpdateRecordLite(i)); + const list = obj?.list ?? []; + setRecords(list); + if (typeof obj?.checktime === "number") setChecktime(obj.checktime); + return list; + } finally { + loadingRef.current = false; + setLoading(false); + } + }, []); + + // 初始化:订阅状态广播、上报页面已打开、拉取当前状态与记录 + useEffect(() => { + const unsub = subscribeMessage("onScriptUpdateCheck", (msg) => { + if (typeof msg.status === "number") { + setChecking((msg.status & UpdateStatusCode.CHECKING_UPDATE) !== 0); + } + if (typeof msg.checktime === "number") setChecktime(msg.checktime); + const finished = typeof msg.status === "number" && (msg.status & UpdateStatusCode.CHECKING_UPDATE) === 0; + if (msg.refreshRecord || finished) { + void loadRecord().then((list) => { + // 仅对用户主动发起的检查在完成后给出 toast 反馈(后台/系统检查不打扰) + if (finished && userCheckPendingRef.current && list) { + userCheckPendingRef.current = false; + const { updates } = categorize(list); + toast.success( + updates.length > 0 + ? t("install:updatepage.toast_found", { count: updates.length }) + : t("install:updatepage.toast_uptodate") + ); + } + }); + } + }); + void scriptClient.fetchCheckUpdateStatus(); + void scriptClient.sendUpdatePageOpened(); + void loadRecord(); + return unsub; + }, [loadRecord]); + + // 自动关闭倒计时:每秒递减一次(标签页不可见或已取消时不动) + useEffect(() => { + const id = window.setInterval(() => { + setAutoClose((s) => (s === null || document.hidden ? s : s - 1)); + }, 1000); + return () => window.clearInterval(id); + }, []); + useEffect(() => { + if (autoClose !== null && autoClose <= 0) window.close(); + }, [autoClose]); + + const cancelAutoClose = useCallback(() => setAutoClose(null), []); + + const { updates, ignored } = useMemo(() => categorize(records, siteRef.current), [records]); + + const onUpdate = useCallback( + (item: UpdateItem) => { + cancelAutoClose(); + void requestBatchUpdateListAction({ + actionCode: BatchUpdateListActionCode.UPDATE, + actionPayload: [{ uuid: item.uuid }], + }); + }, + [cancelAutoClose] + ); + + const onIgnore = useCallback( + (item: UpdateItem) => { + cancelAutoClose(); + void requestBatchUpdateListAction({ + actionCode: BatchUpdateListActionCode.IGNORE, + actionPayload: [{ uuid: item.uuid, ignoreVersion: item.newVersion }], + }); + }, + [cancelAutoClose] + ); + + const onUpdateSelected = useCallback(() => { + cancelAutoClose(); + const payload = updates.filter((u) => selected.has(u.uuid)).map((u) => ({ uuid: u.uuid })); + if (payload.length) { + void requestBatchUpdateListAction({ actionCode: BatchUpdateListActionCode.UPDATE, actionPayload: payload }); + } + setSelected(new Set()); + }, [updates, selected, cancelAutoClose]); + + const onIgnoreSelected = useCallback(() => { + cancelAutoClose(); + const payload = updates + .filter((u) => selected.has(u.uuid)) + .map((u) => ({ uuid: u.uuid, ignoreVersion: u.newVersion })); + if (payload.length) { + void requestBatchUpdateListAction({ actionCode: BatchUpdateListActionCode.IGNORE, actionPayload: payload }); + } + setSelected(new Set()); + }, [updates, selected, cancelAutoClose]); + + const onRestoreAll = useCallback(() => { + cancelAutoClose(); + const payload = ignored.map((u) => ({ uuid: u.uuid })); + if (payload.length) { + void requestBatchUpdateListAction({ actionCode: BatchUpdateListActionCode.UPDATE, actionPayload: payload }); + } + }, [ignored, cancelAutoClose]); + + const onCheckNow = useCallback(() => { + cancelAutoClose(); + userCheckPendingRef.current = true; + void requestCheckScriptUpdate({ checkType: "user" }); + }, [cancelAutoClose]); + + const onToggle = useCallback( + (uuid: string) => { + cancelAutoClose(); + setSelected((prev) => { + const next = new Set(prev); + if (next.has(uuid)) next.delete(uuid); + else next.add(uuid); + return next; + }); + }, + [cancelAutoClose] + ); + + const onToggleAll = useCallback(() => { + cancelAutoClose(); + setSelected((prev) => { + if (updates.length > 0 && updates.every((u) => prev.has(u.uuid))) return new Set(); + return new Set(updates.map((u) => u.uuid)); + }); + }, [updates, cancelAutoClose]); + + const onOpen = useCallback((uuid: string) => { + void requestOpenUpdatePageByUUID(uuid); + }, []); + + return { + updates, + ignored, + totalChecked: records.length, + checktime, + checking, + loading, + selected, + autoClose, + onToggle, + onToggleAll, + onUpdate, + onIgnore, + onRestore: onUpdate, + onUpdateSelected, + onIgnoreSelected, + onRestoreAll, + onCheckNow, + onOpen, + }; +} diff --git a/src/pages/batchupdate/index.css b/src/pages/batchupdate/index.css deleted file mode 100644 index 6954b0a75..000000000 --- a/src/pages/batchupdate/index.css +++ /dev/null @@ -1,19 +0,0 @@ -.batchupdate-mainlayout { - min-height: max-content; - padding-top: 12px; - padding-bottom: 12px; -} - -body .text-clickable { - color: inherit; - cursor: pointer; -} - -body .text-clickable:hover { - color: inherit; - text-decoration: underline; -} - -.script-card.card-disabled { - filter: contrast(0.9); -} diff --git a/src/pages/batchupdate/logic.test.ts b/src/pages/batchupdate/logic.test.ts new file mode 100644 index 000000000..52a83618a --- /dev/null +++ b/src/pages/batchupdate/logic.test.ts @@ -0,0 +1,228 @@ +// can be tested with vitest-environment node +import { describe, it, expect } from "vitest"; +import { initLanguage } from "@App/locales/locales"; +import type { Script } from "@App/app/repo/scripts"; +import type { TBatchUpdateRecord, TBatchUpdateRecordObject } from "@App/app/service/service_worker/types"; +import { riskLevel, getSource, toUpdateItem, categorize, assembleRecord } from "./logic"; + +function mkScript(p: Partial + ScriptCat + + + +
+ + diff --git a/src/pages/confirm/App.test.tsx b/src/pages/confirm/App.test.tsx new file mode 100644 index 000000000..5faaaaf9e --- /dev/null +++ b/src/pages/confirm/App.test.tsx @@ -0,0 +1,202 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { render, cleanup, screen, fireEvent, waitFor, act } from "@testing-library/react"; +import { initLanguage } from "@App/locales/locales"; +import type { ConfirmParam } from "@App/app/service/service_worker/permission_verify"; + +// 授权数据走后台消息,统一打桩 +const { getPermissionInfo, confirm, isMobile } = vi.hoisted(() => ({ + getPermissionInfo: vi.fn(), + confirm: vi.fn(), + isMobile: { value: false }, +})); +vi.mock("@App/pages/store/features/script", () => ({ + permissionClient: { getPermissionInfo, confirm }, +})); +// 单一移动断点来源,按需切换桌面/移动外壳 +vi.mock("@App/pages/components/use-is-mobile", () => ({ + useIsMobile: () => isMobile.value, + MOBILE_BREAKPOINT: 768, +})); + +import { PermissionConfirm } from "./App"; + +const baseInfo = (over: Partial = {}, likeNum = 0) => ({ + script: { uuid: "u1", name: "Bilibili 视频下载助手", metadata: { version: ["1.2.0"] } }, + confirm: { + permission: "cors", + permissionValue: "api.bilibili.com", + title: "脚本正在试图访问跨域资源", + describe: "请确认是否允许该脚本访问跨域资源。", + wildcard: true, + permissionContent: "域名", + metadata: { + 脚本名称: "Bilibili 视频下载助手", + 请求域名: "api.bilibili.com", + 请求地址: "https://api.bilibili.com/x/player/playurl", + }, + ...over, + } as ConfirmParam, + likeNum, +}); + +beforeEach(() => { + initLanguage("zh-CN"); + vi.clearAllMocks(); + vi.spyOn(window, "close").mockImplementation(() => {}); + confirm.mockResolvedValue(undefined); + isMobile.value = false; +}); +afterEach(() => { + cleanup(); + vi.useRealTimers(); +}); + +describe("授权确认页 · 渲染", () => { + it("加载后应展示标题、描述与请求域名", async () => { + getPermissionInfo.mockResolvedValue(baseInfo()); + render(); + expect(await screen.findByText("脚本正在试图访问跨域资源")).toBeInTheDocument(); + expect(screen.getByText("请确认是否允许该脚本访问跨域资源。")).toBeInTheDocument(); + expect(screen.getByText("api.bilibili.com")).toBeInTheDocument(); + }); + + it("脚本名应只出现在身份行,不在请求信息中重复", async () => { + getPermissionInfo.mockResolvedValue(baseInfo()); + render(); + await screen.findByText("脚本正在试图访问跨域资源"); + // 身份行展示脚本名,但 metadata 的「脚本名称」标签不应再次出现 + expect(screen.getByText("Bilibili 视频下载助手")).toBeInTheDocument(); + expect(screen.queryByText("脚本名称")).not.toBeInTheDocument(); + }); +}); + +describe("授权确认页 · 时长与范围映射", () => { + it("默认时长为「仅此次」,点击允许应以 type 1 确认", async () => { + getPermissionInfo.mockResolvedValue(baseInfo()); + render(); + fireEvent.click(await screen.findByRole("button", { name: "允许" })); + await waitFor(() => expect(confirm).toHaveBeenCalledWith("u1", { allow: true, type: 1 })); + }); + + it("选择「永久」后点击允许应以 type 5 确认", async () => { + getPermissionInfo.mockResolvedValue(baseInfo()); + render(); + fireEvent.click(await screen.findByRole("button", { name: "永久" })); + fireEvent.click(screen.getByRole("button", { name: "允许" })); + await waitFor(() => expect(confirm).toHaveBeenCalledWith("u1", { allow: true, type: 5 })); + }); + + it("通配权限且 likeNum>2 时,开启通配并选永久,允许应为 type 4", async () => { + getPermissionInfo.mockResolvedValue(baseInfo({ wildcard: true }, 3)); + render(); + fireEvent.click(await screen.findByRole("button", { name: "永久" })); + fireEvent.click(screen.getByRole("switch")); + fireEvent.click(screen.getByRole("button", { name: "允许" })); + await waitFor(() => expect(confirm).toHaveBeenCalledWith("u1", { allow: true, type: 4 })); + }); + + it("点击拒绝应以当前时长 type 确认 allow=false", async () => { + getPermissionInfo.mockResolvedValue(baseInfo()); + render(); + fireEvent.click(await screen.findByRole("button", { name: "拒绝" })); + await waitFor(() => expect(confirm).toHaveBeenCalledWith("u1", { allow: false, type: 1 })); + }); + + it("点击忽略应以 type 0 确认(忽略不留授权记录)", async () => { + getPermissionInfo.mockResolvedValue(baseInfo()); + render(); + await screen.findByText("脚本正在试图访问跨域资源"); + fireEvent.click(screen.getByRole("button", { name: /忽略/ })); + await waitFor(() => expect(confirm).toHaveBeenCalledWith("u1", { allow: false, type: 0 })); + }); +}); + +describe("授权确认页 · 选项可见性", () => { + it("通配权限但 likeNum≤2 时不显示通配开关", async () => { + getPermissionInfo.mockResolvedValue(baseInfo({ wildcard: true }, 2)); + render(); + await screen.findByText("脚本正在试图访问跨域资源"); + expect(screen.queryByRole("switch")).not.toBeInTheDocument(); + }); + + it("persistentOnly 权限不展示「临时」选项", async () => { + getPermissionInfo.mockResolvedValue(baseInfo({ persistentOnly: true, wildcard: false })); + render(); + await screen.findByText("脚本正在试图访问跨域资源"); + expect(screen.getByRole("button", { name: "仅此次" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "永久" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "临时" })).not.toBeInTheDocument(); + }); + + it("cookie 权限应展示高敏感警示", async () => { + getPermissionInfo.mockResolvedValue( + baseInfo({ permission: "cookie", title: "脚本正在试图访问网站 Cookie", wildcard: false }) + ); + render(); + expect(await screen.findByText("高敏感权限")).toBeInTheDocument(); + }); +}); + +describe("授权确认页 · 站点访问变体", () => { + it("仅展示「请求权限」单按钮,不展示时长选择与允许/拒绝", async () => { + getPermissionInfo.mockResolvedValue( + baseInfo({ permission: "extension-site-access", title: "ScriptCat 需要站点访问权限", wildcard: false }) + ); + render(); + expect(await screen.findByRole("button", { name: "请求权限" })).toBeInTheDocument(); + expect(screen.queryByText("授权时长")).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "允许" })).not.toBeInTheDocument(); + }); + + it("点击请求权限应先申请站点访问再以 type 1 确认", async () => { + const requestSpy = vi.spyOn(chrome.permissions, "request").mockResolvedValue(true as never); + getPermissionInfo.mockResolvedValue( + baseInfo({ + permission: "extension-site-access", + title: "ScriptCat 需要站点访问权限", + wildcard: false, + extensionSiteAccessOrigins: ["https://example.com/*"], + }) + ); + render(); + fireEvent.click(await screen.findByRole("button", { name: "请求权限" })); + await waitFor(() => expect(requestSpy).toHaveBeenCalledWith({ origins: ["https://example.com/*"] })); + await waitFor(() => expect(confirm).toHaveBeenCalledWith("u1", { allow: true, type: 1 })); + }); +}); + +describe("授权确认页 · 移动外壳", () => { + it("桌面下允许/拒绝按钮应横向排列", async () => { + getPermissionInfo.mockResolvedValue(baseInfo()); + render(); + const row = await screen.findByTestId("confirm-button-row"); + expect(row.className).toContain("flex-row"); + expect(row.className).not.toContain("flex-col"); + }); + + it("移动端下允许/拒绝按钮应纵向堆叠", async () => { + isMobile.value = true; + getPermissionInfo.mockResolvedValue(baseInfo()); + render(); + const row = await screen.findByTestId("confirm-button-row"); + expect(row.className).toContain("flex-col"); + expect(row.className).not.toContain("flex-row"); + }); +}); + +describe("授权确认页 · 倒计时", () => { + it("倒计时归零应以忽略(type 0)自动关闭", async () => { + vi.useFakeTimers(); + getPermissionInfo.mockResolvedValue(baseInfo()); + render(); + // 等待数据加载(promise microtask) + await act(async () => { + await Promise.resolve(); + }); + await act(async () => { + vi.advanceTimersByTime(31000); + }); + expect(confirm).toHaveBeenCalledWith("u1", { allow: false, type: 0 }); + }); +}); diff --git a/src/pages/confirm/App.tsx b/src/pages/confirm/App.tsx index 56ff92be7..e6c2d4bc8 100644 --- a/src/pages/confirm/App.tsx +++ b/src/pages/confirm/App.tsx @@ -1,193 +1,388 @@ -import type { ConfirmParam } from "@App/app/service/service_worker/permission_verify"; -import { Button, Message, Space } from "@arco-design/web-react"; -import React, { useEffect, useMemo } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { permissionClient } from "../store/features/script"; +import { toast } from "sonner"; +import { Cookie, FolderSync, Globe, ShieldCheck, TriangleAlert, CircleAlert, type LucideIcon } from "lucide-react"; +import { permissionClient } from "@App/pages/store/features/script"; +import { Button } from "@App/pages/components/ui/button"; +import { Switch } from "@App/pages/components/ui/switch"; +import { useIsMobile } from "@App/pages/components/use-is-mobile"; +import { cn } from "@App/pkg/utils/cn"; +import { + resolveConfirmType, + availableDurations, + canApplyToAll, + isSiteAccess, + isHighSensitive, + type Duration, +} from "./confirm-options"; +import { versionDisplay } from "../utils"; -// 权限确认组件 -function PermissionConfirmRequest({ uuid }: { uuid: string }) { - const [confirm, setConfirm] = React.useState(); - const [likeNum, setLikeNum] = React.useState(0); - const [second, setSecond] = React.useState(30); +type ConfirmInfo = Awaited>; - const { t } = useTranslation(); +const DURATION_LABEL: Record = { + once: "duration_once", + temporary: "duration_temporary", + permanent: "duration_permanent", +}; +// 不同权限的图标与配色(语义令牌类名)。图标底色为对应语义色的浅色蒙版(~12% 透明度), +// 在亮/暗两套主题下都呈现一致的柔和色晕,与错误态的 destructive/10 处理一致。 +function permissionVisual(permission: string): { Icon: LucideIcon; bgClass: string; iconClass: string } { + switch (permission) { + case "cookie": + return { Icon: Cookie, bgClass: "bg-warning/10", iconClass: "text-warning" }; + case "file_storage": + return { Icon: FolderSync, bgClass: "bg-secondary", iconClass: "text-foreground" }; + case "extension-site-access": + return { Icon: ShieldCheck, bgClass: "bg-primary/10", iconClass: "text-primary" }; + default: + return { Icon: Globe, bgClass: "bg-primary/10", iconClass: "text-primary" }; + } +} + +function BrandMark() { + return ( +
+
+ {"S"} +
+ {"ScriptCat"} +
+ ); +} + +function PageShell({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} +
+ ); +} + +const cardClass = "flex w-full max-w-[480px] flex-col gap-5 rounded-2xl border bg-card p-7 shadow-xl"; + +export function PermissionConfirm({ uuid }: { uuid: string }) { + const { t } = useTranslation(["permission", "common"]); + const isMobile = useIsMobile(); + const [info, setInfo] = useState(); + const [loadError, setLoadError] = useState(false); + const [duration, setDuration] = useState("once"); + const [applyToAll, setApplyToAll] = useState(false); + const [second, setSecond] = useState(30); + const decidedRef = useRef(false); + + const decide = useCallback( + async (allow: boolean, type: number) => { + if (decidedRef.current) return; + decidedRef.current = true; + try { + await permissionClient.confirm(uuid, { allow, type }); + window.close(); + } catch (e) { + toast.error((e as Error)?.message || t("common:confirm_error")); + setTimeout(() => window.close(), 3000); + } + }, + [uuid, t] + ); + + const ignore = useCallback(() => decide(false, 0), [decide]); + const ignoreRef = useRef(ignore); + ignoreRef.current = ignore; + + // 加载授权信息 useEffect(() => { + permissionClient + .getPermissionInfo(uuid) + .then(setInfo) + .catch(() => setLoadError(true)); + }, [uuid]); + + // 倒计时:归零按「忽略」自动关闭 + useEffect(() => { + if (!info) return; const timer = setInterval(() => { setSecond((s) => { if (s <= 1) { clearInterval(timer); - window.close(); + ignoreRef.current(); return 0; } return s - 1; }); }, 1000); return () => clearInterval(timer); - }, []); + }, [info]); + // 用户直接关闭窗口时记为忽略,避免脚本调用悬挂 useEffect(() => { const handler = () => { - permissionClient.confirm(uuid, { - allow: false, - type: 0, - }); + if (!decidedRef.current) permissionClient.confirm(uuid, { allow: false, type: 0 }); }; - window.addEventListener("beforeunload", handler, false); - return () => window.removeEventListener("beforeunload", handler, false); + window.addEventListener("beforeunload", handler); + return () => window.removeEventListener("beforeunload", handler); }, [uuid]); + // 加载失败:3 秒后自动关闭 useEffect(() => { - permissionClient - .getPermissionInfo(uuid) - .then((data) => { - console.log(data); - setConfirm(data.confirm); - setLikeNum(data.likeNum); - }) - .catch((e: any) => { - Message.error(e.message || t("get_confirm_error")); - }); - }, [uuid, t]); + if (!loadError) return; + const timer = setTimeout(() => window.close(), 3000); + return () => clearTimeout(timer); + }, [loadError]); - const handleConfirm = (allow: boolean, type: number) => { - return async () => { - try { - if (allow && confirm?.extensionSiteAccessOrigins?.length) { - const granted = await chrome.permissions.request({ - origins: confirm.extensionSiteAccessOrigins, - }); - if (!granted) { - await permissionClient - .confirm(uuid, { - allow: false, - type: 0, - }) - .catch(() => {}); - window.close(); - return; - } - } - await permissionClient.confirm(uuid, { - allow, - type, - }); - window.close(); - } catch (e: any) { - Message.error(e.message || t("confirm_error")); - setTimeout(() => { - window.close(); - }, 3000); + if (loadError) { + return ( + +
+
+ +
+
+

{t("permission:confirm_expired_title")}

+

{t("permission:confirm_expired_desc")}

+
+ + {t("permission:auto_close_in", { second: 3 })} +
+
+ ); + } + + if (!info) { + return ( + +
+ {/* 头部骨架 */} +
+
+
+
+
+ {/* 身份骨架 */} +
+
+
+
+
+
+
+ {/* 请求目标骨架 */} +
+
+
+
+ {/* 授权时长骨架 */} +
+
+
+
+ {/* 操作骨架 */} +
+
+
+
+
+
+
+
+ + ); + } + + const { script, confirm, likeNum } = info; + const siteAccess = isSiteAccess(confirm); + const durations = availableDurations(confirm); + const showWildcard = canApplyToAll(confirm, likeNum); + const { Icon, bgClass, iconClass } = permissionVisual(confirm.permission); + const effectiveApplyToAll = showWildcard && applyToAll && duration !== "once"; + const type = resolveConfirmType(duration, effectiveApplyToAll); + + const scriptNameLabel = t("common:script_name"); + const metaEntries = Object.entries(confirm.metadata || {}).filter(([k]) => k !== scriptNameLabel); + const version = script.metadata?.version?.[0]; + const initials = + Array.from((script.name || "?").trim()) + .slice(0, 2) + .join("") || "?"; + + const requestSiteAccess = async () => { + if (decidedRef.current) return; + const origins = confirm.extensionSiteAccessOrigins; + if (origins?.length) { + const granted = await chrome.permissions.request({ origins }).catch(() => false); + if (!granted) { + ignore(); + return; } - }; + } + decide(true, 1); }; - const metadata = useMemo(() => (confirm && confirm.metadata && Object.keys(confirm.metadata)) || [], [confirm]); - const isExtensionSiteAccessConfirm = confirm?.permission === "extension-site-access"; - return ( -
- - {confirm?.title} - {metadata.map((key) => ( - - {`${key}: ${confirm!.metadata![key]}`} - - ))} - {confirm?.describe} - {isExtensionSiteAccessConfirm ? ( - <> -
- + +
+ {/* 头部 */} +
+
+ +
+
+

{confirm.title}

+ {confirm.describe && ( +

{confirm.describe}

+ )} +
+
+ + {/* 高敏感警示 */} + {isHighSensitive(confirm) && ( +
+ +
+ {t("permission:cookie_warning_title")} + + {t("permission:cookie_warning_desc")} + +
+
+ )} + + {/* 脚本身份 */} +
+
+ {initials} +
+
+ {script.name} + + {t("permission:user_script_type")} + {versionDisplay(version)} + +
+
+ + {/* 请求目标 */} + {metaEntries.length > 0 && ( +
+ {metaEntries.map(([k, v], i) => ( +
0 && "border-t border-border")}> + {k} + + {v} + +
+ ))} +
+ )} + + {/* 授权时长 + 通配范围(站点访问无此区) */} + {!siteAccess && ( +
+ {t("permission:auth_duration")} +
+ {durations.map((d) => ( + + ))}
- + {showWildcard && ( +
+
+ + {t("permission:apply_to_all_domains")} + + {t("permission:apply_to_all_domains_desc")} +
+ +
+ )} +
+ )} + + {/* 操作区 */} + {siteAccess ? ( +
+ + +
) : ( - <> -
- +
-
- - - - {likeNum > 2 && ( - - )} - - {likeNum > 2 && ( - - )} - -
-
- - - - {likeNum > 2 && ( - - )} - - {likeNum > 2 && ( - - )} - -
- + +
)} - -
+
+ ); } -function App() { - const params = new URLSearchParams(location.search); - const uuid = params.get("uuid"); - - if (uuid) { - return ; - } - - return null; +export default function App() { + const uuid = new URLSearchParams(location.search).get("uuid"); + if (!uuid) return null; + return ; } - -export default App; diff --git a/src/pages/confirm/confirm-options.test.ts b/src/pages/confirm/confirm-options.test.ts new file mode 100644 index 000000000..16d6a248e --- /dev/null +++ b/src/pages/confirm/confirm-options.test.ts @@ -0,0 +1,67 @@ +// can be tested with vitest-environment node +import { describe, it, expect } from "vitest"; +import { + resolveConfirmType, + availableDurations, + canApplyToAll, + isSiteAccess, + isHighSensitive, +} from "./confirm-options"; +import type { ConfirmParam } from "@App/app/service/service_worker/permission_verify"; + +const cp = (over: Partial = {}): ConfirmParam => ({ permission: "cors", ...over }); + +describe("授权选项 · 时长与范围到 type 的映射", () => { + it("仅此次应映射为 type 1(与是否通配无关)", () => { + expect(resolveConfirmType("once", false)).toBe(1); + expect(resolveConfirmType("once", true)).toBe(1); + }); + it("临时·仅此项应为 type 3,临时·全部应为 type 2", () => { + expect(resolveConfirmType("temporary", false)).toBe(3); + expect(resolveConfirmType("temporary", true)).toBe(2); + }); + it("永久·仅此项应为 type 5,永久·全部应为 type 4", () => { + expect(resolveConfirmType("permanent", false)).toBe(5); + expect(resolveConfirmType("permanent", true)).toBe(4); + }); +}); + +describe("授权选项 · 可选时长", () => { + it("普通权限应提供 仅此次/临时/永久", () => { + expect(availableDurations(cp())).toEqual(["once", "temporary", "permanent"]); + }); + it("persistentOnly 权限应隐藏「临时」(临时不会被缓存,等同一次性)", () => { + expect(availableDurations(cp({ persistentOnly: true }))).toEqual(["once", "permanent"]); + }); +}); + +describe("授权选项 · 通配范围开关可见性", () => { + it("非通配权限不显示通配开关", () => { + expect(canApplyToAll(cp({ wildcard: false }), 5)).toBe(false); + }); + it("通配权限但同类等待请求未超过 2 个时不显示", () => { + expect(canApplyToAll(cp({ wildcard: true }), 2)).toBe(false); + }); + it("通配权限且同类等待请求超过 2 个时显示", () => { + expect(canApplyToAll(cp({ wildcard: true }), 3)).toBe(true); + }); +}); + +describe("授权选项 · 高敏感权限警示", () => { + it("cookie 权限应标记为高敏感(展示警示条)", () => { + expect(isHighSensitive(cp({ permission: "cookie" }))).toBe(true); + }); + it("cors/文件存储等不标记为高敏感", () => { + expect(isHighSensitive(cp({ permission: "cors" }))).toBe(false); + expect(isHighSensitive(cp({ permission: "file_storage" }))).toBe(false); + }); +}); + +describe("授权选项 · 站点访问识别", () => { + it("extension-site-access 应识别为站点访问(单按钮变体)", () => { + expect(isSiteAccess(cp({ permission: "extension-site-access" }))).toBe(true); + }); + it("其它权限不是站点访问", () => { + expect(isSiteAccess(cp({ permission: "cors" }))).toBe(false); + }); +}); diff --git a/src/pages/confirm/confirm-options.ts b/src/pages/confirm/confirm-options.ts new file mode 100644 index 000000000..6c4a801b8 --- /dev/null +++ b/src/pages/confirm/confirm-options.ts @@ -0,0 +1,43 @@ +import type { ConfirmParam } from "@App/app/service/service_worker/permission_verify"; + +// 授权时长 +export type Duration = "once" | "temporary" | "permanent"; + +/** + * 时长 × 是否应用于全部(通配)→ UserConfirm.type + * type 含义:1 允许一次 / 2 临时全部 / 3 临时此 / 4 永久全部 / 5 永久此 + */ +export function resolveConfirmType(duration: Duration, applyToAll: boolean): number { + switch (duration) { + case "once": + return 1; + case "temporary": + return applyToAll ? 2 : 3; + case "permanent": + return applyToAll ? 4 : 5; + } +} + +/** + * 可选的授权时长。persistentOnly 模式下「临时」不会被缓存(等同一次性),故隐藏。 + */ +export function availableDurations(confirm: ConfirmParam): Duration[] { + return confirm.persistentOnly ? ["once", "permanent"] : ["once", "temporary", "permanent"]; +} + +/** + * 「应用到所有域名(通配)」开关是否可见:权限支持通配,且同类等待确认请求超过 2 个时才解锁。 + */ +export function canApplyToAll(confirm: ConfirmParam, likeNum: number): boolean { + return !!confirm.wildcard && likeNum > 2; +} + +/** 是否为站点访问权限(单按钮变体)。 */ +export function isSiteAccess(confirm: ConfirmParam): boolean { + return confirm.permission === "extension-site-access"; +} + +/** 是否为高敏感权限:展示顶部警示条提醒用户谨慎授权。 */ +export function isHighSensitive(confirm: ConfirmParam): boolean { + return confirm.permission === "cookie"; +} diff --git a/src/pages/confirm/main.tsx b/src/pages/confirm/main.tsx index 51985671b..aaeab6364 100644 --- a/src/pages/confirm/main.tsx +++ b/src/pages/confirm/main.tsx @@ -1,13 +1,11 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; -import { AppProvider } from "../store/AppContext.tsx"; -import MainLayout from "../components/layout/MainLayout.tsx"; import LoggerCore from "@App/app/logger/core.ts"; import { message } from "../store/global.ts"; import MessageWriter from "@App/app/logger/message_writer.ts"; -import "@arco-design/web-react/dist/css/arco.css"; -import "@App/locales/locales"; +import { ThemeProvider } from "../components/theme-provider.tsx"; +import { Toaster } from "../components/ui/sonner.tsx"; import "@App/index.css"; // 初始化日志组件 @@ -19,11 +17,10 @@ const loggerCore = new LoggerCore({ loggerCore.logger().debug("confirm page start"); const Root = ( - - - - - + + + + ); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/src/pages/import.html b/src/pages/import.html new file mode 100644 index 000000000..53a01f254 --- /dev/null +++ b/src/pages/import.html @@ -0,0 +1,20 @@ + + + + + + + ScriptCat + + + +
+ + diff --git a/src/pages/import/App.tsx b/src/pages/import/App.tsx index 755242d52..0bf6925fa 100644 --- a/src/pages/import/App.tsx +++ b/src/pages/import/App.tsx @@ -1,334 +1,10 @@ -import React, { useEffect, useState } from "react"; -import { Button, Card, Checkbox, Divider, List, Message, Space, Switch, Typography } from "@arco-design/web-react"; -import { useTranslation } from "react-i18next"; // 导入react-i18next的useTranslation钩子 -import { loadAsyncJSZip } from "@App/pkg/utils/jszip-x"; -import type { ScriptOptions, ScriptData, SubscribeData } from "@App/pkg/backup/struct"; -import { prepareScriptByCode } from "@App/pkg/utils/script"; -import { SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE, ScriptDAO } from "@App/app/repo/scripts"; -import { cacheInstance } from "@App/app/cache"; -import { CACHE_KEY_IMPORT_FILE } from "@App/app/cache_key"; -import { parseBackupZipFile } from "@App/pkg/backup/utils"; -import { scriptClient, synchronizeClient, valueClient } from "../store/features/script"; -import { sleep } from "@App/pkg/utils/utils"; -import type { TKeyValuePair } from "@App/pkg/utils/message_value"; -import { encodeRValue } from "@App/pkg/utils/message_value"; - -const ScriptListItem = React.memo( - ({ - item, - index, - t, - onToggle, - onStatusToggle, - }: { - item: ScriptData; - index: number; - t: (a: string) => string; - onToggle: (index: number) => void; - onStatusToggle: (index: number, checked: boolean) => void; - }) => { - return ( -
onToggle(index)} - > - - - {item.script?.script?.name || item.error || t("unknown")} - - {`${t("author")}: ${item.script?.script?.metadata.author?.[0]}`} - - {`${t("description")}: ${item.script?.script?.metadata.description?.[0]}`} - - - {`${t("source")}: ${item.options?.meta.file_url || t("local_creation")}`} - - - {`${t("operation")}: `} - {(item.install && (item.script?.oldScript ? t("update") : t("add_new"))) || - (item.error - ? `${t("error")}: ${item.options?.meta.name} - ${item.options?.meta.uuid}` - : t("no_operation"))} - - -
- {t("enable_script")} -
- onStatusToggle(index, checked)} - /> -
-
-
- ); - }, - (prevProps, nextProps) => { - return prevProps.index === nextProps.index && prevProps.item === nextProps.item && prevProps.t === nextProps.t; - } -); - -ScriptListItem.displayName = "ScriptListItem"; - -function App() { - const [scripts, setScripts] = useState([]); - const [subscribes, setSubscribes] = useState([]); - const [selectAll, setSelectAll] = useState([true, true]); - const [installNum, setInstallNum] = useState([0, 0]); - const [loading, setLoading] = useState(true); - const { t } = useTranslation(); // 使用useTranslation钩子获取翻译函数 - - const fetchData = async () => { - try { - const url = new URL(window.location.href); - const uuid = url.searchParams.get("uuid") || ""; - const cacheKey = `${CACHE_KEY_IMPORT_FILE}${uuid}`; - const resp = await cacheInstance.get<{ filename: string; url: string }>(cacheKey); - if (!resp) throw new Error("fetchData failed"); - const filedata = await fetch(resp.url).then((resp) => resp.blob()); - const zip = await loadAsyncJSZip(filedata); - const backData = await parseBackupZipFile(zip); - const backDataScript = backData.script as ScriptData[]; - - // 使用缓存优化脚本加载速度 - const scriptDAO = new ScriptDAO(); - scriptDAO.enableCache(); - - // setScripts(backDataScript); - // 获取各个脚本现在已经存在的信息 - await Promise.all( - backDataScript.map(async (item) => { - try { - const prepareScript = await prepareScriptByCode( - item.code, - item.options?.meta.file_url || "", - item.options?.meta.sc_uuid || undefined, - true, - scriptDAO - ); - item.script = prepareScript; - } catch (e: any) { - item.error = e.toString(); - return item; - } - if (!item.options) { - item.options = { - options: {} as ScriptOptions, - meta: { - name: item.script?.script.name, - // 此uuid是对tm的兼容处理 - uuid: item.script?.script.uuid, - sc_uuid: item.script?.script.uuid, - file_url: item.script?.script.downloadUrl || "", - modified: item.script?.script.createtime, - subscribe_url: item.script?.script.subscribeUrl, - }, - settings: { - enabled: - item.enabled === false - ? false - : !(item.script?.script.metadata.background || item.script?.script.metadata.crontab), - position: item.script?.script.sort, - }, - }; - } - item.script.script.sort = item.options.settings.position || 0; - item.script.script.status = - item.enabled !== false && item.options.settings.enabled ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE; - item.install = true; - return item; - }) - ); - const results = backDataScript.slice().sort((a, b) => { - const aName = a.script?.script?.name || ""; - const bName = b.script?.script?.name || ""; - if (aName && bName) return aName.localeCompare(bName); - return 0; - }); - setScripts(results); - setSelectAll([true, true]); - setLoading(false); - } catch (e) { - Message.error(`获取导入文件失败: ${e}`); - } - }; - - useEffect(() => { - fetchData(); - }, []); - - const scriptImportAsync = async (item: ScriptData) => { - try { - if (item.script?.script) { - if (item.script.script.ignoreVersion) item.script.script.ignoreVersion = ""; - } - const scriptDetails = item.script!.script!; - const createtime = item.lastModificationDate; - const updatetime = item.lastModificationDate; - await scriptClient.install({ script: scriptDetails, code: item.code, createtime, updatetime }); - await Promise.all([ - (async () => { - // 导入资源 - if (!item.requires || !item.resources || !item.requiresCss) return; - if (!item.requires[0] && !item.resources[0] && !item.requiresCss[0]) return; - await sleep(((Math.random() * 600) | 0) + 200); - await synchronizeClient.importResources( - item.script?.script.uuid, - item.requires, - item.resources, - item.requiresCss - ); - })(), - (async () => { - // 导入数据 - const { data } = item.storage; - const ts = item.storage.ts || 0; - const entries = Object.entries(data); - if (entries.length === 0) return; - await sleep(((Math.random() * 600) | 0) + 200); - const uuid = item.script!.script.uuid!; - const keyValuePairs = [] as TKeyValuePair[]; - for (const [key, value] of entries) { - keyValuePairs.push([key, encodeRValue(value)]); - } - await valueClient.setScriptValues({ uuid: uuid, keyValuePairs, isReplace: false, ts: ts }); - })(), - ]); - setInstallNum((prev) => [prev[0] + 1, prev[1]]); - } catch (e: any) { - // 跳過失敗 - item.error = e.toString(); - } - }; - - const importScripts = async (scripts: ScriptData[]) => { - const promises: Promise[] = []; - for (const item of scripts) { - if (item.install && !item.error) { - promises.push(scriptImportAsync(item)); - } - } - return Promise.all(promises); - }; - - const handleScriptToggle = (index: number) => { - let bool: boolean; - setScripts((prevScripts) => { - prevScripts = prevScripts.map((script, i) => (i === index ? { ...script, install: !script.install } : script)); - bool = prevScripts.every((script) => script.install); - return prevScripts; - }); - setSelectAll((prev) => [bool, prev[1]]); - }; - - const { - importButtonClick, - closeButtonClick, - handleSelectAllScripts, - handleSelectAllSubscribes, - handleScriptToggleClick, - handleScriptStatusToggle, - } = { - importButtonClick: async () => { - setInstallNum((prev) => [0, prev[1]]); - setLoading(true); - await importScripts(scripts); - setLoading(false); - Message.success(t("import_success")!); - }, - closeButtonClick: () => window.close(), - handleSelectAllScripts: () => { - setSelectAll((prev) => { - const newValue = !prev[0]; - setScripts((prevScripts) => prevScripts.map((script) => ({ ...script, install: newValue }))); - return [newValue, prev[1]]; - }); - }, - handleSelectAllSubscribes: () => { - setSelectAll((prev) => { - const newValue = !prev[1]; - setSubscribes((prevSubscribes) => prevSubscribes.map((subscribe) => ({ ...subscribe, install: newValue }))); - return [prev[0], newValue]; - }); - }, - handleScriptToggleClick: handleScriptToggle, - handleScriptStatusToggle: (index: number, checked: boolean) => { - setScripts((prevScripts) => - prevScripts.map((prevScript, i) => - i === index - ? { - ...prevScript, - script: { - ...prevScript.script!, - script: { - ...prevScript.script!.script, - status: checked ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE, - }, - }, - } - : prevScript - ) - ); - }, - }; - - return ( -
- - - - - - - - {`${t("select_scripts_to_import")}: `} - - {t("select_all")} - - - {`${t("script_import_progress")}: ${installNum[0]}/${scripts.length}`} - - - {`${t("select_subscribes_to_import")}: `} - - {t("select_all")} - - - {`${t("subscribe_import_progress")}: ${installNum[1]}/${subscribes.length}`} - - {scripts.length > 0 && ( - ( - - )} - /> - )} - - -
- ); +import { useIsMobile } from "@App/pages/components/use-is-mobile"; +import { useImport } from "./hooks"; +import { DesktopView } from "./components"; +import { MobileView } from "./mobile"; + +export default function App() { + const view = useImport(); + const isMobile = useIsMobile(); + return isMobile ? : ; } - -export default App; diff --git a/src/pages/import/components.test.tsx b/src/pages/import/components.test.tsx new file mode 100644 index 000000000..437f04675 --- /dev/null +++ b/src/pages/import/components.test.tsx @@ -0,0 +1,218 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeAll, afterEach, vi } from "vitest"; +import { cleanup, screen, fireEvent, within } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { renderWithTooltip } from "@Tests/renderWithTooltip"; +import { DesktopView, type ImportView } from "./components"; +import type { ScriptImportItem, SubscribeImportItem } from "./logic"; + +function mkScriptItem(p: Partial = {}): ScriptImportItem { + return { + id: "u1", + uuid: "u1", + name: "示例脚本", + author: "CodFrm", + iconUrl: "", + op: "add", + oldVersion: "", + newVersion: "1.2.0", + source: { kind: "url", host: "example.com", full: "https://example.com/a.user.js" }, + valueCount: 0, + hasResources: false, + enabled: true, + importable: true, + ...p, + }; +} + +function mkSubItem(p: Partial = {}): SubscribeImportItem { + return { + id: "s1", + url: "https://example.com/a.sub.js", + name: "示例订阅", + op: "add", + importable: true, + ...p, + }; +} + +function mkView(p: Partial = {}): ImportView { + return { + phase: "ready", + filename: "backup-20260617.zip", + errorMessage: "", + scripts: [], + subscribes: [], + selectedScripts: new Set(), + selectedSubscribes: new Set(), + importStatus: {}, + doneCount: 0, + totalCount: 0, + summary: { scripts: 0, subscribes: 0, values: 0 }, + onToggleScript: () => {}, + onToggleAllScripts: () => {}, + onToggleSubscribe: () => {}, + onToggleAllSubscribes: () => {}, + onSetEnabled: () => {}, + onImport: () => {}, + onCancel: () => {}, + onClose: () => {}, + onRetry: () => {}, + onOpenScriptList: () => {}, + ...p, + }; +} + +const renderDesktop = (p: Partial) => renderWithTooltip(); + +beforeAll(() => initTestLanguage("zh-CN")); +afterEach(cleanup); + +describe("导入桌面视图 脚本表格", () => { + it("新增脚本显示「新增」徽章与单版本", () => { + renderDesktop({ scripts: [mkScriptItem({ op: "add", newVersion: "3.2.1" })] }); + expect(screen.getByText("示例脚本")).toBeTruthy(); + expect(screen.getByText("新增")).toBeTruthy(); + expect(screen.getByText("v3.2.1")).toBeTruthy(); + }); + + it("更新脚本显示「更新」徽章与旧→新版本", () => { + renderDesktop({ scripts: [mkScriptItem({ op: "update", oldVersion: "1.0.0", newVersion: "2.0.0" })] }); + expect(screen.getByText("更新")).toBeTruthy(); + expect(screen.getByText("v1.0.0")).toBeTruthy(); + expect(screen.getByText("v2.0.0")).toBeTruthy(); + }); + + it("解析失败脚本显示「解析失败」徽章、复选框禁用、无启用开关", () => { + renderDesktop({ + scripts: [mkScriptItem({ id: "e1", op: "error", importable: false, enabled: false, error: "boom" })], + }); + expect(screen.getByText("解析失败")).toBeTruthy(); + expect(screen.queryByTestId("enable-switch-e1")).toBeNull(); + const cb = screen.getByTestId("script-checkbox-e1") as HTMLButtonElement; + expect(cb.getAttribute("disabled") !== null || cb.getAttribute("data-disabled") !== null).toBe(true); + }); + + it("含 values 的脚本数据列显示条数", () => { + renderDesktop({ scripts: [mkScriptItem({ valueCount: 3 })] }); + expect(screen.getByText("3 项")).toBeTruthy(); + }); + + it("本地创建脚本来源显示「本地创建」", () => { + renderDesktop({ scripts: [mkScriptItem({ source: { kind: "local" } })] }); + expect(screen.getByText("本地创建")).toBeTruthy(); + }); +}); + +describe("导入桌面视图 订阅分区", () => { + it("无订阅时不渲染订阅分区", () => { + renderDesktop({ scripts: [mkScriptItem()] }); + expect(screen.queryByTestId("subscribe-section")).toBeNull(); + }); + + it("有订阅时渲染订阅分区与订阅行", () => { + renderDesktop({ scripts: [mkScriptItem()], subscribes: [mkSubItem({ name: "我的订阅" })] }); + expect(screen.getByTestId("subscribe-section")).toBeTruthy(); + expect(screen.getByText("我的订阅")).toBeTruthy(); + }); +}); + +describe("导入桌面视图 交互", () => { + it("点击导入按钮触发 onImport", () => { + const onImport = vi.fn(); + renderDesktop({ scripts: [mkScriptItem()], selectedScripts: new Set(["u1"]), onImport }); + fireEvent.click(screen.getByTestId("import-btn")); + expect(onImport).toHaveBeenCalledTimes(1); + }); + + it("无勾选项时导入按钮禁用", () => { + renderDesktop({ scripts: [mkScriptItem()], selectedScripts: new Set() }); + expect((screen.getByTestId("import-btn") as HTMLButtonElement).disabled).toBe(true); + }); + + it("点击全选触发 onToggleAllScripts", () => { + const onToggleAllScripts = vi.fn(); + renderDesktop({ scripts: [mkScriptItem()], onToggleAllScripts }); + fireEvent.click(screen.getByTestId("toggle-all-scripts")); + expect(onToggleAllScripts).toHaveBeenCalledTimes(1); + }); + + it("点击行复选框触发 onToggleScript", () => { + const onToggleScript = vi.fn(); + renderDesktop({ scripts: [mkScriptItem({ id: "u9", uuid: "u9" })], onToggleScript }); + fireEvent.click(screen.getByTestId("script-checkbox-u9")); + expect(onToggleScript).toHaveBeenCalledWith("u9"); + }); + + it("切换启用开关触发 onSetEnabled", () => { + const onSetEnabled = vi.fn(); + renderDesktop({ scripts: [mkScriptItem({ id: "u1", enabled: true })], onSetEnabled }); + fireEvent.click(screen.getByTestId("enable-switch-u1")); + expect(onSetEnabled).toHaveBeenCalledWith("u1", false); + }); +}); + +describe("导入桌面视图 状态屏", () => { + it("loading 阶段显示解析中标题", () => { + renderDesktop({ phase: "loading" }); + expect(screen.getByTestId("import-loading")).toBeTruthy(); + expect(screen.getByText("正在解析备份文件")).toBeTruthy(); + }); + + it("error 阶段显示错误标题与信息,点击重试触发 onRetry", () => { + const onRetry = vi.fn(); + renderDesktop({ phase: "error", errorMessage: "Error: failed to parse zip", onRetry }); + expect(screen.getByTestId("import-error")).toBeTruthy(); + expect(screen.getByText("Error: failed to parse zip")).toBeTruthy(); + fireEvent.click(screen.getByTestId("retry-btn")); + expect(onRetry).toHaveBeenCalledTimes(1); + }); + + it("error 阶段原始错误置于等宽固定宽度等宽字体框内(对照设计稿 §9 错误模式)", () => { + renderDesktop({ phase: "error", errorMessage: "Error: corrupted central directory" }); + const box = screen.getByTestId("error-detail-box"); + // 固定等宽框:固定宽度 + 等宽字体 + expect(box.className).toContain("w-[440px]"); + expect(box.className).toContain("font-mono"); + expect(within(box).getByText("Error: corrupted central directory")).toBeTruthy(); + }); + + it("invalid 阶段显示错误屏且无重试按钮、无原始错误框", () => { + renderDesktop({ phase: "invalid" }); + expect(screen.getByTestId("import-error")).toBeTruthy(); + expect(screen.queryByTestId("retry-btn")).toBeNull(); + expect(screen.queryByTestId("error-detail-box")).toBeNull(); + }); + + it("empty 阶段显示空备份屏", () => { + renderDesktop({ phase: "empty" }); + expect(screen.getByTestId("import-empty")).toBeTruthy(); + }); + + it("importing 阶段显示顶部进度条与 N/M 进度,行内状态取代复选框", () => { + renderDesktop({ + phase: "importing", + scripts: [mkScriptItem({ id: "u1" })], + selectedScripts: new Set(["u1"]), + importStatus: { u1: "done" }, + doneCount: 1, + totalCount: 2, + }); + expect(screen.getByRole("progressbar")).toBeTruthy(); + expect(screen.getByTestId("status-done-u1")).toBeTruthy(); + expect(screen.queryByTestId("script-checkbox-u1")).toBeNull(); + }); + + it("done 阶段显示完成屏与统计,点击查看脚本列表触发 onOpenScriptList", () => { + const onOpenScriptList = vi.fn(); + renderDesktop({ + phase: "done", + summary: { scripts: 4, subscribes: 1, values: 12 }, + onOpenScriptList, + }); + const complete = screen.getByTestId("import-complete"); + expect(within(complete).getByText("导入完成")).toBeTruthy(); + fireEvent.click(screen.getByTestId("view-scripts-btn")); + expect(onOpenScriptList).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/pages/import/components.tsx b/src/pages/import/components.tsx new file mode 100644 index 000000000..13a700a2d --- /dev/null +++ b/src/pages/import/components.tsx @@ -0,0 +1,750 @@ +import { useState, type ReactNode } from "react"; +import { + ArrowRight, + Ban, + CircleCheck, + CircleX, + CloudOff, + Database, + Download, + FileArchive, + FileCode, + FileX, + Globe, + List, + Loader2, + PackageOpen, + Pencil, + Plus, + RefreshCw, + Rss, + ShieldCheck, + Timer, + TriangleAlert, +} from "lucide-react"; +import { cn } from "@App/pkg/utils/cn"; +import { t } from "@App/locales/locales"; +import { Button } from "@App/pages/components/ui/button"; +import { Checkbox } from "@App/pages/components/ui/checkbox"; +import { Switch } from "@App/pages/components/ui/switch"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@App/pages/components/ui/tooltip"; +import { + importableScriptIds, + importableSubscribeIds, + type ImportOp, + type ImportSource, + type ScriptImportItem, + type SubscribeImportItem, +} from "./logic"; + +/** install:importpage 命名空间下的翻译快捷方法 */ +export const tk = (key: string, opt?: Record): string => t(`install:importpage.${key}`, opt); + +export type ImportPhase = "loading" | "invalid" | "error" | "empty" | "ready" | "importing" | "done"; + +/** 导入过程中单项的逐行状态 */ +export type ImportItemStatus = "pending" | "importing" | "done" | "skipped"; + +/** 导入页视图(桌面/移动共用)所需的数据与回调 */ +export interface ImportView { + phase: ImportPhase; + /** 备份文件名(工具栏来源 chip / 加载屏展示) */ + filename: string; + /** 失败屏展示的错误信息 */ + errorMessage: string; + scripts: ScriptImportItem[]; + subscribes: SubscribeImportItem[]; + selectedScripts: Set; + selectedSubscribes: Set; + /** 导入进行中/完成时,各项的逐行状态(id → status) */ + importStatus: Record; + doneCount: number; + totalCount: number; + /** 完成屏统计(已勾选可导入项) */ + summary: { scripts: number; subscribes: number; values: number }; + onToggleScript: (id: string) => void; + onToggleAllScripts: () => void; + onToggleSubscribe: (id: string) => void; + onToggleAllSubscribes: () => void; + onSetEnabled: (id: string, enabled: boolean) => void; + onImport: () => void; + onCancel: () => void; + onClose: () => void; + onRetry: () => void; + onOpenScriptList: () => void; +} + +const PILL = "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap"; + +const COL = { + version: "w-[150px] shrink-0", + source: "w-[150px] shrink-0", + data: "w-24 shrink-0", + status: "w-[92px] shrink-0", + enable: "w-[72px] shrink-0", +}; + +function BrandMark() { + return ( +
+
+ {"S"} +
+ {"ScriptCat"} +
+ ); +} + +/** 顶部进度条:导入进行中时贴在 TopBar 下方,不随内容滚动;按 done/total 确定填充 */ +export function TopProgressBar({ done, total }: { done: number; total: number }) { + const pct = total > 0 ? Math.round((done / total) * 100) : 0; + return ( +
+
+
+ ); +} + +/** TopBar 上下文 chip:随阶段切换语义色 + 图标(审阅/解析/导入/完成/失败) */ +const CHIP: Record = { + ready: { cls: "bg-muted text-fg-secondary", icon: , key: "context_review" }, + empty: { cls: "bg-muted text-fg-secondary", icon: , key: "context_review" }, + loading: { + cls: "bg-primary-light text-primary", + icon: , + key: "context_review", + }, + importing: { + cls: "bg-primary-light text-primary", + icon: , + key: "context_importing", + }, + done: { + cls: "bg-success-bg text-success-fg", + icon: , + key: "context_done", + }, + error: { + cls: "bg-destructive/10 text-destructive", + icon: , + key: "context_review", + }, + invalid: { + cls: "bg-destructive/10 text-destructive", + icon: , + key: "context_review", + }, +}; + +export function ContextChip({ phase }: { phase: ImportPhase }) { + const c = CHIP[phase]; + return ( + + {c.icon} + {tk(c.key)} + + ); +} + +/** 外壳:吸顶 TopBar(含上下文 chip)+ 可选顶部进度条 + 滚动 ContentArea + 可选吸底 ActionBar */ +export function ImportLayout({ + phase, + progress, + actions, + children, +}: { + phase: ImportPhase; + progress?: { done: number; total: number }; + actions?: ReactNode; + children: ReactNode; +}) { + return ( +
+
+ + +
+ {progress && } +
+
{children}
+
+ {actions && ( +
+
{actions}
+
+ )} +
+ ); +} + +/** 脚本图标:有 @icon 显示图片,失败/缺省回退图标块;解析失败用红色 file-x 块 */ +export function ScriptAvatar({ item, size = 30 }: { item: ScriptImportItem; size?: number }) { + const [error, setError] = useState(false); + const box = "flex shrink-0 items-center justify-center rounded-lg"; + const style = { width: size, height: size }; + if (item.op === "error") { + return ( + + + + ); + } + if (item.iconUrl && !error) { + return ( + {item.name} setError(true)} + className="shrink-0 rounded-lg object-cover" + style={style} + /> + ); + } + return ( + + + + ); +} + +const OP_CLASS: Record = { + add: "bg-success-bg text-success-fg", + update: "bg-primary-light text-primary", + error: "bg-destructive/10 text-destructive", +}; +const OP_KEY: Record = { add: "op_add", update: "op_update", error: "op_error" }; +const OP_ICON: Record = { + add: , + update: , + error: , +}; + +export function OpBadge({ op }: { op: ImportOp }) { + return ( + + {OP_ICON[op]} + {tk(OP_KEY[op])} + + ); +} + +export function VersionCell({ item }: { item: ScriptImportItem }) { + if (item.op === "error") return {"—"}; + if (item.op === "update") { + return ( +
+ {`v${item.oldVersion}`} + + {`v${item.newVersion}`} +
+ ); + } + return ( + + {`v${item.newVersion}`} + + ); +} + +export function SourceCell({ source }: { source: ImportSource }) { + if (source.kind === "none") return {"—"}; + if (source.kind === "local") { + return ( + + + {tk("source_local")} + + ); + } + return ( + + + + + {source.host} + + + {source.full} + + ); +} + +export function DataCell({ item }: { item: ScriptImportItem }) { + if (item.op === "error" || (item.valueCount === 0 && !item.hasResources)) { + return {"—"}; + } + return ( + + + {item.valueCount > 0 ? tk("data_values", { count: item.valueCount }) : tk("data_resources")} + + ); +} + +const STATUS_ICON: Record = { + pending: , + importing: , + done: , + skipped: , +}; + +export function ImportStatusIcon({ status, id }: { status: ImportItemStatus; id: string }) { + return ( + + {STATUS_ICON[status]} + + ); +} + +/** 桌面端脚本行 */ +function ScriptRow({ item, view }: { item: ScriptImportItem; view: ImportView }) { + const inProgress = view.phase === "importing" || view.phase === "done"; + const status: ImportItemStatus = view.importStatus[item.id] ?? "pending"; + const dim = item.op === "error" ? "opacity-60" : ""; + return ( +
+
+ {inProgress ? ( + + ) : ( + view.onToggleScript(item.id)} + className={item.op === "error" ? "opacity-45" : ""} + /> + )} +
+
+ +
+ {item.name || tk("unknown_script")} + {item.author && {item.author}} +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {item.op === "error" ? ( + {"—"} + ) : ( + view.onSetEnabled(item.id, v)} + /> + )} +
+
+ ); +} + +function ScriptTable({ view }: { view: ImportView }) { + return ( +
+
+
+
{tk("col_script")}
+
{tk("col_version")}
+
{tk("col_source")}
+
{tk("col_data")}
+
{tk("col_status")}
+
{tk("col_enabled")}
+
+ {view.scripts.map((item) => ( + + ))} +
+ ); +} + +/** 订阅行 */ +function SubscribeRow({ item, view }: { item: SubscribeImportItem; view: ImportView }) { + const inProgress = view.phase === "importing" || view.phase === "done"; + const status: ImportItemStatus = view.importStatus[item.id] ?? "pending"; + const dim = item.op === "error" ? "opacity-60" : ""; + return ( +
+
+ {inProgress ? ( + + ) : ( + view.onToggleSubscribe(item.id)} + className={item.op === "error" ? "opacity-45" : ""} + /> + )} +
+
+ + + +
+ {item.name || tk("unknown_script")} + {item.url && {item.url}} +
+
+
+ +
+
+ ); +} + +function SubscribeSection({ view }: { view: ImportView }) { + if (view.subscribes.length === 0) return null; + const importable = importableSubscribeIds(view.subscribes); + const selectedCount = importable.filter((id) => view.selectedSubscribes.has(id)).length; + const allSelected = importable.length > 0 && selectedCount === importable.length; + const reviewing = view.phase === "ready"; + return ( +
+
+
+ + {tk("subscribe_section")} + {view.subscribes.length} +
+ {reviewing && ( +
+ + + {tk("selected_count", { selected: selectedCount, total: importable.length })} + +
+ )} +
+
+ {view.subscribes.map((item) => ( + + ))} +
+
+ ); +} + +/** 备份来源 chip 文案:文件名 · N 脚本 · M 订阅 */ +function backupSourceLabel(view: ImportView): string { + const parts = [view.filename, tk("count_scripts", { count: view.scripts.length })]; + if (view.subscribes.length > 0) parts.push(tk("count_subscribes", { count: view.subscribes.length })); + return parts.filter(Boolean).join(" · "); +} + +function ImportToolbar({ view }: { view: ImportView }) { + const importable = importableScriptIds(view.scripts); + const selectedCount = importable.filter((id) => view.selectedScripts.has(id)).length; + const allSelected = importable.length > 0 && selectedCount === importable.length; + const unimportable = view.scripts.length - importable.length; + return ( +
+
+ + + {tk("selected_count", { selected: selectedCount, total: importable.length })} + + {unimportable > 0 && ( + <> + {"·"} + + {tk("unimportable_count", { count: unimportable })} + + + )} +
+ + + {backupSourceLabel(view)} + +
+ ); +} + +function ImportingToolbar({ view }: { view: ImportView }) { + return ( +
+ + + {tk("importing_progress", { done: view.doneCount, total: view.totalCount })} + + {"·"} + {tk("importing_hint")} +
+ ); +} + +function ReadyActions({ view }: { view: ImportView }) { + const total = view.selectedScripts.size + view.selectedSubscribes.size; + return ( +
+ + + {tk("trust_hint")} + +
+ + +
+
+ ); +} + +function ImportingActions({ view }: { view: ImportView }) { + return ( +
+ + {tk("importing_actionbar_hint")} + {`${view.doneCount} / ${view.totalCount}`} + + +
+ ); +} + +function StatChip({ icon, children }: { icon: ReactNode; children: ReactNode }) { + return ( + + {icon} + {children} + + ); +} + +function CenteredState({ children, testid }: { children: ReactNode; testid: string }) { + return ( +
+ {children} +
+ ); +} + +/** 状态屏的 88px 图标环(语义色面 + 居中图标) */ +function StateRing({ className, children }: { className: string; children: ReactNode }) { + return {children}; +} + +function StateTexts({ title, desc }: { title: string; desc: string }) { + return ( +
+ {title} + {desc} +
+ ); +} + +export function ImportLoading({ filename }: { filename: string }) { + return ( + + + + + +
+
+
+ {filename && ( + + + {filename} + + )} + + ); +} + +export function ImportErrorScreen({ + desc, + detail, + onRetry, + onClose, +}: { + desc: string; + detail?: string; + onRetry?: () => void; + onClose: () => void; +}) { + return ( + + + + + + {detail && ( +
+ + {detail} +
+ )} +
+ + {onRetry && ( + + )} +
+
+ ); +} + +export function EmptyBackup({ onClose }: { onClose: () => void }) { + return ( + + + + + + + + ); +} + +export function ImportComplete({ view }: { view: ImportView }) { + const { summary } = view; + return ( + + + + + +
+ }> + {tk("done_stat_scripts", { count: summary.scripts })} + + {summary.subscribes > 0 && ( + }> + {tk("done_stat_subscribes", { count: summary.subscribes })} + + )} + {summary.values > 0 && ( + }> + {tk("done_stat_values", { count: summary.values })} + + )} +
+
+ + +
+
+ ); +} + +/** 桌面端整页视图 */ +export function DesktopView({ view }: { view: ImportView }) { + if (view.phase === "loading") { + return ( + + + + ); + } + if (view.phase === "invalid") { + return ( + + + + ); + } + if (view.phase === "error") { + return ( + + + + ); + } + if (view.phase === "empty") { + return ( + + + + ); + } + if (view.phase === "done") { + return ( + + + + ); + } + + // ready / importing + const importing = view.phase === "importing"; + return ( + : } + > + {importing ? : } + + + + ); +} diff --git a/src/pages/import/hooks.test.ts b/src/pages/import/hooks.test.ts new file mode 100644 index 000000000..88456d5a0 --- /dev/null +++ b/src/pages/import/hooks.test.ts @@ -0,0 +1,223 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, act, waitFor } from "@testing-library/react"; +import { initLanguage } from "@App/locales/locales"; +import type { Script } from "@App/app/repo/scripts"; +import type { ScriptBackupData, SubscribeBackupData } from "@App/pkg/backup/struct"; + +// useImport 通过 cache→fetch→zip→parse→prepare 装配数据,再逐项调用 store clients 导入。 +// 这里把这些副作用全部打桩,验证「装配/默认勾选/状态机/导入编排」逻辑。 +const h = vi.hoisted(() => ({ + backup: { script: [] as ScriptBackupData[], subscribe: [] as SubscribeBackupData[] }, + cacheGet: vi.fn(), + fetch: vi.fn(), + loadAsyncJSZip: vi.fn(() => Promise.resolve("ZIP")), + parseBackupZipFile: vi.fn(), + prepareScriptByCode: vi.fn(), + prepareSubscribeByCode: vi.fn(), + install: vi.fn((_params: { script: { uuid: string; sort?: number }; code: string }) => + Promise.resolve({ update: false, updatetime: 0 }) + ), + importResources: vi.fn((..._args: unknown[]) => Promise.resolve()), + setScriptValues: vi.fn((_params: { uuid: string; isReplace: boolean }) => Promise.resolve()), + subscribeInstall: vi.fn((_subscribe: { url: string }) => Promise.resolve("url")), +})); + +function mkScript(p: Partial + ScriptCat + + + +
+ + diff --git a/src/pages/install/App.test.tsx b/src/pages/install/App.test.tsx new file mode 100644 index 000000000..d9dd1e745 --- /dev/null +++ b/src/pages/install/App.test.tsx @@ -0,0 +1,166 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, type Mock } from "vitest"; +import { render, screen, cleanup, fireEvent } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { mockMatchMedia } from "@Tests/mockMatchMedia"; + +vi.mock("./useInstallData", () => ({ useInstallData: vi.fn() })); +// Monaco 编辑器无法在 jsdom 渲染(需 worker + ThemeProvider),用桩替换 +vi.mock("@App/pages/components/CodeEditor", () => import("@Tests/mocks/CodeEditor.tsx")); + +import { useInstallData, type InstallView } from "./useInstallData"; +import App from "./App"; + +const mockHook = useInstallData as Mock; + +const baseHook = () => ({ + enabled: true, + setEnabled: vi.fn(), + localFile: false, + watching: false, + toggleWatch: vi.fn(), + install: vi.fn(), + close: vi.fn(), + installSkill: vi.fn(), + cancelSkill: vi.fn(), + retry: vi.fn(), +}); + +const readyView = (over: Partial = {}): InstallView => ({ + isUpdate: false, + isSubscribe: false, + name: "全网每日签到助手", + author: "scriptcat", + source: "example.com", + description: "示例", + version: { kind: "install", version: "2.3.1" }, + permissions: [{ kind: "match", risk: "normal", values: ["https://e.com/*"], sensitive: [] }], + antifeatures: [], + schedule: null, + code: "// a\n// b", + subscribeScripts: [], + ...over, +}); + +beforeEach(() => { + mockMatchMedia(); +}); + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("Install App 状态分流", () => { + it("loading 状态渲染加载屏", () => { + mockHook.mockReturnValue({ ...baseHook(), state: { status: "loading" } }); + render(); + expect(screen.getByText("正在加载脚本")).toBeInTheDocument(); + }); + + it("invalid 状态渲染无效页面", () => { + mockHook.mockReturnValue({ ...baseHook(), state: { status: "invalid" } }); + render(); + expect(screen.getByText("无效页面")).toBeInTheDocument(); + }); + + it("error 状态渲染失败屏与错误信息", () => { + mockHook.mockReturnValue({ ...baseHook(), state: { status: "error", message: "boom-404" } }); + render(); + expect(screen.getByText("boom-404")).toBeInTheDocument(); + }); + + it("error 状态提供重试按钮,点击调用 retry", () => { + const retry = vi.fn(); + mockHook.mockReturnValue({ ...baseHook(), retry, state: { status: "error", message: "boom" } }); + render(); + fireEvent.click(screen.getByText("重试")); + expect(retry).toHaveBeenCalledTimes(1); + }); + + it("invalid 状态不提供重试按钮", () => { + mockHook.mockReturnValue({ ...baseHook(), state: { status: "invalid" } }); + render(); + expect(screen.queryByText("重试")).not.toBeInTheDocument(); + }); + + it("ready 状态渲染身份卡、权限卡、代码卡与安装按钮", () => { + mockHook.mockReturnValue({ ...baseHook(), state: { status: "ready", view: readyView() } }); + render(); + expect(screen.getByText("全网每日签到助手")).toBeInTheDocument(); + expect(screen.getByText("此脚本将获得以下权限")).toBeInTheDocument(); + expect(screen.getByText("2 行")).toBeInTheDocument(); + expect(screen.getByTestId("install-primary")).toHaveTextContent("安装"); + }); + + it("ready 更新态顶部上下文标题为脚本更新", () => { + mockHook.mockReturnValue({ + ...baseHook(), + state: { + status: "ready", + view: readyView({ + isUpdate: true, + version: { kind: "update", oldVersion: "1.0.0", newVersion: "2.0.0", changed: true }, + }), + }, + }); + render(); + expect(screen.getByText("脚本更新")).toBeInTheDocument(); + }); + + it("skill 状态渲染技能安装视图", () => { + mockHook.mockReturnValue({ + ...baseHook(), + state: { + status: "skill", + skill: { + skillMd: "# s", + metadata: { name: "我的技能" }, + prompt: "提示词", + scripts: [], + references: [], + isUpdate: false, + }, + }, + }); + render(); + expect(screen.getByText("我的技能")).toBeInTheDocument(); + expect(screen.getByTestId("skill-install")).toBeInTheDocument(); + }); + + it("订阅安装时渲染脚本列表卡而非权限卡", () => { + mockHook.mockReturnValue({ + ...baseHook(), + state: { + status: "ready", + view: readyView({ isSubscribe: true, subscribeScripts: ["https://s.cat/1.user.js"] }), + }, + }); + render(); + expect(screen.getByText("本订阅将安装以下脚本")).toBeInTheDocument(); + expect(screen.getByText("https://s.cat/1.user.js")).toBeInTheDocument(); + expect(screen.queryByText("此脚本将获得以下权限")).not.toBeInTheDocument(); + }); + + it("本地文件安装时显示监听文件按钮", () => { + mockHook.mockReturnValue({ + ...baseHook(), + localFile: true, + state: { status: "ready", view: readyView() }, + }); + render(); + expect(screen.getByTestId("watch-toggle")).toBeInTheDocument(); + }); + + it("监听中显示监听横幅且安装按钮禁用", () => { + mockHook.mockReturnValue({ + ...baseHook(), + localFile: true, + watching: true, + watchFileName: "checkin.user.js", + state: { status: "ready", view: readyView() }, + }); + render(); + expect(screen.getByTestId("watching-banner")).toBeInTheDocument(); + expect(screen.getByTestId("install-primary")).toBeDisabled(); + }); +}); diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index f0b0227d6..3897686b1 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -1,64 +1,142 @@ -import { Space, Typography } from "@arco-design/web-react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useInstallData } from "./hooks"; -import SkillInstallView from "./components/SkillInstallView"; -import ScriptInstallView from "./components/ScriptInstallView"; +import { Download, RefreshCw, Rss, HardDrive } from "lucide-react"; +import { useIsMobile } from "@App/pages/components/use-is-mobile"; +import { isPermissionOk } from "@App/pkg/utils/utils"; +import { InstallLayout } from "./components/InstallLayout"; +import { ScriptIdentity } from "./components/ScriptIdentity"; +import { PermissionCard } from "./components/PermissionCard"; +import { SubscribeScripts } from "./components/SubscribeScripts"; +import { SkillInstallView } from "./components/SkillInstallView"; +import { CodePreview } from "./components/CodePreview"; +import { InstallActions } from "./components/InstallActions"; +import { InstallWarning } from "./components/InstallWarning"; +import { InstallLoading, InstallError } from "./components/InstallStates"; +import { WatchingBanner } from "./components/WatchingBanner"; +import { BackgroundPrompt, backgroundPromptShownKey } from "./components/BackgroundPrompt"; +import { useInstallData } from "./useInstallData"; -function App() { - const data = useInstallData(); - const { t } = useTranslation(); +export default function App() { + const { t } = useTranslation(["install", "common"]); + const isMobile = useIsMobile(); + const { + state, + enabled, + setEnabled, + localFile, + watching, + watchFileName, + lastSync, + toggleWatch, + install, + close, + installSkill, + cancelSkill, + retry, + } = useInstallData(); + const [bgPrompt, setBgPrompt] = useState<{ scriptType: string } | null>(null); - // Skill ZIP 安装 - if (data.skillPreview) { + // 后台/定时脚本首次安装时,提示开启后台运行(对照 v1.4 checkBackgroundPrompt) + const ready = state.status === "ready" ? state.view : null; + const schedule = ready?.schedule; + useEffect(() => { + if (!ready || ready.isSubscribe || !schedule) return; + if (localStorage.getItem(backgroundPromptShownKey) === "true") return; + let cancelled = false; + isPermissionOk("background").then((ok) => { + if (!cancelled && ok === false) { + setBgPrompt({ + scriptType: schedule.kind === "cron" ? t("install:scheduled_script") : t("install:background_script"), + }); + } + }); + return () => { + cancelled = true; + }; + }, [ready, schedule, t]); + + if (state.status === "loading") { + return ; + } + if (state.status === "invalid") { + return ; + } + if (state.status === "error") { + return ; + } + if (state.status === "skill") { return ( ); } - // URL 加载中 / 错误 / 无效页面 - if (!data.hasValidSourceParam) { - return data.urlHref ? ( -
- - {data.fetchingState.loadingStatus && ( - <> - {t("install_page_loading")} -
- - {data.fetchingState.loadingStatus} - -
-
- - )} - {data.fetchingState.errorStatus && ( - <> - {t("install_page_load_failed")} -
{data.fetchingState.errorStatus}
- - )} -
-
- ) : ( -
- - {t("invalid_page")} - -
- ); - } + const view = state.view; + const baseTitle = view.isSubscribe + ? view.isUpdate + ? t("install:update_subscribe") + : t("install:subscribe") + : view.isUpdate + ? t("install:context_update") + : t("install:context_install"); + // 监听本地文件时,顶栏上下文 chip 切换为品牌蓝脉冲「监听中」(对照设计稿) + const title = watching ? t("install:watching_chip") : baseTitle; + const titleTone = watching ? "watching" : "default"; + const titleIcon = view.isSubscribe ? Rss : view.isUpdate ? RefreshCw : localFile ? HardDrive : Download; - // UserScript / Subscribe 安装 - return ; + return ( + <> + + } + > + + {watching && } + {view.isSubscribe ? ( + + ) : ( + + )} + p.risk === "danger")} + hasAntifeature={view.antifeatures.length > 0} + /> + + + setBgPrompt(null)} /> + + ); } - -export default App; diff --git a/src/pages/install/components/BackgroundPrompt.test.tsx b/src/pages/install/components/BackgroundPrompt.test.tsx new file mode 100644 index 000000000..ec560c805 --- /dev/null +++ b/src/pages/install/components/BackgroundPrompt.test.tsx @@ -0,0 +1,45 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { BackgroundPrompt, backgroundPromptShownKey } from "./BackgroundPrompt"; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(() => { + cleanup(); + localStorage.clear(); + vi.restoreAllMocks(); +}); + +describe("BackgroundPrompt 后台权限弹窗", () => { + it("open 时渲染标题、说明与按钮", () => { + render( {}} />); + expect(screen.getByText("是否开启后台运行?")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "立即启用" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "暂不启用" })).toBeInTheDocument(); + }); + + it("点击立即启用请求 background 权限并回调结果,记录已展示", async () => { + const req = vi.spyOn(chrome.permissions, "request"); + const onResult = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button", { name: "立即启用" })); + await waitFor(() => expect(onResult).toHaveBeenCalledWith(true)); + expect(req).toHaveBeenCalledWith({ permissions: ["background"] }); + expect(localStorage.getItem(backgroundPromptShownKey)).toBe("true"); + }); + + it("点击暂不启用回调 false 且记录已展示", () => { + const onResult = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button", { name: "暂不启用" })); + expect(onResult).toHaveBeenCalledWith(false); + expect(localStorage.getItem(backgroundPromptShownKey)).toBe("true"); + }); + + it("open 为 false 时不渲染对话框", () => { + render( {}} />); + expect(screen.queryByText("是否开启后台运行?")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/BackgroundPrompt.tsx b/src/pages/install/components/BackgroundPrompt.tsx new file mode 100644 index 000000000..96ac3de63 --- /dev/null +++ b/src/pages/install/components/BackgroundPrompt.tsx @@ -0,0 +1,66 @@ +import { useTranslation } from "react-i18next"; +import { Rocket } from "lucide-react"; +import { Button } from "@App/pages/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} from "@App/pages/components/ui/dialog"; + +export const backgroundPromptShownKey = "background_prompt_shown"; + +export function BackgroundPrompt({ + open, + scriptType, + onResult, +}: { + open: boolean; + scriptType: string; + onResult: (enabled: boolean) => void; +}) { + const { t } = useTranslation(["settings", "common"]); + + const enable = async () => { + localStorage.setItem(backgroundPromptShownKey, "true"); + const granted = await chrome.permissions.request({ permissions: ["background"] }).catch(() => false); + onResult(!!granted); + }; + + const later = () => { + localStorage.setItem(backgroundPromptShownKey, "true"); + onResult(false); + }; + + return ( + { + if (!o) later(); + }} + > + + + + + + + {t("settings:enable_background.prompt_title")} + + + {t("settings:enable_background.prompt_description", { scriptType })} + + +

{t("settings:enable_background.settings_hint")}

+ + + + +
+
+ ); +} diff --git a/src/pages/install/components/CodePreview.test.tsx b/src/pages/install/components/CodePreview.test.tsx new file mode 100644 index 000000000..ad4d75dbf --- /dev/null +++ b/src/pages/install/components/CodePreview.test.tsx @@ -0,0 +1,62 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup, fireEvent } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; + +// Monaco 无法在 jsdom 中渲染(需 worker),用轻量桩替换,仅暴露 props 供断言接线 +vi.mock("@App/pages/components/CodeEditor", () => import("@Tests/mocks/CodeEditor.tsx")); + +import { CodePreview } from "./CodePreview"; + +const code = "// line1\nconst a = 1;\nconsole.log(a);"; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("CodePreview 代码卡", () => { + it("渲染语言标签与行数", () => { + render(); + expect(screen.getByText("JavaScript")).toBeInTheDocument(); + expect(screen.getByText("3 行")).toBeInTheDocument(); + }); + + it("默认展开,把代码传给 Monaco 编辑器;折叠后不挂载编辑器", () => { + render(); + const body = screen.getByTestId("code-body"); + expect(body).toBeInTheDocument(); + expect(body).toHaveAttribute("data-code", code); + fireEvent.click(screen.getByTestId("code-toggle")); + expect(screen.queryByTestId("code-body")).not.toBeInTheDocument(); + }); + + it("全新安装(无 oldCode)时 diffCode 为空字符串", () => { + render(); + expect(screen.getByTestId("code-body")).toHaveAttribute("data-diff", ""); + }); + + it("更新态(oldCode 与 code 不同)时 diffCode 取旧代码以触发内联 diff", () => { + const oldCode = "// old\nconst a = 0;"; + render(); + expect(screen.getByTestId("code-body")).toHaveAttribute("data-diff", oldCode); + }); + + it("oldCode 与 code 相同时不触发 diff(diffCode 为空)", () => { + render(); + expect(screen.getByTestId("code-body")).toHaveAttribute("data-diff", ""); + }); + + it("点击复制将代码写入剪贴板", () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true }); + render(); + fireEvent.click(screen.getByTestId("code-copy")); + expect(writeText).toHaveBeenCalledWith(code); + }); + + it("提供 diff 统计时渲染 +N 与 −N", () => { + render(); + expect(screen.getByText("+42")).toBeInTheDocument(); + expect(screen.getByText("−18")).toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/CodePreview.tsx b/src/pages/install/components/CodePreview.tsx new file mode 100644 index 000000000..87af6558b --- /dev/null +++ b/src/pages/install/components/CodePreview.tsx @@ -0,0 +1,82 @@ +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CodeXml, Copy, Check, ChevronDown, ChevronRight } from "lucide-react"; +import CodeEditor from "@App/pages/components/CodeEditor"; + +export interface CodePreviewProps { + code: string; + /** 更新态的旧版本代码;与 code 不同则触发内联 diff,全新安装为 undefined */ + oldCode?: string; + language?: string; + diffStat?: { added: number; removed: number }; + defaultCollapsed?: boolean; +} + +export function CodePreview({ + code, + oldCode, + language = "JavaScript", + diffStat, + defaultCollapsed = false, +}: CodePreviewProps) { + const { t } = useTranslation(["install", "common", "editor"]); + const [collapsed, setCollapsed] = useState(defaultCollapsed); + const [copied, setCopied] = useState(false); + + const lineCount = useMemo(() => code.split("\n").length, [code]); + // diffCode 语义:""=无 diff(普通只读预览),有值=内联 diff;切勿传 undefined(表示不加载) + const diffCode = oldCode && oldCode !== code ? oldCode : ""; + + const copy = () => { + navigator.clipboard?.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }; + + return ( +
+
+ + {t("editor:code")} + {language} + {t("install:code_lines", { count: lineCount })} + {diffStat && ( + + {`+${diffStat.added}`} + {`−${diffStat.removed}`} + + )} +
+ + +
+
+ {!collapsed && ( + + )} +
+ ); +} diff --git a/src/pages/install/components/InstallActions.test.tsx b/src/pages/install/components/InstallActions.test.tsx new file mode 100644 index 000000000..219ed26bc --- /dev/null +++ b/src/pages/install/components/InstallActions.test.tsx @@ -0,0 +1,92 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup, fireEvent, act } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { InstallActions } from "./InstallActions"; + +const baseProps = () => ({ + isUpdate: false, + isSubscribe: false, + onInstall: vi.fn(), + onClose: vi.fn(), + onToggleWatch: vi.fn(), +}); + +const open = async (el: HTMLElement) => { + await act(async () => { + fireEvent.click(el); + }); +}; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("InstallActions 操作区", () => { + it("点击主按钮触发安装", () => { + const p = baseProps(); + render(); + fireEvent.click(screen.getByTestId("install-primary")); + expect(p.onInstall).toHaveBeenCalledTimes(1); + }); + + it("primaryDisabled 时主按钮禁用", () => { + render(); + expect(screen.getByTestId("install-primary")).toBeDisabled(); + }); + + it("展开更多菜单可选择不关闭窗口", async () => { + const p = baseProps(); + render(); + await open(screen.getByTestId("install-more")); + fireEvent.click(screen.getByRole("menuitem", { name: /不关闭窗口/ })); + expect(p.onInstall).toHaveBeenCalledWith({ closeAfterInstall: false }); + }); + + it("非订阅展开菜单含禁止更新项并可点击", async () => { + const p = baseProps(); + render(); + await open(screen.getByTestId("install-more")); + fireEvent.click(screen.getByRole("menuitem", { name: /不再检查更新/ })); + expect(p.onInstall).toHaveBeenCalledWith({ noMoreUpdates: true }); + }); + + it("订阅源隐藏禁止更新项", async () => { + render(); + await open(screen.getByTestId("install-more")); + expect(screen.queryByRole("menuitem", { name: /不再检查更新/ })).not.toBeInTheDocument(); + }); + + it("全新安装关闭为普通按钮并触发关闭", () => { + const p = baseProps(); + render(); + fireEvent.click(screen.getByTestId("close-primary")); + expect(p.onClose).toHaveBeenCalledTimes(1); + expect(screen.queryByTestId("close-more")).not.toBeInTheDocument(); + }); + + it("更新态关闭可选择不再检查更新", async () => { + const p = baseProps(); + render(); + await open(screen.getByTestId("close-more")); + fireEvent.click(screen.getByRole("menuitem", { name: /不再检查更新/ })); + expect(p.onClose).toHaveBeenCalledWith({ noMoreUpdates: true }); + }); + + it("本地文件显示监听按钮,点击切换监听", () => { + const p = baseProps(); + render(); + fireEvent.click(screen.getByTestId("watch-toggle")); + expect(p.onToggleWatch).toHaveBeenCalledTimes(1); + }); + + it("监听中显示停止监听文案", () => { + render(); + expect(screen.getByTestId("watch-toggle")).toHaveTextContent("停止监听"); + }); + + it("操作栏左侧渲染信任提示语(对照设计稿 BarNote)", () => { + render(); + expect(screen.getByTestId("action-bar-note")).toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/InstallActions.tsx b/src/pages/install/components/InstallActions.tsx new file mode 100644 index 000000000..48e8487fc --- /dev/null +++ b/src/pages/install/components/InstallActions.tsx @@ -0,0 +1,162 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { ChevronDown, Download, Eye, EyeOff, Info, RefreshCw } from "lucide-react"; +import { Button } from "@App/pages/components/ui/button"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@App/pages/components/ui/dropdown-menu"; +import { cn } from "@App/pkg/utils/cn"; + +export interface InstallActionsProps { + isUpdate: boolean; + isSubscribe: boolean; + primaryDisabled?: boolean; + localFile?: boolean; + watching?: boolean; + onInstall: (opts?: { closeAfterInstall?: boolean; noMoreUpdates?: boolean }) => void; + onClose: (opts?: { noMoreUpdates?: boolean }) => void; + onToggleWatch?: () => void; +} + +/** + * 拆分按钮的「更多」下拉。受控展开:onPointerDown 阻断 Radix 自身的指针展开, + * 改由 onClick 切换 open —— 在 jsdom 与真实浏览器中行为一致(避免指针/点击双触发)。 + */ +function MoreMenu({ + testid, + label, + variant, + triggerClassName, + disabled, + children, +}: { + testid: string; + label: string; + variant: "default" | "outline"; + triggerClassName?: string; + disabled?: boolean; + children: React.ReactNode; +}) { + const [open, setOpen] = useState(false); + return ( + + + + + {children} + + ); +} + +export function InstallActions({ + isUpdate, + isSubscribe, + primaryDisabled, + localFile, + watching, + onInstall, + onClose, + onToggleWatch, +}: InstallActionsProps) { + const { t } = useTranslation(["install", "common", "editor"]); + + const primaryLabel = isUpdate ? t("install:update_script") : t("install:script"); + const noCloseLabel = isUpdate ? t("install:update_script_no_close") : t("install:script_no_close"); + const noMoreUpdateLabel = isUpdate ? t("install:update_script_no_more_update") : t("install:script_no_more_update"); + const PrimaryIcon = isUpdate ? RefreshCw : Download; + + const note = watching + ? t("install:action_note_watching") + : isUpdate + ? t("install:action_note_update") + : isSubscribe + ? t("install:action_note_subscribe") + : t("install:action_note_install"); + + return ( +
+ + + {note} + +
+ {localFile && ( + + )} + + {isUpdate ? ( +
+ + + onClose({ noMoreUpdates: true })}> + {t("install:close_update_script_no_more_update")} + + +
+ ) : ( + + )} + +
+ + + onInstall({ closeAfterInstall: false })}>{noCloseLabel} + {!isSubscribe && ( + onInstall({ noMoreUpdates: true })}> + {noMoreUpdateLabel} + + )} + +
+
+
+ ); +} diff --git a/src/pages/install/components/InstallLayout.test.tsx b/src/pages/install/components/InstallLayout.test.tsx new file mode 100644 index 000000000..4751de416 --- /dev/null +++ b/src/pages/install/components/InstallLayout.test.tsx @@ -0,0 +1,41 @@ +// @vitest-environment happy-dom +import { describe, it, expect, afterEach } from "vitest"; +import { render, screen, cleanup, within } from "@testing-library/react"; +import { InstallLayout } from "./InstallLayout"; + +afterEach(cleanup); + +describe("InstallLayout 安装页外壳", () => { + it("渲染品牌标识、上下文标题与内容", () => { + render( + {"install"}}> +
{"正文内容"}
+
+ ); + expect(screen.getByText("ScriptCat")).toBeInTheDocument(); + expect(screen.getByText("脚本安装")).toBeInTheDocument(); + expect(screen.getByText("正文内容")).toBeInTheDocument(); + }); + + it("在吸底操作栏渲染 actions", () => { + render( + {"do-update"}}> +
{"x"}
+
+ ); + const bar = screen.getByTestId("action-bar"); + expect(within(bar).getByRole("button", { name: "do-update" })).toBeInTheDocument(); + }); + + it("顶栏与底栏使用 bg-card,与 bg-background 内容区形成对比(对照设计稿)", () => { + render( + {"install"}}> +
{"x"}
+
+ ); + // 设计稿:TopBar/ActionBar 填充 #ffffff/#151515 = bg-card;ContentArea 填充 #fafafa/#1e1e1e = bg-background + expect(screen.getByTestId("install-top-bar").className).toContain("bg-card"); + expect(screen.getByTestId("action-bar").className).toContain("bg-card"); + expect(screen.getByTestId("install-layout").className).toContain("bg-background"); + }); +}); diff --git a/src/pages/install/components/InstallLayout.tsx b/src/pages/install/components/InstallLayout.tsx new file mode 100644 index 000000000..4f9c9d820 --- /dev/null +++ b/src/pages/install/components/InstallLayout.tsx @@ -0,0 +1,33 @@ +import type { LucideIcon } from "lucide-react"; +import { InstallTopBar } from "./InstallTopBar"; + +export interface InstallLayoutProps { + /** 顶部右侧上下文标题(如「脚本安装」「脚本更新」),由调用方按场景翻译 */ + title: string; + /** 上下文 chip 的前导图标(安装=download / 更新=refresh / 订阅=rss / 本地=hard-drive / 技能=sparkles) */ + titleIcon?: LucideIcon; + /** 上下文 chip 配色:默认中性灰,skill=紫色,watching=品牌蓝(配脉冲点) */ + titleTone?: "default" | "skill" | "watching"; + /** 吸底操作栏内容 */ + actions: React.ReactNode; + children: React.ReactNode; +} + +export function InstallLayout({ title, titleIcon, titleTone = "default", actions, children }: InstallLayoutProps) { + return ( +
+ + +
+
{children}
+
+ +
+ {actions} +
+
+ ); +} diff --git a/src/pages/install/components/InstallStates.test.tsx b/src/pages/install/components/InstallStates.test.tsx new file mode 100644 index 000000000..4a48dc7b3 --- /dev/null +++ b/src/pages/install/components/InstallStates.test.tsx @@ -0,0 +1,71 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup, fireEvent } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { InstallLoading, InstallError } from "./InstallStates"; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("InstallLoading 加载中状态屏", () => { + it("渲染加载标题、来源与字节进度文案", () => { + render(); + expect(screen.getByText("正在加载脚本")).toBeInTheDocument(); + expect(screen.getByText("example.com")).toBeInTheDocument(); + expect(screen.getByText("正在下载。已接收 12 KB。")).toBeInTheDocument(); + }); + + it("保留顶部品牌栏(对照设计稿,加载态不丢失外壳)", () => { + render(); + expect(screen.getByTestId("install-top-bar")).toBeInTheDocument(); + }); + + it("提供 percent 时渲染确定进度条且宽度反映百分比", () => { + render(); + const bar = screen.getByTestId("install-progress"); + expect(bar).toHaveStyle({ width: "50%" }); + }); + + it("未提供 percent 时不渲染确定进度条(保持不确定动画条)", () => { + render(); + expect(screen.queryByTestId("install-progress")).not.toBeInTheDocument(); + }); +}); + +describe("InstallError 加载失败状态屏", () => { + it("渲染标题与错误信息", () => { + render( {}} />); + expect(screen.getByText("安装页面加载失败")).toBeInTheDocument(); + expect(screen.getByText("Error: Fetch failed with status 404")).toBeInTheDocument(); + }); + + it("保留顶部品牌栏(对照设计稿,失败态不丢失外壳)", () => { + render( {}} />); + expect(screen.getByTestId("install-top-bar")).toBeInTheDocument(); + }); + + it("提供 onRetry 时渲染重试按钮并可点击", () => { + const onRetry = vi.fn(); + render( {}} />); + fireEvent.click(screen.getByRole("button", { name: "重试" })); + expect(onRetry).toHaveBeenCalledTimes(1); + }); + + it("未提供 onRetry 时不渲染重试按钮", () => { + render( {}} />); + expect(screen.queryByRole("button", { name: "重试" })).not.toBeInTheDocument(); + }); + + it("点击关闭触发 onClose", () => { + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button", { name: "关闭" })); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("可自定义标题(用于无效页面)", () => { + render( {}} />); + expect(screen.getByText("无效页面")).toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/InstallStates.tsx b/src/pages/install/components/InstallStates.tsx new file mode 100644 index 000000000..6a9fbc70d --- /dev/null +++ b/src/pages/install/components/InstallStates.tsx @@ -0,0 +1,91 @@ +import { useTranslation } from "react-i18next"; +import { CloudOff, Download, Globe, Loader2 } from "lucide-react"; +import { Button } from "@App/pages/components/ui/button"; +import { InstallTopBar } from "./InstallTopBar"; + +/** 状态屏外壳:保留品牌顶栏(对照设计稿,加载/失败态不丢失外壳),内容区垂直居中 */ +function StateShell({ children }: { children: React.ReactNode }) { + const { t } = useTranslation(["install", "common"]); + return ( +
+ +
+ {children} +
+
+ ); +} + +export function InstallLoading({ + source, + bytesText, + percent, +}: { + source?: string; + bytesText?: string; + percent?: number; +}) { + const { t } = useTranslation(["install", "common"]); + return ( + + +
+

{t("install:loading_title")}

+

{t("install:loading_desc")}

+
+ {source && ( + + + {source} + + )} + {bytesText && {bytesText}} +
+ {typeof percent === "number" ? ( +
+ ) : ( +
+ )} +
+ + ); +} + +export function InstallError({ + title, + message, + onRetry, + onClose, +}: { + title?: string; + message: string; + onRetry?: () => void; + onClose: () => void; +}) { + const { t } = useTranslation(["install", "common"]); + return ( + +
+ +
+

{title ?? t("install:page_load_failed")}

+
+        {message}
+      
+
+ {onRetry && ( + + )} + +
+
+ ); +} diff --git a/src/pages/install/components/InstallTopBar.tsx b/src/pages/install/components/InstallTopBar.tsx new file mode 100644 index 000000000..4e499f4a4 --- /dev/null +++ b/src/pages/install/components/InstallTopBar.tsx @@ -0,0 +1,55 @@ +import type { LucideIcon } from "lucide-react"; +import { cn } from "@App/pkg/utils/cn"; + +export interface InstallTopBarProps { + /** 上下文 chip 文案(如「脚本安装」「脚本更新」),由调用方按场景翻译;省略则不渲染 chip */ + title?: string; + /** 上下文 chip 的前导图标(安装=download / 更新=refresh / 订阅=rss / 本地=hard-drive / 技能=sparkles) */ + titleIcon?: LucideIcon; + /** 上下文 chip 配色:默认中性灰,skill=紫色,watching=品牌蓝(配脉冲点) */ + titleTone?: "default" | "skill" | "watching"; +} + +/** 品牌标志:纯品牌色圆点 + ScriptCat 字样(对照设计稿,圆点内不放字母) */ +function BrandMark() { + return ( +
+
+ {"ScriptCat"} +
+ ); +} + +/** 安装页统一顶栏:品牌标志 + 右侧上下文 chip。安装/订阅/技能/加载/失败各态共用,保证外壳一致。 */ +export function InstallTopBar({ title, titleIcon: TitleIcon, titleTone = "default" }: InstallTopBarProps) { + return ( +
+ + {title && ( + + {titleTone === "watching" ? ( +
+ ); +} diff --git a/src/pages/install/components/InstallWarning.test.tsx b/src/pages/install/components/InstallWarning.test.tsx new file mode 100644 index 000000000..3fefff0eb --- /dev/null +++ b/src/pages/install/components/InstallWarning.test.tsx @@ -0,0 +1,27 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { InstallWarning } from "./InstallWarning"; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("InstallWarning 安全警示条", () => { + it("渲染警示标题与说明两段(对照设计稿)", () => { + render(); + expect(screen.getByTestId("install-warning-title")).toBeInTheDocument(); + expect(screen.getByTestId("install-warning-desc")).toBeInTheDocument(); + }); + + it("存在危险权限时附加风险提示分句", () => { + render(); + expect(screen.getByTestId("install-warning-risk")).toBeInTheDocument(); + }); + + it("无风险信号时不渲染风险提示分句", () => { + render(); + expect(screen.queryByTestId("install-warning-risk")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/InstallWarning.tsx b/src/pages/install/components/InstallWarning.tsx new file mode 100644 index 000000000..11206e0d1 --- /dev/null +++ b/src/pages/install/components/InstallWarning.tsx @@ -0,0 +1,41 @@ +import { useTranslation } from "react-i18next"; +import { ShieldAlert } from "lucide-react"; + +export interface InstallWarningProps { + /** 是否声明了危险权限(如 @connect *) */ + hasDangerPermission: boolean; + /** 是否声明了已知反特性(推广链接/广告/挖矿等) */ + hasAntifeature: boolean; +} + +/** + * 安全警示条(对照设计稿 Alert Warning):标题「请确认脚本来自可信来源」+ 说明, + * 当声明了危险权限或反特性时,附加一句风险提示,引导用户谨慎安装。 + */ +export function InstallWarning({ hasDangerPermission, hasAntifeature }: InstallWarningProps) { + const { t } = useTranslation(["install", "common"]); + const showRisk = hasDangerPermission || hasAntifeature; + + return ( +
+ +
+ + {t("install:warning_title")} + + + {t("install:from_legitimate_sources_warning")} + {showRisk && ( + + {" "} + {hasDangerPermission && t("install:warning_risk_connect")} + {hasDangerPermission && hasAntifeature && t("install:warning_risk_join")} + {hasAntifeature && t("install:warning_risk_antifeature")} + {t("install:warning_risk_tail")} + + )} + +
+
+ ); +} diff --git a/src/pages/install/components/PermissionCard.test.tsx b/src/pages/install/components/PermissionCard.test.tsx new file mode 100644 index 000000000..475dd7682 --- /dev/null +++ b/src/pages/install/components/PermissionCard.test.tsx @@ -0,0 +1,50 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeAll, afterEach, vi } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; + +let mobile = false; +vi.mock("@App/pages/components/use-is-mobile", () => ({ + useIsMobile: () => mobile, + MOBILE_BREAKPOINT: 768, +})); + +import { PermissionCard } from "./PermissionCard"; +import type { PermissionRow } from "../permissions"; + +const match: PermissionRow = { kind: "match", risk: "normal", values: ["https://a.com/*"], sensitive: [] }; +const connect: PermissionRow = { kind: "connect", risk: "danger", values: ["*"], sensitive: [] }; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(() => { + cleanup(); + mobile = false; +}); + +describe("PermissionCard 权限卡", () => { + it("渲染卡头标题与提示,并为每个权限行输出一行", () => { + render(); + expect(screen.getByText("此脚本将获得以下权限")).toBeInTheDocument(); + expect(screen.getByText("安装前请确认")).toBeInTheDocument(); + expect(screen.getAllByTestId("permission-row")).toHaveLength(2); + }); + + it("无权限行时显示空态文案且不渲染权限行", () => { + render(); + expect(screen.getByText("此脚本不请求任何特殊权限")).toBeInTheDocument(); + expect(screen.queryAllByTestId("permission-row")).toHaveLength(0); + }); + + it("移动端改用 Accordion,默认仅展开高风险项", () => { + mobile = true; + render(); + // 两个分类的折叠触发器都在 + expect(screen.getByText("运行网站")).toBeInTheDocument(); + expect(screen.getByText("跨域访问")).toBeInTheDocument(); + // danger(跨域访问)默认展开,其取值可见 + expect(screen.getByText("*")).toBeInTheDocument(); + // normal(运行网站)默认折叠,其取值不在 DOM + expect(screen.queryByText("https://a.com/*")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/PermissionCard.tsx b/src/pages/install/components/PermissionCard.tsx new file mode 100644 index 000000000..2115a21f6 --- /dev/null +++ b/src/pages/install/components/PermissionCard.tsx @@ -0,0 +1,68 @@ +import { useTranslation } from "react-i18next"; +import { ShieldCheck } from "lucide-react"; +import { cn } from "@App/pkg/utils/cn"; +import { useIsMobile } from "@App/pages/components/use-is-mobile"; +import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@App/pages/components/ui/accordion"; +import type { PermissionRow as PermissionRowData } from "../permissions"; +import { PermissionRow, PermissionChips, KIND_META, RISK_STYLE } from "./PermissionRow"; + +function MobilePermissions({ rows }: { rows: PermissionRowData[] }) { + const { t } = useTranslation(["install", "common"]); + // 默认仅展开高风险(danger)项 + const defaultValue = rows.filter((r) => r.risk === "danger").map((r) => r.kind); + + return ( + + {rows.map((row) => { + const { icon: Icon, labelKey } = KIND_META[row.kind]; + const style = RISK_STYLE[row.risk]; + return ( + + + + + + + {t(labelKey)} + + {row.values.length} + + + + + + + + ); + })} + + ); +} + +export function PermissionCard({ rows }: { rows: PermissionRowData[] }) { + const { t } = useTranslation(["install", "common"]); + const isMobile = useIsMobile(); + + return ( +
+
+ +

{t("install:perm_card_title")}

+ {t("install:perm_card_hint")} +
+
+ {rows.length === 0 ? ( +

{t("install:perm_card_empty")}

+ ) : isMobile ? ( + + ) : ( + rows.map((row, i) => ( +
0 && "border-t border-border")}> + +
+ )) + )} +
+
+ ); +} diff --git a/src/pages/install/components/PermissionRow.test.tsx b/src/pages/install/components/PermissionRow.test.tsx new file mode 100644 index 000000000..d444dc725 --- /dev/null +++ b/src/pages/install/components/PermissionRow.test.tsx @@ -0,0 +1,83 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup, within, fireEvent } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { PermissionRow } from "./PermissionRow"; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("PermissionRow 权限行", () => { + it("渲染跨域访问类别标签、摘要与全部取值 chip", () => { + render( + + ); + expect(screen.getByText("跨域访问")).toBeInTheDocument(); + expect(screen.getByText("可向以下域名发送请求、读取其数据")).toBeInTheDocument(); + expect(screen.getByText("api.a.com")).toBeInTheDocument(); + expect(screen.getByText("api.b.com")).toBeInTheDocument(); + }); + + it("danger 风险在行根节点标记 data-risk=danger", () => { + render(); + expect(screen.getByTestId("permission-row")).toHaveAttribute("data-risk", "danger"); + }); + + it("warn 风险在行根节点标记 data-risk=warn", () => { + render(); + expect(screen.getByTestId("permission-row")).toHaveAttribute("data-risk", "warn"); + }); + + it("敏感取值额外标记 data-sensitive", () => { + render( + + ); + const cookie = screen.getByText("GM_cookie"); + const chip = cookie.closest("[data-chip]")!; + expect(chip).toHaveAttribute("data-sensitive", "true"); + const setValue = screen.getByText("GM_setValue").closest("[data-chip]")!; + expect(setValue).not.toHaveAttribute("data-sensitive", "true"); + }); + + it("外部资源 URL 过长时 chip 限宽且文本可断行,避免横向溢出", () => { + const longUrl = + "https://cdn.jsdelivr.net/npm/some-really-long-package-name@1.2.3/dist/path/to/very/long/file.min.js"; + render(); + const text = screen.getByText(longUrl); + // URL 文本允许在任意字符处断行 + expect(text).toHaveClass("break-all"); + // chip 不得超出容器宽度 + expect(text.closest("[data-chip]")!).toHaveClass("max-w-full"); + }); + + it("取值超过 maxVisible 时折叠为 +N", () => { + render( + + ); + const row = screen.getByTestId("permission-row"); + expect(within(row).getByText("a")).toBeInTheDocument(); + expect(within(row).getByText("c")).toBeInTheDocument(); + expect(within(row).queryByText("d")).not.toBeInTheDocument(); + expect(within(row).getByTestId("permission-more")).toHaveTextContent("+2"); + }); + + it("点击 +N 展开余下取值并隐藏折叠按钮", () => { + render( + + ); + const row = screen.getByTestId("permission-row"); + fireEvent.click(within(row).getByTestId("permission-more")); + expect(within(row).getByText("d")).toBeInTheDocument(); + expect(within(row).getByText("e")).toBeInTheDocument(); + expect(within(row).queryByTestId("permission-more")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/PermissionRow.tsx b/src/pages/install/components/PermissionRow.tsx new file mode 100644 index 000000000..7f15ef7c6 --- /dev/null +++ b/src/pages/install/components/PermissionRow.tsx @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Globe, ArrowLeftRight, ChevronDown, KeyRound, Package, TriangleAlert, type LucideIcon } from "lucide-react"; +import { cn } from "@App/pkg/utils/cn"; +import type { PermissionKind, PermissionRisk, PermissionRow as PermissionRowData } from "../permissions"; + +export const KIND_META: Record = { + match: { icon: Globe, labelKey: "install:perm_match_label", summaryKey: "install:perm_match_summary" }, + connect: { icon: ArrowLeftRight, labelKey: "install:perm_connect_label", summaryKey: "install:perm_connect_summary" }, + grant: { icon: KeyRound, labelKey: "install:perm_grant_label", summaryKey: "install:perm_grant_summary" }, + require: { icon: Package, labelKey: "install:perm_require_label", summaryKey: "install:perm_require_summary" }, +}; + +/** + * 风险只通过「图标块底色 + 计数徽章」表达,标题与普通取值 chip 保持中性; + * 危险行(@connect *)的 chip 整体标红,敏感项(GM_cookie)单独描琥珀边(见 PermissionChips)。对照设计稿。 + */ +export const RISK_STYLE: Record = { + normal: { + icon: "bg-muted text-fg-secondary", + count: "bg-muted text-muted-foreground", + chip: "bg-muted border border-border text-fg-secondary", + }, + warn: { + icon: "bg-warning-bg text-warning-fg", + count: "bg-warning-bg text-warning-fg", + chip: "bg-muted border border-border text-fg-secondary", + }, + danger: { + icon: "bg-destructive/10 text-destructive", + count: "bg-destructive/10 text-destructive", + chip: "bg-destructive/10 border border-destructive/60 text-destructive", + }, +}; + +export const DEFAULT_MAX_VISIBLE = 8; + +/** 权限取值 chip 列表(可见项 + 折叠的 +N);桌面行与移动 Accordion 共用 */ +export function PermissionChips({ + row, + maxVisible = DEFAULT_MAX_VISIBLE, +}: { + row: PermissionRowData; + maxVisible?: number; +}) { + const style = RISK_STYLE[row.risk]; + const sensitive = new Set(row.sensitive); + const [expanded, setExpanded] = useState(false); + const visible = expanded ? row.values : row.values.slice(0, maxVisible); + const hidden = row.values.length - visible.length; + + return ( +
+ {visible.map((v) => { + const isSensitive = sensitive.has(v); + return ( + + {isSensitive && } + {v} + + ); + })} + {hidden > 0 && ( + + )} +
+ ); +} + +export function PermissionRow({ row, maxVisible }: { row: PermissionRowData; maxVisible?: number }) { + const { t } = useTranslation(["install", "common"]); + const { icon: Icon, labelKey, summaryKey } = KIND_META[row.kind]; + const style = RISK_STYLE[row.risk]; + + return ( +
+
+ +
+
+
+ {t(labelKey)} + {row.values.length} + {t(summaryKey)} +
+ +
+
+ ); +} diff --git a/src/pages/install/components/ScriptIdentity.test.tsx b/src/pages/install/components/ScriptIdentity.test.tsx new file mode 100644 index 000000000..1c87217ea --- /dev/null +++ b/src/pages/install/components/ScriptIdentity.test.tsx @@ -0,0 +1,87 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup, fireEvent } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { ScriptIdentity } from "./ScriptIdentity"; + +const base = { + name: "全网每日签到助手", + source: "example.com", + author: "scriptcat", + description: "一个示例脚本", + antifeatures: [] as never[], + schedule: null, + enabled: true, + onEnabledChange: () => {}, +}; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("ScriptIdentity 身份卡", () => { + it("渲染名称、作者、来源与描述", () => { + render(); + expect(screen.getByText("全网每日签到助手")).toBeInTheDocument(); + expect(screen.getByText("scriptcat")).toBeInTheDocument(); + expect(screen.getByText("example.com")).toBeInTheDocument(); + expect(screen.getByText("一个示例脚本")).toBeInTheDocument(); + }); + + it("全新安装显示单枚版本徽章", () => { + render(); + expect(screen.getByTestId("version-single")).toHaveTextContent("2.3.1"); + expect(screen.queryByTestId("version-old")).not.toBeInTheDocument(); + }); + + it("更新显示 旧→新 版本徽章", () => { + render( + + ); + expect(screen.getByTestId("version-old")).toHaveTextContent("2.1.0"); + expect(screen.getByTestId("version-new")).toHaveTextContent("2.3.1"); + }); + + it("反特性渲染为警示徽章", () => { + render( + + ); + expect(screen.getByText("推荐链接")).toBeInTheDocument(); + }); + + it("定时脚本显示 cron 信息条与下次运行", () => { + render( + + ); + expect(screen.getByText("0 8 * * *")).toBeInTheDocument(); + expect(screen.getByText("明天 08:00")).toBeInTheDocument(); + expect(screen.getByText("定时")).toBeInTheDocument(); + }); + + it("后台脚本显示后台运行说明与后台徽章", () => { + render( + + ); + expect(screen.getByText("浏览器开启时自动运行")).toBeInTheDocument(); + expect(screen.getByText("后台")).toBeInTheDocument(); + }); + + it("切换启用开关触发回调", () => { + const onEnabledChange = vi.fn(); + render( + + ); + fireEvent.click(screen.getByRole("switch")); + expect(onEnabledChange).toHaveBeenCalledWith(false); + }); +}); diff --git a/src/pages/install/components/ScriptIdentity.tsx b/src/pages/install/components/ScriptIdentity.tsx new file mode 100644 index 000000000..95b851614 --- /dev/null +++ b/src/pages/install/components/ScriptIdentity.tsx @@ -0,0 +1,159 @@ +import { useTranslation } from "react-i18next"; +import { User, Globe, ArrowRight, Timer, Clock, Power, FileCode } from "lucide-react"; +import { Switch } from "@App/pages/components/ui/switch"; +import { cn } from "@App/pkg/utils/cn"; +import type { VersionDisplay, AntifeatureType, ScheduleInfo } from "../model"; + +const ANTIFEATURE_TITLE_KEY: Record = { + "referral-link": "install:referral_link_title", + ads: "install:ads_title", + payment: "install:payment_title", + miner: "install:miner_title", + membership: "install:membership_title", + tracking: "install:tracking_title", +}; + +function Tag({ tone, children }: { tone: "green" | "amber"; children: React.ReactNode }) { + return ( + + {children} + + ); +} + +export interface ScriptIdentityProps { + name: string; + iconUrl?: string; + version: VersionDisplay; + author?: string; + source: string; + antifeatures: AntifeatureType[]; + schedule: ScheduleInfo; + scheduleNextRun?: string; + description?: string; + enabled: boolean; + onEnabledChange: (v: boolean) => void; +} + +export function ScriptIdentity({ + name, + iconUrl, + version, + author, + source, + antifeatures, + schedule, + scheduleNextRun, + description, + enabled, + onEnabledChange, +}: ScriptIdentityProps) { + const { t } = useTranslation(["install", "common"]); + + return ( +
+
+
+ {iconUrl ? ( + + ) : ( + + )} +
+ +
+
+

{name}

+ {version.kind === "install" ? ( + + {`v${version.version}`} + + ) : ( + + + {`v${version.oldVersion}`} + + + + {`v${version.newVersion}`} + + + )} +
+ +
+ {author && ( + <> + + + {author} + +
+ + {(schedule || antifeatures.length > 0) && ( +
+ {schedule?.kind === "background" && {t("install:badge_background")}} + {schedule?.kind === "cron" && {t("install:badge_scheduled")}} + {antifeatures.map((a) => ( + + {t(ANTIFEATURE_TITLE_KEY[a])} + + ))} +
+ )} +
+ +
+ + {t("install:enabled_label")} +
+
+ + {description &&

{description}

} + + {schedule?.kind === "cron" && ( +
+ + + {t("install:schedule_cron_label")} + + + {schedule.expression} + + {scheduleNextRun && ( + + + {t("install:schedule_next_run")} + {scheduleNextRun} + + )} +
+ )} + + {schedule?.kind === "background" && ( +
+ + {t("install:schedule_background_desc")} +
+ )} +
+ ); +} diff --git a/src/pages/install/components/ScriptInstallView.tsx b/src/pages/install/components/ScriptInstallView.tsx deleted file mode 100644 index 01e3c6626..000000000 --- a/src/pages/install/components/ScriptInstallView.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import { - Button, - Dropdown, - Menu, - Modal, - Space, - Switch, - Tag, - Tooltip, - Typography, - Popover, - Message, -} from "@arco-design/web-react"; -import { IconDown } from "@arco-design/web-react/icon"; -import { useTranslation } from "react-i18next"; -import CodeEditor from "../../components/CodeEditor"; -import { i18nName, i18nDescription } from "@App/locales/locales"; -import { ScriptIcons } from "../../options/routes/utils"; -import { prettyUrl } from "@App/pkg/utils/url-utils"; -import { backgroundPromptShownKey } from "../utils"; -import type { InstallData } from "../hooks"; - -function ScriptInstallView({ data }: { data: InstallData }) { - const { - enable, - btnText, - scriptCode, - scriptInfo, - upsertScript, - diffCode, - oldScriptVersion, - isUpdate, - localFileHandle, - showBackgroundPrompt, - setShowBackgroundPrompt, - watchFile, - metadataLive, - permissions, - descriptionParagraph, - antifeatures, - handleInstallBasic, - handleInstallCloseAfterInstall, - handleInstallNoMoreUpdates, - handleStatusChange, - handleCloseBasic, - handleCloseNoMoreUpdates, - setWatchFileClick, - } = data; - const { t } = useTranslation(); - - return ( -
- {/* 后台运行提示对话框 */} - { - try { - const granted = await chrome.permissions.request({ permissions: ["background"] }); - if (granted) { - Message.success(t("enable_background.title")!); - } else { - Message.info(t("enable_background.maybe_later")!); - } - setShowBackgroundPrompt(false); - localStorage.setItem(backgroundPromptShownKey, "true"); - } catch (e) { - console.error(e); - Message.error(t("enable_background.enable_failed")!); - } - }} - onCancel={() => { - setShowBackgroundPrompt(false); - localStorage.setItem(backgroundPromptShownKey, "true"); - }} - okText={t("enable_background.enable_now")} - cancelText={t("enable_background.maybe_later")} - autoFocus={false} - focusLock={true} - > - - - {t("enable_background.prompt_description", { - scriptType: upsertScript?.metadata?.background ? t("background_script") : t("scheduled_script"), - })} - - {t("enable_background.settings_hint")} - - -
-
- {upsertScript?.metadata.icon && } - {upsertScript && ( - - - {i18nName(upsertScript)} - - - )} - - - -
-
-
- {oldScriptVersion && ( - - {oldScriptVersion} - - )} - {typeof metadataLive.version?.[0] === "string" && metadataLive.version[0] !== oldScriptVersion && ( - - - {metadataLive.version[0]} - - - )} -
-
-
-
-
-
-
-
- {(metadataLive.background || metadataLive.crontab) && ( - - - {t("background_script")} - - - )} - {metadataLive.crontab && ( - - - {t("scheduled_script")} - - - )} - {metadataLive.antifeature?.length && - metadataLive.antifeature.map((antifeature) => { - const item = antifeature.split(" ")[0]; - return ( - antifeatures[item] && ( - - - {antifeatures[item].title} - - - ) - ); - })} -
-
-
- {upsertScript && i18nDescription(upsertScript!)} -
-
- {`${t("author")}: ${metadataLive.author}`} -
-
- - {`${t("source")}: ${prettyUrl(scriptInfo?.url)}`} - -
-
-
-
- {descriptionParagraph?.length ? ( -
- - - {descriptionParagraph} - - -
- ) : ( - <> - )} -
- {permissions.map((item) => ( -
- {item.value?.length > 0 ? ( - <> - - {item.label} - -
- {item.value.map((v) => ( -
- {v} -
- ))} -
- - ) : ( - <> - )} -
- ))} -
-
-
-
- {t("install_from_legitimate_sources_warning")} -
-
- - - - - - {isUpdate ? t("update_script_no_close") : t("install_script_no_close")} - - {!scriptInfo?.userSubscribe && ( - - {isUpdate ? t("update_script_no_more_update") : t("install_script_no_more_update")} - - )} - - } - position="bottom" - disabled={watchFile} - > - - - )} - {isUpdate ? ( - - - - {!scriptInfo?.userSubscribe && ( - - {t("close_update_script_no_more_update")} - - )} - - } - position="bottom" - > - - )} - -
-
-
- -
-
-
- ); -} - -export default ScriptInstallView; diff --git a/src/pages/install/components/SkillInstallView.test.tsx b/src/pages/install/components/SkillInstallView.test.tsx new file mode 100644 index 000000000..41af53efb --- /dev/null +++ b/src/pages/install/components/SkillInstallView.test.tsx @@ -0,0 +1,92 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup, fireEvent } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { SkillInstallView } from "./SkillInstallView"; + +const toolCode = [ + "// ==SkillScript==", + "// @name fetch_data", + "// @description 抓取远程数据", + "// @param url string [required] 目标地址", + "// @grant GM_xmlhttpRequest", + "// ==/SkillScript==", + "console.log('body');", +].join("\n"); + +const baseProps = () => ({ + metadata: { + name: "网页摘要技能", + description: "对当前网页生成摘要", + version: "1.2.0", + config: { + apiKey: { title: "API 密钥", type: "text" as const, required: true, secret: true }, + }, + }, + prompt: "你是一个网页摘要助手。".repeat(20), + scripts: [{ name: "fetch_data.js", code: toolCode }], + references: [{ name: "style-guide.md", content: "..." }], + isUpdate: false, + installUrl: "https://scriptcat.org/skills/summary.zip", + onInstall: vi.fn(), + onCancel: vi.fn(), +}); + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("SkillInstallView 技能安装视图", () => { + it("渲染技能名、Skill 徽章与描述", () => { + render(); + expect(screen.getByText("网页摘要技能")).toBeInTheDocument(); + expect(screen.getByText("Skill")).toBeInTheDocument(); + expect(screen.getByText("对当前网页生成摘要")).toBeInTheDocument(); + }); + + it("提示词默认折叠为预览,点击后展开完整内容", () => { + render(); + expect(screen.queryByTestId("skill-prompt-full")).not.toBeInTheDocument(); + fireEvent.click(screen.getByTestId("skill-prompt-toggle")); + expect(screen.getByTestId("skill-prompt-full")).toBeInTheDocument(); + }); + + it("渲染工具名、参数与 grants 能力", () => { + render(); + expect(screen.getByText("fetch_data")).toBeInTheDocument(); + expect(screen.getByText("url")).toBeInTheDocument(); + expect(screen.getByText("GM_xmlhttpRequest")).toBeInTheDocument(); + // 工具参数与配置项都可能标「必填」,此处确认至少渲染了必填标记 + expect(screen.getAllByText("必填").length).toBeGreaterThanOrEqual(1); + }); + + it("渲染配置项的 key 与 secret 标记", () => { + render(); + expect(screen.getByText("apiKey")).toBeInTheDocument(); + expect(screen.getByText("私密")).toBeInTheDocument(); + }); + + it("渲染参考资料文件名", () => { + render(); + expect(screen.getByText("style-guide.md")).toBeInTheDocument(); + }); + + it("更新态主按钮文案为更新 Skill", () => { + render(); + expect(screen.getByTestId("skill-install")).toHaveTextContent("更新 Skill"); + }); + + it("点击安装与关闭触发回调", () => { + const p = baseProps(); + render(); + fireEvent.click(screen.getByTestId("skill-install")); + expect(p.onInstall).toHaveBeenCalledTimes(1); + fireEvent.click(screen.getByTestId("skill-cancel")); + expect(p.onCancel).toHaveBeenCalledTimes(1); + }); + + it("渲染安全警示卡(对照设计稿 Alert Warning)", () => { + render(); + expect(screen.getByTestId("skill-warning-card")).toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/SkillInstallView.tsx b/src/pages/install/components/SkillInstallView.tsx index dd606602f..cd28e226f 100644 --- a/src/pages/install/components/SkillInstallView.tsx +++ b/src/pages/install/components/SkillInstallView.tsx @@ -1,11 +1,23 @@ import { useState } from "react"; -import { Button, Space, Tag, Typography } from "@arco-design/web-react"; -import { IconDown, IconUp } from "@arco-design/web-react/icon"; import { useTranslation } from "react-i18next"; +import { + Sparkles, + ChevronDown, + ChevronRight, + Wrench, + SlidersHorizontal, + FileText, + Globe, + Lock, + ShieldAlert, +} from "lucide-react"; +import { Button } from "@App/pages/components/ui/button"; +import { cn } from "@App/pkg/utils/cn"; import { parseSkillScriptMetadata } from "@App/pkg/utils/skill_script"; import type { SkillConfigField } from "@App/app/service/agent/core/types"; +import { InstallLayout } from "./InstallLayout"; -interface SkillInstallViewProps { +export interface SkillInstallViewProps { metadata: { name: string; description: string; version?: string; config?: Record }; prompt: string; scripts: Array<{ name: string; code: string }>; @@ -13,10 +25,37 @@ interface SkillInstallViewProps { isUpdate: boolean; installUrl?: string; onInstall: () => void; - onClose: () => void; + onCancel: () => void; } -function SkillInstallView({ +const violetChip = "bg-skill-bg text-skill-fg"; + +function SectionCard({ + icon: Icon, + title, + count, + children, +}: { + icon: typeof Wrench; + title: string; + count?: number; + children: React.ReactNode; +}) { + return ( +
+
+ +

{title}

+ {count !== undefined && ( + {count} + )} +
+
{children}
+
+ ); +} + +export function SkillInstallView({ metadata, prompt, scripts, @@ -24,218 +63,201 @@ function SkillInstallView({ isUpdate, installUrl, onInstall, - onClose, + onCancel, }: SkillInstallViewProps) { - const { t } = useTranslation(); + const { t } = useTranslation(["install", "common"]); const [promptExpanded, setPromptExpanded] = useState(false); + const title = isUpdate ? t("install:context_skill_update") : t("install:context_skill_install"); + const configEntries = Object.entries(metadata.config || {}); return ( -
- {/* Header */} -
-
- - {"Skill"} - - - {metadata.name} - - {metadata.version && ( - - {"v"} - {metadata.version} - - )} - {isUpdate && ( - - {t("update")} - - )} + +

{t("install:skill_warning")}

+
+ + +
-
- - {/* Content */} -
-
-
- {/* Description */} + } + > + {/* 身份卡 */} +
+
+
+ +
+
+
+

{metadata.name}

+ {"Skill"} + {metadata.version && ( + {`v${metadata.version}`} + )} + {isUpdate && ( + + {t("install:update_script")} + + )} +
+
+ {t("install:skill_kind")} + {installUrl && ( + + + {installUrl} + + )} +
{metadata.description && ( -
- {metadata.description} -
- )} - - {/* Install URL */} - {installUrl && ( -
- - {"URL: "} - {installUrl} - -
+

+ {metadata.description} +

)} +
+
+
- {/* Prompt */} - {prompt && ( -
-
setPromptExpanded(!promptExpanded)} - > - - {t("agent_skills_prompt")} - {":"} - - {promptExpanded ? : } -
- {promptExpanded ? ( -
-
-                      {prompt}
-                    
-
- ) : ( -
- - {prompt.length > 150 ? prompt.slice(0, 150) + "..." : prompt} - -
- )} -
+ {/* 提示词卡 */} + {prompt && ( + + + {promptExpanded ? ( +
+              {prompt}
+            
+ ) : ( +

+ {prompt.length > 150 ? `${prompt.slice(0, 150)}...` : prompt} +

+ )} +
+ )} - {/* Tools */} - {scripts.length > 0 && ( -
- {`${t("agent_skills_tools")} (${scripts.length}):`} -
- {scripts.map((script) => { - const toolMeta = parseSkillScriptMetadata(script.code); - return ( -
-
- - {toolMeta?.name || script.name} - + {/* 工具卡 */} + {scripts.length > 0 && ( + +
+ {scripts.map((script) => { + const meta = parseSkillScriptMetadata(script.code); + return ( +
+ + {meta?.name || script.name} + + {meta?.description &&

{meta.description}

} + {meta && meta.params.length > 0 && ( +
+ {meta.params.map((p) => ( +
+ {p.name} + {p.type} + {p.required && ( + + {t("install:skill_required")} + + )} + {p.description && {p.description}}
- {toolMeta?.description && ( - - {toolMeta.description} - - )} - {toolMeta && toolMeta.params.length > 0 && ( -
- {toolMeta.params.map((param) => ( -
- - {param.name} - - - {param.type} - - {param.required && ( - - {t("skill_script_required")} - - )} - {param.description && ( - - {param.description} - - )} -
- ))} -
- )} - {toolMeta && toolMeta.grants.length > 0 && ( -
- {toolMeta.grants.map((grant) => ( - - {grant} - - ))} -
- )} -
- ); - })} -
-
- )} - - {/* Config Fields */} - {metadata.config && Object.keys(metadata.config).length > 0 && ( -
- {`${t("agent_skills_config")} (${Object.keys(metadata.config).length}):`} -
- {Object.entries(metadata.config).map(([key, field]) => ( -
-
- - {key} - - - {field.type} - - {field.required && ( - - {t("skill_script_required")} - - )} - {field.secret && ( - - {"secret"} - - )} -
- {field.title && ( - - {field.title} - - )} + ))} +
+ )} + {meta && meta.grants.length > 0 && ( +
+ {meta.grants.map((g) => ( + + {g} + + ))}
- ))} + )}
-
- )} + ); + })} +
+ + )} - {/* References */} - {references.length > 0 && ( -
- {`${t("agent_skills_references")} (${references.length}):`} -
- {references.map((ref) => ( - - {ref.name} - - ))} -
+ {/* 配置卡 */} + {configEntries.length > 0 && ( + +
+ {configEntries.map(([key, field]) => ( +
+ {key} + {field.type} + {field.required && ( + {t("install:skill_required")} + )} + {field.secret && ( + + + {t("install:skill_secret")} + + )} + {field.title && {field.title}}
- )} + ))}
-
+ + )} - {/* Warning + Actions */} -
-
- {t("install_from_legitimate_sources_warning")} -
-
- - - - + {/* 参考资料卡 */} + {references.length > 0 && ( + +
+ {references.map((ref) => ( + + {ref.name} + + ))}
+
+ )} + + {/* 安全警示卡(对照设计稿 Alert Warning) */} +
+ +
+ {t("install:skill_warning_title")} + {t("install:skill_warning_desc")}
-
+ ); } - -export default SkillInstallView; diff --git a/src/pages/install/components/SubscribeScripts.test.tsx b/src/pages/install/components/SubscribeScripts.test.tsx new file mode 100644 index 000000000..715fd4b7b --- /dev/null +++ b/src/pages/install/components/SubscribeScripts.test.tsx @@ -0,0 +1,23 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { SubscribeScripts } from "./SubscribeScripts"; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("SubscribeScripts 订阅脚本列表卡", () => { + it("渲染标题与每个脚本 URL", () => { + render(); + expect(screen.getByText("本订阅将安装以下脚本")).toBeInTheDocument(); + expect(screen.getByText("https://s.cat/1.user.js")).toBeInTheDocument(); + expect(screen.getByText("https://s.cat/2.user.js")).toBeInTheDocument(); + }); + + it("无脚本时显示空态文案", () => { + render(); + expect(screen.getByText("该订阅暂未声明脚本")).toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/SubscribeScripts.tsx b/src/pages/install/components/SubscribeScripts.tsx new file mode 100644 index 000000000..8e3440130 --- /dev/null +++ b/src/pages/install/components/SubscribeScripts.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from "react-i18next"; +import { ListChecks, FileCode } from "lucide-react"; + +export function SubscribeScripts({ scriptUrls }: { scriptUrls: string[] }) { + const { t } = useTranslation(["install", "common"]); + + return ( +
+
+ +

{t("install:subscribe_scripts_title")}

+ {scriptUrls.length > 0 && ( + + {scriptUrls.length} + + )} +
+
+ {scriptUrls.length === 0 ? ( +

{t("install:subscribe_scripts_empty")}

+ ) : ( + scriptUrls.map((url) => ( +
+ + {url} +
+ )) + )} +
+
+ ); +} diff --git a/src/pages/install/components/WatchingBanner.test.tsx b/src/pages/install/components/WatchingBanner.test.tsx new file mode 100644 index 000000000..5b783cbd2 --- /dev/null +++ b/src/pages/install/components/WatchingBanner.test.tsx @@ -0,0 +1,26 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeAll, afterEach } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { WatchingBanner } from "./WatchingBanner"; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("WatchingBanner 文件监听横幅", () => { + it("渲染监听横幅容器", () => { + render(); + expect(screen.getByTestId("watching-banner")).toBeInTheDocument(); + }); + + it("提供最后同步时间时渲染时间戳区", () => { + render(); + expect(screen.getByTestId("watching-last-sync")).toBeInTheDocument(); + }); + + it("未提供最后同步时间时不渲染时间戳区", () => { + render(); + expect(screen.queryByTestId("watching-last-sync")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/install/components/WatchingBanner.tsx b/src/pages/install/components/WatchingBanner.tsx new file mode 100644 index 000000000..51b56944b --- /dev/null +++ b/src/pages/install/components/WatchingBanner.tsx @@ -0,0 +1,41 @@ +import { useTranslation } from "react-i18next"; +import { CircleCheckBig } from "lucide-react"; + +export interface WatchingBannerProps { + /** 正在监听的本地文件名 */ + fileName: string; + /** 最后一次自动同步的时间(本地化字符串);未同步过则不展示 */ + lastSync?: string; +} + +/** + * 本地文件监听横幅(对照设计稿 Watching Banner):绿色软底 + 左侧脉冲活动点, + * 标题「正在监听文件变化」+ 文件名说明,右侧显示最后同步时间。 + */ +export function WatchingBanner({ fileName, lastSync }: WatchingBannerProps) { + const { t } = useTranslation(["install", "common"]); + + return ( +
+
+ ); +} diff --git a/src/pages/install/hooks.tsx b/src/pages/install/hooks.tsx deleted file mode 100644 index 9b9d0a709..000000000 --- a/src/pages/install/hooks.tsx +++ /dev/null @@ -1,716 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from "react"; -import { useSearchParams } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import { Message, Typography } from "@arco-design/web-react"; -import { uuidv4 } from "@App/pkg/utils/uuid"; -import type { SCMetadata, Script } from "@App/app/repo/scripts"; -import { SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE } from "@App/app/repo/scripts"; -import type { Subscribe } from "@App/app/repo/subscribe"; -import { createScriptInfoLocal, createTempCodeEntry, getTempCode, type ScriptInfo } from "@App/pkg/utils/scriptInstall"; -import { parseMetadata, prepareScriptByCode, prepareSubscribeByCode } from "@App/pkg/utils/script"; -import { nextTimeDisplay } from "@App/pkg/utils/cron"; -import { scriptClient, subscribeClient, agentClient } from "../store/features/script"; -import { type FTInfo, startFileTrack, unmountFileTrack } from "@App/pkg/utils/file-tracker"; -import { cleanupOldHandles, loadHandle, saveHandle } from "@App/pkg/utils/filehandle-db"; -import { dayFormat } from "@App/pkg/utils/day_format"; -import { intervalExecution, timeoutExecution } from "@App/pkg/utils/timer"; -import { formatBytes, isPermissionOk } from "@App/pkg/utils/utils"; -import { i18nName } from "@App/locales/locales"; -import { parseSkillScriptMetadata } from "@App/pkg/utils/skill_script"; -import type { SkillScriptMetadata } from "@App/app/service/agent/core/types"; -import { TempStorageDAO, TempStorageItemType } from "@App/app/repo/tempStorage"; -import { - cIdKey, - backgroundPromptShownKey, - closeWindow, - fetchScriptBody, - startKeepAlive, - type Permission, -} from "./utils"; - -type ScriptOrSubscribe = Script | Subscribe; - -export function useInstallData() { - const [enable, setEnable] = useState(false); - const [btnText, setBtnText] = useState(""); - const [scriptCode, setScriptCode] = useState(""); - const [scriptInfo, setScriptInfo] = useState(); - const [upsertScript, setUpsertScript] = useState(undefined); - const [diffCode, setDiffCode] = useState(); - const [oldScriptVersion, setOldScriptVersion] = useState(null); - const [isUpdate, setIsUpdate] = useState(false); - const [localFileHandle, setLocalFileHandle] = useState(null); - const [showBackgroundPrompt, setShowBackgroundPrompt] = useState(false); - const { t } = useTranslation(); - const [searchParams, setSearchParams] = useSearchParams(); - const [loaded, setLoaded] = useState(false); - const [doBackwards, setDoBackwards] = useState(false); - const [skillScriptMetadata, setSkillScriptMetadata] = useState(null); - const [watchFile, setWatchFile] = useState(false); - const closingWindowRef = useRef(false); - - // Skill 安装相关状态 - const skillInstallUuid = searchParams.get("skill"); - const [skillPreview, setSkillPreview] = useState<{ - metadata: { name: string; description: string; version?: string }; - prompt: string; - scripts: Array<{ name: string; code: string }>; - references: Array<{ name: string; content: string }>; - isUpdate: boolean; - installUrl?: string; - } | null>(null); - - const installOrUpdateScript = async (newScript: Script, code: string) => { - if (newScript.ignoreVersion) newScript.ignoreVersion = ""; - await scriptClient.install({ script: newScript, code }); - const metadata = newScript.metadata; - setScriptInfo((prev) => (prev ? { ...prev, code, metadata } : prev)); - const scriptVersion = metadata.version?.[0]; - const oldScriptVersion = typeof scriptVersion === "string" ? scriptVersion : "N/A"; - setOldScriptVersion(oldScriptVersion); - setUpsertScript(newScript); - setDiffCode(code); - }; - - const getUpdatedNewScript = async (uuid: string, code: string) => { - const oldScript = await scriptClient.info(uuid); - if (!oldScript || oldScript.uuid !== uuid) { - throw new Error("uuid is mismatched"); - } - const { script } = await prepareScriptByCode(code, oldScript.origin || "", uuid); - script.origin = oldScript.origin || script.origin || ""; - if (!script.name) { - throw new Error(t("script_name_cannot_be_set_to_empty")); - } - return script; - }; - - const checkBackgroundPrompt = async (script: Script) => { - if (!script.metadata.background && !script.metadata.crontab) { - return false; - } - const hasShown = localStorage.getItem(backgroundPromptShownKey); - if (hasShown !== "true") { - const permission = await isPermissionOk("background"); - if (permission === false) return true; - } - return false; - }; - - // Skill ZIP 安装:从缓存加载并解析 - const initSkillFromCache = async (uuid: string) => { - try { - setLoaded(true); - if (window.history.length > 1) { - setDoBackwards(true); - } - const data = await agentClient.getSkillInstallData(uuid); - setSkillPreview(data); - } catch (e: any) { - Message.error(t("script_info_load_failed") + " " + e.message); - } - }; - - // Skill 安装确认 - const handleSkillInstall = async () => { - if (!skillInstallUuid) return; - try { - await agentClient.completeSkillInstall(skillInstallUuid); - Message.success(t("install_success")!); - setTimeout(() => { - closeWindow(doBackwards); - }, 500); - } catch (e) { - Message.error(`${t("install_failed")}: ${e}`); - } - }; - - // Skill 安装取消 - const handleSkillCancel = () => { - if (!skillInstallUuid) return; - agentClient.cancelSkillInstall(skillInstallUuid); - closeWindow(doBackwards); - }; - - const initAsync = async () => { - try { - const uuid = searchParams.get("uuid"); - const fid = searchParams.get("file"); - - // 如果有 url 或 没有 uuid 和 file,跳过初始化逻辑 - if (searchParams.get("url") || (!uuid && !fid)) { - return; - } - let info: ScriptInfo | undefined; - let isKnownUpdate: boolean = false; - - if (window.history.length > 1) { - setDoBackwards(true); - } - setLoaded(true); - - let paramOptions = {}; - if (uuid) { - startKeepAlive(uuid); - const cachedInfo = await scriptClient.getInstallInfo(uuid); - if (cachedInfo?.[0]) isKnownUpdate = true; - info = cachedInfo?.[1] || undefined; - paramOptions = cachedInfo?.[2] || {}; - if (!info) { - throw new Error("fetch script info failed"); - } - const code = await getTempCode(uuid); - if (code === undefined) { - throw new Error("failed to load script code from temp storage"); - } - info.code = code; - } else { - // 检查是不是本地文件安装 - if (!fid) { - throw new Error("url param - local file id is not found"); - } - const fileHandle = await loadHandle(fid); - if (!fileHandle) { - throw new Error("invalid file access - fileHandle is null"); - } - const file = await fileHandle.getFile(); - if (!file) { - throw new Error("invalid file access - file is null"); - } - // 处理本地文件的安装流程 - setLocalFileHandle((prev) => { - if (prev instanceof FileSystemFileHandle) unmountFileTrack(prev); - return fileHandle!; - }); - - // 刷新 timestamp, 使 10s~15s 后不会被立即清掉 - intervalExecution(`${cIdKey}liveFileHandle`, () => saveHandle(fid, fileHandle), 5 * 60 * 1000, true); - - const code = await file.text(); - const metadata = parseMetadata(code); - if (!metadata) { - // 非 UserScript,尝试作为 SkillScript 处理 - const skillScriptMeta = parseSkillScriptMetadata(code); - if (!skillScriptMeta) { - throw new Error("parse script info failed"); - } - info = await createScriptInfoLocal( - uuidv4(), - code, - `file:///*from-local*/${file.name}`, - "user", - {} as SCMetadata - ); - info.skillScript = true; - } else { - info = await createScriptInfoLocal(uuidv4(), code, `file:///*from-local*/${file.name}`, "user", metadata); - } - } - - // SkillScript 安装:只需解析元数据并展示 - if (info.skillScript) { - const toolMeta = parseSkillScriptMetadata(info.code); - if (!toolMeta) { - throw new Error("Invalid SkillScript: missing or malformed ==SkillScript== header"); - } - setSkillScriptMetadata(toolMeta); - setScriptCode(info.code); - setScriptInfo(info); - return; - } - - let prepare: - | { script: Script; oldScript?: Script; oldScriptCode?: string } - | { subscribe: Subscribe; oldSubscribe?: Subscribe }; - let action: Script | Subscribe; - - const { code, url } = info; - let oldVersion: string | undefined = undefined; - let diffCode: string | undefined = undefined; - if (info.userSubscribe) { - prepare = await prepareSubscribeByCode(code, url); - action = prepare.subscribe; - if (prepare.oldSubscribe) { - const oldSubscribeVersion = prepare.oldSubscribe.metadata.version?.[0]; - oldVersion = typeof oldSubscribeVersion === "string" ? oldSubscribeVersion : "N/A"; - } - diffCode = prepare.oldSubscribe?.code; - } else { - const knownUUID = isKnownUpdate ? info.uuid : undefined; - prepare = await prepareScriptByCode(code, url, knownUUID, false, undefined, paramOptions); - action = prepare.script; - if (prepare.oldScript) { - const oldScriptVersion = prepare.oldScript.metadata.version?.[0]; - oldVersion = typeof oldScriptVersion === "string" ? oldScriptVersion : "N/A"; - } - diffCode = prepare.oldScriptCode; - } - setScriptCode(code); - setDiffCode(diffCode); - setOldScriptVersion(typeof oldVersion === "string" ? oldVersion : null); - setIsUpdate(typeof oldVersion === "string"); - setScriptInfo(info); - setUpsertScript(action); - - // 检查是否需要显示后台运行提示 - if (!info.userSubscribe) { - setShowBackgroundPrompt(await checkBackgroundPrompt(action as Script)); - } - } catch (e: any) { - Message.error(t("script_info_load_failed") + " " + e.message); - } finally { - const delay = Math.floor(5000 * Math.random()) + 10000; - timeoutExecution(`${cIdKey}cleanupFileHandle`, cleanupOldHandles, delay); - } - }; - - useEffect(() => { - closingWindowRef.current = false; - if (loaded) return; - if (skillInstallUuid) { - initSkillFromCache(skillInstallUuid); - } else { - initAsync(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchParams, loaded]); - - const metadataLive = useMemo(() => (scriptInfo?.metadata || {}) as SCMetadata, [scriptInfo]); - - const permissions = useMemo(() => { - const permissions: Permission = []; - - if (!scriptInfo) return permissions; - - if (scriptInfo.userSubscribe) { - permissions.push({ - label: t("subscribe_install_label"), - color: "#ff0000", - value: metadataLive.scripturl!, - }); - } - - if (metadataLive.match) { - permissions.push({ label: t("script_runs_in"), value: metadataLive.match }); - } - - if (metadataLive.connect) { - permissions.push({ - label: t("script_has_full_access_to"), - color: "#F9925A", - value: metadataLive.connect, - }); - } - - if (metadataLive.require) { - permissions.push({ label: t("script_requires"), value: metadataLive.require }); - } - - return permissions; - }, [scriptInfo, metadataLive, t]); - - const descriptionParagraph = useMemo(() => { - const ret: JSX.Element[] = []; - - if (!scriptInfo) return ret; - - const isCookie = metadataLive.grant?.some((val: string) => val === "GM_cookie"); - if (isCookie) { - ret.push( - - {t("cookie_warning")} - - ); - } - - if (metadataLive.crontab) { - ret.push({t("scheduled_script_description_title")}); - ret.push( -
- {t("scheduled_script_description_description_expr")} - {metadataLive.crontab[0]} - {t("scheduled_script_description_description_next")} - {nextTimeDisplay(metadataLive.crontab[0])} -
- ); - } else if (metadataLive.background) { - ret.push({t("background_script_description")}); - } - - return ret; - }, [scriptInfo, metadataLive, t]); - - const antifeatures: { [key: string]: { color: string; title: string; description: string } } = { - "referral-link": { - color: "purple", - title: t("antifeature_referral_link_title"), - description: t("antifeature_referral_link_description"), - }, - ads: { - color: "orange", - title: t("antifeature_ads_title"), - description: t("antifeature_ads_description"), - }, - payment: { - color: "magenta", - title: t("antifeature_payment_title"), - description: t("antifeature_payment_description"), - }, - miner: { - color: "orangered", - title: t("antifeature_miner_title"), - description: t("antifeature_miner_description"), - }, - membership: { - color: "blue", - title: t("antifeature_membership_title"), - description: t("antifeature_membership_description"), - }, - tracking: { - color: "pinkpurple", - title: t("antifeature_tracking_title"), - description: t("antifeature_tracking_description"), - }, - }; - - // 更新按钮文案和页面标题 - useEffect(() => { - if (skillPreview) { - document.title = `${t("install_script")} - ${skillPreview.metadata.name} - ScriptCat`; - return; - } - if (scriptInfo?.skillScript && skillScriptMetadata) { - document.title = `${t("install_script")} - ${skillScriptMetadata.name} - ScriptCat`; - return; - } - if (scriptInfo?.userSubscribe) { - setBtnText(isUpdate ? t("update_subscribe")! : t("install_subscribe")); - } else { - setBtnText(isUpdate ? t("update_script")! : t("install_script")); - } - if (upsertScript) { - document.title = `${!isUpdate ? t("install_script") : t("update_script")} - ${i18nName(upsertScript!)} - ScriptCat`; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isUpdate, scriptInfo, upsertScript, skillScriptMetadata, t]); - - // 设置脚本状态 - useEffect(() => { - if (upsertScript) { - setEnable(upsertScript.status === SCRIPT_STATUS_ENABLE); - } - }, [upsertScript]); - - const handleInstall = async (options: { closeAfterInstall?: boolean; noMoreUpdates?: boolean } = {}) => { - if (closingWindowRef.current) return; - if (!upsertScript) { - Message.error(t("script_info_load_failed")!); - return; - } - - const { closeAfterInstall: shouldClose = true, noMoreUpdates: disableUpdates = false } = options; - - try { - if (scriptInfo?.userSubscribe) { - await subscribeClient.install(upsertScript as Subscribe); - Message.success(t("subscribe_success")!); - setBtnText(t("subscribe_success")!); - } else { - if (disableUpdates && upsertScript) { - (upsertScript as Script).checkUpdate = false; - } - await scriptClient.install({ script: upsertScript as Script, code: scriptCode }); - if (isUpdate) { - Message.success(t("install.update_success")!); - setBtnText(t("install.update_success")!); - } else { - if (disableUpdates && upsertScript) { - (upsertScript as Script).checkUpdate = false; - } - if ((upsertScript as Script).ignoreVersion) (upsertScript as Script).ignoreVersion = ""; - await scriptClient.install({ script: upsertScript as Script, code: scriptCode }); - if (isUpdate) { - Message.success(t("install.update_success")!); - setBtnText(t("install.update_success")!); - } else { - Message.success(t("install_success")!); - setBtnText(t("install_success")!); - } - } - } - - if (shouldClose) { - if (!closingWindowRef.current) { - closingWindowRef.current = true; - setTimeout(() => { - closeWindow(doBackwards); - }, 500); - } - } - } catch (e) { - const errorMessage = scriptInfo?.userSubscribe ? t("subscribe_failed") : t("install_failed"); - Message.error(`${errorMessage}: ${e}`); - } - }; - - const handleClose = async (options?: { noMoreUpdates: boolean }) => { - if (closingWindowRef.current) return; - const { noMoreUpdates = false } = options || {}; - if (noMoreUpdates && scriptInfo && !scriptInfo.userSubscribe) { - await scriptClient.setCheckUpdateUrl(scriptInfo.uuid, false); - } - if (!closingWindowRef.current) { - closingWindowRef.current = true; - setTimeout(() => { - closeWindow(doBackwards); - }, 50); - } - }; - - const handleInstallBasic = () => handleInstall(); - const handleInstallCloseAfterInstall = () => handleInstall({ closeAfterInstall: false }); - const handleInstallNoMoreUpdates = () => handleInstall({ noMoreUpdates: true }); - const handleStatusChange = (checked: boolean) => { - setUpsertScript((script) => { - if (!script) { - return script; - } - script.status = checked ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE; - setEnable(checked); - return script; - }); - }; - const handleCloseBasic = () => handleClose(); - const handleCloseNoMoreUpdates = () => handleClose({ noMoreUpdates: true }); - const setWatchFileClick = () => { - setWatchFile((prev) => !prev); - }; - - const fileWatchMessageId = `id_${Math.random()}`; - - async function onWatchFileCodeChanged(this: FTInfo, code: string, hideInfo: boolean = false) { - if (this.uuid !== scriptInfo?.uuid) return; - if (this.fileName !== localFileHandle?.name) return; - setScriptCode(code); - const uuid = (upsertScript as Script)?.uuid; - if (!uuid) { - throw new Error("uuid is undefined"); - } - try { - const newScript = await getUpdatedNewScript(uuid, code); - await installOrUpdateScript(newScript, code); - } catch (e) { - Message.error({ - id: fileWatchMessageId, - content: t("install_failed") + ": " + e, - }); - return; - } - if (!hideInfo) { - Message.info({ - id: fileWatchMessageId, - content: `${t("last_updated")}: ${dayFormat()}`, - duration: 3000, - closable: true, - showIcon: true, - }); - } - } - - async function onWatchFileError() { - setWatchFile(false); - } - - const memoWatchFile = useMemo(() => { - return `${watchFile}.${scriptInfo?.uuid}.${localFileHandle?.name}`; - }, [watchFile, scriptInfo, localFileHandle]); - - const setupWatchFile = async (uuid: string, fileName: string, handle: FileSystemFileHandle) => { - try { - const code = `${scriptCode}`; - await installOrUpdateScript(upsertScript as Script, code); - setDiffCode(`${code}`); - const ftInfo: FTInfo = { - uuid, - fileName, - setCode: onWatchFileCodeChanged, - onFileError: onWatchFileError, - }; - startFileTrack(handle, ftInfo); - const file = await handle.getFile(); - const currentCode = await file.text(); - if (currentCode !== code) { - ftInfo.setCode(currentCode, true); - } - } catch (e: any) { - Message.error(`${e.message}`); - console.warn(e); - } - }; - - useEffect(() => { - if (!watchFile || !localFileHandle) { - return; - } - const [handle] = [localFileHandle]; - unmountFileTrack(handle); - const uuid = scriptInfo?.uuid; - const fileName = handle?.name; - if (!uuid || !fileName) { - return; - } - setupWatchFile(uuid, fileName, handle); - return () => { - unmountFileTrack(handle); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [memoWatchFile]); - - // 检查是否有 uuid 或 file - const searchParamUrl = searchParams.get("url"); - const hasValidSourceParam = - !searchParamUrl && !!(searchParams.get("uuid") || searchParams.get("file") || skillInstallUuid); - - const urlHref = useMemo(() => { - if (searchParamUrl) { - try { - const idx = location.search.indexOf("url="); - const rawUrl = idx !== -1 ? location.search.slice(idx + 4) : searchParamUrl; - const urlObject = new URL(rawUrl); - if (urlObject.protocol && urlObject.hostname && urlObject.pathname) { - return rawUrl; - } - } catch { - // ignored - } - } - return ""; - }, [searchParamUrl]); - - const [fetchingState, setFetchingState] = useState({ - loadingStatus: "", - errorStatus: "", - }); - - const loadURLAsync = async (url: string) => { - const fetchValidScript = async () => { - const result = await fetchScriptBody(url, { - onProgress: (info: { receivedLength: number }) => { - setFetchingState((prev) => ({ - ...prev, - loadingStatus: t("downloading_status_text", { bytes: formatBytes(info.receivedLength) }), - })); - }, - }); - if (result.code && result.metadata) { - return { result, url } as const; - } - throw new Error(t("install_page_load_failed")); - }; - - try { - const { result, url } = await fetchValidScript(); - const { code, metadata } = result; - const isSkillScript = "skillScript" in result && result.skillScript === true; - - const uuid = uuidv4(); - const scriptData = await createTempCodeEntry(false, uuid, code, url, "user", metadata, {}); - const info = scriptData[1]; - if (isSkillScript) { - info.skillScript = true; - } - await new TempStorageDAO().save({ - key: uuid, - value: scriptData, - savedAt: Date.now(), - type: TempStorageItemType.tempCode, - }); - - setSearchParams(new URLSearchParams(`?uuid=${uuid}`), { replace: true }); - } catch (err: any) { - setFetchingState((prev) => ({ - ...prev, - loadingStatus: "", - errorStatus: `${err?.message || err}`, - })); - } - }; - - // 从 URL 加载 Skill(.cat.md) - const loadSkillFromUrl = async (url: string) => { - try { - setFetchingState((prev) => ({ - ...prev, - loadingStatus: t("install_page_please_wait"), - })); - const uuid = await agentClient.prepareSkillFromUrl(url); - await initSkillFromCache(uuid); - } catch (err: any) { - setFetchingState((prev) => ({ - ...prev, - loadingStatus: "", - errorStatus: `${err?.message || err}`, - })); - } - }; - - const handleUrlChangeAndFetch = (targetUrlHref: string) => { - // .cat.md URL → Skill 安装流程 - if (targetUrlHref.match(/\.cat\.md(\?|#|$)/i)) { - loadSkillFromUrl(targetUrlHref); - return; - } - setFetchingState((prev) => ({ - ...prev, - loadingStatus: t("install_page_please_wait"), - })); - loadURLAsync(targetUrlHref); - }; - - // 有 url 的话下载内容 - useEffect(() => { - if (urlHref) handleUrlChangeAndFetch(urlHref); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [urlHref]); - - return { - // 状态 - enable, - btnText, - scriptCode, - scriptInfo, - upsertScript, - diffCode, - oldScriptVersion, - isUpdate, - localFileHandle, - showBackgroundPrompt, - setShowBackgroundPrompt, - skillScriptMetadata, - watchFile, - metadataLive, - permissions, - descriptionParagraph, - antifeatures, - hasValidSourceParam, - urlHref, - fetchingState, - // 事件处理 - handleInstallBasic, - handleInstallCloseAfterInstall, - handleInstallNoMoreUpdates, - handleStatusChange, - handleCloseBasic, - handleCloseNoMoreUpdates, - setWatchFileClick, - // Skill 安装 - skillPreview, - skillInstallUuid, - handleSkillInstall, - handleSkillCancel, - // i18n - t, - }; -} - -export type InstallData = ReturnType; diff --git a/src/pages/install/index.css b/src/pages/install/index.css deleted file mode 100644 index b54e8fd9b..000000000 --- a/src/pages/install/index.css +++ /dev/null @@ -1,108 +0,0 @@ -.monaco-diff-editor .diffOverview { - background: var(--vscode-editorGutter-background); -} - -#install-app-container { - display: flex; - flex-direction: column; - min-height: calc(100vh - 50px); - box-sizing: border-box; -} - -#show-code-container { - display: block; - height: calc( 100% - 44px ); - padding: 2px 2px; - position: relative; - box-sizing: border-box; - margin: 0; - border: 0; - flex-grow: 1; - flex-shrink: 0; - contain: strict; -} - -#show-code { /* 配合 .sc-inset-0 */ - margin: 0px; - padding: 0px; - border: 0px; - overflow: hidden; - border: 1px solid var(--color-neutral-5); - box-sizing: border-box; - position: absolute; - background: #071119; -} - -.downloading { - display: flex; - flex-direction: row; - flex-wrap: wrap; - column-gap: 4px; - align-items: center; -} - -.error-message { - color: red; -} - -.error-message:empty { - display: none; -} - -.error-message::before { - content: "ERROR: "; -} - -/* https://css-loaders.com/dots/ */ -.loader { - width: 60px; - aspect-ratio: 2; - --_g: no-repeat radial-gradient(circle closest-side, currentColor 90%, rgba(0,0,0,0)); - background: var(--_g) 0% 50%, var(--_g) 50% 50%, var(--_g) 100% 50%; - background-size: calc(100%/3) 50%; - animation: l3 1s infinite linear; - transform: scale(0.5); -} - -@keyframes l3 { - 20% { - background-position: 0% 0%, 50% 50%, 100% 50% - } - - 40% { - background-position: 0% 100%, 50% 0%, 100% 50% - } - - 60% { - background-position: 0% 50%, 50% 100%, 100% 0% - } - - 80% { - background-position: 0% 50%, 50% 50%, 100% 100% - } - -} - -.tag-container { - inline-size: min-content; - align-content: flex-start; - justify-items: flex-end; - justify-content: flex-end; -} - -.tag-container .arco-tag { - flex-grow: 1; - text-align: center; -} - -div.permission-entry span.arco-typography { - line-height: 1rem; - padding: 0; - margin: 0; -} - -div.permission-entry { - line-height: 1.2rem; - padding: 0; - margin: 0; -} diff --git a/src/pages/install/main.tsx b/src/pages/install/main.tsx index 2c1ac0f69..1449d5eec 100644 --- a/src/pages/install/main.tsx +++ b/src/pages/install/main.tsx @@ -1,19 +1,12 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; -import { AppProvider } from "../store/AppContext.tsx"; -import MainLayout from "../components/layout/MainLayout.tsx"; import LoggerCore from "@App/app/logger/core.ts"; import { message } from "../store/global.ts"; import MessageWriter from "@App/app/logger/message_writer.ts"; -import "@arco-design/web-react/dist/css/arco.css"; -import "@App/locales/locales"; +import { ThemeProvider } from "../components/theme-provider.tsx"; +import { Toaster } from "../components/ui/sonner.tsx"; import "@App/index.css"; -import "./index.css"; -import { registerEditor } from "@App/pkg/utils/monaco-editor"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; - -registerEditor(); // 初始化日志组件 const loggerCore = new LoggerCore({ @@ -23,19 +16,11 @@ const loggerCore = new LoggerCore({ loggerCore.logger().debug("install page start"); -const MyApp = () => ( - - - - - -); const Root = ( - - - } /> - - + + + + ); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/src/pages/install/model.test.ts b/src/pages/install/model.test.ts new file mode 100644 index 000000000..09ee00d6a --- /dev/null +++ b/src/pages/install/model.test.ts @@ -0,0 +1,87 @@ +// can be tested with vitest-environment node +import { describe, it, expect } from "vitest"; +import { deriveVersion, deriveAntifeatures, deriveScheduleInfo, deriveDiffStat } from "./model"; + +describe("deriveVersion 版本展示派生", () => { + it("全新安装返回单版本展示", () => { + expect(deriveVersion("2.3.1", null)).toEqual({ kind: "install", version: "2.3.1" }); + }); + + it("更新返回旧→新版本展示,版本变化时 changed 为 true", () => { + expect(deriveVersion("2.3.1", "2.1.0")).toEqual({ + kind: "update", + oldVersion: "2.1.0", + newVersion: "2.3.1", + changed: true, + }); + }); + + it("更新但版本号未变化时 changed 为 false", () => { + expect(deriveVersion("2.1.0", "2.1.0")).toEqual({ + kind: "update", + oldVersion: "2.1.0", + newVersion: "2.1.0", + changed: false, + }); + }); + + it("缺少版本号时回退为空串", () => { + expect(deriveVersion(undefined, null)).toEqual({ kind: "install", version: "" }); + }); +}); + +describe("deriveAntifeatures 反特性派生", () => { + it("解析已知反特性类型,取首个空格前的 token", () => { + expect(deriveAntifeatures({ antifeature: ["referral-link 含推广链接", "ads"] })).toEqual(["referral-link", "ads"]); + }); + + it("忽略未知反特性类型", () => { + expect(deriveAntifeatures({ antifeature: ["unknown-thing 描述", "miner"] })).toEqual(["miner"]); + }); + + it("无反特性时返回空数组", () => { + expect(deriveAntifeatures({})).toEqual([]); + }); +}); + +describe("deriveScheduleInfo 定时/后台分类派生", () => { + it("定时脚本返回 cron 分类与表达式", () => { + expect(deriveScheduleInfo({ crontab: ["0 8 * * *"] })).toEqual({ + kind: "cron", + expression: "0 8 * * *", + }); + }); + + it("后台脚本返回 background 分类", () => { + expect(deriveScheduleInfo({ background: [""] })).toEqual({ kind: "background" }); + }); + + it("普通脚本返回 null", () => { + expect(deriveScheduleInfo({})).toBeNull(); + }); + + it("同时有 crontab 与 background 时优先 cron", () => { + expect(deriveScheduleInfo({ crontab: ["* * * * *"], background: [""] })).toEqual({ + kind: "cron", + expression: "* * * * *", + }); + }); +}); + +describe("deriveDiffStat 代码增删行统计派生", () => { + it("纯新增行只计 added", () => { + expect(deriveDiffStat("a\nb", "a\nb\nc")).toEqual({ added: 1, removed: 0 }); + }); + + it("纯删除行只计 removed", () => { + expect(deriveDiffStat("a\nb\nc", "a\nb")).toEqual({ added: 0, removed: 1 }); + }); + + it("替换一行计为一增一删", () => { + expect(deriveDiffStat("a\nb\nc", "a\nX\nc")).toEqual({ added: 1, removed: 1 }); + }); + + it("内容完全相同时增删均为 0", () => { + expect(deriveDiffStat("a\nb\nc", "a\nb\nc")).toEqual({ added: 0, removed: 0 }); + }); +}); diff --git a/src/pages/install/model.ts b/src/pages/install/model.ts new file mode 100644 index 000000000..dcbe4e42b --- /dev/null +++ b/src/pages/install/model.ts @@ -0,0 +1,76 @@ +import type { SCMetadata } from "@App/app/repo/metadata"; + +export type VersionDisplay = + | { kind: "install"; version: string } + | { kind: "update"; oldVersion: string; newVersion: string; changed: boolean }; + +/** + * 派生版本徽章展示数据。 + * - 全新安装(无旧版本):单枚版本。 + * - 更新(有旧版本):旧 → 新,并标记版本号是否变化。 + */ +export function deriveVersion(newVersion: string | undefined, oldVersion: string | null): VersionDisplay { + const next = newVersion || ""; + if (oldVersion === null) { + return { kind: "install", version: next }; + } + return { kind: "update", oldVersion, newVersion: next, changed: next !== oldVersion }; +} + +// 已知反特性类型(与 v1.4 antifeatures 映射一致) +export const ANTIFEATURE_TYPES = ["referral-link", "ads", "payment", "miner", "membership", "tracking"] as const; + +export type AntifeatureType = (typeof ANTIFEATURE_TYPES)[number]; + +const ANTIFEATURE_SET = new Set(ANTIFEATURE_TYPES); + +/** + * 从 @antifeature 标签派生已知反特性类型(取首个空格前的 token,忽略未知类型)。 + */ +export function deriveAntifeatures(metadata: SCMetadata): AntifeatureType[] { + const list: AntifeatureType[] = []; + for (const entry of metadata.antifeature || []) { + const type = entry.split(" ")[0]; + if (ANTIFEATURE_SET.has(type)) { + list.push(type as AntifeatureType); + } + } + return list; +} + +export type DiffStat = { added: number; removed: number }; + +/** + * 派生代码更新的增删行统计(按行多重集差): + * removed = 旧代码中未被新代码匹配的行数,added = 新代码中未被旧代码匹配的行数。 + * 改一行计为一增一删(与 git --numstat 语义一致),用于代码卡头的 +N −M 徽章。 + */ +export function deriveDiffStat(oldCode: string, newCode: string): DiffStat { + const freq = (code: string) => { + const m = new Map(); + for (const line of code.split("\n")) m.set(line, (m.get(line) || 0) + 1); + return m; + }; + const oldFreq = freq(oldCode); + const newFreq = freq(newCode); + let added = 0; + let removed = 0; + for (const [line, n] of newFreq) added += Math.max(0, n - (oldFreq.get(line) || 0)); + for (const [line, n] of oldFreq) removed += Math.max(0, n - (newFreq.get(line) || 0)); + return { added, removed }; +} + +export type ScheduleInfo = { kind: "cron"; expression: string } | { kind: "background" } | null; + +/** + * 分类脚本的运行方式:定时(cron)优先,其次纯后台,否则普通页面脚本(null)。 + */ +export function deriveScheduleInfo(metadata: SCMetadata): ScheduleInfo { + if (metadata.crontab?.length) { + return { kind: "cron", expression: metadata.crontab[0] }; + } + if (metadata.background) { + return { kind: "background" }; + } + return null; +} diff --git a/src/pages/install/permissions.test.ts b/src/pages/install/permissions.test.ts new file mode 100644 index 000000000..cc49d0afd --- /dev/null +++ b/src/pages/install/permissions.test.ts @@ -0,0 +1,95 @@ +// can be tested with vitest-environment node +import { describe, it, expect } from "vitest"; +import type { SCMetadata } from "@App/app/repo/metadata"; +import { derivePermissions } from "./permissions"; + +describe("derivePermissions 权限派生", () => { + it("无任何权限元数据时返回空数组", () => { + expect(derivePermissions({})).toEqual([]); + }); + + it("将 @match 派生为运行网站权限行,风险为 normal", () => { + const metadata: SCMetadata = { match: ["https://example.com/*"] }; + const rows = derivePermissions(metadata); + const match = rows.find((r) => r.kind === "match"); + expect(match).toBeDefined(); + expect(match!.risk).toBe("normal"); + expect(match!.values).toEqual(["https://example.com/*"]); + }); + + it("@include 与 @match 合并进运行网站行", () => { + const metadata: SCMetadata = { + match: ["https://a.com/*"], + include: ["https://b.com/*"], + }; + const rows = derivePermissions(metadata); + const match = rows.find((r) => r.kind === "match"); + expect(match!.values).toEqual(["https://a.com/*", "https://b.com/*"]); + }); + + it("@connect 普通域名时跨域访问行风险为 warn", () => { + const rows = derivePermissions({ connect: ["api.example.com"] }); + const connect = rows.find((r) => r.kind === "connect"); + expect(connect!.risk).toBe("warn"); + }); + + it("@connect 为 * 时跨域访问行标记为 danger", () => { + const rows = derivePermissions({ connect: ["*"] }); + const connect = rows.find((r) => r.kind === "connect"); + expect(connect!.risk).toBe("danger"); + }); + + it("@connect 中 * 应排在普通域名前面", () => { + const rows = derivePermissions({ connect: ["api.example.com", "*", "cdn.example.com"] }); + const connect = rows.find((r) => r.kind === "connect"); + expect(connect!.values).toEqual(["*", "api.example.com", "cdn.example.com"]); + }); + + it("@grant 派生为 GM 能力行,风险为 warn", () => { + const rows = derivePermissions({ grant: ["GM_setValue", "GM_getValue"] }); + const grant = rows.find((r) => r.kind === "grant"); + expect(grant!.risk).toBe("warn"); + expect(grant!.values).toEqual(["GM_setValue", "GM_getValue"]); + expect(grant!.sensitive).toEqual([]); + }); + + it("@grant 含 GM_cookie 时标记为敏感能力", () => { + const rows = derivePermissions({ grant: ["GM_setValue", "GM_cookie"] }); + const grant = rows.find((r) => r.kind === "grant"); + expect(grant!.sensitive).toEqual(["GM_cookie"]); + }); + + it("@grant 中敏感 GM 能力应排在普通能力前面", () => { + const rows = derivePermissions({ grant: ["GM_setValue", "GM_cookie", "GM_getValue"] }); + const grant = rows.find((r) => r.kind === "grant"); + expect(grant!.values).toEqual(["GM_cookie", "GM_setValue", "GM_getValue"]); + expect(grant!.sensitive).toEqual(["GM_cookie"]); + }); + + it("@grant 为 none 时不输出 GM 能力行", () => { + const rows = derivePermissions({ grant: ["none"] }); + expect(rows.find((r) => r.kind === "grant")).toBeUndefined(); + }); + + it("@require 与 @resource 合并为外部资源行,风险为 normal", () => { + const rows = derivePermissions({ + require: ["https://cdn.example.com/lib.js"], + resource: ["logo https://cdn.example.com/logo.png"], + }); + const require = rows.find((r) => r.kind === "require"); + expect(require!.risk).toBe("normal"); + expect(require!.values).toEqual(["https://cdn.example.com/lib.js", "logo https://cdn.example.com/logo.png"]); + }); + + it("权限行按高危、告警、普通排序,同风险保持类别顺序", () => { + const metadata: SCMetadata = { + require: ["https://cdn.example.com/lib.js"], + grant: ["GM_setValue"], + connect: ["*"], + match: ["https://example.com/*"], + }; + const rows = derivePermissions(metadata); + expect(rows.map((r) => r.kind)).toEqual(["connect", "grant", "match", "require"]); + expect(rows.map((r) => r.risk)).toEqual(["danger", "warn", "normal", "normal"]); + }); +}); diff --git a/src/pages/install/permissions.ts b/src/pages/install/permissions.ts new file mode 100644 index 000000000..70b5c20ce --- /dev/null +++ b/src/pages/install/permissions.ts @@ -0,0 +1,70 @@ +import type { SCMetadata } from "@App/app/repo/metadata"; + +export type PermissionRisk = "normal" | "warn" | "danger"; + +export type PermissionKind = "match" | "connect" | "grant" | "require"; + +export interface PermissionRow { + kind: PermissionKind; + risk: PermissionRisk; + values: string[]; + /** values 中被判定为敏感的子集(如 GM_cookie),用于额外高亮 */ + sensitive: string[]; +} + +// 需要额外标记的敏感 GM 能力(可访问 Cookie 等隐私数据) +const SENSITIVE_GRANTS = new Set(["GM_cookie"]); +const RISK_ORDER: Record = { + danger: 0, + warn: 1, + normal: 2, +}; + +function sortDangerConnectFirst(values: string[]): string[] { + return [...values].sort((a, b) => Number(b === "*") - Number(a === "*")); +} + +function sortSensitiveGrantFirst(values: string[]): string[] { + return [...values].sort((a, b) => Number(SENSITIVE_GRANTS.has(b)) - Number(SENSITIVE_GRANTS.has(a))); +} + +/** + * 把脚本元数据派生为「权限行」,作为安装页信任决策的核心呈现。 + * 高危/告警权限优先展示,同风险内保持基础类别顺序:运行网站 → 跨域访问 → GM 能力 → 外部资源。 + */ +export function derivePermissions(metadata: SCMetadata): PermissionRow[] { + const rows: PermissionRow[] = []; + + const match = [...(metadata.match || []), ...(metadata.include || [])]; + if (match.length) { + rows.push({ kind: "match", risk: "normal", values: match, sensitive: [] }); + } + + const connect = metadata.connect || []; + if (connect.length) { + const values = sortDangerConnectFirst(connect); + rows.push({ + kind: "connect", + risk: values.includes("*") ? "danger" : "warn", + values, + sensitive: [], + }); + } + + const grant = sortSensitiveGrantFirst((metadata.grant || []).filter((g) => g !== "none")); + if (grant.length) { + rows.push({ + kind: "grant", + risk: "warn", + values: grant, + sensitive: grant.filter((g) => SENSITIVE_GRANTS.has(g)), + }); + } + + const require = [...(metadata.require || []), ...(metadata.resource || [])]; + if (require.length) { + rows.push({ kind: "require", risk: "normal", values: require, sensitive: [] }); + } + + return rows.sort((a, b) => RISK_ORDER[a.risk] - RISK_ORDER[b.risk]); +} diff --git a/src/pages/install/useInstallData.test.ts b/src/pages/install/useInstallData.test.ts new file mode 100644 index 000000000..7ea6c28f8 --- /dev/null +++ b/src/pages/install/useInstallData.test.ts @@ -0,0 +1,479 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeAll, afterEach, type Mock } from "vitest"; +import { renderHook, waitFor, act } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import type { ScriptInfo } from "@App/pkg/utils/scriptInstall"; +import type { Script } from "@App/app/repo/scripts"; +import { SCRIPT_STATUS_ENABLE } from "@App/app/repo/scripts"; + +vi.mock("@App/pages/store/features/script", () => ({ + scriptClient: { getInstallInfo: vi.fn(), install: vi.fn(), setCheckUpdateUrl: vi.fn() }, + subscribeClient: { install: vi.fn() }, + agentClient: { + getSkillInstallData: vi.fn(), + completeSkillInstall: vi.fn(), + cancelSkillInstall: vi.fn(), + prepareSkillFromUrl: vi.fn(), + }, +})); +vi.mock("@App/pkg/utils/scriptInstall", async (io) => ({ + ...(await io>()), + getTempCode: vi.fn(), +})); +vi.mock("@App/pkg/utils/script", async (io) => ({ + ...(await io>()), + prepareScriptByCode: vi.fn(), + prepareSubscribeByCode: vi.fn(), + fetchScriptBody: vi.fn(), + parseMetadata: vi.fn(), +})); +vi.mock("@App/pkg/utils/filehandle-db", () => ({ + loadHandle: vi.fn(), + saveHandle: vi.fn(async () => {}), + cleanupOldHandles: vi.fn(async () => {}), +})); +vi.mock("@App/pkg/utils/file-tracker", () => ({ + startFileTrack: vi.fn(), + unmountFileTrack: vi.fn(async () => {}), +})); +vi.mock("@App/app/repo/tempStorage", () => ({ + TempStorageDAO: class { + update() { + return Promise.resolve(); + } + save() { + return Promise.resolve(); + } + }, + TempStorageItemType: { tempCode: "tempCode" }, +})); + +import { scriptClient, agentClient } from "@App/pages/store/features/script"; +import { getTempCode } from "@App/pkg/utils/scriptInstall"; +import { prepareScriptByCode, fetchScriptBody, parseMetadata } from "@App/pkg/utils/script"; +import { loadHandle } from "@App/pkg/utils/filehandle-db"; +import { startFileTrack, unmountFileTrack } from "@App/pkg/utils/file-tracker"; +import { assembleInstallView, useInstallData } from "./useInstallData"; + +const makeScriptInfo = (metadata: Record, url = "https://example.com/x.user.js"): ScriptInfo => ({ + url, + code: "", + uuid: "u1", + userSubscribe: false, + metadata, + source: "user", +}); + +const makeAction = (metadata: Record): Script => + ({ name: "示例脚本", metadata, status: SCRIPT_STATUS_ENABLE }) as unknown as Script; + +beforeAll(() => initTestLanguage("zh-CN")); + +describe("assembleInstallView 组装安装视图", () => { + it("全新安装组装名称、来源、版本与权限", () => { + const metadata = { + name: ["示例脚本"], + version: ["2.3.1"], + author: ["scriptcat"], + match: ["https://example.com/*"], + connect: ["*"], + }; + const view = assembleInstallView({ + isUpdate: false, + scriptInfo: makeScriptInfo(metadata), + action: makeAction(metadata), + code: "// code", + oldVersion: null, + }); + expect(view.isSubscribe).toBe(false); + expect(view.name).toBe("示例脚本"); + expect(view.author).toBe("scriptcat"); + expect(view.source).toContain("example.com"); + expect(view.version).toEqual({ kind: "install", version: "2.3.1" }); + expect(view.permissions.find((p) => p.kind === "connect")?.risk).toBe("danger"); + }); + + it("更新态组装 旧→新 版本", () => { + const metadata = { name: ["示例脚本"], version: ["2.3.1"] }; + const view = assembleInstallView({ + isUpdate: true, + scriptInfo: makeScriptInfo(metadata), + action: makeAction(metadata), + code: "// code", + oldVersion: "2.1.0", + }); + expect(view.version).toEqual({ kind: "update", oldVersion: "2.1.0", newVersion: "2.3.1", changed: true }); + }); + + it("更新态把旧代码透传到 oldCode 供 diff 使用", () => { + const metadata = { name: ["示例脚本"], version: ["2.3.1"] }; + const view = assembleInstallView({ + isUpdate: true, + scriptInfo: makeScriptInfo(metadata), + action: makeAction(metadata), + code: "// new code", + oldVersion: "2.1.0", + oldCode: "// old code", + }); + expect(view.oldCode).toBe("// old code"); + }); + + it("全新安装无旧代码时 oldCode 为 undefined", () => { + const metadata = { name: ["示例脚本"], version: ["2.3.1"] }; + const view = assembleInstallView({ + isUpdate: false, + scriptInfo: makeScriptInfo(metadata), + action: makeAction(metadata), + code: "// new code", + oldVersion: null, + }); + expect(view.oldCode).toBeUndefined(); + }); + + it("更新态且旧代码不同时填充 diffStat 增删统计", () => { + const metadata = { name: ["示例脚本"], version: ["2.3.1"] }; + const view = assembleInstallView({ + isUpdate: true, + scriptInfo: makeScriptInfo(metadata), + action: makeAction(metadata), + code: "a\nX\nc", + oldVersion: "2.1.0", + oldCode: "a\nb\nc", + }); + expect(view.diffStat).toEqual({ added: 1, removed: 1 }); + }); + + it("全新安装(无旧代码)时 diffStat 为 undefined", () => { + const metadata = { name: ["示例脚本"], version: ["2.3.1"] }; + const view = assembleInstallView({ + isUpdate: false, + scriptInfo: makeScriptInfo(metadata), + action: makeAction(metadata), + code: "a\nb\nc", + oldVersion: null, + }); + expect(view.diffStat).toBeUndefined(); + }); + + it("定时脚本组装 cron 信息条与下次运行", () => { + const metadata = { name: ["定时脚本"], version: ["1.0.0"], crontab: ["0 8 * * *"] }; + const view = assembleInstallView({ + isUpdate: false, + scriptInfo: makeScriptInfo(metadata), + action: makeAction(metadata), + code: "// code", + oldVersion: null, + }); + expect(view.schedule).toEqual({ kind: "cron", expression: "0 8 * * *" }); + expect(typeof view.scheduleNextRun).toBe("string"); + expect(view.scheduleNextRun!.length).toBeGreaterThan(0); + }); +}); + +describe("useInstallData 数据流编排", () => { + afterEach(() => { + vi.clearAllMocks(); + window.history.replaceState({}, "", "/install.html"); + }); + + it("无 uuid 参数时进入 invalid 状态", () => { + window.history.replaceState({}, "", "/install.html"); + const { result } = renderHook(() => useInstallData()); + expect(result.current.state.status).toBe("invalid"); + }); + + it("uuid 全新安装时读取信息并进入 ready 状态", async () => { + window.history.replaceState({}, "", "/install.html?uuid=u1"); + const metadata = { name: ["示例脚本"], version: ["1.0.0"], match: ["https://e.com/*"] }; + const info: ScriptInfo = { + url: "https://e.com/x.user.js", + code: "", + uuid: "u1", + userSubscribe: false, + metadata, + source: "user", + }; + (scriptClient.getInstallInfo as Mock).mockResolvedValue([false, info, {}]); + (getTempCode as Mock).mockResolvedValue("// code"); + (prepareScriptByCode as Mock).mockResolvedValue({ + script: { name: "示例脚本", metadata, status: SCRIPT_STATUS_ENABLE } as unknown as Script, + }); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("ready")); + const state = result.current.state; + if (state.status !== "ready") throw new Error("not ready"); + expect(state.view.name).toBe("示例脚本"); + expect(state.view.isUpdate).toBe(false); + expect(result.current.enabled).toBe(true); + }); + + it("uuid 更新时从 oldScriptCode 取旧代码到 view.oldCode", async () => { + window.history.replaceState({}, "", "/install.html?uuid=u1"); + const metadata = { name: ["示例脚本"], version: ["2.0.0"], match: ["https://e.com/*"] }; + const oldMetadata = { name: ["示例脚本"], version: ["1.0.0"] }; + const info: ScriptInfo = { + url: "https://e.com/x.user.js", + code: "", + uuid: "u1", + userSubscribe: false, + metadata, + source: "user", + }; + (scriptClient.getInstallInfo as Mock).mockResolvedValue([true, info, {}]); + (getTempCode as Mock).mockResolvedValue("// new code"); + (prepareScriptByCode as Mock).mockResolvedValue({ + script: { name: "示例脚本", metadata, status: SCRIPT_STATUS_ENABLE } as unknown as Script, + oldScript: { metadata: oldMetadata } as unknown as Script, + oldScriptCode: "// old code", + }); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("ready")); + const state = result.current.state; + if (state.status !== "ready") throw new Error("not ready"); + expect(state.view.isUpdate).toBe(true); + expect(state.view.oldCode).toBe("// old code"); + }); + + it("?skill= 时读取技能数据进入 skill 状态", async () => { + window.history.replaceState({}, "", "/install.html?skill=sk1"); + const skill = { + skillMd: "# skill", + metadata: { name: "技能X", description: "desc" }, + prompt: "提示词", + scripts: [], + references: [], + isUpdate: false, + }; + (agentClient.getSkillInstallData as Mock).mockResolvedValue(skill); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("skill")); + const state = result.current.state; + if (state.status !== "skill") throw new Error("not skill"); + expect(state.skill.metadata.name).toBe("技能X"); + }); + + it("?url= 指向 .cat.md 时走 Skill 安装流程而非脚本解析", async () => { + window.history.replaceState({}, "", "/install.html?url=https://e.com/foo.cat.md"); + const skill = { + skillMd: "# skill", + metadata: { name: "URL技能", description: "desc" }, + prompt: "提示词", + scripts: [], + references: [], + isUpdate: false, + }; + (agentClient.prepareSkillFromUrl as Mock).mockResolvedValue("sk-from-url"); + (agentClient.getSkillInstallData as Mock).mockResolvedValue(skill); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("skill")); + const state = result.current.state; + if (state.status !== "skill") throw new Error("not skill"); + expect(state.skill.metadata.name).toBe("URL技能"); + expect(agentClient.prepareSkillFromUrl as Mock).toHaveBeenCalledWith("https://e.com/foo.cat.md"); + expect(agentClient.getSkillInstallData as Mock).toHaveBeenCalledWith("sk-from-url"); + // 不应当走脚本下载/解析路径 + expect(fetchScriptBody as Mock).not.toHaveBeenCalled(); + }); + + it("?url= 指向带查询串的 .cat.md 时仍走 Skill 流程(正则匹配 ? 边界)", async () => { + window.history.replaceState({}, "", "/install.html?url=https://e.com/foo.cat.md?v=2"); + const skill = { + skillMd: "# skill", + metadata: { name: "查询串技能", description: "desc" }, + prompt: "提示词", + scripts: [], + references: [], + isUpdate: false, + }; + (agentClient.prepareSkillFromUrl as Mock).mockResolvedValue("sk-q"); + (agentClient.getSkillInstallData as Mock).mockResolvedValue(skill); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("skill")); + expect(agentClient.prepareSkillFromUrl as Mock).toHaveBeenCalledWith("https://e.com/foo.cat.md?v=2"); + // 解析得到的 uuid 应写入 skillUuidRef,供后续 installSkill/cancelSkill 复用 + const state = result.current.state; + if (state.status !== "skill") throw new Error("not skill"); + expect(state.skill.metadata.name).toBe("查询串技能"); + }); + + it("getInstallInfo 无数据时进入 error 状态", async () => { + window.history.replaceState({}, "", "/install.html?uuid=u1"); + (scriptClient.getInstallInfo as Mock).mockResolvedValue(undefined); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("error")); + }); + + it("加载失败后调用 retry 重新加载并进入 ready", async () => { + window.history.replaceState({}, "", "/install.html?uuid=u1"); + const metadata = { name: ["示例脚本"], version: ["1.0.0"], match: ["https://e.com/*"] }; + const info: ScriptInfo = { + url: "https://e.com/x.user.js", + code: "", + uuid: "u1", + userSubscribe: false, + metadata, + source: "user", + }; + // 用开关而非调用计数控制成败:retry 前的任意次加载都返回空(→error),retry 后才放行成功。 + // 这样对 effect 在负载下因 t 引用变化导致的重跑次数不敏感(否则计数法会脆)。 + let allowSuccess = false; + (scriptClient.getInstallInfo as Mock).mockImplementation(async () => + allowSuccess ? [false, info, {}] : undefined + ); + (getTempCode as Mock).mockResolvedValue("// code"); + (prepareScriptByCode as Mock).mockResolvedValue({ + script: { name: "示例脚本", metadata, status: SCRIPT_STATUS_ENABLE } as unknown as Script, + }); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("error")); + + allowSuccess = true; + await act(async () => result.current.retry()); + await waitFor(() => expect(result.current.state.status).toBe("ready")); + }); + + it("?url= 时下载并解析后进入 ready 状态", async () => { + window.history.replaceState({}, "", "/install.html?url=https://e.com/x.user.js"); + const metadata = { name: ["URL脚本"], version: ["1.0.0"], match: ["https://e.com/*"] }; + (fetchScriptBody as Mock).mockResolvedValue("// url code"); + (parseMetadata as Mock).mockReturnValue(metadata); + (prepareScriptByCode as Mock).mockResolvedValue({ + script: { name: "URL脚本", metadata, status: SCRIPT_STATUS_ENABLE } as unknown as Script, + }); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("ready")); + const state = result.current.state; + if (state.status !== "ready") throw new Error("not ready"); + expect(state.view.name).toBe("URL脚本"); + expect(result.current.localFile).toBe(false); + }); + + it("?url= 下载过程中在 loading 状态展示已接收字节与百分比", async () => { + window.history.replaceState({}, "", "/install.html?url=https://e.com/x.user.js"); + const metadata = { name: ["URL脚本"], version: ["1.0.0"] }; + (fetchScriptBody as Mock).mockImplementation( + async ( + _url: string, + _signal: unknown, + onProgress?: (p: { receivedLength: number; totalLength?: number }) => void + ) => { + onProgress?.({ receivedLength: 512, totalLength: 1024 }); + return "// code"; + } + ); + (parseMetadata as Mock).mockReturnValue(metadata); + // 让准备阶段挂起,使视图停留在带进度的 loading 态以便断言 + (prepareScriptByCode as Mock).mockReturnValue(new Promise(() => {})); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => { + const s = result.current.state; + expect(s.status).toBe("loading"); + if (s.status !== "loading") throw new Error("not loading"); + expect(s.percent).toBe(50); + expect(s.bytesText).toContain("512.00 B"); + expect(s.bytesText).toContain("1.00 KB"); + }); + }); + + it("?url= 已接收字节超过 totalLength 时回退为仅显示已接收(不显示错误百分比)", async () => { + window.history.replaceState({}, "", "/install.html?url=https://e.com/x.user.js"); + const metadata = { name: ["URL脚本"], version: ["1.0.0"] }; + (fetchScriptBody as Mock).mockImplementation( + async ( + _url: string, + _signal: unknown, + onProgress?: (p: { receivedLength: number; totalLength?: number }) => void + ) => { + // 解压后字节(3000)超过压缩后的 Content-Length(1000),总量不可信 + onProgress?.({ receivedLength: 3000, totalLength: 1000 }); + return "// code"; + } + ); + (parseMetadata as Mock).mockReturnValue(metadata); + (prepareScriptByCode as Mock).mockReturnValue(new Promise(() => {})); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => { + const s = result.current.state; + expect(s.status).toBe("loading"); + if (s.status !== "loading") throw new Error("not loading"); + expect(s.bytesText).toContain("2.93 KB"); + }); + const s = result.current.state; + if (s.status !== "loading") throw new Error("not loading"); + expect(s.percent).toBeUndefined(); + expect(s.bytesText).not.toContain("1000.00 B"); + }); + + it("?file= 时读取本地文件并进入 ready,localFile 为 true", async () => { + window.history.replaceState({}, "", "/install.html?file=fid1"); + const metadata = { name: ["本地脚本"], version: ["1.0.0"] }; + (loadHandle as Mock).mockResolvedValue({ + name: "x.user.js", + getFile: async () => ({ text: async () => "// file code", name: "x.user.js" }), + }); + (parseMetadata as Mock).mockReturnValue(metadata); + (prepareScriptByCode as Mock).mockResolvedValue({ + script: { name: "本地脚本", metadata, status: SCRIPT_STATUS_ENABLE } as unknown as Script, + }); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("ready")); + expect(result.current.localFile).toBe(true); + }); + + it("本地文件切换监听:开启调用 startFileTrack,关闭调用 unmountFileTrack", async () => { + window.history.replaceState({}, "", "/install.html?file=fid1"); + const metadata = { name: ["本地脚本"], version: ["1.0.0"] }; + (loadHandle as Mock).mockResolvedValue({ + name: "x.user.js", + getFile: async () => ({ text: async () => "// file code", name: "x.user.js" }), + }); + (parseMetadata as Mock).mockReturnValue(metadata); + (prepareScriptByCode as Mock).mockResolvedValue({ + script: { name: "本地脚本", metadata, status: SCRIPT_STATUS_ENABLE, uuid: "u9" } as unknown as Script, + }); + (scriptClient.install as Mock).mockResolvedValue(undefined); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("ready")); + + await act(async () => result.current.toggleWatch()); + expect(result.current.watching).toBe(true); + expect(startFileTrack as Mock).toHaveBeenCalledTimes(1); + + await act(async () => result.current.toggleWatch()); + expect(result.current.watching).toBe(false); + expect(unmountFileTrack as Mock).toHaveBeenCalled(); + }); + + it("开启监听时预装失败则不进入监听也不追踪文件", async () => { + window.history.replaceState({}, "", "/install.html?file=fid1"); + const metadata = { name: ["本地脚本"], version: ["1.0.0"] }; + (loadHandle as Mock).mockResolvedValue({ + name: "x.user.js", + getFile: async () => ({ text: async () => "// file code", name: "x.user.js" }), + }); + (parseMetadata as Mock).mockReturnValue(metadata); + (prepareScriptByCode as Mock).mockResolvedValue({ + script: { name: "本地脚本", metadata, status: SCRIPT_STATUS_ENABLE, uuid: "u9" } as unknown as Script, + }); + (scriptClient.install as Mock).mockRejectedValue(new Error("装不上")); + + const { result } = renderHook(() => useInstallData()); + await waitFor(() => expect(result.current.state.status).toBe("ready")); + + await act(async () => result.current.toggleWatch()); + expect(result.current.watching).toBe(false); + expect(startFileTrack as Mock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/pages/install/useInstallData.ts b/src/pages/install/useInstallData.ts new file mode 100644 index 000000000..0991f46ee --- /dev/null +++ b/src/pages/install/useInstallData.ts @@ -0,0 +1,424 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import type { Script } from "@App/app/repo/scripts"; +import { SCRIPT_STATUS_ENABLE, SCRIPT_STATUS_DISABLE } from "@App/app/repo/scripts"; +import type { Subscribe } from "@App/app/repo/subscribe"; +import type { SCMetadata } from "@App/app/repo/metadata"; +import type { ScriptInfo } from "@App/pkg/utils/scriptInstall"; +import { getTempCode } from "@App/pkg/utils/scriptInstall"; +import { prepareScriptByCode, prepareSubscribeByCode, fetchScriptBody, parseMetadata } from "@App/pkg/utils/script"; +import { uuidv4 } from "@App/pkg/utils/uuid"; +import { nextTimeDisplay } from "@App/pkg/utils/cron"; +import { prettyUrl } from "@App/pkg/utils/url-utils"; +import { formatBytes } from "@App/pkg/utils/utils"; +import { i18nName, i18nDescription } from "@App/locales/locales"; +import { scriptClient, subscribeClient, agentClient } from "@App/pages/store/features/script"; +import type { SkillConfigField } from "@App/app/service/agent/core/types"; +import { loadHandle } from "@App/pkg/utils/filehandle-db"; +import { startFileTrack, unmountFileTrack, type FTInfo } from "@App/pkg/utils/file-tracker"; +import { TempStorageDAO } from "@App/app/repo/tempStorage"; +import { derivePermissions, type PermissionRow } from "./permissions"; +import { + deriveVersion, + deriveAntifeatures, + deriveScheduleInfo, + deriveDiffStat, + type VersionDisplay, + type AntifeatureType, + type ScheduleInfo, + type DiffStat, +} from "./model"; + +export interface InstallView { + isUpdate: boolean; + isSubscribe: boolean; + name: string; + iconUrl?: string; + author?: string; + source: string; + description?: string; + version: VersionDisplay; + permissions: PermissionRow[]; + antifeatures: AntifeatureType[]; + schedule: ScheduleInfo; + scheduleNextRun?: string; + code: string; + /** 更新态时的旧版本代码,用于代码卡内联 diff;全新安装为 undefined */ + oldCode?: string; + /** 更新态代码增删行统计,用于代码卡头 +N −M 徽章;无旧代码/无变化为 undefined */ + diffStat?: DiffStat; + /** 订阅安装时声明的脚本 URL 列表(@scriptURL) */ + subscribeScripts: string[]; +} + +/** + * 纯函数:由「已准备好的脚本/订阅 + 旧版本」组装安装页展示视图。 + * oldVersion 为 null 表示全新安装,字符串表示更新。 + */ +export function assembleInstallView(args: { + isUpdate: boolean; + scriptInfo: ScriptInfo; + action: Script | Subscribe; + code: string; + oldVersion: string | null; + oldCode?: string; +}): InstallView { + const { isUpdate, scriptInfo, action, code, oldVersion, oldCode } = args; + const metadata = scriptInfo.metadata; + const schedule = deriveScheduleInfo(metadata); + return { + isUpdate, + isSubscribe: scriptInfo.userSubscribe, + name: i18nName(action), + iconUrl: metadata.icon?.[0], + author: metadata.author?.[0], + source: prettyUrl(scriptInfo.url), + description: i18nDescription(action), + version: deriveVersion(metadata.version?.[0], oldVersion), + permissions: derivePermissions(metadata), + antifeatures: deriveAntifeatures(metadata), + schedule, + scheduleNextRun: schedule?.kind === "cron" ? nextTimeDisplay(schedule.expression) : undefined, + code, + oldCode, + diffStat: oldCode !== undefined && oldCode !== code ? deriveDiffStat(oldCode, code) : undefined, + subscribeScripts: scriptInfo.userSubscribe ? metadata.scripturl || [] : [], + }; +} + +export interface SkillInstallData { + skillMd: string; + metadata: { name: string; description: string; version?: string; config?: Record }; + prompt: string; + scripts: Array<{ name: string; code: string }>; + references: Array<{ name: string; content: string }>; + isUpdate: boolean; + installUrl?: string; +} + +export type InstallState = + | { status: "loading"; source?: string; bytesText?: string; percent?: number } + | { status: "invalid" } + | { status: "error"; message: string } + | { status: "ready"; view: InstallView } + | { status: "skill"; skill: SkillInstallData }; + +const versionOf = (old: { metadata: { version?: string[] } } | undefined): string | null => + old ? (old.metadata.version?.[0] ?? "N/A") : null; + +const buildScriptInfo = (uuid: string, code: string, url: string, metadata: SCMetadata): ScriptInfo => ({ + url, + code, + uuid, + userSubscribe: metadata.usersubscribe !== undefined, + metadata, + source: "user", +}); + +let keepAliveTimer: ReturnType | undefined; +const startKeepAlive = (uuid: string) => { + const tick = () => { + new TempStorageDAO().update(uuid, { savedAt: Date.now() }).catch(() => {}); + }; + tick(); + clearInterval(keepAliveTimer); + keepAliveTimer = setInterval(tick, 30_000); +}; + +export interface UseInstallData { + state: InstallState; + enabled: boolean; + setEnabled: (v: boolean) => void; + localFile: boolean; + watching: boolean; + /** 正在监听的本地文件名(仅本地文件场景有值) */ + watchFileName?: string; + /** 最后一次因文件变更自动重装的本地化时间(未发生过则为 undefined) */ + lastSync?: string; + toggleWatch: () => void; + install: (opts?: { closeAfterInstall?: boolean; noMoreUpdates?: boolean }) => Promise; + close: (opts?: { noMoreUpdates?: boolean }) => void; + installSkill: () => Promise; + cancelSkill: () => void; + retry: () => void; +} + +export function useInstallData(): UseInstallData { + const { t } = useTranslation(["install", "common"]); + const [state, setState] = useState({ status: "loading" }); + const [enabled, setEnabledState] = useState(false); + const [localFile, setLocalFile] = useState(false); + const [watching, setWatching] = useState(false); + const [watchFileName, setWatchFileName] = useState(); + const [lastSync, setLastSync] = useState(); + const [reloadKey, setReloadKey] = useState(0); + const actionRef = useRef - <%= htmlRspackPlugin.options.title %> - <% if isReactTools=="true" { %> - - <% } %> - + ScriptCat
diff --git a/src/pages/options/App.test.tsx b/src/pages/options/App.test.tsx new file mode 100644 index 000000000..b79a4e868 --- /dev/null +++ b/src/pages/options/App.test.tsx @@ -0,0 +1,50 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vitest"; +import { cleanup } from "@testing-library/react"; +import { Routes, Route } from "react-router-dom"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { mockMatchMedia } from "@Tests/mockMatchMedia"; +import { renderWithThemeRouter } from "@Tests/renderWithThemeRouter"; +import { useIsMobile } from "@App/pages/components/use-is-mobile"; +import { Layout } from "./App"; + +vi.mock("@App/pages/components/use-is-mobile", () => ({ + useIsMobile: vi.fn(), +})); + +const mockedUseIsMobile = vi.mocked(useIsMobile); + +beforeEach(() => { + mockMatchMedia(); +}); + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +function renderLayout() { + return renderWithThemeRouter( + + }> + content
} /> + + , + { initialEntries: ["/"] } + ); +} + +describe("Layout 外壳响应式", () => { + it("移动端渲染底部 Tab 栏,不渲染左侧 Sidebar", () => { + mockedUseIsMobile.mockReturnValue(true); + const { getByTestId, container } = renderLayout(); + expect(getByTestId("bottom-tab-bar")).toBeInTheDocument(); + expect(container.querySelector("aside")).toBeNull(); + }); + + it("桌面端渲染左侧 Sidebar,不渲染底部 Tab 栏", () => { + mockedUseIsMobile.mockReturnValue(false); + const { queryByTestId, container } = renderLayout(); + expect(container.querySelector("aside")).not.toBeNull(); + expect(queryByTestId("bottom-tab-bar")).toBeNull(); + }); +}); diff --git a/src/pages/options/App.tsx b/src/pages/options/App.tsx new file mode 100644 index 000000000..0f3777b37 --- /dev/null +++ b/src/pages/options/App.tsx @@ -0,0 +1,85 @@ +import { HashRouter, Routes, Route, Outlet, Navigate, useLocation } from "react-router-dom"; +import Sidebar from "./layout/Sidebar"; +import ScriptList from "./routes/ScriptList"; +import SubscribeList from "./routes/SubscribeList"; +import ScriptEditor from "./routes/ScriptEditor"; +import Logger from "./routes/Logger"; +import Setting from "./routes/Setting"; +import Tools from "./routes/Tools"; +import AgentChat from "./routes/AgentChat"; +import AgentSkills from "./routes/AgentSkills"; +import AgentProvider from "./routes/AgentProvider"; +import AgentMcp from "./routes/AgentMcp"; +import AgentTasks from "./routes/AgentTasks"; +import AgentOPFS from "./routes/AgentOPFS"; +import AgentSettings from "./routes/AgentSettings"; +import { useIsMobile } from "@App/pages/components/use-is-mobile"; +import MobileHeader from "./layout/MobileHeader"; +import BottomTabBar from "./layout/BottomTabBar"; +import { useScriptDropzone } from "./layout/useScriptDropzone"; +import { DropOverlay } from "./layout/DropOverlay"; +import { handleImportFiles } from "./routes/ScriptList/importHandler"; + +export function Layout() { + const isMobile = useIsMobile(); + // 编辑器在移动端为全屏布局,自带顶栏/底栏,不显示全局 MobileHeader/BottomTabBar + const isFullscreen = useLocation().pathname.startsWith("/script/editor"); + // 全窗拖拽安装:拖入 .js 脚本/订阅、.zip Skill 包即打开安装页(桌面) + const { isDragActive } = useScriptDropzone(handleImportFiles); + if (isMobile) { + if (isFullscreen) { + return ( +
+ +
+ ); + } + return ( +
+ +
+ +
+ +
+ ); + } + return ( + <> +
+ +
+ +
+
+ + + ); +} + +export default function App() { + return ( + + + }> + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + + + + ); +} diff --git a/src/pages/options/components/FileSystemParams.test.tsx b/src/pages/options/components/FileSystemParams.test.tsx new file mode 100644 index 000000000..8a947c43a --- /dev/null +++ b/src/pages/options/components/FileSystemParams.test.tsx @@ -0,0 +1,119 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, screen, fireEvent, cleanup, waitFor } from "@testing-library/react"; + +// 仅测试组件的渲染/可见性/回调逻辑,后端 schema 用受控 mock,避免拉起真实文件系统栈 +vi.mock("@Packages/filesystem/factory", () => ({ + default: { + params: () => ({ + webdav: { + authType: { + title: "auth_type", + type: "select", + options: ["password", "digest", "none", "token"], + minWidth: "140px", + }, + url: { title: "url" }, + username: { title: "username", visibilityFor: ["password", "digest"] }, + password: { title: "password", type: "password", visibilityFor: ["password", "digest"] }, + accessToken: { title: "access_token_bearer", visibilityFor: ["token"] }, + }, + "baidu-netdsik": {}, + onedrive: {}, + googledrive: {}, + dropbox: {}, + s3: { + bucket: { title: "s3_bucket_name" }, + region: { title: "s3_region" }, + accessKeyId: { title: "s3_access_key_id" }, + secretAccessKey: { title: "s3_secret_access_key", type: "password" }, + endpoint: { title: "s3_custom_endpoint" }, + }, + }), + }, +})); + +const { hasNetDiskToken, clearNetDiskToken } = vi.hoisted(() => ({ + hasNetDiskToken: vi.fn(() => Promise.resolve(false)), + clearNetDiskToken: vi.fn(() => Promise.resolve()), +})); +vi.mock("@Packages/filesystem/auth", () => ({ + netDiskTypeMap: { "baidu-netdsik": "baidu", onedrive: "onedrive", googledrive: "googledrive", dropbox: "dropbox" }, + HasNetDiskToken: hasNetDiskToken, + ClearNetDiskToken: clearNetDiskToken, +})); + +import FileSystemParams from "./FileSystemParams"; + +afterEach(() => { + cleanup(); + hasNetDiskToken.mockReset(); + hasNetDiskToken.mockResolvedValue(false); + clearNetDiskToken.mockReset(); + clearNetDiskToken.mockResolvedValue(undefined); +}); + +function setup(overrides: Record = {}) { + const onChangeFileSystemType = vi.fn(); + const onChangeFileSystemParams = vi.fn(); + render( + {"header"}} + fileSystemType="webdav" + fileSystemParams={{}} + onChangeFileSystemType={onChangeFileSystemType} + onChangeFileSystemParams={onChangeFileSystemParams} + {...(overrides as any)} + /> + ); + return { onChangeFileSystemType, onChangeFileSystemParams }; +} + +describe("文件系统参数表单", () => { + it("WebDAV 默认认证下显示 URL/用户名/密码,隐藏 AccessToken", () => { + setup({ fileSystemType: "webdav", fileSystemParams: {} }); + expect(screen.getByLabelText("url")).toBeInTheDocument(); + expect(screen.getByLabelText("username")).toBeInTheDocument(); + expect(screen.getByLabelText("password")).toBeInTheDocument(); + expect(screen.queryByLabelText("access_token_bearer")).not.toBeInTheDocument(); + }); + + it("认证类型为 token 时显示 AccessToken,隐藏用户名/密码", () => { + setup({ fileSystemType: "webdav", fileSystemParams: { authType: "token" } }); + expect(screen.getByLabelText("access_token_bearer")).toBeInTheDocument(); + expect(screen.queryByLabelText("username")).not.toBeInTheDocument(); + expect(screen.queryByLabelText("password")).not.toBeInTheDocument(); + }); + + it("编辑 URL 输入框时以合并后的参数回调", () => { + const { onChangeFileSystemParams } = setup({ fileSystemType: "webdav", fileSystemParams: { url: "" } }); + fireEvent.change(screen.getByLabelText("url"), { target: { value: "https://dav.example.com" } }); + expect(onChangeFileSystemParams).toHaveBeenCalledWith({ url: "https://dav.example.com" }); + }); + + it("S3 后端渲染其专属字段", () => { + setup({ fileSystemType: "s3", fileSystemParams: {} }); + expect(screen.getByLabelText("s3_bucket_name")).toBeInTheDocument(); + expect(screen.getByLabelText("s3_secret_access_key")).toBeInTheDocument(); + expect(screen.queryByLabelText("url")).not.toBeInTheDocument(); + }); + + it("网盘后端已绑定 token 时显示解绑按钮,确认后清除 token", async () => { + hasNetDiskToken.mockResolvedValue(true); + setup({ fileSystemType: "baidu-netdsik", fileSystemParams: {} }); + const unbind = await screen.findByLabelText("netdisk_unbind"); + fireEvent.click(unbind); + // 弹出确认气泡后点击确认按钮(气泡内最后一个按钮) + await waitFor(() => expect(screen.getAllByRole("button").length).toBeGreaterThan(1)); + const buttons = screen.getAllByRole("button"); + fireEvent.click(buttons[buttons.length - 1]); + await waitFor(() => expect(clearNetDiskToken).toHaveBeenCalledWith("baidu")); + }); + + it("非网盘后端不显示解绑按钮", async () => { + hasNetDiskToken.mockResolvedValue(true); + setup({ fileSystemType: "webdav", fileSystemParams: {} }); + await waitFor(() => expect(screen.getByLabelText("url")).toBeInTheDocument()); + expect(screen.queryByLabelText("netdisk_unbind")).not.toBeInTheDocument(); + }); +}); diff --git a/src/pages/options/components/FileSystemParams.tsx b/src/pages/options/components/FileSystemParams.tsx new file mode 100644 index 000000000..fcc5b8749 --- /dev/null +++ b/src/pages/options/components/FileSystemParams.tsx @@ -0,0 +1,141 @@ +import type React from "react"; +import { useEffect, useState } from "react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@App/pages/components/ui/select"; +import { Input } from "@App/pages/components/ui/input"; +import { Button } from "@App/pages/components/ui/button"; +import { Popconfirm } from "@App/pages/components/ui/popconfirm"; +import FileSystemFactory, { type FileSystemType } from "@Packages/filesystem/factory"; +import { ClearNetDiskToken, HasNetDiskToken, netDiskTypeMap } from "@Packages/filesystem/auth"; +import { t } from "@App/locales/locales"; +import { toast } from "sonner"; + +interface FileSystemParamsProps { + /** 选择器左侧的标题/开关等内容 */ + headerContent: React.ReactNode; + /** 选择器右侧的额外操作(保存/重置等按钮) */ + children?: React.ReactNode; + fileSystemType: FileSystemType; + fileSystemParams: Record; + onChangeFileSystemType: (type: FileSystemType) => void; + onChangeFileSystemParams: (params: Record) => void; +} + +/** + * 文件系统连接参数表单:选择后端(WebDAV/网盘/OneDrive/S3 等),并按后端 schema 动态渲染参数字段。 + * 网盘类后端走 OAuth,已绑定时提供解绑入口。 + */ +export default function FileSystemParams({ + headerContent, + children, + fileSystemType, + fileSystemParams, + onChangeFileSystemType, + onChangeFileSystemParams, +}: FileSystemParamsProps) { + const fsParams = FileSystemFactory.params(); + const [hasBoundToken, setHasBoundToken] = useState(false); + + const netDiskType = netDiskTypeMap[fileSystemType]; + + useEffect(() => { + if (!netDiskType) { + setHasBoundToken(false); + return; + } + HasNetDiskToken(netDiskType).then(setHasBoundToken); + }, [netDiskType]); + + const fileSystemList: { key: FileSystemType; name: string }[] = [ + { key: "webdav", name: "WebDAV" }, + { key: "baidu-netdsik", name: t("settings:baidu_netdisk") }, + { key: "onedrive", name: "OneDrive" }, + { key: "googledrive", name: "Google Drive" }, + { key: "dropbox", name: "Dropbox" }, + { key: "s3", name: "Amazon S3" }, + ]; + + const netDiskName = netDiskType ? fileSystemList.find((item) => item.key === fileSystemType)?.name : null; + const fsParam = fsParams[fileSystemType]; + + const unbind = async () => { + if (!netDiskType) return; + try { + await ClearNetDiskToken(netDiskType); + setHasBoundToken(false); + toast.success(t("settings:netdisk_unbind_success", { provider: netDiskName })); + } catch (error) { + toast.error(`${t("settings:netdisk_unbind_error", { provider: netDiskName })}: ${String(error)}`); + } + }; + + return ( +
+
+ {headerContent} + + {children} + {netDiskType && hasBoundToken && ( + + + + )} +
+ +
+ {Object.keys(fsParam).map((key) => { + const props = fsParam[key]; + const selectAuth = fsParam?.authType?.options?.[0]; // webDAV:默认认证类型 + if (selectAuth && props?.visibilityFor?.includes(fileSystemParams?.authType || selectAuth) === false) { + return null; + } + const setParam = (value: string) => onChangeFileSystemParams({ ...fileSystemParams, [key]: value }); + return ( +
+ {props.title} + {props.type === "select" ? ( + + ) : ( + setParam(e.target.value)} + /> + )} +
+ ); + })} +
+
+ ); +} diff --git a/src/pages/options/components/SettingCard.test.tsx b/src/pages/options/components/SettingCard.test.tsx new file mode 100644 index 000000000..cf42969ad --- /dev/null +++ b/src/pages/options/components/SettingCard.test.tsx @@ -0,0 +1,33 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import { SettingCard } from "./SettingCard"; +import { SettingRow } from "./SettingRow"; + +afterEach(cleanup); + +describe("设置卡片原语", () => { + it("SettingCard 渲染标题/描述并用 register 挂 ref", () => { + const reg = vi.fn(() => vi.fn()); + render( + +
{"inner"}
+
+ ); + expect(screen.getByText("同步")).toBeInTheDocument(); + expect(screen.getByText("云端同步")).toBeInTheDocument(); + expect(screen.getByText("inner")).toBeInTheDocument(); + expect(reg).toHaveBeenCalledWith("sync"); + }); + + it("SettingRow 渲染标签/描述与右侧控件", () => { + render( + + + + ); + expect(screen.getByText("语言")).toBeInTheDocument(); + expect(screen.getByText("界面语言")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "ctrl" })).toBeInTheDocument(); + }); +}); diff --git a/src/pages/options/components/SettingCard.tsx b/src/pages/options/components/SettingCard.tsx new file mode 100644 index 000000000..5a48b2c7a --- /dev/null +++ b/src/pages/options/components/SettingCard.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +export function SettingCard({ + id, + title, + titleAction, + description, + register, + children, +}: { + id: string; + title: string; + titleAction?: React.ReactNode; + description?: string; + register: (id: string) => (el: HTMLElement | null) => void; + children: React.ReactNode; +}) { + return ( +
+
+
+

{title}

+ {titleAction} +
+ {description &&

{description}

} +
+
{children}
+
+ ); +} diff --git a/src/pages/options/components/SettingRow.tsx b/src/pages/options/components/SettingRow.tsx new file mode 100644 index 000000000..21cb5c7c5 --- /dev/null +++ b/src/pages/options/components/SettingRow.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +export function SettingRow({ + label, + description, + children, +}: { + label: string; + description?: string; + children: React.ReactNode; +}) { + return ( +
+
+ {label} + {description && {description}} +
+
{children}
+
+ ); +} diff --git a/src/pages/options/hooks/useScrollSpy.test.ts b/src/pages/options/hooks/useScrollSpy.test.ts new file mode 100644 index 000000000..e3eb47ad0 --- /dev/null +++ b/src/pages/options/hooks/useScrollSpy.test.ts @@ -0,0 +1,78 @@ +// @vitest-environment happy-dom +// src/pages/options/hooks/useScrollSpy.test.ts +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, act } from "@testing-library/react"; +import { useScrollSpy } from "./useScrollSpy"; + +// 收集创建的 IntersectionObserver 实例,便于手动触发回调 +type IOEntry = { + target: Element; + isIntersecting: boolean; + intersectionRatio: number; + boundingClientRect: { top: number }; +}; +const observers: Array<{ cb: IntersectionObserverCallback; elements: Set }> = []; + +beforeEach(() => { + observers.length = 0; + // @ts-expect-error 测试桩 + globalThis.IntersectionObserver = class { + cb: IntersectionObserverCallback; + elements = new Set(); + constructor(cb: IntersectionObserverCallback) { + this.cb = cb; + observers.push({ cb, elements: this.elements }); + } + observe(el: Element) { + this.elements.add(el); + } + unobserve(el: Element) { + this.elements.delete(el); + } + disconnect() { + this.elements.clear(); + } + takeRecords() { + return []; + } + }; +}); + +function fireIntersect(entries: IOEntry[]) { + act(() => { + observers[0].cb(entries as unknown as IntersectionObserverEntry[], observers[0] as unknown as IntersectionObserver); + }); +} + +describe("滚动监听 useScrollSpy", () => { + it("初始激活第一个分区", () => { + const { result } = renderHook(() => useScrollSpy(["a", "b", "c"])); + expect(result.current.activeId).toBe("a"); + }); + + it("分区进入视口上部时激活其对应导航", () => { + const { result } = renderHook(() => useScrollSpy(["a", "b", "c"])); + const elB = document.createElement("div"); + elB.dataset.spyId = "b"; + act(() => { + result.current.register("b")(elB); + }); + fireIntersect([{ target: elB, isIntersecting: true, intersectionRatio: 1, boundingClientRect: { top: 10 } }]); + expect(result.current.activeId).toBe("b"); + }); + + it("点击导航 scrollTo 立即激活目标并调用平滑滚动", () => { + const { result } = renderHook(() => useScrollSpy(["a", "b", "c"])); + const elC = document.createElement("div"); + const scrollIntoView = vi.fn(); + elC.scrollIntoView = scrollIntoView; + act(() => { + result.current.register("c")(elC); + }); + act(() => { + result.current.scrollTo("c"); + }); + expect(result.current.activeId).toBe("c"); + expect(scrollIntoView).toHaveBeenCalledWith({ behavior: "smooth", block: "start" }); + }); +}); diff --git a/src/pages/options/hooks/useScrollSpy.ts b/src/pages/options/hooks/useScrollSpy.ts new file mode 100644 index 000000000..87da4c59b --- /dev/null +++ b/src/pages/options/hooks/useScrollSpy.ts @@ -0,0 +1,61 @@ +// src/pages/options/hooks/useScrollSpy.ts +import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react"; + +export interface ScrollSpyResult { + activeId: string; + register: (id: string) => (el: HTMLElement | null) => void; + scrollContainerRef: RefObject; + scrollTo: (id: string) => void; +} + +export function useScrollSpy(ids: string[]): ScrollSpyResult { + const [activeId, setActiveId] = useState(ids[0] ?? ""); + const elements = useRef>(new Map()); + const scrollContainerRef = useRef(null); + // 点击导航期间抑制 IO 回写,避免动画途经分区抢占高亮 + const suppressUntil = useRef(0); + + const register = useCallback( + (id: string) => (el: HTMLElement | null) => { + if (el) elements.current.set(id, el); + else elements.current.delete(id); + }, + [] + ); + + // 用 ids 顺序排序命中分区,取最靠上的可见分区为 active + useEffect(() => { + const root = scrollContainerRef.current; + const visible = new Set(); + const observer = new IntersectionObserver( + (entries) => { + if (performance.now() < suppressUntil.current) return; + for (const e of entries) { + const id = (e.target as HTMLElement).dataset.spyId; + if (!id) continue; + if (e.isIntersecting) visible.add(id); + else visible.delete(id); + } + const firstVisible = ids.find((id) => visible.has(id)); + if (firstVisible) setActiveId(firstVisible); + }, + { root, rootMargin: "-30% 0px -60% 0px", threshold: 0 } + ); + for (const id of ids) { + const el = elements.current.get(id); + if (el) { + el.dataset.spyId = id; + observer.observe(el); + } + } + return () => observer.disconnect(); + }, [ids]); + + const scrollTo = useCallback((id: string) => { + setActiveId(id); + suppressUntil.current = performance.now() + 800; + elements.current.get(id)?.scrollIntoView({ behavior: "smooth", block: "start" }); + }, []); + + return useMemo(() => ({ activeId, register, scrollContainerRef, scrollTo }), [activeId, register, scrollTo]); +} diff --git a/src/pages/options/hooks/useSystemConfig.test.ts b/src/pages/options/hooks/useSystemConfig.test.ts new file mode 100644 index 000000000..121c03384 --- /dev/null +++ b/src/pages/options/hooks/useSystemConfig.test.ts @@ -0,0 +1,39 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, act, waitFor } from "@testing-library/react"; + +const { get, set, subscribe } = vi.hoisted(() => ({ + get: vi.fn(() => Promise.resolve("scriptcat")), + set: vi.fn(), + subscribe: vi.fn(() => () => {}), +})); +vi.mock("@App/pages/store/global", () => ({ + systemConfig: { get, set }, + subscribeMessage: subscribe, +})); + +import { useSystemConfig } from "./useSystemConfig"; + +beforeEach(() => vi.clearAllMocks()); + +describe("配置 Hook useSystemConfig", () => { + it("挂载时读取配置值", async () => { + const { result } = renderHook(() => useSystemConfig("favicon_service")); + await waitFor(() => expect(result.current[0]).toBe("scriptcat")); + expect(get).toHaveBeenCalledWith("favicon_service"); + }); + + it("调用 setter 即时写入 systemConfig 并更新本地值", async () => { + const { result } = renderHook(() => useSystemConfig("favicon_service")); + await waitFor(() => expect(result.current[0]).toBe("scriptcat")); + act(() => result.current[1]("google" as never)); + expect(set).toHaveBeenCalledWith("favicon_service", "google"); + expect(result.current[0]).toBe("google"); + }); + + it("getter 返回同步值时 hook 依然能正确填充", async () => { + get.mockReturnValueOnce("declare const x: any;" as never); + const { result } = renderHook(() => useSystemConfig("editor_type_definition" as never)); + await waitFor(() => expect(result.current[0]).toBe("declare const x: any;")); + }); +}); diff --git a/src/pages/options/hooks/useSystemConfig.ts b/src/pages/options/hooks/useSystemConfig.ts new file mode 100644 index 000000000..06412ec51 --- /dev/null +++ b/src/pages/options/hooks/useSystemConfig.ts @@ -0,0 +1,37 @@ +import { useEffect, useState, useCallback } from "react"; +import { systemConfig, subscribeMessage } from "@App/pages/store/global"; +import { SystemConfigChange, type SystemConfigKey, type SystemConfigValueType } from "@App/pkg/config/config"; +import type { TKeyValue } from "@Packages/message/message_queue"; + +export function useSystemConfig( + key: K +): [SystemConfigValueType | undefined, (v: SystemConfigValueType) => void] { + const [value, setValue] = useState | undefined>(undefined); + + useEffect(() => { + let alive = true; + Promise.resolve(systemConfig.get(key)).then((v) => { + if (alive) setValue(v); + }); + const unsub = subscribeMessage>(SystemConfigChange, ({ key: k }) => { + if (k !== key) return; + Promise.resolve(systemConfig.get(key)).then((v) => { + if (alive) setValue(v); + }); + }); + return () => { + alive = false; + unsub(); + }; + }, [key]); + + const update = useCallback( + (v: SystemConfigValueType) => { + setValue(v); + systemConfig.set(key, v); + }, + [key] + ); + + return [value, update]; +} diff --git a/src/pages/options/index.css b/src/pages/options/index.css deleted file mode 100644 index 6c25f829c..000000000 --- a/src/pages/options/index.css +++ /dev/null @@ -1,99 +0,0 @@ -.show-log-card .arco-list-item { - border-bottom: 0 !important; -} - -h1.arco-typography, -h2.arco-typography, -h3.arco-typography, -h4.arco-typography, -h5.arco-typography, -h6.arco-typography { - margin-top: 0 !important; -} - -.script-list .arco-card-body { - padding: 0 !important; -} - -.max-table-cell .arco-table-cell { - display: block; - max-height: 100px; - overflow: auto; -} - -/* error、wran图标直接用的油猴CodeMirror编辑器图标 待优化*/ -.icon-error { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=); - background-repeat: no-repeat; - background-position: center; - left: 10px !important; -} - -.icon-warn { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=); - background-repeat: no-repeat; - background-position: center; - left: 10px !important; -} - -.actionList { - height: auto !important; -} - -.arco-table-custom-filter { - padding: 10px; - background-color: var(--color-bg-5); - box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15); -} - -.arco-table-custom-filter span.arco-input-inner-wrapper, -.arco-table-custom-filter input.arco-input { - transition: none; -} - -#script-list .arco-table-th-item, -#script-list .arco-table-col-has-sorter .arco-table-cell-with-sorter, -#script-list .arco-table-td { - padding: 4px 8px; -} - -#script-list .arco-table-col-has-sorter { - padding: 0; -} - -#script-list col:not([style]):not([class]) { - max-width: 240px; - min-width: 100px; -} - -.source_cell .arco-table-cell-wrap-value, -.source_cell .arco-tag { - max-width: 100%; -} - -.script-list { - .arco-table-container { - border: none !important; - } - - .arco-table-border .arco-table-th:first-child, - .arco-table-border .arco-table-td:first-child { - border-left: none !important; - - } - - /* 卡片视图样式 */ - .script-card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - } - - .script-card .arco-card-body { - padding: 16px !important; - } - - .script-card-grid { - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - } - -} \ No newline at end of file diff --git a/src/pages/options/layout/BottomTabBar.test.tsx b/src/pages/options/layout/BottomTabBar.test.tsx new file mode 100644 index 000000000..09a54b901 --- /dev/null +++ b/src/pages/options/layout/BottomTabBar.test.tsx @@ -0,0 +1,35 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeAll, afterEach } from "vitest"; +import { render, cleanup, within } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { t } from "@App/locales/locales"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import BottomTabBar from "./BottomTabBar"; + +beforeAll(() => initTestLanguage("zh-CN")); + +afterEach(cleanup); + +describe("BottomTabBar 底部导航", () => { + it("渲染脚本/订阅/日志/工具/设置五个导航项", () => { + const { getByTestId } = render( + + + + ); + const bar = getByTestId("bottom-tab-bar"); + for (const label of [t("script:nav_scripts"), t("script:subscribe"), t("logs"), t("tools"), t("settings")]) { + expect(within(bar).getByText(label)).toBeInTheDocument(); + } + }); + + it("当前路由对应项为激活态(aria-current=page)", () => { + const { getByText } = render( + + + + ); + const logsLink = getByText(t("logs")).closest("a"); + expect(logsLink).toHaveAttribute("aria-current", "page"); + }); +}); diff --git a/src/pages/options/layout/BottomTabBar.tsx b/src/pages/options/layout/BottomTabBar.tsx new file mode 100644 index 000000000..4b76b055c --- /dev/null +++ b/src/pages/options/layout/BottomTabBar.tsx @@ -0,0 +1,38 @@ +import { NavLink } from "react-router-dom"; +import { Package, Rss, ScrollText, Wrench, Settings } from "lucide-react"; +import { cn } from "@App/pkg/utils/cn"; +import { t } from "@App/locales/locales"; + +const tabs = [ + { to: "/", icon: Package, label: () => t("script:nav_scripts"), end: true }, + { to: "/subscribe", icon: Rss, label: () => t("script:subscribe"), end: false }, + { to: "/logs", icon: ScrollText, label: () => t("logs"), end: false }, + { to: "/tools", icon: Wrench, label: () => t("tools"), end: false }, + { to: "/settings", icon: Settings, label: () => t("settings"), end: false }, +]; + +export default function BottomTabBar() { + return ( + + ); +} diff --git a/src/pages/options/layout/DropOverlay.test.tsx b/src/pages/options/layout/DropOverlay.test.tsx new file mode 100644 index 000000000..1fba16806 --- /dev/null +++ b/src/pages/options/layout/DropOverlay.test.tsx @@ -0,0 +1,23 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { initLanguage } from "@App/locales/locales"; +import { DropOverlay } from "./DropOverlay"; + +describe("DropOverlay", () => { + beforeEach(() => { + initLanguage("zh-CN"); + }); + + it("active=false 时不渲染", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("active=true 时显示提示文案与类型标签", () => { + render(); + expect(screen.getByTestId("drop-overlay")).toBeInTheDocument(); + expect(screen.getByText("拖拽脚本或 Skill 到此处安装")).toBeInTheDocument(); + expect(screen.getByText(".zip")).toBeInTheDocument(); + }); +}); diff --git a/src/pages/options/layout/DropOverlay.tsx b/src/pages/options/layout/DropOverlay.tsx new file mode 100644 index 000000000..82b478598 --- /dev/null +++ b/src/pages/options/layout/DropOverlay.tsx @@ -0,0 +1,27 @@ +import { Download } from "lucide-react"; +import { t } from "@App/locales/locales"; + +export function DropOverlay({ active }: { active: boolean }) { + if (!active) return null; + return ( +
+
+
+ +
+
{t("script:drop_to_install")}
+
{t("script:drop_to_install_hint")}
+
+ {[".user.js", ".sub.js", ".zip"].map((x) => ( + + {x} + + ))} +
+
+
+ ); +} diff --git a/src/pages/options/layout/MobileHeader.test.tsx b/src/pages/options/layout/MobileHeader.test.tsx new file mode 100644 index 000000000..93820db39 --- /dev/null +++ b/src/pages/options/layout/MobileHeader.test.tsx @@ -0,0 +1,43 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { cleanup, fireEvent } from "@testing-library/react"; +import { t } from "@App/locales/locales"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { mockMatchMedia } from "@Tests/mockMatchMedia"; +import { renderWithThemeRouter } from "@Tests/renderWithThemeRouter"; +import MobileHeader from "./MobileHeader"; + +beforeEach(() => { + localStorage.clear(); + initTestLanguage("zh-CN"); + mockMatchMedia(); +}); + +afterEach(cleanup); + +function renderHeader() { + return renderWithThemeRouter(); +} + +describe("MobileHeader 移动顶栏", () => { + it("渲染 ScriptCat 标题", () => { + const { getByText } = renderHeader(); + expect(getByText("ScriptCat")).toBeInTheDocument(); + }); + + it("渲染新建脚本图标按钮(带无障碍标签)", () => { + const { getByLabelText } = renderHeader(); + expect(getByLabelText(t("script:create_script"))).toBeInTheDocument(); + }); + + it("默认不展示导航抽屉内容", () => { + const { queryByText } = renderHeader(); + expect(queryByText(t("agent:title"))).toBeNull(); + }); + + it("点击菜单按钮(☰)打开导航抽屉,展示 AI Agent 等导航入口", async () => { + const { getByLabelText, findByText } = renderHeader(); + fireEvent.click(getByLabelText(t("menu"))); + expect(await findByText(t("agent:title"))).toBeInTheDocument(); + }); +}); diff --git a/src/pages/options/layout/MobileHeader.tsx b/src/pages/options/layout/MobileHeader.tsx new file mode 100644 index 000000000..5672445ec --- /dev/null +++ b/src/pages/options/layout/MobileHeader.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; +import { Menu } from "lucide-react"; +import { CreateScriptMenu } from "@App/pages/options/routes/ScriptList/CreateScriptMenu"; +import { Sheet, SheetContent, SheetTitle } from "@App/pages/components/ui/sheet"; +import { t } from "@App/locales/locales"; +import MobileNavDrawer from "./MobileNavDrawer"; + +export default function MobileHeader() { + const [navOpen, setNavOpen] = useState(false); + + return ( +
+ + ScriptCat + {"ScriptCat"} +
+ + + {/* 左侧导航抽屉:镜像桌面 Sidebar,补齐移动端 Agent 等板块的入口 */} + + + {t("menu")} + setNavOpen(false)} /> + + +
+ ); +} diff --git a/src/pages/options/layout/MobileNavDrawer.test.tsx b/src/pages/options/layout/MobileNavDrawer.test.tsx new file mode 100644 index 000000000..429b01187 --- /dev/null +++ b/src/pages/options/layout/MobileNavDrawer.test.tsx @@ -0,0 +1,71 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { cleanup, fireEvent, within } from "@testing-library/react"; +import { t } from "@App/locales/locales"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { mockMatchMedia } from "@Tests/mockMatchMedia"; +import { renderWithThemeRouter } from "@Tests/renderWithThemeRouter"; +import MobileNavDrawer from "./MobileNavDrawer"; + +beforeEach(() => { + localStorage.clear(); + initTestLanguage("zh-CN"); + mockMatchMedia(); +}); + +afterEach(cleanup); + +function renderDrawer(initialPath = "/", onNavigate = () => {}) { + return renderWithThemeRouter(, { initialEntries: [initialPath] }); +} + +const agentSubLabels = () => [ + t("agent:chat"), + t("agent:provider"), + t("agent:skills"), + t("agent:mcp"), + t("agent:tasks"), + t("agent:opfs"), + t("agent:settings"), +]; + +describe("MobileNavDrawer 移动导航抽屉", () => { + it("默认展开,渲染主导航 / AI Agent 分组(7 子项) / 辅助导航全部入口", () => { + const { getByText, getByTestId } = renderDrawer(); + // 主导航 + expect(getByText(t("script:installed_scripts"))).toBeInTheDocument(); + expect(getByText(t("script:subscribe"))).toBeInTheDocument(); + // AI Agent 分组标题 + expect(getByText(t("agent:title"))).toBeInTheDocument(); + // Agent 7 子项默认全部可见 + const submenu = getByTestId("drawer-agent-submenu"); + for (const label of agentSubLabels()) { + expect(within(submenu).getByText(label)).toBeInTheDocument(); + } + // 辅助导航 + expect(getByText(t("logs"))).toBeInTheDocument(); + expect(getByText(t("tools"))).toBeInTheDocument(); + }); + + it("点击 AI Agent 分组标题可折叠子项,再次点击重新展开", () => { + const { getByText, queryByTestId, getByTestId } = renderDrawer(); + expect(getByTestId("drawer-agent-submenu")).toBeInTheDocument(); + fireEvent.click(getByText(t("agent:title"))); + expect(queryByTestId("drawer-agent-submenu")).toBeNull(); + fireEvent.click(getByText(t("agent:title"))); + expect(getByTestId("drawer-agent-submenu")).toBeInTheDocument(); + }); + + it("点击任一导航项触发 onNavigate(用于跳转后关闭抽屉)", () => { + const onNavigate = vi.fn(); + const { getByText } = renderDrawer("/", onNavigate); + fireEvent.click(getByText(t("agent:skills"))); + expect(onNavigate).toHaveBeenCalledTimes(1); + }); + + it("当前路由对应的导航项标记为激活态(aria-current)", () => { + const { getByText } = renderDrawer("/agent/skills"); + const link = getByText(t("agent:skills")).closest("a"); + expect(link).toHaveAttribute("aria-current", "page"); + }); +}); diff --git a/src/pages/options/layout/MobileNavDrawer.tsx b/src/pages/options/layout/MobileNavDrawer.tsx new file mode 100644 index 000000000..19b6a0006 --- /dev/null +++ b/src/pages/options/layout/MobileNavDrawer.tsx @@ -0,0 +1,153 @@ +import { useState } from "react"; +import { NavLink, useLocation } from "react-router-dom"; +import { Bot, ChevronRight, LifeBuoy, Moon, Sun, Monitor } from "lucide-react"; +import { cn } from "@App/pkg/utils/cn"; +import { DocumentationSite } from "@App/app/const"; +import { localePath, t } from "@App/locales/locales"; +import { useTheme, type Theme } from "@App/pages/components/theme-provider"; +import { mainNav, agentNav, auxNav, type NavItem } from "./nav-items"; + +/** + * 移动端导航抽屉内容:1:1 镜像桌面 Sidebar(主导航 + AI Agent 分组 + 辅助导航 + 主题/帮助), + * 由 MobileHeader 的 ☰ 通过 shadcn Sheet 拉出。选中任一项后通过 onNavigate 关闭抽屉。 + */ +export default function MobileNavDrawer({ onNavigate }: { onNavigate?: () => void }) { + // 默认展开 Agent 分组——本入口的核心目的就是让移动端可达整个 Agent 板块 + const [agentOpen, setAgentOpen] = useState(true); + const isAgentActive = useLocation().pathname.startsWith("/agent"); + const { theme, setTheme } = useTheme(); + + const cycleTheme = () => { + const order: Theme[] = ["light", "dark", "auto"]; + setTheme(order[(order.indexOf(theme) + 1) % order.length]); + }; + const themeIcon = theme === "dark" ? Moon : theme === "light" ? Sun : Monitor; + + return ( +
+ {/* Logo */} +
+ ScriptCat + {"ScriptCat"} +
+ +
+ {/* 主导航 + AI Agent 分组 */} + + + {/* 分隔线 */} +
+ + {/* 辅助导航 */} + +
+ + {/* 底部:主题切换 + 帮助 */} +
+
+ + window.open(`${DocumentationSite}${localePath}/docs/use/use/`, "_blank")} + /> +
+
+ ); +} + +// ========== 顶层导航项 ========== +function DrawerItem({ item, onNavigate }: { item: NavItem; onNavigate?: () => void }) { + return ( + + cn( + "flex items-center gap-2.5 h-10 rounded-md text-[14px] px-3 transition-colors", + isActive + ? "bg-sidebar-accent text-sidebar-primary font-medium" + : "text-fg-secondary hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" + ) + } + > + + {item.label()} + + ); +} + +// ========== AI Agent 子项 ========== +function DrawerSubItem({ item, onNavigate }: { item: NavItem; onNavigate?: () => void }) { + return ( + + cn( + "flex items-center gap-2.5 h-9 rounded-md text-[13px] pl-9 pr-3 transition-colors", + isActive + ? "bg-sidebar-accent text-sidebar-primary font-medium" + : "text-fg-secondary hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" + ) + } + > + + {item.label()} + + ); +} + +// ========== 底部按钮(主题/帮助) ========== +function DrawerButton({ + icon: Icon, + label, + onClick, +}: { + icon: React.ComponentType<{ className?: string }>; + label: string; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/src/pages/options/layout/SettingsLayout.test.tsx b/src/pages/options/layout/SettingsLayout.test.tsx new file mode 100644 index 000000000..237b5a099 --- /dev/null +++ b/src/pages/options/layout/SettingsLayout.test.tsx @@ -0,0 +1,89 @@ +// @vitest-environment happy-dom +// src/pages/options/layout/SettingsLayout.test.tsx +import { describe, it, expect, vi, afterEach, beforeEach } from "vitest"; +import { render, screen, fireEvent, cleanup } from "@testing-library/react"; +import { Settings } from "lucide-react"; +import { SettingsLayout } from "./SettingsLayout"; +import { useIsMobile } from "@App/pages/components/use-is-mobile"; + +const scrollTo = vi.fn(); +vi.mock("../hooks/useScrollSpy", () => ({ + useScrollSpy: () => ({ + activeId: "general", + register: () => () => {}, + scrollContainerRef: { current: null }, + scrollTo, + }), +})); + +vi.mock("@App/pages/components/use-is-mobile", () => ({ useIsMobile: vi.fn(() => false) })); +const mockedUseIsMobile = vi.mocked(useIsMobile); + +afterEach(cleanup); +beforeEach(() => { + scrollTo.mockClear(); + mockedUseIsMobile.mockReturnValue(false); +}); + +describe("设置外壳 SettingsLayout", () => { + const cats = [ + { id: "general", icon: Settings, label: "通用" }, + { id: "interface", icon: Settings, label: "界面" }, + ]; + + const renderLayout = () => + render( + + {() =>
{"body"}
} +
+ ); + + describe("桌面端(竖向左栏)", () => { + it("渲染标题与全部分类导航项", () => { + renderLayout(); + expect(screen.getByText("设置")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "通用" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "界面" })).toBeInTheDocument(); + }); + + it("点击导航项调用 scrollTo(id)", () => { + renderLayout(); + fireEvent.click(screen.getByRole("button", { name: "界面" })); + expect(scrollTo).toHaveBeenCalledWith("interface"); + }); + + it("导航为 220px 竖向左栏", () => { + renderLayout(); + expect(screen.getByRole("navigation").className).toContain("w-[220px]"); + }); + + it("左侧分类导航使用 bg-card 背景(设计稿 Category Nav 为白底)", () => { + renderLayout(); + expect(screen.getByRole("navigation").className).toContain("bg-card"); + }); + + it("标题栏使用 bg-card 背景(与其它页面顶栏一致)", () => { + renderLayout(); + const header = screen.getByText("设置").parentElement!; + expect(header.className).toContain("bg-card"); + }); + }); + + describe("移动端(顶部横向栏)", () => { + beforeEach(() => mockedUseIsMobile.mockReturnValue(true)); + + it("分类导航改为横向滚动栏", () => { + renderLayout(); + const nav = screen.getByRole("navigation"); + expect(nav.className).toContain("overflow-x-auto"); + expect(nav.className).not.toContain("w-[220px]"); + }); + + it("横向栏仍渲染全部分类且点击调用 scrollTo(id)", () => { + renderLayout(); + expect(screen.getByRole("button", { name: "通用" })).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "界面" })); + expect(scrollTo).toHaveBeenCalledWith("interface"); + }); + }); +}); diff --git a/src/pages/options/layout/SettingsLayout.tsx b/src/pages/options/layout/SettingsLayout.tsx new file mode 100644 index 000000000..73270ac20 --- /dev/null +++ b/src/pages/options/layout/SettingsLayout.tsx @@ -0,0 +1,99 @@ +// src/pages/options/layout/SettingsLayout.tsx +import React from "react"; +import { cn } from "@App/pkg/utils/cn"; +import { useScrollSpy } from "../hooks/useScrollSpy"; +import { useIsMobile } from "@App/pages/components/use-is-mobile"; + +export interface SettingsCategory { + id: string; + icon: React.ComponentType<{ className?: string }>; + label: string; +} + +export interface SettingsLayoutProps { + title: string; + categories: SettingsCategory[]; + children: (register: (id: string) => (el: HTMLElement | null) => void) => React.ReactNode; +} + +// 激活/未激活配色,左侧竖栏与移动横向栏共用 +const navItemColors = (active: boolean) => + active ? "bg-primary/10 text-primary font-semibold" : "text-muted-foreground hover:bg-accent hover:text-foreground"; + +export function SettingsLayout({ title, categories, children }: SettingsLayoutProps) { + const ids = React.useMemo(() => categories.map((c) => c.id), [categories]); + const { activeId, register, scrollContainerRef, scrollTo } = useScrollSpy(ids); + const isMobile = useIsMobile(); + const activeChipRef = React.useRef(null); + + // 移动横向栏:激活分类滚动到可视区域(仅横向,不影响页面纵向滚动) + React.useEffect(() => { + activeChipRef.current?.scrollIntoView({ block: "nearest", inline: "center" }); + }, [activeId]); + + return ( +
+
+ {title} +
+ + {/* 移动端:标题下方横向滚动分类栏 */} + {isMobile && ( + + )} + +
+ {/* 桌面端:左侧竖向分类导航 */} + {!isMobile && ( + + )} + + {/* 滚动容器始终渲染(切换断点不重挂载,避免 scroll-spy 的 IO root 失效) */} +
+
+ {children(register)} +
+
+
+
+ ); +} diff --git a/src/pages/options/layout/Sidebar.test.tsx b/src/pages/options/layout/Sidebar.test.tsx new file mode 100644 index 000000000..80ff860b8 --- /dev/null +++ b/src/pages/options/layout/Sidebar.test.tsx @@ -0,0 +1,53 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { cleanup, fireEvent, within } from "@testing-library/react"; +import { t } from "@App/locales/locales"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import { mockMatchMedia } from "@Tests/mockMatchMedia"; +import { renderWithThemeRouter } from "@Tests/renderWithThemeRouter"; +import Sidebar from "./Sidebar"; + +beforeEach(() => { + localStorage.clear(); + initTestLanguage("zh-CN"); + mockMatchMedia(); +}); + +afterEach(cleanup); + +function renderSidebar(initialPath = "/") { + return renderWithThemeRouter(, { initialEntries: [initialPath] }); +} + +const subLabels = () => [ + t("agent:chat"), + t("agent:provider"), + t("agent:skills"), + t("agent:mcp"), + t("agent:tasks"), + t("agent:opfs"), + t("agent:settings"), +]; + +describe("Sidebar 侧边栏 AI Agent 菜单", () => { + it("渲染 AI Agent 子菜单入口", () => { + const { getByText } = renderSidebar(); + expect(getByText(t("agent:title"))).toBeInTheDocument(); + }); + + it("默认折叠,点击 AI Agent 后展开显示 7 个子项", () => { + const { getByText, queryByTestId, getByTestId } = renderSidebar(); + expect(queryByTestId("sidebar-agent-submenu")).toBeNull(); + fireEvent.click(getByText(t("agent:title"))); + const submenu = getByTestId("sidebar-agent-submenu"); + for (const label of subLabels()) { + expect(within(submenu).getByText(label)).toBeInTheDocument(); + } + }); + + it("处于 /agent 路由时自动展开且对应子项为激活态", () => { + const { getByText } = renderSidebar("/agent/skills"); + const link = getByText(t("agent:skills")).closest("a"); + expect(link).toHaveAttribute("aria-current", "page"); + }); +}); diff --git a/src/pages/options/layout/Sidebar.tsx b/src/pages/options/layout/Sidebar.tsx new file mode 100644 index 000000000..7223fe61d --- /dev/null +++ b/src/pages/options/layout/Sidebar.tsx @@ -0,0 +1,339 @@ +import { useEffect, useState } from "react"; +import { useHoverMenu } from "../../components/ui/use-hover-menu"; +import { NavLink, useLocation, useNavigate } from "react-router-dom"; +import { + LifeBuoy, + PanelLeftClose, + PanelLeftOpen, + Moon, + Sun, + Monitor, + BookOpen, + Link, + FileCode, + Store, + MessageCircle, + Bot, + ChevronRight, +} from "lucide-react"; +import { GithubIcon } from "../../components/icons/GithubIcon"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, + DropdownMenuTrigger, +} from "../../components/ui/dropdown-menu"; +import { useTheme, type Theme } from "../../components/theme-provider"; +import { DocumentationSite } from "@App/app/const"; +import { localePath, t } from "@App/locales/locales"; +import { mainNav, agentNav, auxNav } from "./nav-items"; + +const SIDEBAR_KEY = "scriptcat-sidebar-collapsed"; + +export default function Sidebar() { + const [collapsed, setCollapsed] = useState(() => localStorage.getItem(SIDEBAR_KEY) === "1"); + const { theme, setTheme } = useTheme(); + + const toggleCollapse = () => { + setCollapsed((prev) => { + localStorage.setItem(SIDEBAR_KEY, prev ? "0" : "1"); + return !prev; + }); + }; + + const cycleTheme = () => { + const order: Theme[] = ["light", "dark", "auto"]; + setTheme(order[(order.indexOf(theme) + 1) % order.length]); + }; + + const themeIcon = theme === "dark" ? Moon : theme === "light" ? Sun : Monitor; + + return ( + + ); +} + +// ========== 导航项 ========== +function SidebarItem({ + to, + icon: Icon, + label, + collapsed, +}: { + to: string; + icon: React.ComponentType<{ className?: string }>; + label: string; + collapsed: boolean; +}) { + return ( + + `flex items-center gap-2.5 h-9 rounded-md text-[14px] transition-colors ${ + collapsed ? "justify-center px-0" : "px-3" + } ${ + isActive + ? "bg-sidebar-accent text-sidebar-primary font-medium" + : "text-fg-secondary hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" + }` + } + > + + {!collapsed && {label}} + + ); +} + +// ========== AI Agent 子菜单 ========== +function AgentMenu({ collapsed }: { collapsed: boolean }) { + const isAgentActive = useLocation().pathname.startsWith("/agent"); + const [open, setOpen] = useState(isAgentActive); + + // 进入 /agent 路由时自动展开(不在离开时强制收起,尊重用户手动操作) + useEffect(() => { + if (isAgentActive) setOpen(true); + }, [isAgentActive]); + + if (collapsed) { + return ; + } + + return ( +
+ + {open && ( +
+ {agentNav.map((item) => ( + + `flex items-center gap-2.5 h-8 rounded-md text-[13px] pl-9 pr-3 transition-colors ${ + isActive + ? "bg-sidebar-accent text-sidebar-primary font-medium" + : "text-fg-secondary hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" + }` + } + > + + {item.label()} + + ))} +
+ )} +
+ ); +} + +// 折叠态下以 hover 浮层展示 AI Agent 子项 +function AgentMenuCollapsed({ isActive }: { isActive: boolean }) { + const navigate = useNavigate(); + const { close, rootProps, hoverProps, contentProps } = useHoverMenu(); + + return ( + + + + + + {agentNav.map((item) => ( + { + close(); + navigate(item.to); + }} + > + + {item.label()} + + ))} + + + ); +} + +// ========== 帮助中心菜单(hover 触发) ========== +function HelpMenu({ collapsed }: { collapsed: boolean }) { + const { close, rootProps, hoverProps, contentProps } = useHoverMenu(); + + const openUrl = (url: string) => { + close(); + window.open(url, "_blank"); + }; + + return ( + + + + + + {/* 外部链接 */} + + + + {t("external_links")} + + + openUrl(`${DocumentationSite}${localePath}/docs/dev/`)}> + + {t("api_docs")} + + openUrl("https://learn.scriptcat.org/docs/%E7%AE%80%E4%BB%8B/")}> + + {t("development_guide")} + + + + + {t("script_gallery")} + + + openUrl("https://scriptcat.org/search")}> + {"ScriptCat"} + + openUrl("https://greasyfork.org/scripts")}> + {"Greasy Fork"} + + openUrl("https://openuserjs.org/")}>{"OpenUserJS"} + + + openUrl("https://bbs.tampermonkey.net.cn/")}> + + {t("community_forum")} + + openUrl("https://github.com/scriptscat/scriptcat")}> + + {"GitHub"} + + + + {/* 使用指南 */} + openUrl(`${DocumentationSite}${localePath}/docs/use/use/`)}> + + {t("user_guide")} + + + + ); +} + +// ========== 底部按钮 ========== +function SidebarButton({ + icon: Icon, + label, + collapsed, + onClick, + testId, +}: { + icon: React.ComponentType<{ className?: string }>; + label: string; + collapsed: boolean; + onClick: () => void; + testId?: string; +}) { + return ( + + ); +} diff --git a/src/pages/options/layout/nav-items.ts b/src/pages/options/layout/nav-items.ts new file mode 100644 index 000000000..4865732fb --- /dev/null +++ b/src/pages/options/layout/nav-items.ts @@ -0,0 +1,47 @@ +import type { ComponentType } from "react"; +import { + Package, + Rss, + ScrollText, + Wrench, + Settings, + MessageSquare, + Server, + Sparkles, + Plug, + CalendarClock, + FolderTree, + SlidersHorizontal, +} from "lucide-react"; +import { t } from "@App/locales/locales"; + +export interface NavItem { + to: string; + icon: ComponentType<{ className?: string }>; + /** 惰性取值,跟随运行时语言切换 */ + label: () => string; +} + +/** 主导航项 */ +export const mainNav: NavItem[] = [ + { to: "/", icon: Package, label: () => t("script:installed_scripts") }, + { to: "/subscribe", icon: Rss, label: () => t("script:subscribe") }, +]; + +/** AI Agent 子导航项 */ +export const agentNav: NavItem[] = [ + { to: "/agent/chat", icon: MessageSquare, label: () => t("agent:chat") }, + { to: "/agent/provider", icon: Server, label: () => t("agent:provider") }, + { to: "/agent/skills", icon: Sparkles, label: () => t("agent:skills") }, + { to: "/agent/mcp", icon: Plug, label: () => t("agent:mcp") }, + { to: "/agent/tasks", icon: CalendarClock, label: () => t("agent:tasks") }, + { to: "/agent/opfs", icon: FolderTree, label: () => t("agent:opfs") }, + { to: "/agent/settings", icon: SlidersHorizontal, label: () => t("agent:settings") }, +]; + +/** 辅助导航项 */ +export const auxNav: NavItem[] = [ + { to: "/logs", icon: ScrollText, label: () => t("logs") }, + { to: "/tools", icon: Wrench, label: () => t("tools") }, + { to: "/settings", icon: Settings, label: () => t("settings") }, +]; diff --git a/src/pages/options/layout/useScriptDropzone.test.ts b/src/pages/options/layout/useScriptDropzone.test.ts new file mode 100644 index 000000000..f17e7b7bf --- /dev/null +++ b/src/pages/options/layout/useScriptDropzone.test.ts @@ -0,0 +1,49 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi } from "vitest"; +import { renderHook, act } from "@testing-library/react"; +import { useScriptDropzone } from "./useScriptDropzone"; + +function dragEvent(type: string, opts: { types?: string[]; files?: File[] } = {}) { + const ev = new Event(type, { bubbles: true, cancelable: true }) as any; + ev.dataTransfer = { + types: opts.types ?? [], + files: opts.files ?? [], + items: (opts.files ?? []).map((f) => ({ kind: "file", getAsFile: () => f })), + }; + return ev; +} + +describe("useScriptDropzone", () => { + it("拖入文件时 isDragActive 为 true,离开后为 false", () => { + const { result } = renderHook(() => useScriptDropzone(() => {})); + act(() => { + window.dispatchEvent(dragEvent("dragenter", { types: ["Files"] })); + }); + expect(result.current.isDragActive).toBe(true); + act(() => { + window.dispatchEvent(dragEvent("dragleave", { types: ["Files"] })); + }); + expect(result.current.isDragActive).toBe(false); + }); + + it("拖入非文件(元素排序)不激活遮罩", () => { + const { result } = renderHook(() => useScriptDropzone(() => {})); + act(() => { + window.dispatchEvent(dragEvent("dragenter", { types: ["text/plain"] })); + }); + expect(result.current.isDragActive).toBe(false); + }); + + it("drop 文件时回调收到 items 且遮罩关闭", async () => { + const onFiles = vi.fn(); + const { result } = renderHook(() => useScriptDropzone(onFiles)); + const file = new File(["// ==UserScript=="], "a.user.js"); + await act(async () => { + window.dispatchEvent(dragEvent("dragenter", { types: ["Files"] })); + window.dispatchEvent(dragEvent("drop", { types: ["Files"], files: [file] })); + await Promise.resolve(); + }); + expect(onFiles).toHaveBeenCalledWith([{ file, handle: null }]); + expect(result.current.isDragActive).toBe(false); + }); +}); diff --git a/src/pages/options/layout/useScriptDropzone.ts b/src/pages/options/layout/useScriptDropzone.ts new file mode 100644 index 000000000..351ee3720 --- /dev/null +++ b/src/pages/options/layout/useScriptDropzone.ts @@ -0,0 +1,70 @@ +import { useEffect, useRef, useState } from "react"; +import type { ImportItem } from "@App/pages/options/routes/ScriptList/importHandler"; + +const hasFiles = (e: DragEvent) => Array.from(e.dataTransfer?.types || []).includes("Files"); + +export function useScriptDropzone(onFiles: (items: ImportItem[]) => void): { isDragActive: boolean } { + const [isDragActive, setActive] = useState(false); + const counter = useRef(0); + const onFilesRef = useRef(onFiles); + onFilesRef.current = onFiles; + + useEffect(() => { + const onEnter = (e: DragEvent) => { + if (!hasFiles(e)) return; + e.preventDefault(); + counter.current++; + setActive(true); + }; + const onOver = (e: DragEvent) => { + if (!hasFiles(e)) return; + e.preventDefault(); + }; + const onLeave = (e: DragEvent) => { + if (!hasFiles(e)) return; + counter.current--; + if (counter.current <= 0) { + counter.current = 0; + setActive(false); + } + }; + const onDrop = async (e: DragEvent) => { + if (!hasFiles(e)) return; + e.preventDefault(); + counter.current = 0; + setActive(false); + const dt = e.dataTransfer!; + const items: ImportItem[] = []; + const dtItems = Array.from(dt.items || []).filter((it) => it.kind === "file"); + if (dtItems.length) { + await Promise.all( + dtItems.map(async (it) => { + let handle: FileSystemFileHandle | null = null; + if ("getAsFileSystemHandle" in it) { + // Chrome 专有:取 FileSystemFileHandle 以支持本地文件监听;Firefox/Safari 无此 API,回退 getAsFile + const h = await (it as any).getAsFileSystemHandle().catch(() => null); + if (h && h.kind === "file") handle = h as FileSystemFileHandle; + } + const file = handle ? await handle.getFile() : it.getAsFile(); + if (file) items.push({ file, handle }); + }) + ); + } else { + for (const file of Array.from(dt.files)) items.push({ file, handle: null }); + } + if (items.length) onFilesRef.current(items); + }; + window.addEventListener("dragenter", onEnter); + window.addEventListener("dragover", onOver); + window.addEventListener("dragleave", onLeave); + window.addEventListener("drop", onDrop); + return () => { + window.removeEventListener("dragenter", onEnter); + window.removeEventListener("dragover", onOver); + window.removeEventListener("dragleave", onLeave); + window.removeEventListener("drop", onDrop); + }; + }, []); + + return { isDragActive }; +} diff --git a/src/pages/options/main.tsx b/src/pages/options/main.tsx index f95c9f909..7e7731efd 100644 --- a/src/pages/options/main.tsx +++ b/src/pages/options/main.tsx @@ -1,36 +1,29 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import MainLayout from "../components/layout/MainLayout.tsx"; -import Sider from "../components/layout/Sider.tsx"; -import { AppProvider } from "../store/AppContext.tsx"; -import "@arco-design/web-react/dist/css/arco.css"; -import "@App/locales/locales"; -import "@App/index.css"; -import "./index.css"; +import App from "./App.tsx"; import LoggerCore from "@App/app/logger/core.ts"; -import { LoggerDAO } from "@App/app/repo/logger.ts"; -import DBWriter from "@App/app/logger/db_writer.ts"; -import { registerEditor } from "@App/pkg/utils/monaco-editor"; -import migrate from "@App/app/migrate.ts"; - -migrate(); - -registerEditor(); +import { message } from "../store/global.ts"; +import MessageWriter from "@App/app/logger/message_writer.ts"; +import { ThemeProvider } from "../components/theme-provider.tsx"; +import { TooltipProvider } from "../components/ui/tooltip.tsx"; +import { Toaster } from "../components/ui/sonner.tsx"; +import "@App/index.css"; // 初始化日志组件 const loggerCore = new LoggerCore({ - writer: new DBWriter(new LoggerDAO()), + writer: new MessageWriter(message), labels: { env: "options" }, }); loggerCore.logger().debug("options page start"); const Root = ( - - - - - + + + + + + ); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/src/pages/options/routes/Agent.tsx b/src/pages/options/routes/Agent.tsx deleted file mode 100644 index 3942429a4..000000000 --- a/src/pages/options/routes/Agent.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Route, Routes } from "react-router-dom"; -import AgentProvider from "./AgentProvider"; -import AgentChat from "./AgentChat"; -import AgentMcp from "./AgentMcp"; -import AgentOPFS from "./AgentOPFS"; -import AgentSkills from "./AgentSkills"; -import AgentTasks from "./AgentTasks"; -import AgentSettings from "./AgentSettings"; - -function Agent() { - return ( - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - ); -} - -export default Agent; diff --git a/src/pages/options/routes/AgentChat/AskUserBlock.test.tsx b/src/pages/options/routes/AgentChat/AskUserBlock.test.tsx new file mode 100644 index 000000000..08222e1d4 --- /dev/null +++ b/src/pages/options/routes/AgentChat/AskUserBlock.test.tsx @@ -0,0 +1,47 @@ +// @vitest-environment happy-dom +import { describe, it, expect, vi, beforeAll, afterEach } from "vitest"; +import { render, cleanup, screen, fireEvent } from "@testing-library/react"; +import { initTestLanguage } from "@Tests/initTestLanguage"; +import AskUserBlock from "./AskUserBlock"; + +beforeAll(() => initTestLanguage("zh-CN")); +afterEach(() => cleanup()); + +describe("用户提问块 AskUserBlock", () => { + it("展示问题文本", () => { + render(); + expect(screen.getByText("选择一个颜色")).toBeInTheDocument(); + }); + + it("单选点击选项后立即提交该选项", () => { + const onRespond = vi.fn(); + render(); + fireEvent.click(screen.getByTestId("ask-option-红")); + expect(onRespond).toHaveBeenCalledWith("q1", "红"); + }); + + it("多选切换并确认后提交 JSON 数组", () => { + const onRespond = vi.fn(); + render(); + fireEvent.click(screen.getByTestId("ask-option-红")); + fireEvent.click(screen.getByTestId("ask-option-绿")); + fireEvent.click(screen.getByTestId("ask-confirm")); + expect(onRespond).toHaveBeenCalledWith("q1", JSON.stringify(["红", "绿"])); + }); + + it("文本输入后发送提交输入内容", () => { + const onRespond = vi.fn(); + render(); + fireEvent.change(screen.getByTestId("ask-input"), { target: { value: "你好" } }); + fireEvent.click(screen.getByTestId("ask-send")); + expect(onRespond).toHaveBeenCalledWith("q1", "你好"); + }); + + it("提交后进入已回答状态且不再展示输入框", () => { + const onRespond = vi.fn(); + render(); + fireEvent.click(screen.getByTestId("ask-option-红")); + expect(screen.queryByTestId("ask-input")).toBeNull(); + expect(screen.getByText("红")).toBeInTheDocument(); + }); +}); diff --git a/src/pages/options/routes/AgentChat/AskUserBlock.tsx b/src/pages/options/routes/AgentChat/AskUserBlock.tsx index 98ced48a2..aaf1a17af 100644 --- a/src/pages/options/routes/AgentChat/AskUserBlock.tsx +++ b/src/pages/options/routes/AgentChat/AskUserBlock.tsx @@ -1,5 +1,7 @@ import { useState, useRef, useEffect } from "react"; -import { IconSend, IconCheckCircleFill, IconCheck } from "@arco-design/web-react/icon"; +import { Check, CheckCircle2, Send } from "lucide-react"; +import { t } from "@App/locales/locales"; +import { cn } from "@App/pkg/utils/cn"; export default function AskUserBlock({ id, @@ -24,8 +26,7 @@ export default function AskUserBlock({ }, []); const handleSubmit = () => { - if (submitted) return; - if (!answer.trim()) return; + if (submitted || !answer.trim()) return; setSubmitted(true); onRespond(id, answer.trim()); }; @@ -56,7 +57,6 @@ export default function AskUserBlock({ const displayAnswer = (() => { if (!answer) return ""; - // 多选时尝试解析为数组展示 if (multiple) { try { const arr = JSON.parse(answer); @@ -73,15 +73,12 @@ export default function AskUserBlock({ // 已提交:紧凑的完成状态 if (submitted) { return ( -
-
- -
-
{question}
-
{displayAnswer}
+
+
+ +
+
{question}
+
{displayAnswer}
@@ -89,47 +86,48 @@ export default function AskUserBlock({ } return ( -
-
+
+
{/* 顶部渐变条 */} -
+
-
+
{/* 问题 */} -
-
- ? -
-
- {question} +
+
+ {"?"}
+
{question}
- {/* 选项区域 */} + {/* 选项 */} {hasOptions && ( -
+
{options.map((opt) => { const isSelected = selectedOptions.includes(opt); return (
)} {/* 文本输入 */} -
+
setAnswer(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSubmit()} - placeholder={hasOptions ? "Or type a custom response..." : "Type your response..."} - className="tw-flex-1 tw-bg-transparent tw-border-none tw-outline-none tw-text-sm tw-text-[var(--color-text-1)] placeholder:tw-text-[var(--color-text-4)] tw-min-w-0" + placeholder={t("agent:chat_input_placeholder")} + className="flex-1 bg-transparent border-none outline-none text-sm text-foreground placeholder:text-muted-foreground min-w-0" />
diff --git a/src/pages/options/routes/AgentChat/AttachmentRenderers.tsx b/src/pages/options/routes/AgentChat/AttachmentRenderers.tsx index 5d8080073..51b455e70 100644 --- a/src/pages/options/routes/AgentChat/AttachmentRenderers.tsx +++ b/src/pages/options/routes/AgentChat/AttachmentRenderers.tsx @@ -1,9 +1,7 @@ import { useState, useEffect, useCallback } from "react"; -import { IconDownload, IconEye } from "@arco-design/web-react/icon"; +import { Download, Eye } from "lucide-react"; import type { Attachment, AudioBlock } from "@App/app/service/agent/core/types"; -import { AgentChatRepo } from "@App/app/repo/agent_chat"; - -const repo = new AgentChatRepo(); +import { agentChatRepo } from "@App/app/repo/agent_chat"; // 图片附件组件:从 OPFS 懒加载并展示 export function AttachmentImage({ attachment }: { attachment: Attachment }) { @@ -12,21 +10,22 @@ export function AttachmentImage({ attachment }: { attachment: Attachment }) { useEffect(() => { let revoked = false; - repo.getAttachment(attachment.id).then((blob) => { + let url: string | null = null; + agentChatRepo.getAttachment(attachment.id).then((blob) => { if (blob && !revoked) { - setBlobUrl(URL.createObjectURL(blob)); + url = URL.createObjectURL(blob); + setBlobUrl(url); } }); return () => { revoked = true; - if (blobUrl) URL.revokeObjectURL(blobUrl); + if (url) URL.revokeObjectURL(url); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [attachment.id]); if (!blobUrl) { return ( -
+
{"Loading..."}
); @@ -34,29 +33,25 @@ export function AttachmentImage({ attachment }: { attachment: Attachment }) { return ( <> -
setPreview(true)}> +
setPreview(true)}> {attachment.name} -
- +
+
- {/* 全屏预览 */} {preview && (
setPreview(false)} > {attachment.name} e.stopPropagation()} />
@@ -65,10 +60,18 @@ export function AttachmentImage({ attachment }: { attachment: Attachment }) { ); } +// 格式化文件大小 +function formatSize(size?: number): string { + if (!size) return ""; + if (size < 1024) return `${size} B`; + if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; + return `${(size / (1024 * 1024)).toFixed(1)} MB`; +} + // 文件附件组件:显示文件信息和下载按钮 export function AttachmentFile({ attachment }: { attachment: Attachment }) { const handleDownload = useCallback(async () => { - const blob = await repo.getAttachment(attachment.id); + const blob = await agentChatRepo.getAttachment(attachment.id); if (!blob) return; const url = URL.createObjectURL(blob); const a = document.createElement("a"); @@ -80,23 +83,17 @@ export function AttachmentFile({ attachment }: { attachment: Attachment }) { URL.revokeObjectURL(url); }, [attachment.id, attachment.name]); - const sizeText = attachment.size - ? attachment.size < 1024 - ? `${attachment.size} B` - : attachment.size < 1024 * 1024 - ? `${(attachment.size / 1024).toFixed(1)} KB` - : `${(attachment.size / (1024 * 1024)).toFixed(1)} MB` - : ""; + const sizeText = formatSize(attachment.size); return (
- -
- {attachment.name} - {sizeText && {sizeText}} + +
+ {attachment.name} + {sizeText && {sizeText}}
); @@ -108,30 +105,31 @@ export function AttachmentAudio({ block }: { block: AudioBlock }) { useEffect(() => { let revoked = false; - repo.getAttachment(block.attachmentId).then((blob) => { + let url: string | null = null; + agentChatRepo.getAttachment(block.attachmentId).then((blob) => { if (blob && !revoked) { - setBlobUrl(URL.createObjectURL(blob)); + url = URL.createObjectURL(blob); + setBlobUrl(url); } }); return () => { revoked = true; - if (blobUrl) URL.revokeObjectURL(blobUrl); + if (url) URL.revokeObjectURL(url); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [block.attachmentId]); if (!blobUrl) { return ( -
+
{"Loading audio..."}
); } return ( -
- {block.name && {block.name}} -