diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 18996606..00000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -build -.vscode \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 148a91c5..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true - }, - "extends": [ - "preact", - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "overrides": [], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": ["react", "@typescript-eslint", "prettier"], - "rules": { - "prettier/prettier": "error", - "react-hooks/exhaustive-deps": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "jest/expect-expect": "off" - } -} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c69aa65..5478db5d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,81 +2,121 @@ name: Test on: push: - branches: [main, release] + branches: [main, release, rewrite-svelte-v2] pull_request: +env: + PNPM_VERSION: 10 + NODE_VERSION: "20.x" + jobs: static-check: name: Static check 🪲 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: - node-version: "20.x" - - - name: Cache node modules - uses: actions/cache@v3 + version: ${{ env.PNPM_VERSION }} + - uses: actions/setup-node@v4 with: - path: ~/.npm - key: cache-node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + node-version: ${{ env.NODE_VERSION }} + cache: pnpm - name: Install - run: npm ci + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm run typecheck + + # `tsc` does not look at .svelte files and biome doesn't process them, so + # without this step every component in src/ui is unchecked — an `await` + # in a non-async function passed CI as green. + - name: Typecheck Svelte + run: pnpm run typecheck:svelte - - name: Typescript - run: npm run tsc + - name: Lint + run: pnpm run lint - - name: Eslint - run: npm run eslint + - name: Unit tests + run: pnpm run test build: name: Build 🏗 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: - node-version: "20.x" - - - name: Cache node modules - uses: actions/cache@v3 + version: ${{ env.PNPM_VERSION }} + - uses: actions/setup-node@v4 with: - path: ~/.npm - key: cache-node-modules-build-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + node-version: ${{ env.NODE_VERSION }} + cache: pnpm - name: Install - run: npm ci + run: pnpm install --frozen-lockfile - name: Build - run: npm run build + run: pnpm run build + + - name: Upload dist + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 - test: - name: Cypress + e2e: + name: E2E (Playwright + Tolgee) runs-on: ubuntu-latest + timeout-minutes: 20 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 with: - node-version: "20.x" - - - name: Cache node modules - uses: actions/cache@v3 + version: ${{ env.PNPM_VERSION }} + - uses: actions/setup-node@v4 with: - path: ~/.npm - key: cache-node-modules-cypress-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + node-version: ${{ env.NODE_VERSION }} + cache: pnpm - name: Install - run: npm ci + run: pnpm install --frozen-lockfile + + # Start Tolgee in the background as early as possible so its slow JVM + # warm-up overlaps with the rest of the build steps. + - name: Start Tolgee platform + run: pnpm run e2e:up + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps chromium - - name: Cypress run - run: npm run cy:run + - name: Build plugin + run: pnpm run build - - uses: actions/upload-artifact@v4 + - name: Wait for Tolgee + run: pnpm run e2e:wait + + - name: Run Playwright + run: pnpm exec playwright test + + - name: Capture Tolgee logs on failure if: failure() + run: docker compose -f e2e/docker-compose.yml logs --no-color > tolgee.log || true + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 with: - name: e2e + name: playwright-report path: | - ./cypress/videos/**/* - ./cypress/screenshots/**/* + e2e/playwright-report/**/* + e2e/test-results/**/* + tolgee.log + retention-days: 7 + + - name: Stop Tolgee + if: always() + run: docker compose -f e2e/docker-compose.yml down -v diff --git a/.gitignore b/.gitignore index b17c95e9..e32758fc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,14 +2,23 @@ *.log *.log.* node_modules +.DS_Store out/ dist/ build code.js manifest.json +# …but the shippable delivery package keeps its own manifest (the bare +# `manifest.json` rule above is only meant for the generated root/dist one). +!delivery/tolgee-plugin/manifest.json -cypress/videos -cypress/screenshots +# Playwright (v2 e2e) +e2e/test-results/ +e2e/playwright-report/ +playwright/.cache/ .vscode + +# Local Claude Code config — machine-specific, never part of a change. +.claude/ diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 99d5b320..00000000 --- a/.prettierignore +++ /dev/null @@ -1,5 +0,0 @@ -build -node_modules - -src/**/*.css.d.ts -apiSchema.generated.ts \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 0967ef42..00000000 --- a/.prettierrc.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..5be4a4f8 --- /dev/null +++ b/biome.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "ignore": ["dist", "node_modules", "**/*.svelte", "**/schema.generated.ts"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "warn" + }, + "style": { + "useImportType": "error", + "useNodejsImportProtocol": "error" + } + } + } +} diff --git a/cypress.config.mjs b/cypress.config.mjs deleted file mode 100644 index 17161e32..00000000 --- a/cypress.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "cypress"; - -export default defineConfig({ - e2e: { - setupNodeEvents(on, config) { - // implement node event listeners here - }, - }, -}); diff --git a/cypress/common/tools.ts b/cypress/common/tools.ts deleted file mode 100644 index 7038d7dd..00000000 --- a/cypress/common/tools.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createShortcutUrl, PluginData } from "@/web/urlConfig"; - -const ORIGIN = "http://localhost:3000"; - -export const visitWithState = (data: Partial) => { - cy.visit(`${ORIGIN}${createShortcutUrl(data)}`); - cy.wait(100); - cy.frameLoaded("#plugin_iframe"); -}; diff --git a/cypress/e2e/copyView.cy.ts b/cypress/e2e/copyView.cy.ts deleted file mode 100644 index d5d2a6ac..00000000 --- a/cypress/e2e/copyView.cy.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { createTestNode, PAGE_COPY, PAGE_COPY_LANGUAGE } from "@/web/urlConfig"; -import { visitWithState } from "../common/tools"; - -describe("Copy view", () => { - it("shows keys if language not set", () => { - visitWithState({ - config: PAGE_COPY, - }); - - cy.iframe().contains("Page copy - keys").should("be.visible"); - }); - - it("shows language if language set", () => { - visitWithState({ - config: PAGE_COPY_LANGUAGE, - }); - - cy.iframe().contains("Page copy - cs").should("be.visible"); - }); - - it("shows unconnected node correctly", () => { - const nodes = [createTestNode({ text: "Test node", key: "test_key" })]; - visitWithState({ - config: PAGE_COPY_LANGUAGE, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframe().contains("Test node").should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_text") - .contains("Test node") - .should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_key") - .contains("Not connected") - .should("be.visible"); - }); - - it("shows connected node correctly", () => { - const nodes = [ - createTestNode({ - text: "Test node", - key: "test_key", - connected: true, - }), - ]; - visitWithState({ - config: PAGE_COPY_LANGUAGE, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframe().contains("Test node").should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_text") - .contains("Test node") - .should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_key") - .contains("test_key") - .should("be.visible"); - - cy.iframe().findDcy("general_node_list_row_namespace").should("be.empty"); - }); - - it("shows connected node with namespace", () => { - const nodes = [ - createTestNode({ - text: "Test node", - key: "test_key", - ns: "test_ns", - connected: true, - }), - ]; - visitWithState({ - config: PAGE_COPY_LANGUAGE, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframe().contains("Test node").should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_text") - .contains("Test node") - .should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_key") - .contains("test_key") - .should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_namespace") - .contains("test_ns") - .should("be.visible"); - }); - - it("pulls changes correctly", () => { - const connectedCzech = [ - createTestNode({ - text: "Na cestě", - key: "on-the-road-title", - connected: true, - }), - ]; - visitWithState({ - config: PAGE_COPY_LANGUAGE, - selectedNodes: connectedCzech, - allNodes: connectedCzech, - }); - - cy.iframe().contains("Na cestě").should("be.visible"); - - cy.iframe().findDcy("copy_view_pull_button").should("be.visible").click(); - - cy.iframe().contains("Na cestu").should("be.visible"); - }); -}); diff --git a/cypress/e2e/index.cy.ts b/cypress/e2e/index.cy.ts deleted file mode 100644 index 28946e3d..00000000 --- a/cypress/e2e/index.cy.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { createTestNode, SIGNED_IN } from "@/web/urlConfig"; -import { visitWithState } from "../common/tools"; - -describe("Index", () => { - it("shows unconnected node correctly", () => { - const nodes = [createTestNode({ text: "Test node", key: "test_key" })]; - visitWithState({ - config: SIGNED_IN, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframe().contains("Test node").should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_text") - .contains("Test node") - .should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_key") - .find("input") - .should("have.value", "test_key"); - - cy.iframe() - .findDcy("general_namespace_select_input") - .should(($input) => { - expect($input.val() || "").to.be.empty; - }); - }); - - it("shows connected node correctly", () => { - const nodes = [ - createTestNode({ text: "Test node", key: "test_key", connected: true }), - ]; - visitWithState({ - config: SIGNED_IN, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframe().contains("Test node").should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_text") - .contains("Test node") - .should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_key") - .contains("test_key") - .should("be.visible"); - - cy.iframe().findDcy("general_node_list_row_namespace").should("be.empty"); - }); - - it("shows connected node with namespace", () => { - const nodes = [ - createTestNode({ - text: "Test node", - key: "test_key", - ns: "test_ns", - connected: true, - }), - ]; - visitWithState({ - config: { ...SIGNED_IN }, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframe().contains("Test node").should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_text") - .contains("Test node") - .should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_key") - .contains("test_key") - .should("be.visible"); - - cy.iframe() - .findDcy("general_node_list_row_namespace") - .contains("test_ns") - .should("be.visible"); - }); -}); diff --git a/cypress/e2e/keyFormatEditor.cy.ts b/cypress/e2e/keyFormatEditor.cy.ts deleted file mode 100644 index 158f5f4c..00000000 --- a/cypress/e2e/keyFormatEditor.cy.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { DEFAULT_CREDENTIALS } from "@/web/urlConfig"; -import { EditorView } from "@codemirror/view"; -import { visitWithState } from "../common/tools"; - -function openEditor() { - visitWithState({ - config: { - apiUrl: DEFAULT_CREDENTIALS.apiUrl, - apiKey: DEFAULT_CREDENTIALS.apiKey, - language: "en", - namespace: "", - documentInfo: true, - pageInfo: true, - prefillKeyFormat: true, - keyFormat: "", - }, - }); - - cy.iframeBody() - .find('[data-cy="index_settings_button"]') - .should("be.visible") - .click(); - cy.iframeBody() - .find('[data-cy="settings_expandable_strings"]') - .should("be.visible") - .click(); - cy.iframeBody() - .find('[data-cy="global-editor"]') - .should("be.visible") - .click(); - cy.iframeBody().find(".cm-content").as("editorContent"); - cy.iframeBody().find(".cm-tooltip-autocomplete").should("be.visible"); -} - -function getEditorView(): Cypress.Chainable { - return cy.get("@editorContent").then(($content) => { - const view = EditorView.findFromDOM($content[0]); - expect(view).not.to.be.null; - return view!; - }); -} - -function acceptSelectedCompletion(): Cypress.Chainable { - cy.wait(200); - return getEditorView() - .then((view) => { - const frameWindow = view.contentDOM.ownerDocument.defaultView; - expect(frameWindow).not.to.be.null; - view.contentDOM.dispatchEvent( - new frameWindow!.KeyboardEvent("keydown", { - key: "Enter", - code: "Enter", - bubbles: true, - cancelable: true, - }), - ); - return view.state.selection.main.assoc; - }) - .then((assoc) => - cy - .iframeBody() - .find(".cm-tooltip-autocomplete") - .should("not.exist") - .then(() => assoc), - ); -} - -describe("Key format editor", () => { - it("preserves cursor association after completion", () => { - openEditor(); - acceptSelectedCompletion().should("equal", -1); - }); - - it("opens completion when a placeholder badge is clicked", () => { - openEditor(); - acceptSelectedCompletion(); - - cy.iframeBody().find(".placeholder-widget").click(); - cy.iframeBody().find(".cm-tooltip-autocomplete").should("be.visible"); - }); - - it("keeps separators between inserted placeholders", () => { - openEditor(); - acceptSelectedCompletion(); - - getEditorView().then((view) => { - const position = view.state.selection.main.head; - const insert = ".component"; - view.dispatch({ - changes: { from: position, insert }, - selection: { anchor: position + insert.length }, - userEvent: "input.type", - }); - }); - cy.iframeBody().find(".cm-tooltip-autocomplete").should("be.visible"); - acceptSelectedCompletion(); - - getEditorView() - .its("state.doc") - .invoke("toString") - .should("equal", "{artboard}.{component}"); - cy.get("@editorContent").should("have.text", "artboard.component"); - }); -}); diff --git a/cypress/e2e/language.cy.ts b/cypress/e2e/language.cy.ts deleted file mode 100644 index 3474e90b..00000000 --- a/cypress/e2e/language.cy.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createTestNode, SIGNED_IN } from "@/web/urlConfig"; -import { visitWithState } from "../common/tools"; - -describe("Change language", () => { - it("switches language", () => { - const nodes = [ - createTestNode({ - text: "On the road", - key: "on-the-road-title", - connected: true, - }), - ]; - visitWithState({ - config: SIGNED_IN, - selectedNodes: [], - allNodes: nodes, - }); - - cy.frameLoaded("#plugin_iframe"); - cy.wait(100); - cy.iframe().findDcy("index_language_select").select("de"); - - cy.iframe().contains("This action will replace translations in 1 text(s)"); - cy.iframe().findDcy("pull_submit_button").should("exist").click(); - }); -}); diff --git a/cypress/e2e/pageSetup.cy.ts b/cypress/e2e/pageSetup.cy.ts deleted file mode 100644 index 76fa5bb4..00000000 --- a/cypress/e2e/pageSetup.cy.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NEW_PAGE } from "@/web/urlConfig"; -import { visitWithState } from "../common/tools"; - -describe("Page setup", () => { - it("shows form with language", () => { - visitWithState({ - config: NEW_PAGE, - }); - - cy.iframe().contains("Page setup").should("be.visible"); - - cy.iframe() - .findDcy("page_setup_button_save") - .should("be.visible") - .should("be.disabled"); - - cy.iframe() - .findDcy("page_setup_input_language") - .should("be.visible") - .select("cs"); - - cy.iframe().findDcy("page_setup_button_save").should("be.visible").click(); - - cy.iframe().contains("Select texts for translation").should("be.visible"); - }); -}); diff --git a/cypress/e2e/pull.cy.ts b/cypress/e2e/pull.cy.ts deleted file mode 100644 index 1d6283a6..00000000 --- a/cypress/e2e/pull.cy.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { createTestNode, SIGNED_IN } from "@/web/urlConfig"; -import { visitWithState } from "../common/tools"; - -describe("Pull", () => { - it("everything up to date", () => { - const nodes = [ - createTestNode({ - text: "On the road", - key: "on-the-road-title", - connected: true, - }), - ]; - visitWithState({ - config: SIGNED_IN, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.frameLoaded("#plugin_iframe"); - cy.iframe().contains("On the road").should("exist"); - cy.iframe().findDcy("index_pull_button").click(); - - cy.iframe().contains("Everything up to date").should("be.visible"); - cy.iframe().findDcy("pull_ok_button").should("be.visible").click(); - }); - - it("pulling change", () => { - const nodes = [ - createTestNode({ - text: "On the old road", - key: "on-the-road-title", - connected: true, - }), - createTestNode({ - text: "Connected to nonexistant", - key: "nonexistant-key", - connected: true, - }), - ]; - visitWithState({ - config: SIGNED_IN, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.frameLoaded("#plugin_iframe"); - cy.iframe().contains("On the old road").should("exist"); - cy.iframe().findDcy("index_pull_button").click(); - - cy.iframe() - .contains("This action will replace translations in 1") - .should("be.visible"); - cy.iframe().contains("Missing keys").should("be.visible"); - - cy.iframe().contains("nonexistant-key").should("be.visible"); - - cy.iframe().findDcy("pull_submit_button").should("be.visible").click(); - - cy.iframe().contains("On the road").should("exist"); - cy.iframe().contains("Connected to nonexistant").should("exist"); - }); -}); diff --git a/cypress/e2e/push.cy.ts b/cypress/e2e/push.cy.ts deleted file mode 100644 index fa587b48..00000000 --- a/cypress/e2e/push.cy.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { createTestNode, SIGNED_IN } from "@/web/urlConfig"; -import { visitWithState } from "../common/tools"; - -describe("Push", () => { - it("shows differences correctly", () => { - const nodes = [ - createTestNode({ text: "New key", key: "new_key" }), - createTestNode({ text: "Changed key", key: "on-the-road-subtitle" }), - ]; - visitWithState({ - config: SIGNED_IN, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.frameLoaded("#plugin_iframe"); - - cy.iframe().contains("New key").should("exist"); - - cy.iframe().findDcy("index_push_button").should("be.visible").click(); - - cy.iframe().contains("New keys").should("exist"); - - cy.iframe() - .findDcy("changes_new_keys") - .contains("new_key") - .should("be.visible"); - - cy.iframe() - .findDcy("changes_changed_keys") - .contains("on-the-road-subtitle") - .should("be.visible"); - }); - - it("catches conflicts", () => { - const nodes = [ - createTestNode({ text: "First value", key: "new_key" }), - createTestNode({ text: "Second value", key: "new_key" }), - ]; - visitWithState({ - config: SIGNED_IN, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.frameLoaded("#plugin_iframe"); - - cy.iframe().contains("First value").should("exist"); - - cy.iframe().findDcy("index_push_button").should("be.visible").click(); - - cy.iframe() - .contains("There are multiple different translations for single key") - .should("exist"); - }); - - it("pushes screenshot correctly", () => { - const nodes = [ - createTestNode({ text: "On the road", key: "on-the-road-title" }), - ]; - visitWithState({ - config: { ...SIGNED_IN, updateScreenshots: true }, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.frameLoaded("#plugin_iframe"); - - cy.iframe().contains("On the road").should("exist"); - cy.iframe().findDcy("index_push_button").should("be.visible").click(); - - cy.iframe().contains("No changes necessary").should("be.visible"); - cy.iframe().findDcy("push_upload_screenshots_checkbox").should("exist"); - cy.iframe().findDcy("push_submit_button").should("be.visible").click(); - - cy.iframe() - .contains("Successfully updated 0 key(s) and uploaded 1 screenshot(s).") - .should("be.visible"); - cy.iframe().findDcy("push_ok_button").should("be.visible").click(); - }); - - it("shows correct key count in success message after pushing new keys", () => { - const nodes = [ - createTestNode({ text: "New key 1", key: "new_key_1" }), - createTestNode({ text: "New key 2", key: "new_key_2" }), - ]; - visitWithState({ - config: SIGNED_IN, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.frameLoaded("#plugin_iframe"); - - cy.iframe().contains("New key 1").should("exist"); - cy.iframe().findDcy("index_push_button").should("be.visible").click(); - - // Verify we see the new keys in the diff - cy.iframe().contains("New keys").should("exist"); - cy.iframe() - .findDcy("changes_new_keys") - .contains("new_key_1") - .should("be.visible"); - cy.iframe() - .findDcy("changes_new_keys") - .contains("new_key_2") - .should("be.visible"); - - // Push the changes - cy.iframe().findDcy("push_submit_button").should("be.visible").click(); - - // Verify success message shows correct count (2 keys) - cy.iframe().contains("Successfully updated 2 key(s)").should("be.visible"); - cy.iframe().findDcy("push_ok_button").should("be.visible").click(); - }); - - it("shows correct key count in success message after pushing changed keys", () => { - const nodes = [ - createTestNode({ text: "Changed text 1", key: "on-the-road-subtitle" }), - createTestNode({ text: "Changed text 2", key: "on-the-road-title" }), - ]; - visitWithState({ - config: SIGNED_IN, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.frameLoaded("#plugin_iframe"); - - cy.iframe().contains("Changed text 1").should("exist"); - cy.iframe().findDcy("index_push_button").should("be.visible").click(); - - // Verify we see the changed keys in the diff - cy.iframe().contains("Changed keys").should("exist"); - cy.iframe() - .findDcy("changes_changed_keys") - .contains("on-the-road-subtitle") - .should("be.visible"); - cy.iframe() - .findDcy("changes_changed_keys") - .contains("on-the-road-title") - .should("be.visible"); - - // Push the changes - cy.iframe().findDcy("push_submit_button").should("be.visible").click(); - - // Verify success message shows correct count (2 keys) - cy.iframe().contains("Successfully updated 2 key(s)").should("be.visible"); - cy.iframe().findDcy("push_ok_button").should("be.visible").click(); - }); - - it("deduplicates screenshot entries when a key has multiple nodes in a frame", () => { - // Two unconnected text nodes that share the same key+characters end up in - // the same frame screenshot's `keys` list. The push payload must list - // that screenshot only once for the key (with both positions), not once - // per node — otherwise the API receives duplicate KeyScreenshotDto - // objects with the same uploadedImageId. - // Unique key per run so this test stays idempotent against the docker - // server state that survives across cypress runs. - const dedupKey = `dedup_save_${Date.now()}`; - const nodes = [ - createTestNode({ text: "Save", key: dedupKey }), - createTestNode({ text: "Save", key: dedupKey }), - ]; - visitWithState({ - config: { ...SIGNED_IN, updateScreenshots: true }, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.intercept("POST", "**/v2/projects/single-step-import-resolvable").as( - "pushKeys", - ); - - cy.iframe().findDcy("index_push_button").should("be.visible").click(); - // Wait for the diff view to actually render before reaching for the - // submit button — without this Cypress can latch onto a stale iframe - // body and the visibility check throws getBoundingClientRect. - cy.iframe().contains("New keys").should("be.visible"); - cy.iframe().findDcy("push_submit_button").should("be.visible").click(); - - cy.wait("@pushKeys").then((interception) => { - const body = interception.request.body as { - keys: Array<{ - name: string; - screenshots: Array<{ - uploadedImageId: number; - positions: Array<{ x: number; y: number }>; - }>; - }>; - }; - const target = body.keys.find((k) => k.name === dedupKey); - expect(target, "key in payload").to.exist; - // Single screenshot reference for the key, not one per node. - expect(target!.screenshots).to.have.length(1); - // Both node positions are reported under that single screenshot. - expect(target!.screenshots[0].positions).to.have.length(2); - }); - - cy.iframe().contains("Successfully updated").should("be.visible"); - }); - - it("shows no changes when re-opening Push immediately after a successful push", () => { - // Regression test for the race where the fire-and-forget connectNodes - // mutation hadn't completed by the time the user navigated back into - // Push, so the next Push view recomputed the diff against stale local - // node data and re-displayed the just-pushed changes. - const uniqueKey = `race_${Date.now()}`; - const nodes = [createTestNode({ text: "Hello race", key: uniqueKey })]; - visitWithState({ - config: SIGNED_IN, - selectedNodes: nodes, - allNodes: nodes, - }); - - // First push: shows the new key as a change. - cy.iframe().findDcy("index_push_button").should("be.visible").click(); - // Wait for the diff view before reaching for the contained key. - cy.iframe().contains("New keys").should("be.visible"); - cy.iframe() - .findDcy("changes_new_keys") - .contains(uniqueKey) - .should("be.visible"); - cy.iframe().findDcy("push_submit_button").should("be.visible").click(); - - cy.iframe().contains("Successfully updated 1 key(s)").should("be.visible"); - cy.iframe().findDcy("push_ok_button").should("be.visible").click(); - - // Back at Index, push again right away — should be a no-op now. - cy.iframe().contains("Hello race").should("be.visible"); - cy.iframe().findDcy("index_push_button").should("be.visible").click(); - cy.iframe().contains("No changes necessary").should("be.visible"); - }); - - it("doesn't push screenshot when disabled", () => { - const nodes = [ - createTestNode({ text: "Changed text 2", key: "on-the-road-title" }), - ]; - visitWithState({ - config: { ...SIGNED_IN, updateScreenshots: false }, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.frameLoaded("#plugin_iframe"); - - cy.iframe().contains("Changed text 2").should("exist"); - cy.iframe().findDcy("index_push_button").should("be.visible").click(); - - cy.iframe().contains("No changes necessary").should("be.visible"); - - cy.iframe().findDcy("push_finish_button").should("be.visible").click(); - }); -}); diff --git a/cypress/e2e/settings.cy.ts b/cypress/e2e/settings.cy.ts deleted file mode 100644 index a29ab19c..00000000 --- a/cypress/e2e/settings.cy.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { createTestNode, DEFAULT_CREDENTIALS } from "@/web/urlConfig"; -import { visitWithState } from "../common/tools"; - -describe("Settings", () => { - it("hides navigation", () => { - visitWithState({ - config: {}, - }); - - cy.iframe().findDcy("top_bar_back_button").should("not.exist"); - cy.iframe().findDcy("settings_button_close").should("not.exist"); - }); - - it("works when filling empty", () => { - visitWithState({}); - - cy.iframe() - .findDcy("settings_input_api_url") - .type(DEFAULT_CREDENTIALS.apiUrl); - - cy.iframe() - .findDcy("settings_input_api_key") - .type(DEFAULT_CREDENTIALS.apiKey); - - cy.iframe().findDcy("settings_button_validate").click(); - - cy.iframe() - .contains("examples was successfully connected") - .should("be.visible"); - - cy.iframe().contains("Current language").should("be.visible"); - - cy.iframe() - .findDcy("settings_input_language") - .should("be.visible") - .select("en"); - - cy.iframe().findDcy("settings_button_save").click(); - }); - - it("works when filling existing", () => { - visitWithState({ - config: { - apiUrl: "invalid", - apiKey: undefined, - language: "cs", - namespace: "", - documentInfo: true, - pageInfo: true, - }, - }); - - cy.iframe().findDcy("index_settings_button").should("be.visible").click(); - cy.iframe().contains("Settings").should("be.visible"); - - cy.iframe().findDcy("settings_expandable_project").should("be.visible"); - - cy.iframe() - .findDcy("settings_input_api_url") - .type(DEFAULT_CREDENTIALS.apiUrl); - cy.iframe() - .findDcy("settings_input_api_key") - .type(DEFAULT_CREDENTIALS.apiKey); - - cy.iframe().findDcy("settings_button_validate").click(); - - cy.iframe() - .contains("examples was successfully connected") - .should("be.visible"); - cy.iframe().contains("Current language").should("be.visible"); - - cy.iframe() - .findDcy("settings_input_language") - .should("be.visible") - .select("en"); - - cy.iframe().findDcy("settings_button_save").click(); - - cy.contains("Select texts for translation").should("be.visible"); - }); - - it("tests strings settings configuration", () => { - visitWithState({ - config: { - apiUrl: DEFAULT_CREDENTIALS.apiUrl, - apiKey: DEFAULT_CREDENTIALS.apiKey, - language: "en", - namespace: "", - documentInfo: true, - pageInfo: true, - }, - }); - - cy.iframe().findDcy("index_settings_button").should("be.visible").click(); - cy.iframe().contains("Settings").should("be.visible"); - - cy.iframe().findDcy("settings_expandable_strings").should("exist").click(); - cy.iframe().findDcy("settings_checkbox_prefill_key_name").should("exist"); - cy.iframe().findDcy("settings_checkbox_prefill_key_name").click(); - - // Test key format editor - check if it exists first - cy.iframe().findDcy("global-editor").should("exist"); - - // Test variable casing dropdown - only if it's visible - cy.iframe().findDcy("settings_dropdown_variable_casing").should("exist"); - - // Verify preview exists - cy.iframe().findDcy("settings_text_preview").should("not.exist"); - - cy.iframe().findDcy("global-editor").click(); - cy.wait(150); - cy.iframe().findDcy("global-editor").type("{enter}"); - cy.iframe().findDcy("settings_text_preview").should("exist"); - - // Verify My Artboard is in the preview - cy.iframe() - .findDcy("settings_text_preview") - .should("contain", "My Artboard"); - - // Test ignore settings - only if they're visible - cy.iframe().findDcy("settings_checkbox_ignore_text_layers").should("exist"); - - // Test ignore prefix input - only if it's visible - cy.iframe().findDcy("settings_input_ignore_prefix").should("exist"); - - // Save settings - cy.iframe().findDcy("settings_button_save").click(); - cy.contains("Select texts for translation").should("be.visible"); - }); - - it("clears prefilled keys for unconnected nodes when prefill is toggled off", () => { - const nodes = [ - createTestNode({ text: "Unconnected node", key: "prefilled_key" }), - ]; - visitWithState({ - config: { - apiUrl: DEFAULT_CREDENTIALS.apiUrl, - apiKey: DEFAULT_CREDENTIALS.apiKey, - language: "en", - namespace: "", - documentInfo: true, - pageInfo: true, - prefillKeyFormat: true, - }, - selectedNodes: nodes, - allNodes: nodes, - }); - - // Sanity check: the previously prefilled key is shown. - cy.iframe() - .findDcy("index_unconnected_key_input") - .should("have.value", "prefilled_key"); - - // Open settings and toggle prefill off. - cy.iframe().findDcy("index_settings_button").should("be.visible").click(); - cy.iframe().findDcy("settings_expandable_strings").should("exist").click(); - cy.iframe().findDcy("settings_checkbox_prefill_key_name").click(); - - // Save and return to Index. - cy.iframe().findDcy("settings_button_save").click(); - cy.iframe().contains("Unconnected node").should("be.visible"); - - // The prefilled key has been cleared. - cy.iframe().findDcy("index_unconnected_key_input").should("have.value", ""); - }); - - it("preserves connected node keys when prefill is toggled off", () => { - const nodes = [ - createTestNode({ text: "Unconnected node", key: "prefilled_key" }), - createTestNode({ - text: "Connected node", - key: "connected_key", - connected: true, - }), - ]; - visitWithState({ - config: { - apiUrl: DEFAULT_CREDENTIALS.apiUrl, - apiKey: DEFAULT_CREDENTIALS.apiKey, - language: "en", - namespace: "", - documentInfo: true, - pageInfo: true, - prefillKeyFormat: true, - }, - selectedNodes: nodes, - allNodes: nodes, - }); - - // Both nodes are visible up front, with their respective keys. - cy.iframe() - .findDcy("index_unconnected_key_input") - .should("have.value", "prefilled_key"); - cy.iframe() - .findDcy("general_node_list_row_key") - .contains("connected_key") - .should("be.visible"); - - // Toggle prefill off. - cy.iframe().findDcy("index_settings_button").should("be.visible").click(); - cy.iframe().findDcy("settings_expandable_strings").should("exist").click(); - cy.iframe().findDcy("settings_checkbox_prefill_key_name").click(); - cy.iframe().findDcy("settings_button_save").click(); - cy.iframe().contains("Connected node").should("be.visible"); - - // Unconnected key cleared, connected key untouched. - cy.iframe().findDcy("index_unconnected_key_input").should("have.value", ""); - cy.iframe() - .findDcy("general_node_list_row_key") - .contains("connected_key") - .should("be.visible"); - }); - - it("tests push settings configuration", () => { - visitWithState({ - config: { - apiUrl: DEFAULT_CREDENTIALS.apiUrl, - apiKey: DEFAULT_CREDENTIALS.apiKey, - language: "en", - namespace: "", - documentInfo: true, - pageInfo: true, - }, - }); - cy.iframe().findDcy("index_settings_button").should("be.visible").click(); - cy.iframe().contains("Settings").should("be.visible"); - - cy.iframe().findDcy("settings_expandable_push").should("exist").click(); - - // Test update screenshots checkbox - cy.iframe().findDcy("settings_checkbox_update_screenshots").should("exist"); - - // Test add tags checkbox - cy.iframe().findDcy("settings_checkbox_add_tags").should("exist"); - - // Save settings - cy.iframe().findDcy("settings_button_save").click(); - cy.contains("Select texts for translation").should("be.visible"); - }); -}); diff --git a/cypress/e2e/stringDetails.cy.ts b/cypress/e2e/stringDetails.cy.ts deleted file mode 100644 index 78751ca3..00000000 --- a/cypress/e2e/stringDetails.cy.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { - createTestNode, - PAGE_STRING_DETAILS, - SIGNED_IN, -} from "@/web/urlConfig"; -import { visitWithState } from "../common/tools"; - -describe("String details", () => { - it("should get there from index page", () => { - const nodes = [ - createTestNode({ - text: "Test node", - key: "test_key", - ns: "test_ns", - connected: true, - }), - ]; - visitWithState({ - config: { ...SIGNED_IN }, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframe().contains("test_key").should("be.visible"); - - cy.iframe().findDcy("key_options_button").should("be.visible").click(); - - cy.iframe().findDcy("dropdown").should("be.visible"); - - cy.iframe().findDcy("string_details_cy").should("be.visible"); - }); - - it("should show the correct preview text", () => { - const nodes = [ - createTestNode({ - text: "Test node", - key: "test_key", - ns: "test_ns", - connected: true, - }), - ]; - visitWithState({ - config: { ...PAGE_STRING_DETAILS, nodeInfo: nodes[0] }, - selectedNodes: nodes, - allNodes: nodes, - }); - - // Should show the key as preview text - cy.iframe() - .findDcy("string_details_preview_text") - .should("be.visible") - .contains("Test node"); - }); - - it("should react to key changes", async () => { - const nodes = [ - createTestNode({ - text: "Test node", - key: "test_key", - ns: "test_ns", - connected: true, - }), - ]; - visitWithState({ - config: { ...PAGE_STRING_DETAILS, nodeInfo: nodes[0] }, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframeBody() - .find('[data-cy="string_details_input_key"]') - .should("have.value", "test_key"); - - cy.iframeBody() - .find('[data-cy="string_details_input_key"]') - .type("new_key"); - - cy.iframeBody() - .find('[data-cy="string_details_input_key"]') - .should("have.value", "new_key"); - - cy.iframeBody().find('[data-cy="tooltip"]').click(); - - cy.iframeBody() - .contains("You can use basic HTML tags") - .should("be.visible"); - - cy.wait(500); - }); - - it("should show negative warning on missmatched translation", () => { - const nodes = [ - createTestNode({ - text: "Test node", - key: "test_key", - ns: "test_ns", - isPlural: true, - translation: "Test translation", - connected: true, - }), - ]; - visitWithState({ - config: { ...PAGE_STRING_DETAILS, nodeInfo: nodes[0] }, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframe().contains("Advanced text format detected").should("be.visible"); - - cy.iframe().find('[class^="_warningContainer_"]').should("be.visible"); - }); - - it("should show positive warning on plural keys", () => { - const nodes = [ - createTestNode({ - text: "Test node", - key: "test_key", - ns: "test_ns", - isPlural: true, - translation: "Test node", - connected: true, - }), - ]; - visitWithState({ - config: { ...PAGE_STRING_DETAILS, nodeInfo: nodes[0] }, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframe().contains("Advanced text format detected").should("be.visible"); - - cy.iframe() - .find('[class^="_warningNoticeContainer_"]') - .should("be.visible"); - }); - - it("should not show warning on simple keys", () => { - const nodes = [ - createTestNode({ - text: "Test node", - key: "test_key", - ns: "test_ns", - isPlural: false, - translation: "Test", - connected: true, - }), - ]; - visitWithState({ - config: { ...PAGE_STRING_DETAILS, nodeInfo: nodes[0] }, - selectedNodes: nodes, - allNodes: nodes, - }); - - cy.iframeBody() - .contains("Advanced text format detected") - .should("not.exist"); - }); -}); diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json deleted file mode 100644 index 02e42543..00000000 --- a/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts deleted file mode 100644 index ce1e452e..00000000 --- a/cypress/support/commands.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable @typescript-eslint/no-namespace */ -import "cypress-iframe"; - -Cypress.Commands.add( - "closestDcy", - { prevSubject: true }, - (subject: Cypress.Chainable, dataCy) => { - return subject.closest(`[data-cy="${dataCy}"]`); - }, -); - -Cypress.Commands.add("gcy", (dataCy) => { - return cy.get(`[data-cy="${dataCy}"]`); -}); - -Cypress.Commands.add( - "findDcy", - { prevSubject: true }, - (subject: Cypress.Chainable, dataCy) => { - return subject.find(`[data-cy="${dataCy}"]`, { timeout: 30000 }); - }, -); - -declare global { - namespace Cypress { - interface Chainable { - /** - * Custom command to get the document of the plugin iframe. - * @example cy.iframeDocument().should('exist') - */ - iframeDocument(): Chainable; - /** - * Custom command to get the body of the plugin iframe. - * @example cy.iframeBody().find('button').click() - */ - iframeBody(): Chainable>; - } - } -} - -Cypress.Commands.add("iframeDocument", () => { - return cy - .get('iframe[data-cy="plugin_iframe"]') - .its("0.contentDocument") - .should("exist"); -}); - -Cypress.Commands.add("iframeBody", () => { - return cy - .iframeDocument() - .its("body") - .should("not.be.undefined") - .then((e) => cy.wrap(e as HTMLBodyElement)); -}); - -// Export {} to ensure this file is treated as a module by TypeScript. -export {}; diff --git a/cypress/support/custom.d.ts b/cypress/support/custom.d.ts deleted file mode 100644 index 0ebec2b3..00000000 --- a/cypress/support/custom.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare namespace Cypress { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Chainable { - closestDcy(dataCy: string): Chainable; - - gcy(dataCy: string): Chainable; - - findDcy(dataCy: string): Chainable; - } -} diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts deleted file mode 100644 index db9b0358..00000000 --- a/cypress/support/e2e.ts +++ /dev/null @@ -1,26 +0,0 @@ -// *********************************************************** -// This example support/e2e.ts is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Alternatively you can use CommonJS syntax: -// require('./commands') -import "./commands"; - -Cypress.on("uncaught:exception", () => { - // returning false here prevents Cypress from - // failing the test - return false; -}); - -export default {}; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json deleted file mode 100644 index c6d24589..00000000 --- a/cypress/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "types": ["cypress"] - }, - "include": ["."] -} diff --git a/delivery/INSTALL.md b/delivery/INSTALL.md new file mode 100644 index 00000000..6f5aed43 --- /dev/null +++ b/delivery/INSTALL.md @@ -0,0 +1,82 @@ +# Tolgee Figma Plugin — Preview build + +Thanks for helping test the new version of the Tolgee Figma plugin! This is a +private preview build. You install it locally — it is **not** published to the +Figma Community and only appears on your own machine. + +## Requirements + +- **Figma desktop app** (Windows or macOS). Importing a local plugin is only + possible in the desktop app, not in the browser. + Download: https://www.figma.com/downloads/ +- A Tolgee account and a **Project API key** (see step 4). + +## 1. Unzip + +Unzip `tolgee-plugin.zip` somewhere permanent (e.g. `Documents/tolgee-plugin`). +**Keep the folder** — Figma loads the plugin from these files every time, so +don't delete or move them after importing. + +The folder contains: `manifest.json`, `main.js`, `ui.html`, `ui-inspect.html`. + +## 2. Import into Figma + +1. Open the **Figma desktop app**. +2. Open any file (or create a new one). +3. Top menu → **Plugins → Development → Import plugin from manifest…** + (Or right‑click canvas → **Plugins → Development → Import plugin from manifest…**) +4. Select the **`manifest.json`** file from the unzipped folder. + +The plugin now appears under **Plugins → Development → Tolgee**. + +## 3. ⚠️ Test on a copy first + +For your first run, please test on a **duplicate** of a file, not a critical +production file: + +- In the Figma file browser, right‑click your file → **Duplicate**, and work in + the copy. + +Why: this preview shares plugin data with the regular Tolgee plugin, so your +existing key connections show up automatically (great for testing) — but until +you've confirmed everything works for you, it's safest to try it on a copy. + +## 4. Connect your Tolgee project + +1. Run the plugin: **Plugins → Development → Tolgee**. +2. Open **Settings / Connect** and paste your **Project API key**. + - In Tolgee: your project → **Integrate** / **Developer settings → API keys** + → create a key with the scopes the plugin asks for. + - Self‑hosted Tolgee? Also set your instance URL in the plugin settings. +3. That's it — your keys and translations load from your project. + +Your API key is stored **only on your machine** (Figma client storage) and is +never shared with anyone. + +## 5. Using it + +- Select a text layer → connect it to an existing Tolgee key, or create a new one. +- Pull to update the canvas from Tolgee; Push to send changes back. +- Dev Mode: the plugin also works in Figma **Dev Mode** (inspect panel). + +## Updating to a newer build + +When you get a new zip: + +1. Replace the old files with the new ones (same folder is fine). +2. In Figma: **Plugins → Development**, right‑click **Tolgee → Remove**, then + re‑import via **Import plugin from manifest…** (step 2). + (If you kept the same folder path, Figma often just picks up the new files on + next run — re‑import only if something looks stale.) + +## Troubleshooting + +- **"Import plugin from manifest" is missing** → you're in the browser. Use the + **desktop app**. +- **Plugin loads but can't reach Tolgee** → check the API key and, for + self‑hosted, the instance URL in settings. +- **Nothing happens / errors** → open the plugin's console: + **Plugins → Development → Open console**, and send us the messages. + +Questions or bugs — just reply to us with a screenshot and, if possible, the +console output. Thank you! 🙏 diff --git a/delivery/tolgee-plugin.zip b/delivery/tolgee-plugin.zip new file mode 100644 index 00000000..f364d24f Binary files /dev/null and b/delivery/tolgee-plugin.zip differ diff --git a/delivery/tolgee-plugin/INSTALL.md b/delivery/tolgee-plugin/INSTALL.md new file mode 100644 index 00000000..30520e42 --- /dev/null +++ b/delivery/tolgee-plugin/INSTALL.md @@ -0,0 +1,82 @@ +# Tolgee Figma Plugin — Preview build + +Thanks for helping test the new version of the Tolgee Figma plugin! This is a +private preview build. You install it locally — it is **not** published to the +Figma Community and only appears on your own machine. + +## Requirements + +- **Figma desktop app** (Windows or macOS). Importing a local plugin is only + possible in the desktop app, not in the browser. + Download: https://www.figma.com/downloads/ +- A Tolgee account and a **Project API key** (see step 4). + +## 1. Unzip + +Unzip `tolgee-plugin.zip` somewhere permanent (e.g. `Documents/tolgee-plugin`). +**Keep the folder** — Figma loads the plugin from these files every time, so +don't delete or move them after importing. + +The folder contains: `manifest.json`, `main.js`, `ui.html`, and this `INSTALL.md`. + +## 2. Import into Figma + +1. Open the **Figma desktop app**. +2. Open any file (or create a new one). +3. Top menu → **Plugins → Development → Import plugin from manifest…** + (Or right‑click canvas → **Plugins → Development → Import plugin from manifest…**) +4. Select the **`manifest.json`** file from the unzipped folder. + +The plugin now appears under **Plugins → Development → Tolgee**. + +## 3. ⚠️ Test on a copy first + +For your first run, please test on a **duplicate** of a file, not a critical +production file: + +- In the Figma file browser, right‑click your file → **Duplicate**, and work in + the copy. + +Why: this preview shares plugin data with the regular Tolgee plugin, so your +existing key connections show up automatically (great for testing) — but until +you've confirmed everything works for you, it's safest to try it on a copy. + +## 4. Connect your Tolgee project + +1. Run the plugin: **Plugins → Development → Tolgee**. +2. Open **Settings / Connect** and paste your **Project API key**. + - In Tolgee: your project → **Integrate** / **Developer settings → API keys** + → create a key with the scopes the plugin asks for. + - Self‑hosted Tolgee? Also set your instance URL in the plugin settings. +3. That's it — your keys and translations load from your project. + +Your API key is stored **only on your machine** (Figma client storage) and is +never shared with anyone. + +## 5. Using it + +- Select a text layer → connect it to an existing Tolgee key, or create a new one. +- Pull to update the canvas from Tolgee; Push to send changes back. +- Dev Mode: the plugin also works in Figma **Dev Mode** (inspect panel). + +## Updating to a newer build + +When you get a new zip: + +1. Replace the old files with the new ones (same folder is fine). +2. In Figma: **Plugins → Development**, right‑click **Tolgee → Remove**, then + re‑import via **Import plugin from manifest…** (step 2). + (If you kept the same folder path, Figma often just picks up the new files on + next run — re‑import only if something looks stale.) + +## Troubleshooting + +- **"Import plugin from manifest" is missing** → you're in the browser. Use the + **desktop app**. +- **Plugin loads but can't reach Tolgee** → check the API key and, for + self‑hosted, the instance URL in settings. +- **Nothing happens / errors** → open the plugin's console: + **Plugins → Development → Open console**, and send us the messages. + +Questions or bugs — just reply to us with a screenshot and, if possible, the +console output. Thank you! 🙏 diff --git a/delivery/tolgee-plugin/main.js b/delivery/tolgee-plugin/main.js new file mode 100644 index 00000000..ee436d8f --- /dev/null +++ b/delivery/tolgee-plugin/main.js @@ -0,0 +1,2 @@ +"use strict";var et=Object.defineProperty,tt=Object.defineProperties;var nt=Object.getOwnPropertyDescriptors;var G=Object.getOwnPropertySymbols;var re=Object.prototype.hasOwnProperty,ie=Object.prototype.propertyIsEnumerable;var ae=(e,t,n)=>t in e?et(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,y=(e,t)=>{for(var n in t||(t={}))re.call(t,n)&&ae(e,n,t[n]);if(G)for(var n of G(t))ie.call(t,n)&&ae(e,n,t[n]);return e},B=(e,t)=>tt(e,nt(t));var se=(e,t)=>{var n={};for(var o in e)re.call(e,o)&&t.indexOf(o)<0&&(n[o]=e[o]);if(e!=null&&G)for(var o of G(e))t.indexOf(o)<0&&ie.call(e,o)&&(n[o]=e[o]);return n};var ce={"apply-translations":"canvas","create-copy":"canvas","save-config":"metadata","persist-project-id":"metadata",reset:"metadata","set-language":"metadata","set-branch":"metadata","set-nodes-data":"metadata","request-page-connected-nodes":"read","resolve-parent-names":"read","request-screenshots":"read","scroll-to-node":"read","request-copy-staleness":"read","ui-ready":"ui",resize:"ui",close:"ui",notify:"ui","open-external":"ui"},le=new Set(["read","metadata","ui"]);var de={};function p(e){figma.ui.postMessage(e)}function P(e,t){de[e]=t}function fe(){figma.ui.onmessage=async e=>{if(figma.editorType==="dev"&&!le.has(ce[e.type])){figma.notify("Not available in Dev Mode"),console.warn("[tolgee:main] blocked canvas message in Dev Mode:",e.type);return}let t=de[e.type];if(t)try{await t(e)}catch(n){console.error("[tolgee:main] handler failed",e.type,n),figma.notify("Something went wrong. Please try again."),p({type:"handler-error",forType:e.type,correlationId:"correlationId"in e&&typeof e.correlationId=="string"?e.correlationId:void 0})}else console.warn("[tolgee:main] unhandled message",e.type)}}var w="tolgee_config",T="tolgee_info";var X={MINIMUM:{width:400,height:160},DEFAULT:{width:500,height:600},EXPANDED:{width:500,height:800}};var ot=/^\d+$/,at=/^[\d\s.,+\-]+$/,E=(e,t,n,o=e.characters)=>{var i,s,c;if(o.trim().length===0||((i=n.ignoreNumbers)==null||i)&&(n.ignoreFormattedNumbers?at:ot).test(o))return!0;let a=(s=n.ignorePrefix)!=null?s:"_";if(n.ignoreTextLayers&&a&&e.name.startsWith(a))return!0;let r=(c=n.ignoreHiddenLayers)!=null?c:!0;return!!(r&&!e.visible||r&&n.ignoreHiddenLayersIncludingChildren&&t)};var ge=e=>{let t=e.getPluginData(T);if(!t)return{};try{return JSON.parse(t)}catch(n){return{}}},S=(e,t)=>{var o,a;let n=ge(e);return{id:e.id,name:e.name,characters:t!=null?t:e.characters,visible:e.visible,key:(o=n.key)!=null?o:"",ns:n.ns,translation:(a=n.translation)!=null?a:"",isPlural:!!n.isPlural,pluralParamValue:n.pluralParamValue,paramsValues:n.paramsValues,connected:!!n.connected,prefilledKey:n.prefilledKey}},j=(e,t)=>{var r,i,s,c,g,l;let n=ge(e),o={key:(i=(r=t.key)!=null?r:n.key)!=null?i:"",ns:"ns"in t?t.ns:n.ns,translation:(c=(s=t.translation)!=null?s:n.translation)!=null?c:"",isPlural:!!((g=t.isPlural)!=null?g:n.isPlural),pluralParamValue:"pluralParamValue"in t?t.pluralParamValue:n.pluralParamValue,paramsValues:"paramsValues"in t?t.paramsValues:n.paramsValues,connected:!!((l=t.connected)!=null?l:n.connected),prefilledKey:"prefilledKey"in t?t.prefilledKey:n.prefilledKey},a={};for(let[u,d]of Object.entries(o))d!==void 0&&(a[u]=d);return e.setPluginData(T,JSON.stringify(a)),y({id:e.id,name:e.name,characters:e.characters,visible:e.visible},o)};var ue=async()=>(await figma.currentPage.loadAsync(),figma.currentPage.findAllWithCriteria({types:["TEXT"],pluginData:{keys:[T]}})),rt=50,it=100;function R(e){let t=e.parent;for(;t&&t.type!=="PAGE";){if("visible"in t&&t.visible===!1)return!0;t=t.parent}return!1}async function pe(e,t,n,o){let a=e.length,r=[],i=0;for(let s of e){let c=s.characters,g=n?R(s):!1;E(s,g,t,c)||r.push(S(s,c)),i++,i%rt===0&&(a>it&&(o==null||o(i,a)),await new Promise(l=>setTimeout(l,0)))}return r}var st=200,me=async(e,t,n,o,a)=>{if(o.visited++,!(o.visited%st===0&&(await new Promise(r=>setTimeout(r,0)),a()))){if(e.type==="TEXT"){t.push({node:e,ancestorHidden:n});return}if("children"in e){let r=n||e.visible===!1;for(let i of e.children){if(a())return;await me(i,t,r,o,a)}}}},ye=async(e,t=()=>!1)=>{await figma.currentPage.loadAsync();let n=[];if(!e){let a=0;for(let r of figma.currentPage.selection){if(t())return n;if(r.type==="TEXT")n.push({node:r,ancestorHidden:!1});else if("children"in r){let i=0;for(let s of r.children){if(t())return n;if(s.type==="TEXT")n.push({node:s,ancestorHidden:!1});else if("findAllWithCriteria"in s){let c=s.findAllWithCriteria({types:["TEXT"]});for(let g of c)n.push({node:g,ancestorHidden:!1})}i++,i%10===0&&await new Promise(c=>setTimeout(c,0))}}a++,a%25===0&&await new Promise(i=>setTimeout(i,0))}return n}let o={visited:0};for(let a of figma.currentPage.selection){if(t())return n;await me(a,n,R(a),o,t)}return n};var Pe=/\s*<\/br>||<\/br>/gi;function he(e){return e.replace(/<[^>]*>/g,"")}var ct=["strong","b","em","i","u"];function k(e,t){var a;let n=[],o=new RegExp(`<${t}>(.*?)`,"g");for(let r of e.matchAll(o)){let i=0,s=e.substring(0,r.index);for(let u of ct){let d=new RegExp(``,"g");for(let f of s.matchAll(d))i+=f[0].length;s=s.replace(d,"")}let c=(a=r[1])!=null?a:"",g=r.index-i,l=g+c.length;n.push({start:g,end:l})}return n}var lt=["Bold","Semibold","Semi Bold","Medium","Extra Bold","Heavy","Black","Ultra Bold"],dt=["Italic","Regular Italic","Medium Italic","Light Italic","Semi Bold Italic","Semibold Italic","Bold Italic","Oblique","Regular Oblique"],O=null;function ft(){return O||(O=figma.listAvailableFontsAsync(),O.catch(()=>{O=null})),O}var Ne=new Set;async function b(e){let t=`${e.family}\0${e.style}`;Ne.has(t)||(await figma.loadFontAsync(e),Ne.add(t))}async function gt(e,t){for(let o of lt)if(t.some(r=>r.fontName.family===e&&r.fontName.style===o))try{return await b({family:e,style:o}),o}catch(r){}let n=t.find(o=>o.fontName.family===e&&o.fontName.style.toLowerCase().includes("bold"));if(n)try{return await b(n.fontName),n.fontName.style}catch(o){return null}return null}async function ut(e,t){for(let o of dt)if(t.some(r=>r.fontName.family===e&&r.fontName.style===o))try{return await b({family:e,style:o}),o}catch(r){}let n=t.find(o=>o.fontName.family===e&&(o.fontName.style.toLowerCase().includes("italic")||o.fontName.style.toLowerCase().includes("oblique")));if(n)try{return await b(n.fontName),n.fontName.style}catch(o){return null}return null}function K(e,t){let n=e.characters.length;if(t.start>=n)return null;let o=e.getRangeFontName(t.start,Math.min(n,t.start+1));return o===figma.mixed?null:o}function we(e,t,n){let o=Math.min(e.characters.length,t.end);t.start>=o||e.setRangeFontName(t.start,o,n)}function pt(e,t){let n=Math.min(e.characters.length,t.end);t.start>=n||e.setRangeTextDecoration(t.start,n,"UNDERLINE")}async function U(e,t,n={}){e.autoRename=!1;let o=e.characters.length;if(o===0){let d=e.fontName;d!==figma.mixed&&await b(d)}else{let d=e.getRangeAllFontNames(0,o);await Promise.all(d.map(f=>b(f)))}let a=t.replace(Pe,` +`),r=he(a);if(e.characters=r,n.plainOnly)return;let i=[...k(a,"strong"),...k(a,"b")],s=[...k(a,"em"),...k(a,"i")],c=k(a,"u");if(i.length===0&&s.length===0&&c.length===0)return;let g=await ft(),l=new Map,u=new Map;for(let d of i){let f=K(e,d);!f||l.has(f.family)||l.set(f.family,await gt(f.family,g))}for(let d of s){let f=K(e,d);!f||u.has(f.family)||u.set(f.family,await ut(f.family,g))}for(let d of i){let f=K(e,d);if(!f)continue;let m=l.get(f.family);m&&we(e,d,{family:f.family,style:m})}for(let d of s){let f=K(e,d);if(!f)continue;let m=u.get(f.family);m&&we(e,d,{family:f.family,style:m})}for(let d of c)pt(e,d)}function Ie(e,t){return`${e!=null?e:""}\0${t}`}var Ce=10;async function W(e,t={}){var s;let n=await yt(e.sourcePageId);if(!n)return{ok:!1,createdPageIds:[],error:"The original page no longer exists."};await n.loadAsync();let o=!!(((s=t.ignoreHiddenLayers)==null||s)&&t.ignoreHiddenLayersIncludingChildren),a=[],r=[],i=!1;try{if(e.mode==="keys"){let c=`${n.name} - keys`;i||(i=(await Te({name:c,sourcePageId:n.id},n)).switchedAwayFromCurrent);let g=n.clone();g.name=c,figma.root.appendChild(g),a.push(g.id),await g.loadAsync();let l=g.findAllWithCriteria({types:["TEXT"],pluginData:{keys:[T]}}),u=l.length,d=0;for(let f of l){let m=f.characters,C=o?R(f):!1;if(!E(f,C,t,m)){let h=S(f,m);if(h.connected&&h.key){let N=h.ns&&e.namespacesEnabled?`${h.ns}.${h.key}`:h.key;await ht(f,N,{plainOnly:!0})||r.push(f.name)}}d++,d%Ce===0&&(p({type:"create-copy-progress",correlationId:e.correlationId,current:d,total:u,phase:"writing-keys"}),await J())}xe(g,n.id),i&&await figma.setCurrentPageAsync(g)}else{let c=e.languages.length,g=[];for(let l=0;l{if(i===figma.currentPage){let s=t!=null?t:figma.root.children.find(c=>c!==i&&c.type==="PAGE");s&&(await s.loadAsync(),await figma.setCurrentPageAsync(s),n=!0)}i.remove(),o=!0},r=[];for(let i of[...figma.root.children])if(i.type==="PAGE"){if(i.name!==e.name){r.push(i);continue}await i.loadAsync(),Ee(i)!==null&&await a(i)}if(o)return{switchedAwayFromCurrent:n};for(let i of r)await i.loadAsync(),Pt(i,e)&&await a(i);return{switchedAwayFromCurrent:n}}function xe(e,t,n){let o={pageCopy:!0,pageInfo:!0,sourcePageId:t};n&&(o.language=n,o.copyLanguage=n),e.setPluginData(w,JSON.stringify(o))}async function ht(e,t,n){return e.hasMissingFont?!1:(await U(e,t,n),!0)}function J(){return new Promise(e=>setTimeout(e,0))}async function be(e){var c,g;let t=e.getPluginData(w),n;try{n=t?JSON.parse(t).sourcePageId:void 0}catch(l){return{ok:!1,error:"Could not read this copy's plugin data."}}if(!n)return{ok:!1,error:"This copy predates staleness tracking."};let o=await figma.getNodeByIdAsync(n);if(!o||o.type!=="PAGE")return{ok:!1,error:"The original page no longer exists."};await o.loadAsync(),await e.loadAsync();let a=await Se(o),r=await Se(e),i=0;for(let[l,u]of a)i+=Math.max(0,u-((c=r.get(l))!=null?c:0));let s=0;for(let[l,u]of r)s+=Math.max(0,u-((g=a.get(l))!=null?g:0));return{ok:!0,missingCount:i,removedCount:s}}async function Se(e){var a;let t=e.findAllWithCriteria({types:["TEXT"],pluginData:{keys:[T]}}),n=new Map,o=0;for(let r of t){let i=S(r);if(i.connected&&i.key){let s=Ie(i.ns,i.key);n.set(s,((a=n.get(s))!=null?a:0)+1)}o++,o%100===0&&await J()}return n}function H(e){return/\{(component|instance|frame|artboard|section|group)\}/.test(e!=null?e:"")}var Nt=["language","pageInfo","pageCopy","pageStringDetails","nodeInfo","sourcePageId","copyLanguage"],Ae=["apiKey","apiUrl","ignorePrefix","ignoreNumbers","ignoreFormattedNumbers"],wt=new Set(Nt),It=new Set(Ae);function _e(e,t,n){return y(y(y({},e),t),n)}function ve(e){let t={},n={},o={};for(let a of Object.keys(e)){let r=e[a];r!==void 0&&(wt.has(a)?o[a]=r:(It.has(a)&&(t[a]=r),n[a]=r))}return{global:t,doc:n,page:o}}function q(e){let t={};for(let n of Ae){let o=e[n];o!==void 0&&(t[n]=o)}return t}function z(e,t){if(e==null||e==="")return{};if(typeof e=="object")return e;if(typeof e!="string")return console.warn(`[tolgee] ignoring non-string ${t} settings of type ${typeof e}`),{};try{let n=JSON.parse(e);return n&&typeof n=="object"?n:(console.warn(`[tolgee] ${t} settings parsed to a non-object, resetting to defaults`),{})}catch(n){return console.warn(`[tolgee] failed to parse stored ${t} settings, resetting to defaults`,n),{}}}function Z(e){for(let t in e)return!1;return!0}async function Q(){let e=await figma.clientStorage.getAsync(w);return z(e,"global")}async function Re(e){if(Z(e)){await figma.clientStorage.deleteAsync(w);return}await figma.clientStorage.setAsync(w,JSON.stringify(e))}async function ke(){await figma.clientStorage.deleteAsync(w)}function ee(){let e=figma.root.getPluginData(w);return z(e,"document")}function te(e){if(Z(e)){figma.root.setPluginData(w,"");return}figma.root.setPluginData(w,JSON.stringify(e))}function ne(e){let t=e.getPluginData(w);return z(t,"page")}function oe(e,t){if(Z(t)){e.setPluginData(w,"");return}e.setPluginData(w,JSON.stringify(t))}var Oe=!0,A=null;function L(){A=null}async function x(){return A||(A=Ct(),A.catch(()=>{A=null})),A}async function Ct(){Oe&&await figma.currentPage.loadAsync();let e=q(await Q()),t=ee(),n=ne(figma.currentPage);return _e(e,t,n)}async function M(e){L();let t=ve(e);Oe&&await figma.currentPage.loadAsync();let n=await Q(),o=ee(),a=ne(figma.currentPage),r=q(y(y({},n),t.global)),i=y(y({},o),t.doc),s=y(y({},a),t.page);await Re(r),te(i),oe(figma.currentPage,s),L()}async function Le(){L(),await ke(),te({});for(let e of figma.root.children)e.type==="PAGE"&&(await e.loadAsync(),oe(e,{}))}async function Tt(e,t){let n=t==null?void 0:t.get(e.id);if(n)return n;let o=e.getMainComponentAsync().then(a=>{var r;if(a)return((r=a.parent)==null?void 0:r.type)==="COMPONENT_SET"?a.parent.name:a.name});return t==null||t.set(e.id,o),o}async function $(e,t){let n={},o,a,r=e.parent;for(;r;)r.type==="FRAME"?(o===void 0&&(o=r.name),a=r.name):(r.type==="COMPONENT"||r.type==="COMPONENT_SET")&&n.component===void 0?n.component=r.name:r.type==="INSTANCE"?(n.instance===void 0&&(n.instance=r.name),n.component===void 0&&(n.component=await Tt(r,t))):r.type==="SECTION"&&n.section===void 0?n.section=r.name:r.type==="GROUP"&&n.group===void 0&&(n.group=r.name),r=r.parent;return n.frame=o,n.artboard=a,n}async function xt(e,t){await U(e,t)}var St=100,Me=async(e=()=>!1,t)=>{var f;if(!(figma.currentPage.selection.length>0))return{nodes:[],basedOnSelection:!1};let o=await x(),a=!!(((f=o.ignoreHiddenLayers)==null||f)&&o.ignoreHiddenLayersIncludingChildren),r=!!(o.prefillKeyFormat&&H(o.keyFormat)),i=new Map,s=await ye(a,e);if(e())return{nodes:[],basedOnSelection:!0};let c=[],g=[],l=!0,u=async()=>{if(g.length===0)return!0;if(r){let C=0;for(let{node:h,info:N}of g){let I=await $(h,i);if(N.component=I.component,N.instance=I.instance,N.frame=I.frame,N.artboard=I.artboard,N.section=I.section,N.group=I.group,C++,C%25===0&&(await new Promise(D=>setTimeout(D,0)),e()))return!1}}let m=g.map(({info:C})=>C);return c.push(...m),g=[],t==null||t(m,l),l=!1,!0},d=0;for(let{node:m,ancestorHidden:C}of s){let h=m.characters;if(E(m,C,o,h)||g.push({node:m,info:S(m,h)}),d++,d%50===0&&(await new Promise(N=>setTimeout(N,0)),e()))return{nodes:c,basedOnSelection:!0};if(g.length>=St&&!await u())return{nodes:c,basedOnSelection:!0}}return await u()?{nodes:c,basedOnSelection:!0}:{nodes:c,basedOnSelection:!0}},Fe=async(e,t)=>{let n=!0,o=[],a=e.length,r=0;for(let i of e){try{let s=await figma.getNodeByIdAsync(i.id);s&&s.type==="TEXT"&&o.push(j(s,i.info))}catch(s){n=!1}r++,r%50===0&&(a>100&&(t==null||t(r,a)),await new Promise(s=>setTimeout(s,0)))}return{ok:n,nodes:o}},De=async(e,t)=>{let n=[],o=[],a=e.length,r=0;for(let i of e){try{let s=await figma.getNodeByIdAsync(i.id);if(!s||s.type!=="TEXT"){n.push(`Node ${i.id} is not a text node`);continue}await xt(s,i.text);let c={translation:i.translation};i.isPlural!==void 0&&(c.isPlural=i.isPlural),i.pluralParamValue!==void 0&&(c.pluralParamValue=i.pluralParamValue),i.paramsValues!==void 0&&(c.paramsValues=i.paramsValues),i.key!==void 0&&(c.key=i.key),i.ns!==void 0&&(c.ns=i.ns),i.connected!==void 0&&(c.connected=i.connected),o.push(j(s,c))}catch(s){let c=s instanceof Error?s.message:String(s);n.push(`Node ${i.id}: ${c}`)}r++,r%10===0&&(a>100&&(t==null||t(r,a)),await new Promise(s=>setTimeout(s,0)))}return{ok:n.length===0,errors:n,nodes:o}};var Et={type:"SOLID",color:{r:1,g:.41,b:.58}},bt=500,_=new Map;async function Ge(e){var i;if(figma.editorType!=="figma")return;let t=await figma.getNodeByIdAsync(e);if(!t||t.type!=="TEXT")return;figma.viewport.scrollAndZoomIntoView([t]);let n=t.fills;if(n===figma.mixed)return;let o=_.get(e),a=(i=o==null?void 0:o.originalFills)!=null?i:n;o&&clearTimeout(o.timer),t.fills=[Et];let r=setTimeout(()=>{let s=_.get(e);if(s){_.delete(e);try{if(t.removed)return;t.fills=s.originalFills}catch(c){}}},bt);_.set(e,{node:t,originalFills:a,timer:r})}function Be(){for(let[,e]of _){clearTimeout(e.timer);try{e.node.removed||(e.node.fills=e.originalFills)}catch(t){}}_.clear()}var At=200;async function Ke(){let e=0,t=0;for(let o of figma.root.children){if(o.type!=="PAGE")continue;await o.loadAsync();let a=o.findAllWithCriteria({types:["TEXT"],pluginData:{keys:[T]}});for(let r of a){let i=r.getPluginData(T);if(!i)continue;let s;try{s=JSON.parse(i)}catch(u){continue}if(s.connected)continue;let c=s.key;if(typeof c!="string"||c===""||(t++,t%At===0&&await new Promise(u=>setTimeout(u,0)),s.prefilledKey!==c))continue;let n=s,{prefilledKey:g}=n,l=se(n,["prefilledKey"]);r.setPluginData(T,JSON.stringify(B(y({},l),{key:""}))),e++}}return e}function _t(e){let t=e.parent,n=null;for(;t;)t.type==="FRAME"&&(n=t),t=t.parent;return n}function Ue(e){let t=new Map;for(let n of e){let o=_t(n);if(!o)continue;let a=t.get(o);a?a.push(n):t.set(o,[n])}return t}function He(e,t){let n=e.absoluteBoundingBox,o=t.absoluteBoundingBox;return!n||!o?{x:0,y:0,width:0,height:0}:{x:n.x-o.x,y:n.y-o.y,width:n.width,height:n.height}}var vt=1,Rt=5,kt=50,$e=()=>new Promise(e=>setTimeout(e,0));function Ot(e){let t=e;for(;t;){if(t.type==="PAGE")return t;t=t.parent}return null}async function Ve(e){let t=Ot(e);t&&await t.loadAsync()}async function Lt(e){return await Ve(e),e.exportAsync({format:"PNG",constraint:{type:"SCALE",value:vt}})}async function Ye(e,t){var c,g;let n=[];for(let l of e)n.push(await figma.getNodeByIdAsync(l)),n.length%kt===0&&await $e();let o=[];for(let l of n)l&&l.type==="TEXT"&&o.push(l);let a=Ue(o),r=0,i=0,s=0;for(let[l,u]of a){s+=1,s%Rt===0&&await $e();try{await Ve(l);let d=l.absoluteBoundingBox,f=await Lt(l),m={id:l.id,name:l.name,width:(c=d==null?void 0:d.width)!=null?c:0,height:(g=d==null?void 0:d.height)!=null?g:0},C=u.map(h=>{let N=He(h,l),I=S(h);return y(y({},I),N)});t({image:f,info:m,keys:C}),r+=1}catch(d){i+=1,console.warn(`[tolgee:main] screenshot export failed for frame "${l.name}":`,d instanceof Error?d.message:d)}}return{delivered:r,failed:i}}var Je,We,qe,ze,Ze,Mt=figma.editorType==="dev"?(qe=(We=(Je=__uiFiles__==null?void 0:__uiFiles__.dev)!=null?Je:__uiFiles__==null?void 0:__uiFiles__.figma)!=null?We:__html__)!=null?qe:"":(Ze=(ze=__uiFiles__==null?void 0:__uiFiles__.figma)!=null?ze:__html__)!=null?Ze:"";figma.skipInvisibleInstanceChildren=!0;figma.showUI(Mt,{width:X.DEFAULT.width,height:X.DEFAULT.height,themeColors:!0});fe();var Y=0,Xe=!1;async function F(e=!0){let t=++Y,n=()=>t!==Y;if(e&&p({type:"selection-pending"}),await new Promise(a=>setTimeout(a,0)),n())return;let o=0;try{await Me(n,(a,r)=>{n()||(o+=a.length,p({type:"selection-batch",nodes:a,first:r}))})}catch(a){console.warn("[tolgee:main] selection scan failed",a)}t===Y&&p({type:"selection-done",hasUserSelection:figma.currentPage.selection.length>0,total:o})}var Ft=150,Dt=60,V=null,je=0;function Gt(){Y++,p({type:"selection-pending"});let e=Date.now(),t=e-je{V=null,F(!1)},Dt)}async function Bt(){let e=await x();p({type:"page-changed",config:e,pageName:figma.currentPage.name}),await F()}figma.on("selectionchange",()=>{Gt()});figma.on("currentpagechange",()=>{(async()=>(L(),await Bt()))()});figma.on("close",()=>{Be()});P("ui-ready",async()=>{let e=await x();p({type:"init",config:e,selectedNodes:[],hasUserSelection:figma.currentPage.selection.length>0,editorType:figma.editorType,pageName:figma.currentPage.name}),!Xe&&(Xe=!0,await F())});P("resize",e=>{figma.ui.resize(e.size.width,e.size.height)});P("close",()=>{figma.closePlugin()});P("notify",e=>{figma.notify(e.text,{error:e.error})});P("open-external",e=>{figma.openExternal(e.url)});var Kt=["ignoreNumbers","ignoreFormattedNumbers","ignorePrefix","ignoreTextLayers","ignoreHiddenLayers","ignoreHiddenLayersIncludingChildren"],Ut=["prefillKeyFormat","keyFormat","variableCasing"];P("save-config",async e=>{let t=await x();await M(B(y({},e.config),{documentInfo:!0,pageInfo:!0}));let n=await x();p({type:"config-changed",config:n});let o=Kt.some(s=>t[s]!==n[s]),a=Ut.some(s=>t[s]!==n[s]),r=!!t.prefillKeyFormat&&!n.prefillKeyFormat;r&&await Ke(),(o||r||a&&n.prefillKeyFormat&&H(n.keyFormat))&&await F()});P("persist-project-id",async e=>{await M({projectId:e.projectId});let t=await x();p({type:"config-changed",config:t})});P("reset",async()=>{await Le(),p({type:"config-changed",config:{}})});P("set-language",async e=>{await M({language:e.language}),p({type:"config-changed",config:await x()})});P("set-branch",async e=>{await M({branch:e.branch}),p({type:"config-changed",config:await x()})});P("request-page-connected-nodes",async e=>{var r;let t=await ue(),n=await x(),o=!!(((r=n.ignoreHiddenLayers)==null||r)&&n.ignoreHiddenLayersIncludingChildren),a=await pe(t,n,o,(i,s)=>{p({type:"page-connected-nodes-progress",correlationId:e.correlationId,done:i,total:s})});p({type:"page-connected-nodes-result",correlationId:e.correlationId,nodes:a})});P("set-nodes-data",async e=>{let t=await Fe(e.nodes,(n,o)=>{p({type:"nodes-set-progress",correlationId:e.correlationId,done:n,total:o})});p({type:"nodes-set-result",correlationId:e.correlationId,ok:t.ok,nodes:t.nodes})});P("resolve-parent-names",async e=>{let t=new Map,n=[],o=0;for(let a of e.nodeIds){let r=await figma.getNodeByIdAsync(a);n.push([a,r?await $(r,t):{}]),o++,o%25===0&&await new Promise(i=>setTimeout(i,0))}p({type:"parent-names-result",correlationId:e.correlationId,parents:Object.fromEntries(n)})});P("request-screenshots",async e=>{let t=0,{delivered:n,failed:o}=await Ye(e.nodeIds,a=>{p({type:"screenshot-frame",correlationId:e.correlationId,screenshot:a,index:t++})});p({type:"screenshots-done",correlationId:e.correlationId,total:n,failed:o})});P("scroll-to-node",async e=>{if(await Ge(e.id),figma.editorType==="dev"){let t=await figma.getNodeByIdAsync(e.id);t&&"x"in t&&figma.viewport.scrollAndZoomIntoView([t])}});P("apply-translations",async e=>{let{ok:t,errors:n,nodes:o}=await De(e.updates,(a,r)=>{p({type:"apply-translations-progress",correlationId:e.correlationId,done:a,total:r})});p({type:"apply-translations-result",correlationId:e.correlationId,ok:t,errors:n,nodes:o})});P("create-copy",async e=>{var o;let t=await x(),n=e.mode==="keys"?await W({mode:"keys",correlationId:e.correlationId,sourcePageId:e.sourcePageId,namespacesEnabled:e.namespacesEnabled},t):await W({mode:"languages",correlationId:e.correlationId,languages:(o=e.languages)!=null?o:[],sourcePageId:e.sourcePageId},t);p({type:"create-copy-result",correlationId:e.correlationId,ok:n.ok,createdPageIds:n.createdPageIds,pages:n.pages,error:n.error,skippedMissingFont:n.skippedMissingFont})});P("request-copy-staleness",async e=>{let t;try{t=await be(figma.currentPage)}catch(n){t={ok:!1,error:n instanceof Error?n.message:String(n)}}p({type:"copy-staleness-result",correlationId:e.correlationId,ok:t.ok,missingCount:t.missingCount,removedCount:t.removedCount,error:t.error})}); diff --git a/delivery/tolgee-plugin/manifest.json b/delivery/tolgee-plugin/manifest.json new file mode 100644 index 00000000..2f3bc0a1 --- /dev/null +++ b/delivery/tolgee-plugin/manifest.json @@ -0,0 +1,25 @@ +{ + "api": "1.0.0", + "id": "1212381421658754793", + "name": "Tolgee", + "main": "main.js", + "ui": { + "figma": "ui.html", + "dev": "ui.html" + }, + "editorType": [ + "figma", + "dev" + ], + "documentAccess": "dynamic-page", + "capabilities": [ + "inspect", + "vscode" + ], + "networkAccess": { + "allowedDomains": [ + "*" + ], + "reasoning": "Self-hosted Tolgee instances may run on arbitrary domains." + } +} diff --git a/delivery/tolgee-plugin/ui.html b/delivery/tolgee-plugin/ui.html new file mode 100644 index 00000000..0827a3b1 --- /dev/null +++ b/delivery/tolgee-plugin/ui.html @@ -0,0 +1,220 @@ + + + + + + Tolgee + + + + +
+ + diff --git a/docs/superpowers/plans/2026-07-10-key-format-cursor.md b/docs/superpowers/plans/2026-07-10-key-format-cursor.md deleted file mode 100644 index f84ba1eb..00000000 --- a/docs/superpowers/plans/2026-07-10-key-format-cursor.md +++ /dev/null @@ -1,204 +0,0 @@ -# Key Format Cursor Fix Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Keep separators and the cursor at the intended position when users combine multiple placeholders in the key-format editor. - -**Architecture:** Use CodeMirror's completion lifecycle and content click handler -instead of restarting completion from bubbled events. The visual-caret follow-up -customizes only the completion transaction's cursor association so CodeMirror -draws the caret on the logical side of an atomic placeholder. - -**Tech Stack:** TypeScript, Preact, CodeMirror 6, Cypress, Bun - -## Global Constraints - -- Keep click-to-open completion. -- Do not change placeholder names, formatting, preview generation, or persistence. -- Add no dependencies or new abstractions. -- Use Bun for project commands. -- Do not add or run Cypress coverage for the visual-caret follow-up; the reporter - will verify it manually. - ---- - -### Task 1: Preserve the cursor between key-format placeholders - -**Files:** - -- Create: `cypress/e2e/keyFormatEditor.cy.ts` -- Modify: `src/ui/views/Settings/StringsEditor.tsx:93-108,163-193,227-242` - -**Interfaces:** - -- Consumes: CodeMirror `Completion.apply`, `EditorView.domEventHandlers`, and the existing `StringsEditor` props. -- Produces: unchanged `StringsEditor` props and unchanged key-format strings through `onChange`. - -- [x] **Step 1: Write the failing Cypress regression** - -```typescript -import { DEFAULT_CREDENTIALS } from "@/web/urlConfig"; -import { visitWithState } from "../common/tools"; - -describe("Key format editor", () => { - it("keeps separators between inserted placeholders", () => { - visitWithState({ - config: { - apiUrl: DEFAULT_CREDENTIALS.apiUrl, - apiKey: DEFAULT_CREDENTIALS.apiKey, - language: "en", - namespace: "", - documentInfo: true, - pageInfo: true, - prefillKeyFormat: true, - keyFormat: "", - }, - }); - - cy.iframe().findDcy("index_settings_button").click(); - cy.iframe().findDcy("settings_expandable_strings").click(); - cy.iframe().findDcy("global-editor").click(); - cy.iframe().find(".cm-content").as("editorContent"); - cy.iframe().find(".cm-tooltip-autocomplete").should("be.visible"); - cy.wait(150); - - cy.get("@editorContent").type("{enter}").should("have.text", "artboard"); - cy.iframe().find(".cm-tooltip-autocomplete").should("not.exist"); - cy.get("@editorContent").type(".component"); - cy.iframe().find(".cm-tooltip-autocomplete").should("be.visible"); - cy.wait(150); - cy.get("@editorContent") - .type("{enter}") - .should("have.text", "artboard.component"); - cy.iframe().find(".cm-tooltip-autocomplete").should("not.exist"); - }); -}); -``` - -- [x] **Step 2: Run the focused test and verify RED** - -Run: - -```bash -bunx cypress run --spec cypress/e2e/keyFormatEditor.cy.ts -``` - -Expected: FAIL because `.cm-tooltip-autocomplete` still exists after accepting the first placeholder, demonstrating that completion was restarted by the wrapper event. - -- [x] **Step 3: Delegate insertion and click handling to CodeMirror** - -Replace `createCompletionOption` with CodeMirror's string `apply` contract: - -```typescript -function createCompletionOption( - label: string, - detail: string, - insertText: string, -) { - return { - label, - detail, - type: "keyword" as const, - apply: insertText, - }; -} -``` - -Add a content-scoped click handler to the editor extensions: - -```typescript -EditorView.domEventHandlers({ - click(_event, view) { - startCompletion(view); - return false; - }, -}), -``` - -Remove the Preact `onClick` and `onKeyDown` props from the editor wrapper. This prevents completion-option clicks and ordinary key presses from starting a stale explicit completion. - -- [x] **Step 4: Run focused and static verification** - -Run: - -```bash -bunx cypress run --spec cypress/e2e/keyFormatEditor.cy.ts -bun run tsc -bun run build -``` - -Expected: the Cypress spec passes, TypeScript reports no errors, and the plugin build completes successfully. - -- [x] **Step 5: Commit the bug fix** - -```bash -git add cypress/e2e/keyFormatEditor.cy.ts src/ui/views/Settings/StringsEditor.tsx docs/superpowers/plans/2026-07-10-key-format-cursor.md -git commit -m "fix(settings): preserve key format cursor position" -``` - -### Task 2: Render the caret on the logical side of a placeholder - -**Files:** - -- Modify: `src/ui/views/Settings/StringsEditor.tsx:3,16-21,93-106` -- Modify: `docs/superpowers/specs/2026-07-10-key-format-cursor-design.md:37-53` - -**Interfaces:** - -- Consumes: CodeMirror `Completion`, `EditorSelection.cursor`, - `pickedCompletion`, and the existing placeholder completion range. -- Produces: the existing completion option shape with a custom `apply` function - that places the cursor at `from + insertText.length` with `assoc: -1`. - -- [x] **Step 1: Apply the completion with an explicit cursor association** - -Import the selection and completion APIs: - -```typescript -import { Compartment, EditorSelection, EditorState } from "@codemirror/state"; -import { - autocompletion, - Completion, - CompletionContext, - CompletionResult, - pickedCompletion, - startCompletion, -} from "@codemirror/autocomplete"; -``` - -Replace the string `apply` value in `createCompletionOption` with a typed -function that preserves CodeMirror's completion annotation: - -```typescript -apply(view: EditorView, completion: Completion, from: number, to: number) { - view.dispatch({ - changes: { from, to, insert: insertText }, - selection: EditorSelection.cursor(from + insertText.length, -1), - annotations: pickedCompletion.of(completion), - scrollIntoView: true, - userEvent: "input.complete", - }); -}, -``` - -- [x] **Step 2: Run static verification** - -Run: - -```bash -bun run tsc -bun run build -``` - -Expected: TypeScript reports no errors and the plugin build completes -successfully. Do not run Cypress; the reporter will verify left/right caret -rendering in Figma. - -- [x] **Step 3: Commit the follow-up fix** - -```bash -git add src/ui/views/Settings/StringsEditor.tsx \ - docs/superpowers/specs/2026-07-10-key-format-cursor-design.md \ - docs/superpowers/plans/2026-07-10-key-format-cursor.md -git commit -m "fix(settings): render cursor after placeholder badge" -``` diff --git a/docs/superpowers/specs/2026-07-10-key-format-cursor-design.md b/docs/superpowers/specs/2026-07-10-key-format-cursor-design.md deleted file mode 100644 index 4cfdce74..00000000 --- a/docs/superpowers/specs/2026-07-10-key-format-cursor-design.md +++ /dev/null @@ -1,54 +0,0 @@ -# Key format cursor fix - -## Problem - -The key-format editor starts completion on every `keydown`, before CodeMirror -has applied the typed character. Its custom completion transaction also bypasses -CodeMirror's built-in completion lifecycle. After inserting a placeholder, the -next separator or placeholder can therefore use a stale completion range and -move the cursor unexpectedly. - -After accepting a placeholder, the logical cursor position is correct, but the -caret is rendered on the left side of the placeholder badge. The same visual -position is shown when the cursor is logically before or after the badge. - -## Design - -- Keep click-to-open completion. -- Remove the editor wrapper's `onKeyDown` completion trigger. CodeMirror already - activates completion for normal typing. -- Represent each placeholder's `apply` value as the inserted string so CodeMirror - owns the document change, selection mapping, and completion closing. -- Do not change placeholder names, formatting, preview generation, or persistence. - -### Placeholder caret association - -The placeholder is rendered as a CodeMirror widget with `side: 1`. CodeMirror -uses a cursor range's `assoc` value to choose the visual side at an ambiguous -widget boundary. A completion currently leaves `assoc` at its default value, so -the caret is drawn on the badge's left edge even though the cursor is logically -after it. - -Apply placeholder completions with a cursor range whose `assoc` is `-1`. This -associates the cursor with the preceding content and renders it on the right -edge of the badge. Preserve CodeMirror's `pickedCompletion` annotation so its -completion lifecycle and events continue to behave normally. - -Arrow-key movement remains owned by CodeMirror. Consider a local cursor overlay -only if manual verification shows that CodeMirror does not preserve the correct -association while crossing the atomic placeholder. - -## Verification - -Add a Cypress regression that opens the key-format editor, inserts a placeholder, -types `.` at the cursor, inserts another placeholder, and verifies the separator -remains between the two placeholders. Run the focused test first to prove the -current implementation fails, then run it again after the fix together with the -TypeScript and build checks. - -The reporter will manually verify the primary caret's position relative to the -placeholder badge; do not add or run Cypress coverage for this follow-up: - -- directly after accepting a placeholder, the caret is at the badge's right edge; -- after moving left across the placeholder, the caret is at its left edge; -- after moving right across the placeholder, the caret is again at its right edge. diff --git a/cypress/docker-compose.yaml b/e2e/docker-compose.yml similarity index 59% rename from cypress/docker-compose.yaml rename to e2e/docker-compose.yml index ab0fa46e..2aea6059 100644 --- a/cypress/docker-compose.yaml +++ b/e2e/docker-compose.yml @@ -1,21 +1,24 @@ -version: "3" services: - app: - container_name: tolgee_js_e2e_server + tolgee: + container_name: tolgee_figma_e2e image: tolgee/tolgee:latest ports: - - 8080:8080 - - 8092:8091 + - "8080:8080" environment: + - JAVA_TOOL_OPTIONS=-XX:+UseG1GC -XX:MaxGCPauseMillis=100 - tolgee.authentication.enabled=true - tolgee.internal.controllerEnabled=true - - "tolgee.screenshots-url=http://localhost:8080/screenshots" + - tolgee.screenshots-url=http://localhost:8080/screenshots - tolgee.authentication.needs-email-verification=true - tolgee.authentication.registrations-allowed=true - tolgee.internal.fake-emails-sent=true - tolgee.authentication.initialPassword=admin - tolgee.import.dir=/data/import-data - tolgee.import.create-implicit-api-key=true - - JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,address=*:8091,server=y,suspend=n volumes: - ./import-data:/data/import-data + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:8080/ >/dev/null 2>&1 || exit 1"] + interval: 3s + timeout: 5s + retries: 60 diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts new file mode 100644 index 00000000..df6b7d95 --- /dev/null +++ b/e2e/global-setup.ts @@ -0,0 +1,43 @@ +/** + * Playwright global setup: warms up the Tolgee JVM auth code paths before + * any test runs. + * + * The first request to `/v2/api-keys/current` triggers Spring class-loading + * and JIT compilation, which can take 30+ seconds on a cold JVM. Running it + * here ensures all tests start with a warm JVM and sub-second auth responses. + */ +const TOLGEE_URL = process.env.TOLGEE_URL ?? "http://localhost:8080"; +const API_KEY = "examples-admin-imported-project-implicit"; + +async function warmupRequest(): Promise { + try { + const res = await fetch(`${TOLGEE_URL}/v2/api-keys/current`, { + headers: { "X-API-Key": API_KEY }, + signal: AbortSignal.timeout(90_000), + }); + return res.status < 500; + } catch { + return false; + } +} + +export default async function globalSetup(): Promise { + const startMs = Date.now(); + process.stdout.write("[global-setup] Warming up Tolgee auth..."); + + for (let i = 0; i < 10; i++) { + const ok = await warmupRequest(); + const elapsed = ((Date.now() - startMs) / 1000).toFixed(1); + if (ok) { + process.stdout.write(` done in ${elapsed}s\n`); + return; + } + process.stdout.write("."); + await new Promise((r) => setTimeout(r, 2_000)); + } + + const elapsed = ((Date.now() - startMs) / 1000).toFixed(1); + process.stdout.write( + ` WARN: auth warmup failed after ${elapsed}s — tests may be slow\n`, + ); +} diff --git a/e2e/host/fixtures.ts b/e2e/host/fixtures.ts new file mode 100644 index 00000000..13c48134 --- /dev/null +++ b/e2e/host/fixtures.ts @@ -0,0 +1,94 @@ +/** + * Test fixtures shared between Playwright specs and the host shim. + * + * Mirrors `cypress/common/tools.ts` + `src/web/urlConfig.ts` from v1 but + * targets the v2 message bus (no `@create-figma-plugin/utilities`). + */ +import type { NodeInfo, TolgeeConfig } from "../../src/shared/types"; + +/** Default credentials matching the Tolgee container in `docker-compose.yml`. */ +export const DEFAULT_CREDENTIALS = { + apiUrl: "http://localhost:8080", + /** + * Implicit API key minted by the `examples` import data. Requires + * `tolgee.import.create-implicit-api-key=true` on the platform side. + */ + apiKey: "examples-admin-imported-project-implicit", +}; + +/** Pre-canned config presets used across specs. */ +export const SIGNED_IN: Partial = { + ...DEFAULT_CREDENTIALS, + language: "en", + namespace: "", + pageInfo: true, + documentInfo: true, +}; + +export const SIGNED_IN_NO_PAGE: Partial = { + ...DEFAULT_CREDENTIALS, + documentInfo: true, +}; + +export const PAGE_COPY: Partial = { + ...DEFAULT_CREDENTIALS, + namespace: "", + documentInfo: true, + pageCopy: true, +}; + +export const PAGE_STRING_DETAILS: Partial = { + ...DEFAULT_CREDENTIALS, + language: "en", + namespace: "", + documentInfo: true, + pageStringDetails: true, +}; + +type NodeProps = { + text: string; + key?: string; + ns?: string; + connected?: boolean; + translation?: string; + isPlural?: boolean; + id?: string; +}; + +export function createTestNode(props: NodeProps): NodeInfo { + return { + name: props.text, + characters: props.text, + id: props.id ?? Math.random().toString(36).slice(2), + translation: props.translation ?? props.text, + isPlural: props.isPlural ?? false, + key: props.key ?? "", + ns: props.ns, + connected: Boolean(props.connected), + }; +} + +export type HostInitState = { + config?: Partial | null; + allNodes?: NodeInfo[]; + selectedNodes?: NodeInfo[]; + hasUserSelection?: boolean; + editorType?: "figma" | "dev"; + /** Optional initial route to navigate to immediately after init (used by E2E tests). */ + route?: string; +}; + +/** + * Build the `?state=…` query string the E2E host page expects. + * + * Mirrors v1's `createShortcutUrl` so call sites read the same way: + * + * await page.goto(hostUrl(SIGNED_IN, { selectedNodes: [...] })); + */ +export function hostUrl( + config: Partial | null, + rest: Omit = {}, +): string { + const state: HostInitState = { config, ...rest }; + return `/?state=${encodeURIComponent(JSON.stringify(state))}`; +} diff --git a/e2e/host/index.html b/e2e/host/index.html new file mode 100644 index 00000000..f872bdd4 --- /dev/null +++ b/e2e/host/index.html @@ -0,0 +1,44 @@ + + + + + + Tolgee Figma Plugin — E2E Host + + + +
+ +
+ + + diff --git a/e2e/host/main.ts b/e2e/host/main.ts new file mode 100644 index 00000000..3b0e1f99 --- /dev/null +++ b/e2e/host/main.ts @@ -0,0 +1,249 @@ +/** + * E2E host shim: a Figma-free stand-in for `src/main/main.ts`. + * + * Plays the role of the plugin's "main" thread in tests: listens for + * `pluginMessage` posts from the UI iframe (the production Svelte bundle) + * and responds with synthesized `MainToUi` messages from in-memory state. + * + * This lets the UI bundle run unmodified — every HTTP call still hits the + * real Tolgee platform started by `e2e/docker-compose.yml` — while we + * inject selection / config behaviour from the test harness. + * + * Initial state is read from `?state=` so tests can + * construct deep-link URLs (mirroring the v1 `createShortcutUrl` pattern). + */ +import type { MainToUi, UiToMain } from "../../src/shared/messages"; +import type { NodeInfo, TolgeeConfig } from "../../src/shared/types"; + +type HostState = { + config: Partial | null; + allNodes: NodeInfo[]; + selectedNodes: NodeInfo[]; + hasUserSelection: boolean; + editorType: "figma" | "dev"; + pageName: string; + route?: string; +}; + +function defaultState(): HostState { + return { + config: null, + allNodes: [], + selectedNodes: [], + hasUserSelection: false, + editorType: "figma", + pageName: "Page 1", + route: undefined, + }; +} + +function readInitialState(): HostState { + const params = new URLSearchParams(window.location.search); + const raw = params.get("state"); + if (!raw) return defaultState(); + try { + const data = JSON.parse(decodeURIComponent(raw)) as Partial; + return { ...defaultState(), ...data }; + } catch { + return defaultState(); + } +} + +const state = readInitialState(); + +const iframe = document.getElementById("plugin-iframe") as HTMLIFrameElement; + +function send(msg: MainToUi): void { + iframe.contentWindow?.postMessage({ pluginMessage: msg }, "*"); +} + +function emitSelectionChanged(): void { + send({ + type: "selection-changed", + nodes: state.selectedNodes, + hasUserSelection: state.hasUserSelection, + }); +} + +function applyNodeUpdates( + id: string, + patch: Partial, +): void { + const a = state.allNodes.find((n) => n.id === id); + if (a) Object.assign(a, patch); + const s = state.selectedNodes.find((n) => n.id === id); + if (s) Object.assign(s, patch); +} + +/** Post-write snapshots for a write result, like the real main thread's + * `setNodesData`/`applyTranslations` return values. */ +function collectUpdated(ids: string[]): NodeInfo[] { + const out: NodeInfo[] = []; + for (const id of ids) { + const n = + state.selectedNodes.find((x) => x.id === id) ?? state.allNodes.find((x) => x.id === id); + if (n) out.push({ ...n }); + } + return out; +} + +window.addEventListener("message", (event) => { + const msg = event.data?.pluginMessage as UiToMain | undefined; + if (!msg) return; + + switch (msg.type) { + case "ui-ready": { + send({ + type: "init" as const, + config: state.config, + selectedNodes: state.selectedNodes, + hasUserSelection: state.hasUserSelection, + editorType: state.editorType, + pageName: state.pageName, + ...(state.route ? { initialRoute: state.route } : {}), + }); + return; + } + case "resize": { + iframe.style.width = `${msg.size.width}px`; + iframe.style.height = `${msg.size.height}px`; + return; + } + case "save-config": { + // Mirror the real main thread: every save stamps documentInfo/pageInfo + // (see main.ts). Without this the first-run onboarding gate would never + // clear after Save in tests. + state.config = { + ...(state.config ?? {}), + ...msg.config, + documentInfo: true, + pageInfo: true, + }; + send({ type: "config-changed", config: state.config }); + return; + } + case "persist-project-id": { + state.config = { ...(state.config ?? {}), projectId: msg.projectId }; + send({ type: "config-changed", config: state.config }); + return; + } + case "reset": { + state.config = null; + send({ type: "config-changed", config: {} }); + return; + } + case "set-language": { + state.config = { ...(state.config ?? {}), language: msg.language }; + send({ type: "config-changed", config: state.config }); + return; + } + case "set-branch": { + state.config = { ...(state.config ?? {}), branch: msg.branch }; + send({ type: "config-changed", config: state.config }); + return; + } + case "request-page-connected-nodes": { + send({ + type: "page-connected-nodes-result", + correlationId: msg.correlationId, + nodes: state.allNodes.filter((n) => n.key), + }); + return; + } + case "set-nodes-data": { + for (const { id, info } of msg.nodes) applyNodeUpdates(id, info); + // Mirrors the real main thread: the result carries fresh snapshots of + // just the written nodes and there is NO selection re-emit — the UI + // patches its list in place from `nodes`. + send({ + type: "nodes-set-result", + correlationId: msg.correlationId, + ok: true, + nodes: collectUpdated(msg.nodes.map((n) => n.id)), + }); + return; + } + case "apply-translations": { + for (const u of msg.updates) { + const patch: Partial = { + characters: u.text, + translation: u.translation, + }; + if (u.isPlural !== undefined) patch.isPlural = u.isPlural; + if (u.pluralParamValue !== undefined) + patch.pluralParamValue = u.pluralParamValue; + if (u.paramsValues !== undefined) patch.paramsValues = u.paramsValues; + if (u.key !== undefined) patch.key = u.key; + if (u.ns !== undefined) patch.ns = u.ns; + if (u.connected !== undefined) patch.connected = u.connected; + applyNodeUpdates(u.id, patch); + } + send({ + type: "apply-translations-result", + correlationId: msg.correlationId, + ok: true, + errors: [], + nodes: collectUpdated(msg.updates.map((u) => u.id)), + }); + return; + } + case "request-screenshots": { + // Stub: no real screenshots in the host shim. Pull/push flows that + // upload screenshots will simply skip them. Mirrors the streamed + // protocol: zero frames, immediate terminal marker. + send({ + type: "screenshots-done", + correlationId: msg.correlationId, + total: 0, + failed: 0, + }); + return; + } + case "scroll-to-node": + case "notify": + case "open-external": + case "close": { + // No-ops in the host. Tests can assert these were sent by spying on + // postMessage from the page level if needed. + return; + } + case "create-copy": { + // No pages → the UI's apply phase is a no-op (mirrors an empty page). + send({ + type: "create-copy-result", + correlationId: msg.correlationId, + ok: true, + createdPageIds: [], + pages: [], + }); + return; + } + case "request-copy-staleness": { + // No staleness by default — power-tests can push a different result + // via `window.__e2e.send(...)` if a spec needs to exercise the banner. + send({ + type: "copy-staleness-result", + correlationId: msg.correlationId, + ok: true, + missingCount: 0, + removedCount: 0, + }); + return; + } + } +}); + +// Expose for power-tests that need to push state changes mid-test +// (e.g. simulating a selection change without re-navigating). Kept off +// the `window` global type by attaching dynamically. +(window as unknown as { __e2e: unknown }).__e2e = { + get state() { + return state; + }, + send, + selectNodes(nodes: NodeInfo[], hasUserSelection = true): void { + state.selectedNodes = nodes; + state.hasUserSelection = hasUserSelection; + emitSelectionChanged(); + }, +}; diff --git a/cypress/import-data/examples/cs.json b/e2e/import-data/examples/cs.json similarity index 100% rename from cypress/import-data/examples/cs.json rename to e2e/import-data/examples/cs.json diff --git a/cypress/import-data/examples/de.json b/e2e/import-data/examples/de.json similarity index 100% rename from cypress/import-data/examples/de.json rename to e2e/import-data/examples/de.json diff --git a/cypress/import-data/examples/en.json b/e2e/import-data/examples/en.json similarity index 100% rename from cypress/import-data/examples/en.json rename to e2e/import-data/examples/en.json diff --git a/cypress/import-data/examples/fr.json b/e2e/import-data/examples/fr.json similarity index 100% rename from cypress/import-data/examples/fr.json rename to e2e/import-data/examples/fr.json diff --git a/cypress/import-data/examples/namespaced/cs.json b/e2e/import-data/examples/namespaced/cs.json similarity index 100% rename from cypress/import-data/examples/namespaced/cs.json rename to e2e/import-data/examples/namespaced/cs.json diff --git a/cypress/import-data/examples/namespaced/de.json b/e2e/import-data/examples/namespaced/de.json similarity index 100% rename from cypress/import-data/examples/namespaced/de.json rename to e2e/import-data/examples/namespaced/de.json diff --git a/cypress/import-data/examples/namespaced/en.json b/e2e/import-data/examples/namespaced/en.json similarity index 100% rename from cypress/import-data/examples/namespaced/en.json rename to e2e/import-data/examples/namespaced/en.json diff --git a/cypress/import-data/examples/namespaced/fr.json b/e2e/import-data/examples/namespaced/fr.json similarity index 100% rename from cypress/import-data/examples/namespaced/fr.json rename to e2e/import-data/examples/namespaced/fr.json diff --git a/cypress/import-data/examples/translation/cs.json b/e2e/import-data/examples/translation/cs.json similarity index 100% rename from cypress/import-data/examples/translation/cs.json rename to e2e/import-data/examples/translation/cs.json diff --git a/cypress/import-data/examples/translation/de.json b/e2e/import-data/examples/translation/de.json similarity index 100% rename from cypress/import-data/examples/translation/de.json rename to e2e/import-data/examples/translation/de.json diff --git a/cypress/import-data/examples/translation/en.json b/e2e/import-data/examples/translation/en.json similarity index 100% rename from cypress/import-data/examples/translation/en.json rename to e2e/import-data/examples/translation/en.json diff --git a/cypress/import-data/examples/translation/fr.json b/e2e/import-data/examples/translation/fr.json similarity index 100% rename from cypress/import-data/examples/translation/fr.json rename to e2e/import-data/examples/translation/fr.json diff --git a/e2e/tests/branches.spec.ts b/e2e/tests/branches.spec.ts new file mode 100644 index 00000000..c800ee89 --- /dev/null +++ b/e2e/tests/branches.spec.ts @@ -0,0 +1,276 @@ +import { expect, test, type Page } from "@playwright/test"; +import { SIGNED_IN, hostUrl } from "../host/fixtures"; + +const IFRAME_SELECTOR = '[data-testid="plugin-iframe"]'; + +const MOCK_BRANCHES = ["main", "develop", "feature/new-ui"]; + +/** + * Find the branch is visually replaced by a custom circle+dot (see + // CreateCopy.svelte) but keeps its radio role/name for a11y + Playwright. + await expect( + ui.getByText("Create page with key names"), + ).toBeVisible(); + await expect( + ui.getByText("Create page per language"), + ).toBeVisible(); + }); + + test("back arrow returns to Index", async ({ page }) => { + const node = createTestNode({ + text: "On the road", + key: "on-the-road-title", + connected: true, + }); + + await page.goto( + hostUrl(SIGNED_IN, { + allNodes: [node], + selectedNodes: [node], + hasUserSelection: true, + }), + ); + + const ui = page.frameLocator(IFRAME_SELECTOR); + await expect(ui.getByText("1 string")).toBeVisible({ timeout: 60_000 }); + + await ui.getByRole("button", { name: "Create page copy" }).click(); + await expect(ui.getByRole("heading", { name: "Create copy" })).toBeVisible({ + timeout: 5_000, + }); + + await ui.getByRole("button", { name: "Back" }).click(); + + await expect(ui.getByText("1 string")).toBeVisible({ timeout: 5_000 }); + }); + + test("Create button is enabled in keys mode by default", async ({ page }) => { + const ui = await openCreateCopy(page); + + // "keys" is the default mode — canSubmit = !isBusy, so Create is immediately enabled. + const createButton = ui.getByRole("button", { name: "Create" }); + await expect(createButton).toBeEnabled({ timeout: 5_000 }); + }); + + test("language mode shows language checkboxes after loading", async ({ + page, + }) => { + const ui = await openCreateCopy(page); + + await ui.getByText("Create page per language").click(); + + // Languages are fetched from Tolgee; checkboxes appear once the list + // loads. CheckboxField renders a + + + + + {/snippet} + {@render section("Icon sizing (with text)", iconSizingBody)} + + + {#snippet typographyBody()} +

+ Base font: Inter, body + 11px / line-height 1.4 (set in + styles.css). Counts = occurrences in src. +

+ +

Sizes

+
+ {#each fontSizes as fs (fs.cls)} +
+ + {fs.cls} + + + {fs.px} + + + ×{fs.count} + + + The quick brown fox + +
+ {/each} +
+ +

Weights

+
+ {#each fontWeights as fw (fw.cls)} +
+ + {fw.cls} + + + {fw.weight} + + + ×{fw.count} + + + The quick brown fox + +
+ {/each} +
+ +

Styles

+
+ {#each fontStyles as st (st.cls)} +
+ + {st.cls} + + + ×{st.count} + + + The quick brown fox + +
+ {/each} +
+ {/snippet} + {@render section("Typography", typographyBody)} + + + {#snippet linksBody()} +

+ Default = no underline. On + hover they underline only (colour + stays). Two colours: secondary (default) and brand (Tolgee-related). + Hover them to see it. +

+
+ + +
+

+ + text-text-secondary hover:underline
text-text-brand hover:underline +
+

+ {/snippet} + {@render section("Links", linksBody)} + + + {#snippet buttonsBody()} +
+ {#each buttonSizes as size (size)} +
+ {size} + {#each buttonVariants as variant (variant)} + + {/each} +
+ {/each} +
+ off + {#each buttonVariants as variant (variant)} + + {/each} +
+
+ {/snippet} + {@render section("Buttons", buttonsBody)} + + + {#snippet syncButtonsBody()} +

+ The canonical data-transfer actions. Always use + SyncButton for these — never hand-roll the label/icon + combo. The arrow points at the destination's brand mark, so direction + + target read at a glance. +

+
+ + +
+
+ + +
+

+ Usage: + <SyncButton direction="upload" onclick={"{...}"} /> + — upload = primary → Tolgee, download = + secondary → Figma. +

+ {/snippet} + {@render section("Sync buttons (Upload / Download)", syncButtonsBody)} + + + {#snippet iconButtonBody()} +

+ Square icon-only action button — one canonical hover (subtle + background + icon turning brand-pink) so every icon action matches. + Forwards native button props, so it also works as a Tooltip/ + DropdownMenu trigger child. Used across ViewHeader, + Header and NodeListItem's row actions. +

+
+
+ + + + sm +
+
+ + + + md +
+
+ + + + disabled +
+
+

+ Usage: <IconButton size="sm|md" aria-label><Icon /></IconButton> +

+ {/snippet} + {@render section("Icon button", iconButtonBody)} + + + {#snippet tooltipIconButtonBody()} +

+ The canonical "icon action with a tooltip": collapses the repeated + Tooltip.Root → Trigger → + IconButton → Tooltip.Content boilerplate + into one element. label doubles as the + aria-label and the tooltip text unless + tooltip overrides it. Must sit inside a + Tooltip.Provider (list/view components already wrap + one). Used for connect/disconnect in NodeListItem and + the back button in ViewHeader. +

+ +
+ + + + + + + + + +
+
+

+ Usage: <TooltipIconButton label tooltip? side? onclick><Icon /></TooltipIconButton> + — hover the buttons above to see it. +

+ {/snippet} + {@render section("Tooltip icon button", tooltipIconButtonBody)} + + + {#snippet inputsBody()} +
+
+ + +
+
+ + +
+
+ + +
+

value: {inputValue}

+
+ {/snippet} + {@render section("Inputs & Labels", inputsBody)} + + + {#snippet selectBody()} +
+ +

selected: {selectValue}

+
+ {/snippet} + {@render section("Select", selectBody)} + + + {#snippet searchInputBody()} +

+ Search field used across the plugin: a magnifying-glass on the left + and, once there's a value, a clear "✕" on the right that empties the + box. Wraps the base Input so the icon padding and clear + behaviour live in one place. Used for the Connect screen's key + search and the Index list filter. +

+
+ +
+

+ Value: {searchInputValue || "(empty)"} +

+

+ Usage: <SearchInput bind:value placeholder? /> +

+ {/snippet} + {@render section("Search input", searchInputBody)} + + + {#snippet namespaceInputBody()} +

+ Single-value namespace combobox: pick an existing namespace or type + a NEW one (created on push). Mirrors TagInput's look/ + behaviour; "" is the "<none>" (default) namespace. Used in + NodeListItem's inline namespace field and Settings → + project namespace. +

+
+ (namespaceValue = v)} + options={sampleNamespaces} + /> +
+

+ Value: {namespaceValue || ""} +

+

+ Usage: <NamespaceInput value onChange options? placeholder? /> +

+ {/snippet} + {@render section("Namespace input (autocomplete)", namespaceInputBody)} + + + {#snippet switchBody()} +

+ Not yet used anywhere in the product — kept as a reserved + primitive. Its only current instance is this gallery's own + dark-mode toggle (meta-UI, not a product screen). +

+
+ + + +
+ {/snippet} + {@render section("Switch", switchBody)} + + + {#snippet checkboxBody()} +

+ 16×16px box, same + border-border + bg-bg as + Input (so fields and checkboxes match). Presentational — + wrap it in a button or menu item for interactivity; + indeterminate renders the "some selected" bar for master + checkboxes. +

+
+ + unchecked + + + checked + + + indeterminate + +
+ {/snippet} + {@render section("Checkbox", checkboxBody)} + + + {#snippet checkboxFieldBody()} +

+ A labelled checkbox ROW: left checkbox + label, clickable as one. The + OFF state stays clearly visible (vs a right-side switch). Optional + trailing snippet renders after the label OUTSIDE the + toggle (for an inline input, an (i) hint, …). Used across Settings → + Strings and Keys. +

+
+ (cbA = v)} + > + {#snippet trailing()} + + {/snippet} + + (cbB = v)} + /> + (cbC = v)} + /> +
+

+ <CheckboxField label checked onChange + {"{#snippet trailing()}…{/snippet}"} /> +

+ {/snippet} + {@render section("Checkbox field", checkboxFieldBody)} + + + {#snippet keyFormatBody()} +

+ Token field for building a key format: placeholders render as inline + chips, free text stays editable. Type to open the autocomplete — + anchored to the CARET (flips above when there's no room below), filter + by what you type, Enter / click inserts a chip. Serializes to/from the + {"{placeholder}"} string. Used in Settings → Key format and + the bulk "Generate key names" action. +

+
+ (kfValue = v)} + placeholder={"{artboard}.{elementName}"} + /> +
+

+ Value: {kfValue || "(empty)"} +

+

+ <KeyFormatInput value onChange onSubmit? placeholder? /> +

+ {/snippet} + {@render section("Key format input (autocomplete)", keyFormatBody)} + + + {#snippet tagInputBody()} +

+ Tag multi-select: selected tags are removable chips, the input + autocompletes the project's existing tags and offers to CREATE a new + one when what you type isn't there (new tags are created on push). + Dropdown is field-anchored (flips above when needed). Used in Settings + → Add tags. +

+
+ (tagsValue = t)} + fetchSuggestions={demoSuggestTags} + /> +
+

+ Value: {tagsValue.join(", ") || "(none)"} +

+

+ <TagInput value onChange fetchSuggestions? /> +

+ {/snippet} + {@render section("Tag input (autocomplete)", tagInputBody)} + + + {#snippet truncBody()} +

+ The DS truncation pattern: one-line text that ellipsizes when it + overflows, with a tooltip showing the FULL text on hover — but only + when it's actually clipped. Pass onclick to render it as a + link. Put it in a min-w-0 flex parent. Used for the Settings + project link. +

+
+ + + {}} + class="font-semibold text-secondary-dark underline underline-offset-2" + /> +
+

+ <TruncatedText text onclick? side? /> +

+ {/snippet} + {@render section("Truncated text (+ tooltip)", truncBody)} + + + {#snippet cardBody()} + +
+ Card title + + Cards group related content with a subtle border and padding. + +
+
+ {/snippet} + {@render section("Card", cardBody)} + + + {#snippet badgesBody()} +

+ Tiny status pills (plural, duplicate count, …) — neutral = muted grey + (text-text-secondary), 10px, outlined. The base pill for + FilterChip too, so every pill matches in size + colour. +

+
+ Plural + 2 +
+

+ Usage: <Badge>plural</Badge> +

+ {/snippet} + {@render section("Badges", badgesBody)} + + + {#snippet conflictIconsBody()} +

+ The two in-list conflict flags — both plain red {ICON.marker}px icons + (text-error, ICON.marker), no badge/count, to + keep the row uncluttered. The current shipping icon is + outlined. +

+ + +

+ Same key · different text + — one key, two diverging strings; only one uploads +

+
+ {#each sameKeyIcons as ic (ic.name)} +
+ + {ic.name} + {#if ic.current} + current + {/if} +
+ {/each} +
+ + +

+ Manual change + — Figma text edited directly, diverged from the saved translation +

+
+ {#each manualChangeIcons as ic (ic.name)} +
+ + {ic.name} + {#if ic.current} + current + {/if} +
+ {/each} +
+ + +

In a row (real StatusMarker — hover to see the unified affordance):

+ +
+
+ Submit + + + + same key +
+
+ Welcome back + + + + manual change +
+
+
+

+ Usage: + <StatusMarker label onclick><Icon/></StatusMarker> + — red, ICON.marker, unified hover:opacity-70. +

+ {/snippet} + {@render section("Conflict flag icons", conflictIconsBody)} + + + {#snippet statBody()} +

+ A summary statistic: a prominent number (text-lg) above a + 12px label, one component so every stat matches. tone + colours the value. Used for the Push New / Changed / Unchanged + breakdown. +

+ +
+ + + +
+
+

+ Usage: + <Stat value label tone="brand|secondary|muted" /> +

+ {/snippet} + {@render section("Stat (summary numbers)", statBody)} + + + {#snippet progressBarBody()} +

+ Thin (h-[5px]), fully rounded bar in the brand colour — + the single canonical progress bar for the whole plugin (Index + bulk-write progress, Push diff-computation + screenshot/upload + progress, Pull page-scan/download/apply progress, and CreateCopy's + fetch stage). Two modes: DETERMINATE when total is a + known positive number (real percentage fill), and INDETERMINATE + when total is null (or 0) — a + ~40%-wide bar slides across instead of an empty/fake fill. This + replaces the old PullProgress/PushProgress + domain components, which each had their own near-duplicate + indeterminate CSS. +

+
+
+

No label (Index write progress)

+ +
+
+

With label (Push diff progress)

+ +
+
+

+ Indeterminate (unknown total — page scan / fetch not started yet) +

+ +
+ +
+

+ Usage: <ProgressBar loaded total label? /> +

+ {/snippet} + {@render section("Progress bar", progressBarBody)} + + + {#snippet filterChipsBody()} +

+ Removable "active filter" pills shown above the list. It IS a neutral Badge (same 10px / muted colour / border / radius as the plural + duplicate + badges) + a trailing ✕. One row holds every active view filter (the + exact-key filter from a conflict click, exact-text from a duplicate + badge, "hide connected", …) so each can be cleared individually. +

+
+ {}}>generatedkey.my_test + {}}>"Submit" + {}}>hide connected +
+

+ Usage: <FilterChip onclear>label</FilterChip> +

+ {/snippet} + {@render section("Filter chips", filterChipsBody)} + + + {#snippet messagesBody()} +

+ Inline status messages in the semantic palette (error / warning / + success / info). Icon + colour come from the variant; the tint is + mixed from the same colour, so they adapt to dark mode. +

+
+ + Invalid API key. Please check your project API key. + + + 2 key(s) reuse the same name with different text in Figma. + + + My Project was successfully connected. + + + My Project was successfully connected. + (teal "connected" state) + + + Translations rarely change during a session — they're cached for 30s. + +
+

+ Usage: <Message variant="error">…</Message> +

+ {/snippet} + {@render section("Messages", messagesBody)} + + + {#snippet errorBannerBody()} +

+ App-shell-level banner rendered above every route in + src/ui/App.svelte whenever + appState.value.errorBanner is set — unlike + Message (which a route places inline), this sits at the + very top of the plugin regardless of which screen is open. Two + severities, both plain (no icon): error (red) and + warning (yellow). The gallery's Screens + tab renders routes directly and skips the App.svelte + shell, so this never otherwise appears here — shown below on mock + data. +

+
+ + +
+

+ Usage: <ErrorBanner banner={"{{ message, severity: \"error\"|\"warning\" }}"} /> +

+ {/snippet} + {@render section("Error banner", errorBannerBody)} + + + {#snippet emptyStatesBody()} +

+ One EmptyState pattern everywhere: a muted, opaque icon + (text-icon-muted) centred both axes on the available area, + a title, and an optional description. Fills its container + (flex-1), so it sits in the vertical centre — never + pinned to the top. +

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+

+ Usage: + + <EmptyState icon={"{Meh}"} title="…" description="…" /> + + — icon is any component with the size + + class API (lucide icon or the Tolgee mark). +

+ {/snippet} + {@render section("Empty states", emptyStatesBody)} + + + {#snippet countsBody()} +

+ Two count formats, kept visually distinct so they never read as the + same thing. +

+
+
+ 5 strings + + Results count — always the + number of items currently shown (after search/filter). The plain + "N strings" form, never "5 of 7". + +
+
+ 1/7 + + Selection count — selected / + total (N/M), used ONLY in the bulk action bar. The + slash is reserved for this so it can't be confused with the + results count. + +
+
+ {/snippet} + {@render section("Counts", countsBody)} + + + {#snippet tabsBody()} + + + Push + Pull + Settings + + +

Push tab content.

+
+ +

Pull tab content.

+
+ +

Settings tab content.

+
+
+ {/snippet} + {@render section("Tabs", tabsBody)} + + + {#snippet dialogBody()} + + + {#snippet child({ props })} + + {/snippet} + + +
+ Confirm push + + This will push your changes to the connected Tolgee project. + +
+ + {#snippet child({ props })} + + {/snippet} + + + {#snippet child({ props })} + + {/snippet} + +
+
+
+
+ {/snippet} + {@render section("Dialog", dialogBody)} + + + {#snippet tooltipBody()} + + + + {#snippet child({ props })} + + {/snippet} + + This is a tooltip + + + {/snippet} + {@render section("Tooltip", tooltipBody)} + + + {#snippet dropdownMenuBody()} +

+ The overflow ("⋮") menu — Root / Trigger / + Content / Item / Separator, + wrapping bits-ui's primitive with the plugin's + colours/spacing. The trigger is usually an IconButton + via the {"{#snippet child({ props })}"} pattern (so + the button forwards ARIA/open-state props). Used for + NodeListItem's row overflow menu. +

+ + + {#snippet child({ props })} + + + + {/snippet} + + + (dropdownLastAction = "String details")}> + String details + + (dropdownLastAction = "Move to string")}> + Move to string + + + (dropdownLastAction = "Connection detail")}> + Connection detail + + + +

+ Last selected: {dropdownLastAction} +

+

+ Usage: + <DropdownMenu.Root><DropdownMenu.Trigger>…</DropdownMenu.Trigger><DropdownMenu.Content><DropdownMenu.Item + onSelect>…</DropdownMenu.Item></DropdownMenu.Content></DropdownMenu.Root> +

+ {/snippet} + {@render section("Dropdown menu", dropdownMenuBody)} + + + {#snippet copyFeedbackBody()} +

+ Dev Mode's NodeListItem ⋮ menu adds Copy key + and Copy string. Because the notify() + toast lands on the Figma canvas (not the panel), the row's ⋮ trigger + also flashes to a green checkmark for 1.2s as an in-panel + confirmation, then reverts. Open the menu and pick an item to see it. +

+ + + {#snippet child({ props })} + + {#if copyDemoFlash} + + {:else} + + {/if} + + {/snippet} + + + copyDemo("Key copied")}> + Copy key + + copyDemo("String copied")}> + Copy string + + + +

+ Last toast: {copyDemoLast} +

+ {/snippet} + {@render section("Copy feedback (Dev Mode)", copyFeedbackBody)} + {/if} + + diff --git a/gallery/Copy.svelte b/gallery/Copy.svelte new file mode 100644 index 00000000..39895220 --- /dev/null +++ b/gallery/Copy.svelte @@ -0,0 +1,500 @@ + + + +{#snippet messagePreview(item: Entry)} + + {item.text} + +{/snippet} + +
+
+

+ All user-facing copy (messages, tooltips, empty states, hints, + placeholders, notable buttons) and where each appears. Dynamic parts shown + as {"{placeholder}"}. +

+ +

+ {shown} of {total} entries{q ? ` matching “${q}”` : ""} +

+
+ + {#each filtered as group (group.title)} +
+

+ {group.title} + + {group.items.length} + +

+ {#if group.note} +

{group.note}

+ {/if} +
    + {#each group.items as item (item.text)} + {@const w = splitWhere(item.where)} +
  • + {#if group.render === "message"} + {@render messagePreview(item)} + {:else} +

    {item.text}

    + {/if} +

    + + {w.file} + + {#if w.detail}{w.detail}{/if} +

    +
  • + {/each} +
+
+ {/each} + + {#if filtered.length === 0} +

+ No copy matches “{q}”. +

+ {/if} +
diff --git a/gallery/Onboarding.svelte b/gallery/Onboarding.svelte new file mode 100644 index 00000000..f66f7b97 --- /dev/null +++ b/gallery/Onboarding.svelte @@ -0,0 +1,78 @@ + + +
+

+ First-run setup. Left is what the current + flow drops the user into; right is the + real Onboarding route — the guided wizard that reuses the + Settings sections, one step at a time. (The mock here is pre-connected, so + step 1 shows the connected state.) +

+ +
+ +
+
+

Before

+ the "Sign in" dead-end +
+ +
+
+ Strings + +
+
+

Sign in to connect this document with Tolgee.

+ +
+
+ +
    +
  1. This "Sign in" screen — an extra screen + click before anything.
  2. +
  3. Then the full Settings page: three tabs, all at once.
  4. +
  5. No sense of progress, no clear "done".
  6. +
+
+ + +
+
+

After

+ the real wizard, click through +
+ +
+ + + +
+ +

+ Straight into setup on first run (no "Sign in" screen), the same three + Settings sections one step at a time, Back / Next, ending on the plugin + ready to use. Reuses the real components — change a section once, it + updates in Settings and here. +

+
+
+
diff --git a/gallery/Screens.svelte b/gallery/Screens.svelte new file mode 100644 index 00000000..3de2365d --- /dev/null +++ b/gallery/Screens.svelte @@ -0,0 +1,260 @@ + + +
+ +
+ {#each routes as p (p.label)} + {@const active = routeActive(p)} + + {/each} +
+ + +
+ Variant: + +
+ + +
+ Empty states: + {#each scenarios as s (s.id)} + + {/each} +
+ + +
+ Project: + {#each features as f (f.id)} + + {/each} +
+ + +
+ + +
+ + +
+ +
+ + {#if route.name === "index"} + + {:else if route.name === "push"} + + {:else if route.name === "pull"} + + {:else if route.name === "settings"} + + {:else if route.name === "connect"} + + {:else if route.name === "stringDetails"} + + {:else if route.name === "pageSetup"} + + {:else if route.name === "copyView"} + + {:else if route.name === "createCopy"} + + {:else} + + {/if} + + {#snippet failed(error, reset)} +
+

+ This screen needs a bit more mock data to render fully. +

+

+ {error instanceof Error ? error.message : String(error)} +

+ +
+ {/snippet} +
+ +
+
+
+ +

+ Tip: this is the real plugin UI wired to mock data. Clicking + Push/Pull/Settings inside the frame navigates for real. Screens that fetch + from Tolgee show their empty/error state (no backend here). +

+
diff --git a/gallery/app.css b/gallery/app.css new file mode 100644 index 00000000..6ee0a67a --- /dev/null +++ b/gallery/app.css @@ -0,0 +1,13 @@ +/* + * Tailwind entry for the gallery. + * + * Reuses the plugin's real stylesheet (Tailwind import + `@theme` token + * mappings + Tolgee brand overrides). But because the gallery's Vite root is + * `gallery/`, Tailwind v4 only auto-scans that folder for class names — so + * utilities that live exclusively in `src/ui` components (e.g. the Button + * variant classes) would never be generated. Register those sources here. + */ +@import "../src/ui/styles.css"; + +@source "../src/ui"; +@source "../gallery"; diff --git a/gallery/index.html b/gallery/index.html new file mode 100644 index 00000000..4f346bb0 --- /dev/null +++ b/gallery/index.html @@ -0,0 +1,12 @@ + + + + + + Tolgee Plugin — Design System + + +
+ + + diff --git a/gallery/main.ts b/gallery/main.ts new file mode 100644 index 00000000..0268b3d9 --- /dev/null +++ b/gallery/main.ts @@ -0,0 +1,12 @@ +import { mount } from "svelte"; +// Tailwind entry: pulls in the plugin's real stylesheet and registers `src/ui` +// as a scan source so component-only utilities are generated too. +import "./app.css"; +// Browser-only fallbacks for the `--figma-color-*` tokens that Figma injects +// at runtime but don't exist in a plain browser. +import "./theme.css"; +import App from "./App.svelte"; + +const target = document.getElementById("app"); +if (!target) throw new Error("#app missing"); +mount(App, { target }); diff --git a/gallery/mock.ts b/gallery/mock.ts new file mode 100644 index 00000000..20cbf888 --- /dev/null +++ b/gallery/mock.ts @@ -0,0 +1,376 @@ +import { shouldIgnoreNode } from "$main/nodes/filter"; +import type { NodeInfo, TolgeeConfig } from "$shared/types"; +import { appState } from "$ui/lib/stores/app.svelte"; +import { auth } from "$ui/lib/stores/auth.svelte"; + +/** + * Seed the singleton stores with believable demo data so the real route + * components render a fully populated UI in a plain browser — no Figma main + * thread and no Tolgee backend required. + * + * The TanStack queries inside the routes are gated on `authenticated` / + * `branchingEnabled` / having a selection, so with branching off and a + * non-empty selection the Index view never hits the network. Routes that *do* + * fetch (Push/Pull/…) get a Proxy "client" whose every method rejects, which + * lands them in their normal error/empty state instead of crashing. + */ + +/** Sample Tolgee keys so the Connect screen renders real result rows. */ +const SAMPLE_KEYS = [ + { id: 1, name: "Component_button_my_string", namespace: null, translation: "Bla bla bla", baseTranslation: "Bla bla bla", plural: false }, + { id: 2, name: "Component_button_save", namespace: null, translation: "My string", baseTranslation: "My string", plural: false }, + { id: 3, name: "Component_button_count", namespace: "common", translation: "My string 2", baseTranslation: "My string 2", plural: true }, + { id: 4, name: "Component_button_save_3", namespace: null, translation: "My string 3", baseTranslation: "My string 3", plural: false }, + // Exact match for the "Submit" sample nodes → "Connect by exact match" links them. + { id: 5, name: "submit", namespace: "common", translation: "Submit", baseTranslation: "Submit", plural: false }, + // Two keys with the SAME text "Welcome" → ambiguous (skipped & listed). + { id: 6, name: "page.welcome", namespace: null, translation: "Welcome", baseTranslation: "Welcome", plural: false }, + { id: 7, name: "home.welcome", namespace: "home", translation: "Welcome", baseTranslation: "Welcome", plural: false }, +]; + +/** + * Client stand-in — a plain object (NOT a Proxy: Svelte's `$state` wraps the + * stored value and a Proxy's custom traps don't survive that, so methods must + * be real own-properties). `GET /…/keys/search` returns sample keys so the + * Connect screen has rows; everything else resolves to an empty/error result. + */ +const mockClient = { + // biome-ignore lint/suspicious/noExplicitAny: loose mock signature + GET: async (path: string, opts?: any) => { + if (typeof path === "string" && path.includes("keys/search")) { + // Filter the sample keys by the search term (key name + translations) so + // the Connect screen behaves realistically — and so a non-matching query + // shows its empty state. + const search = String(opts?.params?.query?.search ?? "") + .trim() + .toLowerCase(); + const keys = search + ? SAMPLE_KEYS.filter( + (k) => + k.name.toLowerCase().includes(search) || + (k.translation ?? "").toLowerCase().includes(search) || + (k.baseTranslation ?? "").toLowerCase().includes(search), + ) + : SAMPLE_KEYS; + return { data: { _embedded: { keys } } }; + } + if (typeof path === "string" && path.includes("projects/stats")) { + // Project-wide key total, so Connect's empty state can say "0 of N keys". + return { data: { keyCount: 42 } }; + } + if (typeof path === "string" && path.includes("projects/tags")) { + // Existing project tags for the Settings → Add tags autocomplete. + const search = String(opts?.params?.query?.search ?? "") + .trim() + .toLowerCase(); + const all = ["mobile", "web", "marketing", "v2", "draft", "legal"]; + const tags = (search + ? all.filter((t) => t.includes(search)) + : all + ).map((name, id) => ({ id, name })); + return { data: { _embedded: { tags } } }; + } + return { data: null, error: "mock: no backend" }; + }, + POST: async () => ({ data: null, error: "mock: no backend" }), + PUT: async () => ({ data: null, error: "mock: no backend" }), + DELETE: async () => ({ data: null, error: "mock: no backend" }), +}; + +export function makeNode(partial: Partial & { id: string }): NodeInfo { + return { + name: "", + characters: "", + translation: "", + isPlural: false, + key: "", + ns: undefined, + connected: false, + visible: true, + ...partial, + }; +} + +/** + * The full, *unfiltered* selection — what the Figma main thread would scan + * before applying the ignore rules. Includes a numbers-only layer and an + * `_`-prefixed layer so the ignore filters have something to act on. The + * gallery runs these through the real `shouldIgnoreNode` (see `applyFilters`). + */ +export const allSampleNodes: NodeInfo[] = [ + makeNode({ + id: "n1", + name: "Date label", + characters: "Wednesday, June 12", + key: "home.day", + connected: false, + }), + makeNode({ + id: "n2", + name: "Greeting", + characters: "Hello, Alex", + key: "home.intro", + connected: false, + isPlural: true, + }), + makeNode({ + id: "n3", + name: "CTA", + characters: "Get started", + translation: "Get started", + key: "home.cta", + ns: "common", + connected: true, + }), + // Numbers-only (incl. formatting) → hidden while "ignore numbers" is on + // (default). Matches digits + spaces + . , + - so "1,234.00" counts too. + makeNode({ + id: "n4", + name: "Price", + characters: "1,234.00", + connected: false, + }), + // Layer name starts with the ignore prefix ("_") → hidden once "ignore + // strings with prefix" is toggled on. + makeNode({ + id: "n5", + name: "_internal note", + characters: "Do not translate", + connected: false, + }), + // Two layers with identical text → each shows a "×2" duplicate badge. + makeNode({ id: "n6", name: "CTA copy", characters: "Submit", connected: false }), + makeNode({ id: "n7", name: "CTA copy 2", characters: "Submit", connected: false }), + // Two layers with the SAME key but DIFFERENT text → push conflict warning. + makeNode({ id: "n8", name: "Title A", characters: "Welcome", key: "page.title", connected: false }), + makeNode({ id: "n9", name: "Title B", characters: "Hello there", key: "page.title", connected: false }), + // Advanced (plural) CONNECTED string the user never edited — its stored ICU + // Plural with NO stored sample params: the render-compare can't run reliably + // (it would compare raw `{count, plural, …}` to "10 apples"), so the guard + // SKIPS it — must NOT be flagged. + makeNode({ + id: "n10", + name: "Apples count", + characters: "10 apples", + translation: "{count, plural, one {# apple} other {# apples}}", + pluralParamValue: "count", + isPlural: true, + key: "special.apples", + connected: true, + }), + // Plain connected string edited directly in Figma: a normal "changed" push, + // NOT a manual-change conflict → must NOT be flagged (advanced-only). + makeNode({ + id: "n11", + name: "Plain label", + characters: "Edited in Figma", + translation: "Saved label", + key: "home.label", + connected: true, + }), + // ADVANCED string WITH params edited directly in Figma: renders to "Hi, Alex" + // but the canvas was overwritten → genuinely diverged → SHOULD be flagged. + makeNode({ + id: "n12", + name: "Greeting param", + characters: "manually rewritten", + translation: "Hi, {name}", + paramsValues: { name: "Alex" }, + key: "home.greeting", + connected: true, + }), + // Inline markup only (no params) → "Formatted" badge from the tag alone. + makeNode({ + id: "n15", + name: "Rich label", + characters: "Read the terms", + translation: "Read the terms", + key: "legal.terms", + connected: true, + }), + // CONNECTED same-key conflict: two strings connected to one key but with + // DIFFERENT Figma text. They share the Tolgee translation, so a translation + // compare would miss it — the conflict must be detected on `characters`. + makeNode({ + id: "n13", + name: "Brown A", + characters: "brown 111", + translation: "brown", + key: "generatedkey.brown", + connected: true, + }), + makeNode({ + id: "n14", + name: "Brown B", + characters: "brown", + translation: "brown", + key: "generatedkey.brown", + connected: true, + }), +]; + +/** + * Replicates the main thread's selection filtering: keep connected nodes + * always, otherwise drop anything `shouldIgnoreNode` rejects under the current + * config. NodeInfo carries the same `name`/`characters`/`visible` fields the + * filter reads, so we can feed it straight in. + */ +function applyFilters(config: Partial): NodeInfo[] { + return allSampleNodes.filter( + (n) => + n.connected || + !shouldIgnoreNode(n as unknown as TextNode, null, config), + ); +} + +/** Mirrors the screenshot: the post-filter view of the selection. */ +export const sampleNodes: NodeInfo[] = allSampleNodes; + +/** A single node for the node-scoped routes (Connect / String details). */ +export const sampleNode: NodeInfo = sampleNodes[0]; + +/** A plural node (proper ICU plural translation) so the gallery can show the + * chip-based PluralEditor + locked Plural checkbox in String details. */ +export const samplePluralNode: NodeInfo = + allSampleNodes.find((n) => n.id === "n10") ?? sampleNodes[0]; + +// --- Selection scenarios for the gallery's "Empty states" demos ------------- +// Index decides what to render from the selection store, so these flip it into +// each state the real plugin can be in. `hasUserSelection` is the second arg: +// true = the user has a Figma selection (a frame/layers) +// false = nothing selected (Index falls back to page-connected nodes) + +/** Normal demo: a real selection with translatable strings. */ +export function showSampleSelection(): void { + appState.setSelection(applyFilters(appState.value.config ?? {}), true); +} + +/** + * A selection exists (e.g. a frame) but it has no translatable strings — all + * text was filtered out, or the frame holds no text. Index shows + * "No translatable strings in your selection". + */ +export function showEmptySelection(): void { + appState.setSelection([], true); +} + +/** + * Nothing selected at all and no connected nodes on the page. Index shows + * "Select texts for translation". (In the gallery the page-node query never + * resolves, so it stays empty — exactly the empty-page case.) + */ +export function showNoSelection(): void { + appState.setSelection([], false); +} + +// --- Project-feature scenarios ---------------------------------------------- +// Flip branching / namespaces on the auth store (and seed branches) so the +// branch picker (header), namespace badges (rows), stale-branch banner (Index) +// and copy branch indicators become visible — none of which show under the +// default "simple" project. + +const SAMPLE_BRANCHES = [{ name: "main", isDefault: true }, { name: "feature-x" }]; + +/** Default: no branching, no namespaces — the plain single-branch project. */ +export function showSimpleProject(): void { + auth.setProjectFeatures({ + branchingEnabled: false, + namespacesEnabled: false, + projectName: "Figma 2.0", + }); + appState.setConfig({ ...(appState.value.config ?? {}), branch: "" }); +} + +/** Branching + namespaces enabled: header branch picker, row ns badges. */ +export function showBranchingProject(): void { + auth.setProjectFeatures({ + branchingEnabled: true, + namespacesEnabled: true, + projectName: "Figma 2.0", + }); + auth.setBranches(SAMPLE_BRANCHES, "main", true); + appState.setConfig({ ...(appState.value.config ?? {}), branch: "main" }); +} + +/** Branching enabled, but the CONFIGURED branch no longer exists on Tolgee → + * Index shows the "branch no longer exists" banner + a picker to recover. */ +export function showStaleBranchProject(): void { + auth.setProjectFeatures({ + branchingEnabled: true, + namespacesEnabled: true, + projectName: "Figma 2.0", + }); + auth.setBranches(SAMPLE_BRANCHES, "main", true); + appState.setConfig({ ...(appState.value.config ?? {}), branch: "old-release" }); +} + +let seeded = false; +export function seedMockData(): void { + if (seeded) return; + seeded = true; + + auth.setAuth({ + // biome-ignore lint/suspicious/noExplicitAny: mock stand-in for TolgeeClient + client: mockClient as any, + apiUrl: "https://app.tolgee.io", + apiKey: "tgpak_demo", + projectId: 1, + scopes: ["translations.view", "translations.edit", "keys.edit", "keys.create"], + }); + auth.setProjectFeatures({ + branchingEnabled: false, + namespacesEnabled: false, + projectName: "Figma 2.0", + }); + auth.setLanguages([ + { tag: "en", name: "English" }, + { tag: "de", name: "German" }, + { tag: "fr", name: "French" }, + { tag: "cs", name: "Czech" }, + ]); + auth.setNamespaces([{ name: "common" }, { name: "home" }]); + + const initialConfig: Partial = { + language: "en", + namespace: "", + // Ignore-filter defaults so the demo shows real behaviour: numbers hidden + // by default; a prefix is set but its toggle starts off. + ignoreNumbers: true, + ignoreHiddenLayers: true, + ignoreTextLayers: false, + ignorePrefix: "_", + // Prefill key format on, so keyless unconnected rows get a generated key + // (which must now PERSIST + reach push, like the old plugin). + prefillKeyFormat: true, + keyFormat: "generatedkey.{elementText}", + variableCasing: "snake_case", + }; + appState.setConfig(initialConfig); + appState.setSelection(applyFilters(initialConfig), true); + + // Stand in for the Figma main thread: the UI's `send()` posts UI→main + // messages to `parent` (== this window in the gallery). We mirror the two + // main-thread paths the UI relies on so changes are actually reflected: + // • save-config → re-run the ignore filter and push the new selection back + // (like the real `save-config` → `emitSelection`). + // • set-nodes-data → merge the partial info onto the matching nodes and + // re-emit the selection (like the real handler), so bulk Connect / + // Disconnect / Edit key name visibly update the list. + window.addEventListener("message", (event) => { + const msg = event.data?.pluginMessage; + if (!msg) return; + if (msg.type === "save-config") { + const merged = { ...(appState.value.config ?? {}), ...msg.config }; + appState.setConfig(merged); + appState.setSelection(applyFilters(merged), true); + return; + } + if (msg.type === "set-nodes-data" && Array.isArray(msg.nodes)) { + for (const upd of msg.nodes) { + const node = allSampleNodes.find((n) => n.id === upd.id); + if (node) Object.assign(node, upd.info); + } + appState.setSelection(applyFilters(appState.value.config ?? {}), true); + } + }); +} diff --git a/gallery/theme.css b/gallery/theme.css new file mode 100644 index 00000000..2170d72b --- /dev/null +++ b/gallery/theme.css @@ -0,0 +1,50 @@ +/* + * Figma runtime-token fallbacks for the gallery. + * + * Inside Figma, the plugin iframe inherits `--figma-color-*` custom properties + * that reflect the user's current (light/dark) UI theme. Outside Figma those + * don't exist, so we approximate Figma's own light + dark palettes here. The + * brand tokens are intentionally NOT redefined — `src/ui/styles.css` already + * maps them to Tolgee pink and is imported before this file. + * + * Dark mode is toggled by adding the `.figma-dark` class to , matching + * the `@custom-variant dark` declaration in `styles.css`. + */ + +:root { + --figma-color-bg: #ffffff; + --figma-color-bg-secondary: #f5f5f5; + --figma-color-bg-hover: rgba(0, 0, 0, 0.06); + --figma-color-bg-danger: #f24822; + --figma-color-bg-danger-tertiary: rgba(242, 72, 34, 0.1); + --figma-color-text: rgba(0, 0, 0, 0.9); + --figma-color-text-secondary: rgba(0, 0, 0, 0.5); + --figma-color-text-danger: #b3261e; + --figma-color-text-ondanger: #ffffff; + --figma-color-border-danger: rgba(242, 72, 34, 0.5); + --figma-color-border: rgba(0, 0, 0, 0.1); + --figma-color-icon: rgba(0, 0, 0, 0.9); + --figma-color-icon-secondary: rgba(0, 0, 0, 0.3); +} + +.figma-dark { + --figma-color-bg: #2c2c2c; + --figma-color-bg-secondary: #383838; + --figma-color-bg-hover: rgba(255, 255, 255, 0.08); + --figma-color-bg-danger: #f24822; + --figma-color-bg-danger-tertiary: rgba(242, 72, 34, 0.18); + --figma-color-text: rgba(255, 255, 255, 0.9); + --figma-color-text-secondary: rgba(255, 255, 255, 0.5); + --figma-color-text-danger: #ff9d8f; + --figma-color-text-ondanger: #ffffff; + --figma-color-border-danger: rgba(242, 72, 34, 0.6); + --figma-color-border: rgba(255, 255, 255, 0.1); + --figma-color-icon: rgba(255, 255, 255, 0.9); + --figma-color-icon-secondary: rgba(255, 255, 255, 0.4); +} + +/* The gallery is a full-page document (the plugin UI is a small panel), so let + * it scroll and give it comfortable padding. */ +body { + min-height: 100%; +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 3d16de99..00000000 --- a/package-lock.json +++ /dev/null @@ -1,13818 +0,0 @@ -{ - "name": "@tolgee/figma-plugin", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@tolgee/figma-plugin", - "version": "0.0.1", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.18.6", - "@create-figma-plugin/ui": "3.2.0", - "@create-figma-plugin/utilities": "3.2.0", - "@floating-ui/dom": "^1.6.13", - "@tginternal/editor": "^1.15.2", - "clsx": "^1.2.1", - "codemirror": "^6.0.1", - "dompurify": "^3.2.5", - "intl-messageformat": "^10.7.15", - "preact": "^10.11.3", - "react-query": "^3.39.3", - "scheduler": "^0.23.2", - "use-context-selector": "^1.4.1", - "use-debounce": "^9.0.3" - }, - "devDependencies": { - "@create-figma-plugin/build": "^3.2.0", - "@create-figma-plugin/tsconfig": "^3.2.0", - "@figma/plugin-typings": "^1.89.0", - "@types/jest": "^29.4.0", - "@typescript-eslint/eslint-plugin": "^8.56.1", - "@typescript-eslint/parser": "^8.56.1", - "colors": "^1.4.0", - "concurrently": "^7.6.0", - "cypress": "^14.3.3", - "cypress-iframe": "^1.0.1", - "esbuild": "^0.27.3", - "eslint": "^9.7", - "eslint-config-preact": "^2.0.0", - "eslint-plugin-jest": "^29.15.0", - "eslint-plugin-prettier": "^5.5.5", - "eslint-plugin-react": "^7.37.5", - "jest": "^29.3.1", - "nodemon": "^3.1.11", - "openapi-typescript": "^7.12.0", - "prettier": "^3.8.1", - "serve": "^14.2.0", - "terminate": "^2.6.1", - "ts-node": "^10.9.1", - "typescript": "^5.4.5" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/eslint-parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.6.tgz", - "integrity": "sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0", - "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" - } - }, - "node_modules/@babel/eslint-parser/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/@babel/eslint-parser/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@codemirror/autocomplete": { - "version": "6.20.1", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", - "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", - "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.12.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", - "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.5.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.9.5", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", - "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.35.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", - "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.37.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", - "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.39.16", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.16.tgz", - "integrity": "sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.5.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@create-figma-plugin/build": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@create-figma-plugin/build/-/build-3.2.1.tgz", - "integrity": "sha512-AOTrz6bH71FZa1S2j4zCGwqisOWlafOFyv2r04wFAIDtS8S6zFdmX3143kkYC5tSu20/h6a8yj8VrYM4aqtzaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@create-figma-plugin/common": "^3.2.1", - "chokidar": "^3.6.0", - "cssnano": "^7.0.5", - "esbuild": "0.23.0", - "find-up": "^7.0.0", - "globby": "^14.0.2", - "indent-string": "^5.0.0", - "kleur": "^4.1.5", - "path-exists": "^5.0.0", - "postcss": "^8.4.41", - "postcss-modules": "^6.0.0", - "rev-hash": "^4.1.0", - "sade": "^1.8.1", - "temp-write": "^6.0.0", - "tempy": "^3.1.0", - "typed-css-modules": "^0.9.1" - }, - "bin": { - "build-figma-plugin": "lib/cli.js" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@figma/plugin-typings": ">=1", - "typescript": ">=4" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@create-figma-plugin/build/node_modules/esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" - } - }, - "node_modules/@create-figma-plugin/common": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@create-figma-plugin/common/-/common-3.2.1.tgz", - "integrity": "sha512-jf7c0F2p2GFrj2d2xJY539flMlF/grUa2IY6mdLugHTDqFdcy7NlxIAY36TMtd1A3zG1pRFG7V0j0JttiwdIaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/slugify": "^2.2.1", - "kleur": "^4.1.5", - "path-exists": "^5.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@create-figma-plugin/tsconfig": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@create-figma-plugin/tsconfig/-/tsconfig-3.2.1.tgz", - "integrity": "sha512-jayhlYYTUAlGvDeL2GXuWZSWZE9xkYG9qU8Uy6EQMKcUHW4bFW9G1xoDGGoSh8rtvjxmUGpQM0BaKdJGzaI2DQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@create-figma-plugin/ui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@create-figma-plugin/ui/-/ui-3.2.0.tgz", - "integrity": "sha512-E0is67aaoVECDgSMAlkp584wI24yglENH6M4OIJwDMbHBXidIbtAk9RIbUDr0gC+2NWNRLpIyFQH6LnZcqO+0A==", - "license": "MIT", - "dependencies": { - "@create-figma-plugin/utilities": "^3.2.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "preact": ">=10" - } - }, - "node_modules/@create-figma-plugin/utilities": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@create-figma-plugin/utilities/-/utilities-3.2.0.tgz", - "integrity": "sha512-+5JddDqI6XjVJs0EqgLd+6B9ukjhHFEBesqPI/e0okqS+Ay7Gt3xsD0vMrZjVYfJcmRAprgJhdGBGJZjOZiQIA==", - "license": "MIT", - "dependencies": { - "hex-rgb": "^5.0.0", - "natural-compare-lite": "1.4.0", - "rgb-hex": "^4.1.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@cypress/request": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz", - "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~4.0.4", - "http-signature": "~1.4.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "performance-now": "^2.1.0", - "qs": "~6.14.1", - "safe-buffer": "^5.1.2", - "tough-cookie": "^5.0.0", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", - "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" - } - }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", - "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.3", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", - "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@figma/plugin-typings": { - "version": "1.123.0", - "resolved": "https://registry.npmjs.org/@figma/plugin-typings/-/plugin-typings-1.123.0.tgz", - "integrity": "sha512-NLv2aQ8R9dP5psDplWpq+pJxRUGsJ1YEYYbBV2oTd03kS+aau7N9XWLjw52s1uVgi8jQ33N001EX3f7vSCztjQ==", - "dev": true, - "license": "MIT License" - }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, - "node_modules/@formatjs/ecma402-abstract": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", - "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", - "license": "MIT", - "dependencies": { - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/intl-localematcher": "0.6.2", - "decimal.js": "^10.4.3", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/fast-memoize": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", - "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", - "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "@formatjs/icu-skeleton-parser": "1.8.16", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.16", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", - "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/intl-localematcher": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", - "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/console/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@lezer/common": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", - "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", - "license": "MIT" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", - "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.3.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", - "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "license": "MIT" - }, - "node_modules/@mdn/browser-compat-data": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-6.1.5.tgz", - "integrity": "sha512-PzdZZzRhcXvKB0begee28n5lvwAcinGKYuLZOVxHAZm+n7y01ddEGfdS1ZXRuVcV+ndG6mSEAE8vgudom5UjYg==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-scope": "5.1.1" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@redocly/ajv": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", - "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js-replace": "^1.0.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/@redocly/config": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", - "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@redocly/openapi-core": { - "version": "1.34.10", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.10.tgz", - "integrity": "sha512-XCBR/9WHJ0cpezuunHMZjuFMl4KqUo7eiFwzrQrvm7lTXt0EBd3No8UY+9OyzXpDfreGEMMtxmaLZ+ksVw378g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@redocly/ajv": "8.11.2", - "@redocly/config": "0.22.0", - "colorette": "1.4.0", - "https-proxy-agent": "7.0.6", - "js-levenshtein": "1.1.6", - "js-yaml": "4.1.1", - "minimatch": "5.1.9", - "pluralize": "8.0.0", - "yaml-ast-parser": "0.0.43" - }, - "engines": { - "node": ">=18.17.0", - "npm": ">=9.5.0" - } - }, - "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@redocly/openapi-core/node_modules/colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@redocly/openapi-core/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sindresorhus/slugify": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", - "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/transliterate": "^1.0.0", - "escape-string-regexp": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sindresorhus/transliterate": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", - "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@tginternal/editor": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@tginternal/editor/-/editor-1.15.2.tgz", - "integrity": "sha512-Ylb9kSCz/pNP/Xk8taFXaie9i5wHRiFvRYZcZSiAfqeSLjdaUYDBNnHOkTLcb8X8OUibId0SK+bNa8YdqKFozg==", - "peerDependencies": { - "@codemirror/lint": "^6.4.2", - "@codemirror/state": "^6.4.0", - "@codemirror/view": "^6.23.1" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/cypress": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@types/cypress/-/cypress-1.1.6.tgz", - "integrity": "sha512-CfeLLD3+6vIWe2AO5hR63f1c8EbRzrp/j1ExubAwOTpwZFZvF3Nm9cOPQiUwzNmAUmZuhO0QVH98Qlujni6nPw==", - "deprecated": "This is a stub types definition. cypress provides its own type definitions, so you do not need this installed.", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "cypress": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.3.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", - "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", - "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sizzle": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", - "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.56.1", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@zeit/schemas": { - "version": "2.36.0", - "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", - "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aggregate-error/node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/ast-metadata-inferer": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/ast-metadata-inferer/-/ast-metadata-inferer-0.8.1.tgz", - "integrity": "sha512-ht3Dm6Zr7SXv6t1Ra6gFo0+kLDglHGrEbYihTkcycrbHw7WCcuhBzPlJYHEsIpycaUwzsJHje+vUcxXUX4ztTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mdn/browser-compat-data": "^5.6.19" - } - }, - "node_modules/ast-metadata-inferer/node_modules/@mdn/browser-compat-data": { - "version": "5.7.6", - "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.7.6.tgz", - "integrity": "sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", - "dev": true, - "license": "MIT" - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-jest/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "license": "Unlicense", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/blob-util": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", - "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, - "node_modules/boxen": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", - "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.0", - "chalk": "^5.0.1", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/boxen/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/boxen/node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/boxen/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/boxen/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/boxen/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/broadcast-channel": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", - "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "detect-node": "^2.1.0", - "js-sha3": "0.8.0", - "microseconds": "0.2.0", - "nano-time": "1.0.0", - "oblivious-set": "1.0.0", - "rimraf": "3.0.2", - "unload": "2.2.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cachedir": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", - "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001777", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", - "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk-template": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", - "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/chalk-template?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/change-case": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", - "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/check-more-types": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", - "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-table3": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", - "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "colors": "1.4.0" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clipboardy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", - "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "arch": "^2.2.0", - "execa": "^5.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clipboardy/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/clipboardy/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clipboardy/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/codemirror": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", - "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/concurrently": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz", - "integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "date-fns": "^2.29.1", - "lodash": "^4.17.21", - "rxjs": "^7.0.0", - "shell-quote": "^1.7.3", - "spawn-command": "^0.0.2-1", - "supports-color": "^8.1.0", - "tree-kill": "^1.2.2", - "yargs": "^17.3.1" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/css-declaration-sorter": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.1.tgz", - "integrity": "sha512-gz6x+KkgNCjxq3Var03pRYLhyNfwhkKF1g/yoLgDNtFvVu0/fOLV9C8fFEZRjACp/XQLumjAYo7JVjzH3wLbxA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.2.tgz", - "integrity": "sha512-HYOPBsNvoiFeR1eghKD5C3ASm64v9YVyJB4Ivnl2gqKoQYvjjN/G0rztvKQq8OxocUtC6sjqY8jwYngIB4AByA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-preset-default": "^7.0.10", - "lilconfig": "^3.1.3" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/cssnano-preset-default": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.10.tgz", - "integrity": "sha512-6ZBjW0Lf1K1Z+0OKUAUpEN62tSXmYChXWi2NAA0afxEVsj9a+MbcB1l5qel6BHJHmULai2fCGRthCeKSFbScpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.27.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^5.0.1", - "postcss-calc": "^10.1.1", - "postcss-colormin": "^7.0.5", - "postcss-convert-values": "^7.0.8", - "postcss-discard-comments": "^7.0.5", - "postcss-discard-duplicates": "^7.0.2", - "postcss-discard-empty": "^7.0.1", - "postcss-discard-overridden": "^7.0.1", - "postcss-merge-longhand": "^7.0.5", - "postcss-merge-rules": "^7.0.7", - "postcss-minify-font-values": "^7.0.1", - "postcss-minify-gradients": "^7.0.1", - "postcss-minify-params": "^7.0.5", - "postcss-minify-selectors": "^7.0.5", - "postcss-normalize-charset": "^7.0.1", - "postcss-normalize-display-values": "^7.0.1", - "postcss-normalize-positions": "^7.0.1", - "postcss-normalize-repeat-style": "^7.0.1", - "postcss-normalize-string": "^7.0.1", - "postcss-normalize-timing-functions": "^7.0.1", - "postcss-normalize-unicode": "^7.0.5", - "postcss-normalize-url": "^7.0.1", - "postcss-normalize-whitespace": "^7.0.1", - "postcss-ordered-values": "^7.0.2", - "postcss-reduce-initial": "^7.0.5", - "postcss-reduce-transforms": "^7.0.1", - "postcss-svgo": "^7.1.0", - "postcss-unique-selectors": "^7.0.4" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/cssnano-utils": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.1.tgz", - "integrity": "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/cypress": { - "version": "14.5.4", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.4.tgz", - "integrity": "sha512-0Dhm4qc9VatOcI1GiFGVt8osgpPdqJLHzRwcAB5MSD/CAAts3oybvPUPawHyvJZUd8osADqZe/xzMsZ8sDTjXw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@cypress/request": "^3.0.9", - "@cypress/xvfb": "^1.2.4", - "@types/sinonjs__fake-timers": "8.1.1", - "@types/sizzle": "^2.3.2", - "arch": "^2.2.0", - "blob-util": "^2.0.2", - "bluebird": "^3.7.2", - "buffer": "^5.7.1", - "cachedir": "^2.3.0", - "chalk": "^4.1.0", - "check-more-types": "^2.24.0", - "ci-info": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-table3": "0.6.1", - "commander": "^6.2.1", - "common-tags": "^1.8.0", - "dayjs": "^1.10.4", - "debug": "^4.3.4", - "enquirer": "^2.3.6", - "eventemitter2": "6.4.7", - "execa": "4.1.0", - "executable": "^4.1.1", - "extract-zip": "2.0.1", - "figures": "^3.2.0", - "fs-extra": "^9.1.0", - "getos": "^3.2.1", - "hasha": "5.2.2", - "is-installed-globally": "~0.4.0", - "lazy-ass": "^1.6.0", - "listr2": "^3.8.3", - "lodash": "^4.17.21", - "log-symbols": "^4.0.0", - "minimist": "^1.2.8", - "ospath": "^1.2.2", - "pretty-bytes": "^5.6.0", - "process": "^0.11.10", - "proxy-from-env": "1.0.0", - "request-progress": "^3.0.0", - "semver": "^7.7.1", - "supports-color": "^8.1.1", - "tmp": "~0.2.3", - "tree-kill": "1.2.2", - "untildify": "^4.0.0", - "yauzl": "^2.10.0" - }, - "bin": { - "cypress": "bin/cypress" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - } - }, - "node_modules/cypress-iframe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cypress-iframe/-/cypress-iframe-1.0.1.tgz", - "integrity": "sha512-Ne+xkZmWMhfq3x6wbfzK/SzsVTCrJru3R3cLXsoSAZyfUtJDamXyaIieHXeea3pQDXF4wE2w4iUuvCYHhoD31g==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/cypress": "^1.1.0" - } - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, - "node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT" - }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/dompurify": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", - "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "engines": { - "node": ">=20" - }, - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", - "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", - "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.3", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-preact": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-preact/-/eslint-config-preact-2.0.0.tgz", - "integrity": "sha512-TFj70lEE7y3R9DQAFJ/clRfVmyaXdwE3q56gA9zm+iTmlpYjtZKtV1jv/jtgdF2LqgvJjlGlGE1rHVwE9yNdkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.13.16", - "@babel/eslint-parser": "^7.27.5", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-jsx": "^7.12.13", - "@eslint/js": "^9.29.0", - "eslint-plugin-compat": "^6.0.2", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", - "globals": "^16.2.0" - }, - "peerDependencies": { - "eslint": "^8.57.1 || ^9.0.0" - } - }, - "node_modules/eslint-config-preact/node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-compat": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-6.2.1.tgz", - "integrity": "sha512-gLKqUH+lQcCL+HzsROUjBDvakc5Zaga51Y4ZAkPCXc41pzKBfyluqTr2j8zOx8QQQb7zyglu1LVoL5aSNWf2SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mdn/browser-compat-data": "^6.1.1", - "ast-metadata-inferer": "^0.8.1", - "browserslist": "^4.25.2", - "find-up": "^5.0.0", - "globals": "^15.7.0", - "lodash.memoize": "^4.1.2", - "semver": "^7.6.2" - }, - "engines": { - "node": ">=18.x" - }, - "peerDependencies": { - "eslint": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-compat/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-compat/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-compat/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-compat/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-compat/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint-plugin-jest": { - "version": "29.15.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.15.0.tgz", - "integrity": "sha512-ZCGr7vTH2WSo2hrK5oM2RULFmMruQ7W3cX7YfwoTiPfzTGTFBMmrVIz45jZHd++cGKj/kWf02li/RhTGcANJSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^8.0.0" - }, - "engines": { - "node": "^20.12.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^8.0.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "jest": "*", - "typescript": ">=4.8.4 <6.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", - "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.1", - "synckit": "^0.11.12" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, - "node_modules/eventemitter2": { - "version": "6.4.7", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", - "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/executable": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", - "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", - "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^7.2.0", - "path-exists": "^5.0.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", - "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true, - "license": "MIT" - }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/generic-names": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz", - "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^3.2.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/getos": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", - "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^3.2.0" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", - "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.3", - "ignore": "^7.0.3", - "path-type": "^6.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/globby/node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasha": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", - "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-stream": "^2.0.0", - "type-fest": "^0.8.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hasha/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hex-rgb": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-5.0.0.tgz", - "integrity": "sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-signature": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", - "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^2.0.2", - "sshpk": "^1.18.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8.12.0" - } - }, - "node_modules/icss-replace-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", - "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==", - "dev": true, - "license": "ISC" - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/index-to-position": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", - "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/intl-messageformat": { - "version": "10.7.18", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", - "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", - "license": "BSD-3-Clause", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/icu-messageformat-parser": "2.11.4", - "tslib": "^2.8.0" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-port-reachable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", - "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-there": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/is-there/-/is-there-4.5.2.tgz", - "integrity": "sha512-ixMkfz3rtS1vEsLf0TjgjqUn96Q0ukpUVDMnPYVocJyTzu2G/QgEtqYddcHZawHO+R31cKVPggJmBLrm1vJCOg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-changed-files/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/jest-changed-files/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-changed-files/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve/node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/jest-resolve/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runtime/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true, - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsprim": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", - "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lazy-ass": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", - "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "> 0.8" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", - "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.1", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, - "node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true - }, - "node_modules/match-sorter": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", - "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.8", - "remove-accents": "0.5.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/microseconds": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", - "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==", - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nano-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", - "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", - "license": "ISC", - "dependencies": { - "big-integer": "^1.6.16" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-exports-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/node-exports-info/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nodemon": { - "version": "3.1.14", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", - "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^10.2.1", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/oblivious-set": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", - "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==", - "license": "MIT" - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openapi-typescript": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", - "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@redocly/openapi-core": "^1.34.6", - "ansi-colors": "^4.1.3", - "change-case": "^5.4.4", - "parse-json": "^8.3.0", - "supports-color": "^10.2.2", - "yargs-parser": "^21.1.1" - }, - "bin": { - "openapi-typescript": "bin/cli.js" - }, - "peerDependencies": { - "typescript": "^5.x" - } - }, - "node_modules/openapi-typescript/node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openapi-typescript/node_modules/supports-color": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/openapi-typescript/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ospath": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", - "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", - "dev": true, - "license": "MIT" - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate/node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-to-regexp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", - "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", - "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dev": true, - "license": [ - "MIT", - "Apache2" - ], - "dependencies": { - "through": "~2.3" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-calc": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", - "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12 || ^20.9 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.38" - } - }, - "node_modules/postcss-colormin": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.5.tgz", - "integrity": "sha512-ekIBP/nwzRWhEMmIxHHbXHcMdzd1HIUzBECaj5KEdLz9DVP2HzT065sEhvOx1dkLjYW7jyD0CngThx6bpFi2fA==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.27.0", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-convert-values": { - "version": "7.0.8", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.8.tgz", - "integrity": "sha512-+XNKuPfkHTCEo499VzLMYn94TiL3r9YqRE3Ty+jP7UX4qjewUONey1t7CG21lrlTLN07GtGM8MqFVp86D4uKJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.27.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-discard-comments": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.5.tgz", - "integrity": "sha512-IR2Eja8WfYgN5n32vEGSctVQ1+JARfu4UH8M7bgGh1bC+xI/obsPJXaBpQF7MAByvgwZinhpHpdrmXtvVVlKcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.1.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.2.tgz", - "integrity": "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-discard-empty": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.1.tgz", - "integrity": "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.1.tgz", - "integrity": "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.5.tgz", - "integrity": "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^7.0.5" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-merge-rules": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.7.tgz", - "integrity": "sha512-njWJrd/Ms6XViwowaaCc+/vqhPG3SmXn725AGrnl+BgTuRPEacjiLEaGq16J6XirMJbtKkTwnt67SS+e2WGoew==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.27.0", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^5.0.1", - "postcss-selector-parser": "^7.1.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.1.tgz", - "integrity": "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.1.tgz", - "integrity": "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^5.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-minify-params": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.5.tgz", - "integrity": "sha512-FGK9ky02h6Ighn3UihsyeAH5XmLEE2MSGH5Tc4tXMFtEDx7B+zTG6hD/+/cT+fbF7PbYojsmmWjyTwFwW1JKQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.27.0", - "cssnano-utils": "^5.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.5.tgz", - "integrity": "sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "postcss-selector-parser": "^7.1.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-modules": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-6.0.1.tgz", - "integrity": "sha512-zyo2sAkVvuZFFy0gc2+4O+xar5dYlaVy/ebO24KT0ftk/iJevSNyPyQellsBLlnccwh7f6V6Y4GvuKRYToNgpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "generic-names": "^4.0.0", - "icss-utils": "^5.1.0", - "lodash.camelcase": "^4.3.0", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "string-hash": "^1.1.3" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.1.tgz", - "integrity": "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.1.tgz", - "integrity": "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.1.tgz", - "integrity": "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.1.tgz", - "integrity": "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-string": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.1.tgz", - "integrity": "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.1.tgz", - "integrity": "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.5.tgz", - "integrity": "sha512-X6BBwiRxVaFHrb2WyBMddIeB5HBjJcAaUHyhLrM2FsxSq5TFqcHSsK7Zu1otag+o0ZphQGJewGH1tAyrD0zX1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.27.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-url": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.1.tgz", - "integrity": "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.1.tgz", - "integrity": "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-ordered-values": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.2.tgz", - "integrity": "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-utils": "^5.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.5.tgz", - "integrity": "sha512-RHagHLidG8hTZcnr4FpyMB2jtgd/OcyAazjMhoy5qmWJOx1uxKh4ntk0Pb46ajKM0rkf32lRH4C8c9qQiPR6IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.27.0", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.1.tgz", - "integrity": "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.0.tgz", - "integrity": "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^4.0.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.4.tgz", - "integrity": "sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.1.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/preact": { - "version": "10.28.4", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.4.tgz", - "integrity": "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", - "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prompts/node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", - "dev": true, - "license": "MIT" - }, - "node_modules/ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "event-stream": "=3.3.4" - }, - "bin": { - "ps-tree": "bin/ps-tree.js" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-query": { - "version": "3.39.3", - "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", - "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.5.5", - "broadcast-channel": "^3.4.1", - "match-sorter": "^6.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "rc": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remove-accents": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", - "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", - "license": "MIT" - }, - "node_modules/request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "throttleit": "^1.0.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rev-hash": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rev-hash/-/rev-hash-4.1.0.tgz", - "integrity": "sha512-e0EGnaveLY2IYpYwHNdh43WZ2M84KgW3Z/T4F6+Z/BlZI/T1ZbxTWj36xgYgUPOieGXYo2q225jTeUXn+LWYjw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/rgb-hex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rgb-hex/-/rgb-hex-4.1.0.tgz", - "integrity": "sha512-UZLM57BW09Yi9J1R3OP8B1yCbbDK3NT8BDtihGZkGkGEs2b6EaV85rsfJ6yK4F+8UbxFFmfA+9xHT5ZWhN1gDQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", - "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/serve": { - "version": "14.2.6", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.6.tgz", - "integrity": "sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@zeit/schemas": "2.36.0", - "ajv": "8.18.0", - "arg": "5.0.2", - "boxen": "7.0.0", - "chalk": "5.0.1", - "chalk-template": "0.4.0", - "clipboardy": "3.0.0", - "compression": "1.8.1", - "is-port-reachable": "4.0.0", - "serve-handler": "6.1.7", - "update-check": "1.5.4" - }, - "bin": { - "serve": "build/main.js" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/serve-handler": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", - "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.0.0", - "content-disposition": "0.5.2", - "mime-types": "2.1.18", - "minimatch": "3.1.5", - "path-is-inside": "1.0.2", - "path-to-regexp": "3.3.0", - "range-parser": "1.2.0" - } - }, - "node_modules/serve-handler/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-handler/node_modules/mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-handler/node_modules/mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "~1.33.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/serve/node_modules/chalk": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", - "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/serve/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spawn-command": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", - "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", - "dev": true - }, - "node_modules/split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dev": true, - "license": "MIT", - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexer": "~0.1.1" - } - }, - "node_modules/string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-mod": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", - "license": "MIT" - }, - "node_modules/stylehacks": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.7.tgz", - "integrity": "sha512-bJkD0JkEtbRrMFtwgpJyBbFIwfDDONQ1Ov3sDLZQP8HuJ73kBOyx66H4bOcAbVWmnfLdvQ0AJwXxOMkpujcO6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.27.0", - "postcss-selector-parser": "^7.1.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svgo": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", - "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^11.1.0", - "css-select": "^5.1.0", - "css-tree": "^3.0.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.1.1", - "sax": "^1.5.0" - }, - "bin": { - "svgo": "bin/svgo.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/synckit": { - "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/temp-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", - "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/temp-write": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/temp-write/-/temp-write-6.0.1.tgz", - "integrity": "sha512-6bj9LlNld+knzEOQvnZK6YxiPF+foOUjvG/WoWj1/Mt9c6f2kQCPsh8KZ+NyTk0AejubTQSPpx2alcswE1bF8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.11", - "is-stream": "^4.0.1", - "temp-dir": "^3.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/temp-write/node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.2.0.tgz", - "integrity": "sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-stream": "^3.0.0", - "temp-dir": "^3.0.0", - "type-fest": "^2.12.2", - "unique-string": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terminate": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/terminate/-/terminate-2.8.0.tgz", - "integrity": "sha512-bcbjJEg0wY5nuJXvGxxHfmoEPkyHLCctUKO6suwtxy7jVSgGcgPeGwpbLDLELFhIaxCGRr3dPvyNg1yuz2V0eg==", - "dev": true, - "license": "GPL-2.0", - "dependencies": { - "ps-tree": "^1.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/throttleit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", - "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true, - "license": "Unlicense" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-css-modules": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/typed-css-modules/-/typed-css-modules-0.9.1.tgz", - "integrity": "sha512-W2HWKncdKd+bLWsnuWB2EyuQBzZ7KJ9Byr/67KLiiyGegcN52rOveun9JR8yAvuL5IXunRMxt0eORMtAUj5bmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "chalk": "^4.0.0", - "chokidar": "^3.4.0", - "glob": "^10.3.10", - "icss-replace-symbols": "^1.1.0", - "is-there": "^4.4.2", - "mkdirp": "^3.0.0", - "postcss": "^8.0.0", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "yargs": "^17.7.2" - }, - "bin": { - "tcm": "lib/cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/typed-css-modules/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/typed-css-modules/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-css-modules/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/typed-css-modules/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unload": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", - "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.6.2", - "detect-node": "^2.0.4" - } - }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/update-check": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", - "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "registry-auth-token": "3.3.2", - "registry-url": "3.1.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/uri-js-replace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", - "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", - "dev": true, - "license": "MIT" - }, - "node_modules/use-context-selector": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-1.4.4.tgz", - "integrity": "sha512-pS790zwGxxe59GoBha3QYOwk8AFGp4DN6DOtH+eoqVmgBBRXVx4IlPDhJmmMiNQAgUaLlP+58aqRC3A4rdaSjg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": "*", - "react-native": "*", - "scheduler": ">=0.19.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/use-debounce": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-9.0.4.tgz", - "integrity": "sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "license": "MIT" - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/widest-line/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/widest-line/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml-ast-parser": { - "version": "0.0.43", - "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", - "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json index 3e0b1a39..e6fdabbc 100644 --- a/package.json +++ b/package.json @@ -1,84 +1,65 @@ { "name": "@tolgee/figma-plugin", - "version": "0.0.1", - "description": "Tolgee Figma Plugin", - "main": "code.js", + "version": "2.0.0", + "description": "Tolgee Figma Plugin (Svelte 5 rewrite)", + "private": true, + "type": "module", "scripts": { - "build": "build-figma-plugin --typecheck --minify", - "watch": "build-figma-plugin --typecheck --watch", - "schema": "openapi-typescript 'https://app.tolgee.io/v3/api-docs/Accessible with Project API key (All)' --output ./src/ui/client/apiSchema.generated.ts", - "eslint": "eslint --fix --ext .ts,.tsx,.js,.jsx src", - "watch:web": "npm run --prefix web watch", - "dev:web": "ts-node ./scripts/runAfterApp.ts \"npm run watch:web\"", - "dev:figma": "ts-node ./scripts/runAfterApp.ts \"npm run watch\"", - "server": "cd cypress && docker compose up --force-recreate", - "cy:run": "ts-node ./scripts/runAfterApp.ts \"npm run watch:web\" \"npx cypress run\"", - "cy:open": "ts-node ./scripts/runAfterApp.ts \"npm run watch:web\" \"npx cypress open\"", - "tsc": "tsc --noEmit", - "prettier": "prettier . --write" + "build": "pnpm run build:main && pnpm run build:ui && pnpm run build:manifest", + "build:main": "node scripts/build-main.mjs", + "build:main:watch": "node scripts/build-main.mjs --watch", + "build:ui": "vite build --config vite.config.ui.ts", + "build:ui:watch": "vite build --config vite.config.ui.ts --watch", + "build:manifest": "node scripts/build-manifest.mjs", + "dev": "concurrently -k -n main,ui -c yellow,green \"pnpm run build:main:watch\" \"pnpm run build:ui:watch\"", + "gallery": "vite --config vite.config.gallery.ts", + "schema": "openapi-typescript 'https://app.tolgee.io/v3/api-docs/Accessible with Project API key (All)' --output ./src/ui/lib/api/schema.generated.ts", + "typecheck": "tsc --noEmit -p tsconfig.json", + "typecheck:svelte": "svelte-check --tsconfig ./tsconfig.json --threshold error", + "test": "vitest run", + "test:watch": "vitest", + "format": "biome format --write src", + "lint": "biome lint src", + "check": "biome check --write src", + "e2e:up": "docker compose -f e2e/docker-compose.yml up -d --force-recreate", + "e2e:down": "docker compose -f e2e/docker-compose.yml down -v", + "e2e:wait": "node e2e/wait-for-tolgee.mjs", + "e2e:host": "vite --config e2e/vite.config.host.ts", + "e2e": "pnpm run e2e:up && pnpm run e2e:wait && pnpm run build && playwright test --ui" }, "author": "Tolgee", "license": "MIT", "devDependencies": { - "@create-figma-plugin/build": "^3.2.0", - "@create-figma-plugin/tsconfig": "^3.2.0", - "@figma/plugin-typings": "^1.89.0", - "@types/jest": "^29.4.0", - "@typescript-eslint/eslint-plugin": "^8.56.1", - "@typescript-eslint/parser": "^8.56.1", - "colors": "^1.4.0", - "concurrently": "^7.6.0", - "cypress": "^14.3.3", - "cypress-iframe": "^1.0.1", + "@biomejs/biome": "^1.9.4", + "@figma/plugin-typings": "^1.125.0", + "@playwright/test": "^1.49.0", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/vite": "^4.0.0", + "@testing-library/svelte": "^5.2.4", + "@types/node": "^22.10.5", + "concurrently": "^9.1.2", "esbuild": "^0.27.3", - "eslint": "^9.7", - "eslint-config-preact": "^2.0.0", - "eslint-plugin-jest": "^29.15.0", - "eslint-plugin-prettier": "^5.5.5", - "eslint-plugin-react": "^7.37.5", - "jest": "^29.3.1", - "nodemon": "^3.1.11", + "happy-dom": "^15.11.7", "openapi-typescript": "^7.12.0", - "prettier": "^3.8.1", - "serve": "^14.2.0", - "terminate": "^2.6.1", - "ts-node": "^10.9.1", - "typescript": "^5.4.5" + "rollup-plugin-visualizer": "^7.0.1", + "svelte": "^5.51.0", + "svelte-check": "^4.1.1", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.3", + "vite": "^6.0.7", + "vite-plugin-singlefile": "^2.1.0", + "vitest": "^2.1.8" }, "dependencies": { - "@codemirror/autocomplete": "^6.18.6", - "@create-figma-plugin/ui": "3.2.0", - "@create-figma-plugin/utilities": "3.2.0", - "@floating-ui/dom": "^1.6.13", - "@tginternal/editor": "^1.15.2", - "clsx": "^1.2.1", - "codemirror": "^6.0.1", - "dompurify": "^3.2.5", - "intl-messageformat": "^10.7.15", - "preact": "^10.11.3", - "react-query": "^3.39.3", - "scheduler": "^0.23.2", - "use-context-selector": "^1.4.1", - "use-debounce": "^9.0.3" + "@tanstack/svelte-query": "^6.1.28", + "bits-ui": "^1.0.0", + "clsx": "^2.1.1", + "intl-messageformat": "^11.2.4", + "lucide-svelte": "^0.469.0", + "openapi-fetch": "^0.13.4" }, "figma-plugin": { "id": "1212381421658754793", - "name": "Tolgee", - "main": "src/main/main.ts", - "ui": "src/ui/ui.tsx", - "editorType": [ - "figma", - "dev" - ], - "capabilities": [ - "inspect", - "vscode" - ], - "networkAccess": { - "allowedDomains": [ - "*" - ], - "reasoning": "For self-hosted instances, Tolgee needs to be able to fetch data from any URL." - } + "name": "Tolgee" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..cb85b553 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,61 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Playwright config for the Figma-plugin v2 E2E suite. + * + * Bring-up sequence (also encoded in `.github/workflows/test.yml`): + * 1. `docker compose -f e2e/docker-compose.yml up -d` — starts Tolgee + * with the `e2e/import-data/examples` project pre-seeded. + * 2. `node e2e/wait-for-tolgee.mjs` — polls until the platform responds + * 200 (Tolgee takes ~30-60s to bootstrap on a cold start). + * 3. `pnpm build` — produces `dist/ui.html` (the single-file Svelte + * bundle the plugin ships to Figma). + * 4. `vite preview --config e2e/vite.config.host.ts` (started via the + * `webServer` block below) — serves `e2e/host/` + `dist/`. + * 5. Playwright runs `e2e/tests/**`. + * + * Locally, `pnpm e2e` performs the same dance. + */ +const CI = !!process.env.CI; + +export default defineConfig({ + testDir: "./e2e/tests", + outputDir: "./e2e/test-results", + globalSetup: "./e2e/global-setup.ts", + fullyParallel: true, + forbidOnly: CI, + retries: CI ? 2 : 2, + timeout: 120_000, + // Run a single worker locally for predictable Tolgee state; CI gets a + // single worker too since all specs hit the same shared platform and we + // do not have multi-tenant isolation in the import data. + workers: 1, + reporter: CI + ? [["html", { open: "never", outputFolder: "e2e/playwright-report" }], ["github"]] + : [["html", { open: "never", outputFolder: "e2e/playwright-report" }], ["list"]], + use: { + baseURL: "http://localhost:4173", + trace: "on-first-retry", + screenshot: "only-on-failure", + video: CI ? "retain-on-failure" : "off", + }, + projects: [ + { + name: "chromium", + // Figma's plugin host is Chromium-based, so we target that exclusively. + // Cross-browser coverage is not meaningful for a plugin that only ever + // runs inside Figma. + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: { + command: "pnpm exec vite --config e2e/vite.config.host.ts", + url: "http://localhost:4173", + reuseExistingServer: !CI, + stdout: "pipe", + stderr: "pipe", + // Vite dev startup is near-instant; the slow step is the plugin build, + // which is run as a separate npm script before Playwright. + timeout: 30_000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..2e06c8f0 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3249 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tanstack/svelte-query': + specifier: ^6.1.28 + version: 6.1.28(svelte@5.55.5) + bits-ui: + specifier: ^1.0.0 + version: 1.8.0(svelte@5.55.5) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + intl-messageformat: + specifier: ^11.2.4 + version: 11.2.4 + lucide-svelte: + specifier: ^0.469.0 + version: 0.469.0(svelte@5.55.5) + openapi-fetch: + specifier: ^0.13.4 + version: 0.13.8 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.4 + version: 1.9.4 + '@figma/plugin-typings': + specifier: ^1.125.0 + version: 1.125.0 + '@playwright/test': + specifier: ^1.49.0 + version: 1.60.0 + '@sveltejs/vite-plugin-svelte': + specifier: ^5.0.3 + version: 5.1.1(svelte@5.55.5)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)) + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.3.0(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)) + '@testing-library/svelte': + specifier: ^5.2.4 + version: 5.3.1(svelte@5.55.5)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0))(vitest@2.1.9(@types/node@22.19.18)(happy-dom@15.11.7)(lightningcss@1.32.0)) + '@types/node': + specifier: ^22.10.5 + version: 22.19.18 + concurrently: + specifier: ^9.1.2 + version: 9.2.1 + esbuild: + specifier: ^0.27.3 + version: 0.27.7 + happy-dom: + specifier: ^15.11.7 + version: 15.11.7 + openapi-typescript: + specifier: ^7.12.0 + version: 7.13.0(typescript@5.9.3) + rollup-plugin-visualizer: + specifier: ^7.0.1 + version: 7.0.1(rollup@4.60.3) + svelte: + specifier: ^5.51.0 + version: 5.55.5 + svelte-check: + specifier: ^4.1.1 + version: 4.4.8(picomatch@4.0.4)(svelte@5.55.5)(typescript@5.9.3) + tailwindcss: + specifier: ^4.0.0 + version: 4.3.0 + typescript: + specifier: ^5.6.3 + version: 5.9.3 + vite: + specifier: ^6.0.7 + version: 6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0) + vite-plugin-singlefile: + specifier: ^2.1.0 + version: 2.3.3(rollup@4.60.3)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)) + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.19.18)(happy-dom@15.11.7)(lightningcss@1.32.0) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@figma/plugin-typings@1.125.0': + resolution: {integrity: sha512-8cXB4iKyRFl+/DryImvTngkFtgnowZUeFu/dt/jSaFL04mOKhGoZE1d1Vz+sUKUdZWXibGIWexCCdFK5gH5zxg==} + + '@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/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@formatjs/fast-memoize@3.1.4': + resolution: {integrity: sha512-Lbke1aOrsygKKR09Ux0NrZgbTqpDmiwXOgzyDOJ8Owr1zd5qOKTauf62hH+Seeku3ju77rHWH9I5SfX2CN0vuA==} + + '@formatjs/icu-messageformat-parser@3.5.7': + resolution: {integrity: sha512-wJxRZ+SiUCIMTL86bQlZU9bEKDQqqvgk2ezQ1BySUdWRfHqOzj4IKUVFeUZKS9w58M4e7wMSG0Sl86LAPb7Qww==} + + '@formatjs/icu-skeleton-parser@2.1.7': + resolution: {integrity: sha512-cIw1SFP0bi0CUBiJ2jzp99ws3OJNQDfStcHq9Z0iHWzItmiIikihFO+npR8C80yDlp7ZuBCLXCcKjgWjHicksA==} + + '@internationalized/date@3.12.1': + resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} + hasBin: true + + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} + + '@redocly/openapi-core@1.34.14': + resolution: {integrity: sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + + '@rollup/rollup-android-arm-eabi@4.60.3': + resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.3': + resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.3': + resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.3': + resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.3': + resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.3': + resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.3': + resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.3': + resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.3': + resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.3': + resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.3': + resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.3': + resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.3': + resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.3': + resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} + cpu: [x64] + os: [win32] + + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1': + resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^5.0.0 + svelte: ^5.0.0 + vite: ^6.0.0 + + '@sveltejs/vite-plugin-svelte@5.1.1': + resolution: {integrity: sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.0.0 + + '@swc/helpers@0.5.21': + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + + '@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/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/query-core@5.100.9': + resolution: {integrity: sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==} + + '@tanstack/svelte-query@6.1.28': + resolution: {integrity: sha512-B4uh/fvn+t67LyWzY8Sb+yqyxJS2hc27Nwf+bfz7vy+ilzfJIHORoCm9/6Arsg4pdH8DCBMLMWZTvMgvSUd/pg==} + peerDependencies: + svelte: ^5.25.0 + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/svelte-core@1.0.0': + resolution: {integrity: sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ==} + engines: {node: '>=16'} + peerDependencies: + svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 + + '@testing-library/svelte@5.3.1': + resolution: {integrity: sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w==} + engines: {node: '>= 10'} + peerDependencies: + svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 + vite: '*' + vitest: '*' + peerDependenciesMeta: + vite: + optional: true + vitest: + optional: true + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@22.19.18': + resolution: {integrity: sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bits-ui@1.8.0: + resolution: {integrity: sha512-CXD6Orp7l8QevNDcRPLXc/b8iMVgxDWT2LyTwsdLzJKh9CxesOmPuNePSPqAxKoT59FIdU4aFPS1k7eBdbaCxg==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.11.0 + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + 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.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + 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'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.8.0: + resolution: {integrity: sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enhanced-resolve@5.21.2: + resolution: {integrity: sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.2.7: + resolution: {integrity: sha512-Dl7o7btn2YXca1VXx+PVl+lKuZdHBm8oCFuckUxqchMvNMdHMJ/qF31wtPaVyWvFYLQePkbXJrirWzbAP6Yamw==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + happy-dom@15.11.7: + resolution: {integrity: sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==} + engines: {node: '>=18.0.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + intl-messageformat@11.2.4: + resolution: {integrity: sha512-iKP6+uJXn+XcfRgYfGPE3+mqCoODV2vATrXDLo/YkYgIdelJHJPBEbc0GZThipAYPuk+8QJFiPgOfblU085ABg==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + 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'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lucide-svelte@0.469.0: + resolution: {integrity: sha512-PMIJ8jrFqVUsXJz4d1yfAQplaGhNOahwwkzbunha8DhpiD73xqX24n8dE1dPpUk3vcrdWVsHc1y/liHHotOnGQ==} + deprecated: Package deprecated. Please use @lucide/svelte instead. + peerDependencies: + svelte: ^3 || ^4 || ^5.0.0-next.42 + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + openapi-fetch@0.13.8: + resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==} + + openapi-typescript-helpers@0.0.15: + resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} + hasBin: true + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rollup-plugin-visualizer@7.0.1: + resolution: {integrity: sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==} + engines: {node: '>=22'} + hasBin: true + peerDependencies: + rolldown: 1.x || ^1.0.0-beta || ^1.0.0-rc + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + + rollup@4.60.3: + resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + runed@0.23.4: + resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==} + peerDependencies: + svelte: ^5.7.0 + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + svelte-check@4.4.8: + resolution: {integrity: sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-toolbelt@0.7.1: + resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.0.0 + + svelte@5.55.5: + resolution: {integrity: sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==} + engines: {node: '>=18'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + 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'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-plugin-singlefile@2.3.3: + resolution: {integrity: sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==} + engines: {node: '>18.0.0'} + peerDependencies: + rollup: ^4.59.0 + vite: ^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + rollup: + optional: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/runtime@7.29.2': {} + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@figma/plugin-typings@1.125.0': {} + + '@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/utils@0.2.11': {} + + '@formatjs/fast-memoize@3.1.4': {} + + '@formatjs/icu-messageformat-parser@3.5.7': + dependencies: + '@formatjs/icu-skeleton-parser': 2.1.7 + + '@formatjs/icu-skeleton-parser@2.1.7': {} + + '@internationalized/date@3.12.1': + dependencies: + '@swc/helpers': 0.5.21 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@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 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@playwright/test@1.60.0': + dependencies: + playwright: 1.60.0 + + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.0': {} + + '@redocly/openapi-core@1.34.14(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.1.1 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + + '@rollup/rollup-android-arm-eabi@4.60.3': + optional: true + + '@rollup/rollup-android-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-x64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.3': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.3': + optional: true + + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.5)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.55.5)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.5)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)) + debug: 4.4.3(supports-color@10.2.2) + svelte: 5.55.5 + vite: 6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.5)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.5)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)))(svelte@5.55.5)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)) + debug: 4.4.3(supports-color@10.2.2) + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.21 + svelte: 5.55.5 + vite: 6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0) + vitefu: 1.1.3(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)) + transitivePeerDependencies: + - supports-color + + '@swc/helpers@0.5.21': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.2 + jiti: 2.7.0 + 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/vite@4.3.0(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0) + + '@tanstack/query-core@5.100.9': {} + + '@tanstack/svelte-query@6.1.28(svelte@5.55.5)': + dependencies: + '@tanstack/query-core': 5.100.9 + svelte: 5.55.5 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/svelte-core@1.0.0(svelte@5.55.5)': + dependencies: + svelte: 5.55.5 + + '@testing-library/svelte@5.3.1(svelte@5.55.5)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0))(vitest@2.1.9(@types/node@22.19.18)(happy-dom@15.11.7)(lightningcss@1.32.0))': + dependencies: + '@testing-library/dom': 10.4.1 + '@testing-library/svelte-core': 1.0.0(svelte@5.55.5) + svelte: 5.55.5 + optionalDependencies: + vite: 6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0) + vitest: 2.1.9(@types/node@22.19.18)(happy-dom@15.11.7)(lightningcss@1.32.0) + + '@types/aria-query@5.0.4': {} + + '@types/estree@1.0.8': {} + + '@types/estree@1.0.9': {} + + '@types/node@22.19.18': + dependencies: + undici-types: 6.21.0 + + '@types/trusted-types@2.0.7': {} + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.18)(lightningcss@1.32.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.19.18)(lightningcss@1.32.0) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + argparse@2.0.1: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.1: {} + + assertion-error@2.0.1: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + bits-ui@1.8.0(svelte@5.55.5): + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/dom': 1.7.6 + '@internationalized/date': 3.12.1 + css.escape: 1.5.1 + esm-env: 1.2.2 + runed: 0.23.4(svelte@5.55.5) + svelte: 5.55.5 + svelte-toolbelt: 0.7.1(svelte@5.55.5) + tabbable: 6.4.0 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + change-case@5.4.4: {} + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@1.4.0: {} + + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + css.escape@1.5.1: {} + + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + + deep-eql@5.0.2: {} + + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + devalue@5.8.0: {} + + dom-accessibility-api@0.5.16: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + enhanced-resolve@5.21.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@4.5.0: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escalade@3.2.0: {} + + esm-env@1.2.2: {} + + esrap@2.2.7: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + graceful-fs@4.2.11: {} + + happy-dom@15.11.7: + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + + has-flag@4.0.0: {} + + https-proxy-agent@7.0.6(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + index-to-position@1.2.0: {} + + inline-style-parser@0.2.7: {} + + intl-messageformat@11.2.4: + dependencies: + '@formatjs/fast-memoize': 3.1.4 + '@formatjs/icu-messageformat-parser': 3.5.7 + + is-docker@3.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-number@7.0.0: {} + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + jiti@2.7.0: {} + + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-schema-traverse@1.0.0: {} + + kleur@4.1.5: {} + + 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 + + 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: + 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 + + locate-character@3.0.0: {} + + loupe@3.2.1: {} + + lucide-svelte@0.469.0(svelte@5.55.5): + dependencies: + svelte: 5.55.5 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + + mri@1.2.0: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + openapi-fetch@0.13.8: + dependencies: + openapi-typescript-helpers: 0.0.15 + + openapi-typescript-helpers@0.0.15: {} + + openapi-typescript@7.13.0(typescript@5.9.3): + dependencies: + '@redocly/openapi-core': 1.34.14(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 5.9.3 + yargs-parser: 21.1.1 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + index-to-position: 1.2.0 + type-fest: 4.41.0 + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + playwright-core@1.60.0: {} + + playwright@1.60.0: + dependencies: + playwright-core: 1.60.0 + optionalDependencies: + fsevents: 2.3.2 + + pluralize@8.0.0: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + react-is@17.0.2: {} + + readdirp@4.1.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + rollup-plugin-visualizer@7.0.1(rollup@4.60.3): + dependencies: + open: 11.0.0 + picomatch: 4.0.4 + source-map: 0.7.6 + yargs: 18.0.0 + optionalDependencies: + rollup: 4.60.3 + + rollup@4.60.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.3 + '@rollup/rollup-android-arm64': 4.60.3 + '@rollup/rollup-darwin-arm64': 4.60.3 + '@rollup/rollup-darwin-x64': 4.60.3 + '@rollup/rollup-freebsd-arm64': 4.60.3 + '@rollup/rollup-freebsd-x64': 4.60.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 + '@rollup/rollup-linux-arm-musleabihf': 4.60.3 + '@rollup/rollup-linux-arm64-gnu': 4.60.3 + '@rollup/rollup-linux-arm64-musl': 4.60.3 + '@rollup/rollup-linux-loong64-gnu': 4.60.3 + '@rollup/rollup-linux-loong64-musl': 4.60.3 + '@rollup/rollup-linux-ppc64-gnu': 4.60.3 + '@rollup/rollup-linux-ppc64-musl': 4.60.3 + '@rollup/rollup-linux-riscv64-gnu': 4.60.3 + '@rollup/rollup-linux-riscv64-musl': 4.60.3 + '@rollup/rollup-linux-s390x-gnu': 4.60.3 + '@rollup/rollup-linux-x64-gnu': 4.60.3 + '@rollup/rollup-linux-x64-musl': 4.60.3 + '@rollup/rollup-openbsd-x64': 4.60.3 + '@rollup/rollup-openharmony-arm64': 4.60.3 + '@rollup/rollup-win32-arm64-msvc': 4.60.3 + '@rollup/rollup-win32-ia32-msvc': 4.60.3 + '@rollup/rollup-win32-x64-gnu': 4.60.3 + '@rollup/rollup-win32-x64-msvc': 4.60.3 + fsevents: 2.3.3 + + run-applescript@7.1.0: {} + + runed@0.23.4(svelte@5.55.5): + dependencies: + esm-env: 1.2.2 + svelte: 5.55.5 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + shell-quote@1.8.3: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + supports-color@10.2.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + svelte-check@4.4.8(picomatch@4.0.4)(svelte@5.55.5)(typescript@5.9.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.55.5 + typescript: 5.9.3 + transitivePeerDependencies: + - picomatch + + svelte-toolbelt@0.7.1(svelte@5.55.5): + dependencies: + clsx: 2.1.1 + runed: 0.23.4(svelte@5.55.5) + style-to-object: 1.0.14 + svelte: 5.55.5 + + svelte@5.55.5: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@types/estree': 1.0.9 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.8.0 + esm-env: 1.2.2 + esrap: 2.2.7 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + + tabbable@6.4.0: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tree-kill@1.2.2: {} + + tslib@2.8.1: {} + + type-fest@4.41.0: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + uri-js-replace@1.0.1: {} + + vite-node@2.1.9(@types/node@22.19.18)(lightningcss@1.32.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@10.2.2) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.19.18)(lightningcss@1.32.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-singlefile@2.3.3(rollup@4.60.3)(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)): + dependencies: + micromatch: 4.0.8 + vite: 6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0) + optionalDependencies: + rollup: 4.60.3 + + vite@5.4.21(@types/node@22.19.18)(lightningcss@1.32.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.14 + rollup: 4.60.3 + optionalDependencies: + '@types/node': 22.19.18 + fsevents: 2.3.3 + lightningcss: 1.32.0 + + vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.14 + rollup: 4.60.3 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 22.19.18 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + + vitefu@1.1.3(vite@6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0)): + optionalDependencies: + vite: 6.4.2(@types/node@22.19.18)(jiti@2.7.0)(lightningcss@1.32.0) + + vitest@2.1.9(@types/node@22.19.18)(happy-dom@15.11.7)(lightningcss@1.32.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.18)(lightningcss@1.32.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3(supports-color@10.2.2) + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.19.18)(lightningcss@1.32.0) + vite-node: 2.1.9(@types/node@22.19.18)(lightningcss@1.32.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.18 + happy-dom: 15.11.7 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + webidl-conversions@7.0.0: {} + + whatwg-mimetype@3.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + y18n@5.0.8: {} + + yaml-ast-parser@0.0.43: {} + + yargs-parser@21.1.1: {} + + yargs-parser@22.0.0: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + zimmerframe@1.1.4: {} diff --git a/scripts/build-main.mjs b/scripts/build-main.mjs new file mode 100644 index 00000000..abbdf27c --- /dev/null +++ b/scripts/build-main.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node +import process from 'node:process'; +import * as esbuild from 'esbuild'; + +const watch = process.argv.includes('--watch'); +const isProd = !watch && process.env.NODE_ENV !== 'development'; + +/** @type {import('esbuild').BuildOptions} */ +const options = { + entryPoints: ['src/main/main.ts'], + outfile: 'dist/main.js', + bundle: true, + // Figma's plugin sandbox is a QuickJS host whose syntactical surface is + // ES2017. Features like optional chaining (`?.`), nullish coalescing (`??`), + // and logical assignment operators (`??=`, `||=`, `&&=`) are ES2020+ and + // surface as "Unexpected token {" in QuickJS. `target: 'es2017'` makes + // esbuild down-level them to equivalent ES2017 code. + format: 'cjs', + target: 'es2017', + platform: 'browser', + minify: isProd, + sourcemap: watch ? 'inline' : false, + define: { + 'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development'), + }, + logLevel: 'info', +}; + +if (watch) { + const ctx = await esbuild.context({ + ...options, + plugins: [ + { + name: 'rebuild-logger', + setup(build) { + build.onEnd((result) => { + const ts = new Date().toLocaleTimeString(); + if (result.errors.length > 0) { + console.log(`[${ts}] main.js rebuild failed with ${result.errors.length} error(s)`); + } else { + console.log(`[${ts}] main.js rebuilt successfully`); + } + }); + }, + }, + ], + }); + await ctx.watch(); + console.log('Watching src/main for changes...'); +} else { + await esbuild.build(options); + console.log('Built dist/main.js'); +} diff --git a/scripts/build-manifest.mjs b/scripts/build-manifest.mjs new file mode 100644 index 00000000..878b6402 --- /dev/null +++ b/scripts/build-manifest.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve(__dirname, '..'); +const distDir = path.join(rootDir, 'dist'); +const manifestPath = path.join(distDir, 'manifest.json'); + +const manifest = { + api: '1.0.0', + // Matches the published Tolgee plugin's identity, so a local/dev build is the + // same plugin to Figma (shares its stored data) rather than a separate one. + id: '1212381421658754793', + name: 'Tolgee', + main: 'main.js', + // ONE shared UI bundle for both editors — matching production, which ships + // a single `ui` for figma and dev alike. Dev-Mode safety lives in code, not + // in a separate bundle: the message-impact guard ($shared/messagePolicy + + // $main/bus.ts) blocks canvas writes, the navigation gate + per-component + // hiding keep design-only affordances out of the dev UI. The per-editor map + // form is kept (vs a plain string) so a future dev-specific bundle would be + // a one-line change. + ui: { figma: 'ui.html', dev: 'ui.html' }, + editorType: ['figma', 'dev'], + // Required for plugins built after 2024 — forces async page access, which is + // what our `getNodeByIdAsync` / `page.loadAsync()` code already assumes. + documentAccess: 'dynamic-page', + // `vscode` makes the existing read-only inspect panel (above) also launchable + // from the Figma-for-VS-Code extension — no extra code needed, `figma.vscode` + // is an empty marker interface. Matches production; dropped here by omission. + capabilities: ['inspect', 'vscode'], + networkAccess: { + allowedDomains: ['*'], + reasoning: 'Self-hosted Tolgee instances may run on arbitrary domains.', + }, + // No `menu`: with one, Figma shows a submenu on launch instead of opening the + // plugin directly. No `relaunchButtons`: a canvas-level "Edit Tolgee key" + // shortcut was scaffolded but never wired (nothing ever called + // `setRelaunchData`, so the button never appeared) — dropped to match + // upstream, which has none. +}; + +if (!fs.existsSync(distDir)) { + fs.mkdirSync(distDir, { recursive: true }); +} + +fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8'); +console.log(`Wrote ${path.relative(rootDir, manifestPath)}`); diff --git a/scripts/runAfterApp.ts b/scripts/runAfterApp.ts deleted file mode 100644 index 6b46e059..00000000 --- a/scripts/runAfterApp.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { ChildProcess, spawn } from "child_process"; -import path from "path"; -import colors from "colors"; -import terminate from "terminate"; -import http from "http"; - -function systemColor(text: string) { - return colors.grey(text); -} - -const afterArgs = process.argv.slice(2); - -const dockerComposeProcess = spawn( - "docker", - ["compose", "up", "--force-recreate"], - { - cwd: path.join(__dirname, "..", "cypress"), - }, -); - -const appProcesses = [dockerComposeProcess]; - -const afterProcesses: ChildProcess[] = []; - -dockerComposeProcess.stdout.on("data", (data) => { - process.stdout.write(data); -}); - -dockerComposeProcess.stderr.pipe(process.stderr); - -function checkServerStatus(callback: () => void) { - const MAX_RETRIES = 300; // 300 seconds timeout - let retryCount = 0; - const options = { - host: "localhost", - port: 8080, - path: "/", // Or any other endpoint - timeout: 2000, - }; - - const req = http.get(options, (res) => { - if (res.statusCode === 200) { - console.log(systemColor("Server is up and running!")); - callback(); - } else { - if (retryCount++ >= MAX_RETRIES) { - console.log( - systemColor( - "Server failed to start after maximum retries. Exiting...", - ), - ); - finish(1); - return; - } - setTimeout(() => checkServerStatus(callback), 1000); - } - req.end(); - }); - - req.on("error", () => { - console.log(systemColor("Waiting for server to start...")); - if (retryCount++ >= MAX_RETRIES) { - console.log( - systemColor("Server failed to start after maximum retries. Exiting..."), - ); - finish(1); - return; - } - setTimeout(() => checkServerStatus(callback), 1000); - req.end(); - }); -} - -function runAfterProcesses() { - if (afterArgs.length) { - afterArgs.forEach((command) => { - const afterProcess = spawn(command, [], { - stdio: "inherit", - shell: true, - }); - afterProcess.on("close", (code) => - onChildFinish(getProcessName(afterProcess), code || 0), - ); - afterProcesses.push(afterProcess); - }); - } -} - -checkServerStatus(() => { - runAfterProcesses(); -}); - -let isFinishing = false; -async function finish(code: number) { - if (isFinishing) { - return true; - } - isFinishing = true; - const allProcesses = [...appProcesses, ...afterProcesses]; - const runningProcesses = allProcesses.filter((p) => p.kill(0)); - const childrenPending = runningProcesses.map( - (p) => - new Promise((resolve, reject) => { - console.log(systemColor(`Terminating process "${getProcessName(p)}"`)); - terminate(p.pid!, { timeout: 5000 }, (err) => - err ? reject(err) : resolve(), - ); - }), - ); - - await Promise.all(childrenPending); - - // if all children are terminated, exit - console.log(systemColor(`Finished with status ${code}`)); - process.exit(code); -} - -function getProcessName(p?: ChildProcess) { - if (!p) { - return `${process.argv.join(" ")}`; - } - return `${p.spawnargs.join(" ")}`; -} - -function onChildFinish(finishedProcess: string, code: number) { - console.log(systemColor(`"${finishedProcess}" finished with code ${code}`)); - finish(code); -} - -process.on("SIGINT", () => { - console.log(systemColor("Terminated by user")); - finish(1); -}); - -appProcesses.forEach((p) => - p.on("close", (code) => onChildFinish(getProcessName(p), code || 0)), -); diff --git a/src/createFormatIcu.ts b/src/createFormatIcu.ts deleted file mode 100644 index 30bf5792..00000000 --- a/src/createFormatIcu.ts +++ /dev/null @@ -1,53 +0,0 @@ -import IntlMessageFormat from "intl-messageformat"; - -export type FormatterMiddlewareFormatParams = { - translation: string; - language: string; - params: Record | undefined; -}; - -export type FinalFormatterMiddleware = { - format: (props: FormatterMiddlewareFormatParams) => any; -}; - -export const createFormatIcu = (): FinalFormatterMiddleware => { - const locales = new Map() as Map; - - function isLocaleValid(locale: string) { - try { - return Boolean(Intl.NumberFormat.supportedLocalesOf(locale).length); - } catch { - return false; - } - } - - function getLanguage(language: string) { - if (!locales.get(language)) { - let localeCandidate: string = String(language).replace(/[^a-zA-Z]/g, "-"); - while (!isLocaleValid(localeCandidate)) { - localeCandidate = - localeCandidate.split("-").slice(0, -1).join("-") || "en"; - } - locales.set(language, localeCandidate); - } - return locales.get(language); - } - - const format: FinalFormatterMiddleware["format"] = ({ - translation, - language, - params, - }) => { - const ignoreTag = !Object.values(params || {}).find( - (p) => typeof p === "function", - ); - - const locale = getLanguage(language); - - return new IntlMessageFormat(translation, locale, undefined, { - ignoreTag, - }).format(params); - }; - - return Object.freeze({ getLanguage, format }); -}; diff --git a/src/custom.d.ts b/src/custom.d.ts deleted file mode 100644 index 9d2833b6..00000000 --- a/src/custom.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.svg" { - const content: FunctionComponent>; - export default content; -} diff --git a/src/main/__tests__/bus.test.ts b/src/main/__tests__/bus.test.ts new file mode 100644 index 00000000..06228c03 --- /dev/null +++ b/src/main/__tests__/bus.test.ts @@ -0,0 +1,106 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { attachBus, on } from "$main/bus"; + +/** + * The bus dispatch's error boundary. Figma discards an unhandled rejection + * from `figma.ui.onmessage` silently, so a handler that throws used to leave + * the UI with no response, no error, and whatever pending state it armed for + * that request stuck until the plugin was force-quit. + */ +type FakeFigma = { + editorType: "figma" | "dev"; + notify: ReturnType; + ui: { postMessage: ReturnType; onmessage?: (msg: unknown) => unknown }; +}; + +function installFigma(editorType: "figma" | "dev" = "figma"): FakeFigma { + const figma: FakeFigma = { + editorType, + notify: vi.fn(), + ui: { postMessage: vi.fn() }, + }; + (globalThis as unknown as { figma: unknown }).figma = figma; + return figma; +} + +afterEach(() => { + (globalThis as unknown as { figma?: unknown }).figma = undefined; + vi.restoreAllMocks(); +}); + +describe("attachBus error boundary", () => { + it("answers with handler-error instead of letting the rejection vanish", async () => { + const figma = installFigma(); + vi.spyOn(console, "error").mockImplementation(() => {}); + on("request-copy-staleness", async () => { + throw new Error("boom"); + }); + attachBus(); + + await figma.ui.onmessage?.({ type: "request-copy-staleness", correlationId: "c-1" }); + + const sent = figma.ui.postMessage.mock.calls.map((c) => c[0] as Record); + expect(sent).toEqual([ + { type: "handler-error", forType: "request-copy-staleness", correlationId: "c-1" }, + ]); + }); + + it("does not reject out of onmessage — Figma would swallow it", async () => { + const figma = installFigma(); + vi.spyOn(console, "error").mockImplementation(() => {}); + on("request-copy-staleness", async () => { + throw new Error("boom"); + }); + attachBus(); + + await expect( + figma.ui.onmessage?.({ type: "request-copy-staleness", correlationId: "c-2" }), + ).resolves.not.toThrow(); + }); + + it("tells the user something went wrong, and logs the cause", async () => { + const figma = installFigma(); + const logged = vi.spyOn(console, "error").mockImplementation(() => {}); + const cause = new Error("the actual reason"); + on("request-copy-staleness", async () => { + throw cause; + }); + attachBus(); + + await figma.ui.onmessage?.({ type: "request-copy-staleness", correlationId: "c-3" }); + + expect(figma.notify).toHaveBeenCalledTimes(1); + expect(logged).toHaveBeenCalledWith( + "[tolgee:main] handler failed", + "request-copy-staleness", + cause, + ); + }); + + it("omits the correlationId when the failed request carried none", async () => { + const figma = installFigma(); + vi.spyOn(console, "error").mockImplementation(() => {}); + on("reset", async () => { + throw new Error("boom"); + }); + attachBus(); + + await figma.ui.onmessage?.({ type: "reset" }); + + const sent = figma.ui.postMessage.mock.calls[0]?.[0] as { correlationId?: string }; + expect(sent.correlationId).toBeUndefined(); + }); + + it("leaves a successful handler completely alone", async () => { + const figma = installFigma(); + const handled = vi.fn(); + on("reset", handled); + attachBus(); + + await figma.ui.onmessage?.({ type: "reset" }); + + expect(handled).toHaveBeenCalledTimes(1); + expect(figma.notify).not.toHaveBeenCalled(); + expect(figma.ui.postMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/src/main/__tests__/main.test.ts b/src/main/__tests__/main.test.ts new file mode 100644 index 00000000..e076f920 --- /dev/null +++ b/src/main/__tests__/main.test.ts @@ -0,0 +1,350 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +/** + * Narrow test for the `on("set-nodes-data", ...)` handler wired up in + * `main.ts`. `setNodesData` itself (in `$main/nodes/selection`) is already + * well covered (see `nodes/__tests__/selection.test.ts`, task 7) — this test + * only checks that the main-thread MESSAGE HANDLER correctly forwards + * `setNodesData`'s `onProgress` callback into `nodes-set-progress` UI + * messages, and sends `nodes-set-result` at the end. + * + * `main.ts` has no exported, individually-invokable handler functions — every + * `on(...)` registration is an inline arrow function, and the file runs + * side-effecting top-level code (`figma.showUI`, `figma.on(...)`, + * `attachBus()`) at import time. Rather than refactor the whole file's + * handler-registration pattern just to make ONE handler unit-testable in + * isolation (out of proportion for this cleanup task — see task 14g), this + * test drives it the way the real bridge does: `attachBus()` (called at + * module top level) assigns `figma.ui.onmessage` to the actual dispatcher, so + * invoking that with a `set-nodes-data` message exercises the exact handler + * registered in `main.ts`, with no behavioural changes to the source file. + */ + +/** Minimal TEXT-node stand-in — same shape `selection.test.ts` uses for + * `setNodesData`'s `figma.getNodeByIdAsync` lookups. */ +function makeTextNode(id: string, characters: string) { + const pluginData = new Map(); + return { + id, + type: "TEXT" as const, + name: `Layer ${id}`, + characters, + visible: true, + autoRename: true, + getPluginData: (key: string) => pluginData.get(key) ?? "", + setPluginData: (key: string, value: string) => { + pluginData.set(key, value); + }, + getRangeAllFontNames: () => [{ family: "Inter", style: "Regular" }], + }; +} + +type FakeNode = ReturnType; +type FakeFigma = { + editorType: "figma" | "dev"; + command: string; + currentPage: { + selection: unknown[]; + name: string; + loadAsync: () => Promise; + getPluginData: (key: string) => string; + }; + root: { getPluginData: (key: string) => string }; + clientStorage: { getAsync: (key: string) => Promise }; + skipInvisibleInstanceChildren: boolean; + showUI: ReturnType; + notify: ReturnType; + on: ReturnType; + ui: { postMessage: ReturnType; onmessage?: (msg: unknown) => unknown }; + getNodeByIdAsync: (id: string) => Promise; +}; + +/** Installs a fresh `figma` global (+ the two build-time UI-html injected + * globals `main.ts` reads at module scope) and imports `main.ts` fresh, so + * its top-level `attachBus()` wires `figma.ui.onmessage` for this test. + * + * `selection` (optional) populates `figma.currentPage.selection` for tests + * that exercise `ui-ready`'s startup scan; the config/pluginData reads all + * return empty (merged config = defaults), which is all those tests need. */ +async function loadMain( + nodes: FakeNode[], + editorType: "figma" | "dev" = "figma", + selection: FakeNode[] = [], +): Promise { + const byId = new Map(nodes.map((n) => [n.id, n])); + const figma: FakeFigma = { + editorType, + command: "", + currentPage: { + selection, + name: "Page 1", + loadAsync: async () => {}, + getPluginData: () => "", + }, + root: { getPluginData: () => "" }, + clientStorage: { getAsync: async () => undefined }, + skipInvisibleInstanceChildren: false, + showUI: vi.fn(), + notify: vi.fn(), + on: vi.fn(), + ui: { postMessage: vi.fn() }, + getNodeByIdAsync: async (id: string) => byId.get(id) ?? null, + }; + (globalThis as unknown as { figma: unknown }).figma = figma; + (globalThis as unknown as { __uiFiles__: unknown }).__uiFiles__ = { figma: "" }; + (globalThis as unknown as { __html__: unknown }).__html__ = ""; + + vi.resetModules(); + await import("../main"); + + return figma; +} + +afterEach(() => { + (globalThis as unknown as { figma?: unknown }).figma = undefined; + (globalThis as unknown as { __uiFiles__?: unknown }).__uiFiles__ = undefined; + (globalThis as unknown as { __html__?: unknown }).__html__ = undefined; + vi.restoreAllMocks(); +}); + +describe('main.ts on("set-nodes-data", ...) handler', () => { + it("forwards setNodesData's onProgress into nodes-set-progress, then sends nodes-set-result", async () => { + const nodes = Array.from({ length: 250 }, (_, i) => makeTextNode(`1:${i}`, `text ${i}`)); + const figma = await loadMain(nodes); + expect(figma.ui.onmessage).toBeTypeOf("function"); + + const updates = nodes.map((n) => ({ id: n.id, info: { key: `k${n.id}` } })); + await figma.ui.onmessage?.({ + type: "set-nodes-data", + correlationId: "corr-1", + nodes: updates, + }); + + const calls = figma.ui.postMessage.mock.calls.map((c) => c[0] as { type: string }); + + const progressMsgs = calls.filter((m) => m.type === "nodes-set-progress") as Array<{ + type: "nodes-set-progress"; + correlationId: string; + done: number; + total: number; + }>; + // setNodesData yields (and reports progress) every 50 updates, only when + // total > 100 — 250 updates -> 5 progress messages, done reaching total. + expect(progressMsgs).toHaveLength(5); + for (const msg of progressMsgs) { + expect(msg.correlationId).toBe("corr-1"); + expect(msg.total).toBe(250); + } + expect(progressMsgs[0]?.done).toBe(50); + expect(progressMsgs.at(-1)?.done).toBe(250); + + const resultMsgs = calls.filter((m) => m.type === "nodes-set-result") as Array<{ + type: "nodes-set-result"; + correlationId: string; + ok: boolean; + nodes: unknown[]; + }>; + expect(resultMsgs).toHaveLength(1); + expect(resultMsgs[0]?.correlationId).toBe("corr-1"); + expect(resultMsgs[0]?.ok).toBe(true); + expect(resultMsgs[0]?.nodes).toHaveLength(250); + }); + + it("sends no progress messages for a small batch (<=100), still sends the result", async () => { + const nodes = Array.from({ length: 10 }, (_, i) => makeTextNode(`1:${i}`, `text ${i}`)); + const figma = await loadMain(nodes); + + const updates = nodes.map((n) => ({ id: n.id, info: { key: `k${n.id}` } })); + await figma.ui.onmessage?.({ + type: "set-nodes-data", + correlationId: "corr-2", + nodes: updates, + }); + + const calls = figma.ui.postMessage.mock.calls.map((c) => c[0] as { type: string }); + expect(calls.filter((m) => m.type === "nodes-set-progress")).toHaveLength(0); + expect(calls.filter((m) => m.type === "nodes-set-result")).toHaveLength(1); + }); +}); + +describe("Dev-Mode canvas guard (attachBus + MESSAGE_IMPACT)", () => { + it("blocks a canvas message in dev: handler never runs, user gets a toast", async () => { + const figma = await loadMain([], "dev"); + // Silence the guard's console.warn — it's the expected path here. + vi.spyOn(console, "warn").mockImplementation(() => {}); + + await figma.ui.onmessage?.({ + type: "apply-translations", + correlationId: "corr-dev-1", + updates: [], + }); + + // Blocked BEFORE the handler: no apply-translations-result ever posted. + const calls = figma.ui.postMessage.mock.calls.map((c) => c[0] as { type: string }); + expect(calls.filter((m) => m.type === "apply-translations-result")).toHaveLength(0); + expect(figma.notify).toHaveBeenCalledWith("Not available in Dev Mode"); + }); + + it("lets a metadata message through in dev (production parity: Push works there)", async () => { + const node = makeTextNode("1:1", "Hello"); + const figma = await loadMain([node], "dev"); + + await figma.ui.onmessage?.({ + type: "set-nodes-data", + correlationId: "corr-dev-2", + nodes: [{ id: "1:1", info: { key: "k" } }], + }); + + const calls = figma.ui.postMessage.mock.calls.map((c) => c[0] as { type: string }); + expect(calls.filter((m) => m.type === "nodes-set-result")).toHaveLength(1); + expect(figma.notify).not.toHaveBeenCalled(); + }); + + it("does not interfere in the design editor: canvas messages run, no toast", async () => { + const figma = await loadMain([], "figma"); + + await figma.ui.onmessage?.({ + type: "apply-translations", + correlationId: "corr-fig-1", + updates: [], + }); + + const calls = figma.ui.postMessage.mock.calls.map((c) => c[0] as { type: string }); + expect(calls.filter((m) => m.type === "apply-translations-result")).toHaveLength(1); + expect(figma.notify).not.toHaveBeenCalled(); + }); +}); + +describe('main.ts on("ui-ready", ...) — config-first init + idempotent scan (task 58/63)', () => { + type Posted = { type: string; [k: string]: unknown }; + + it("replies with a config-first init immediately, then STREAMS the selection", async () => { + const selection = [makeTextNode("1:1", "Hello"), makeTextNode("1:2", "World")]; + const figma = await loadMain(selection, "figma", selection); + + await figma.ui.onmessage?.({ type: "ui-ready" }); + + const calls = figma.ui.postMessage.mock.calls.map((c) => c[0] as Posted); + const initIdx = calls.findIndex((m) => m.type === "init"); + const batchIdx = calls.findIndex((m) => m.type === "selection-batch"); + + // (c) init carries config + editorType + pageName … + const init = calls[initIdx] as Posted; + expect(init).toBeDefined(); + expect(init.editorType).toBe("figma"); + expect(init.pageName).toBe("Page 1"); + expect(init.config).toBeTypeOf("object"); + // … and NO selection — the nodes arrive via the stream, not the init. + expect(init.selectedNodes).toEqual([]); + expect(init.hasUserSelection).toBe(true); + + // (b) init goes out BEFORE the first streamed batch. + expect(initIdx).toBeGreaterThanOrEqual(0); + expect(batchIdx).toBeGreaterThan(initIdx); + + // The streamed batch actually carries the selected nodes. + const batch = calls[batchIdx] as Posted & { nodes: unknown[] }; + expect(batch.nodes).toHaveLength(2); + }); + + it("runs the scan AT MOST ONCE across repeated ui-ready retries", async () => { + const selection = [makeTextNode("1:1", "Hello"), makeTextNode("1:2", "World")]; + const figma = await loadMain(selection, "figma", selection); + + // Two retries land (the UI resends ui-ready until it sees init). + await figma.ui.onmessage?.({ type: "ui-ready" }); + await figma.ui.onmessage?.({ type: "ui-ready" }); + + const calls = figma.ui.postMessage.mock.calls.map((c) => c[0] as Posted); + + // Both ui-readys reply with an init (cheap) … + expect(calls.filter((m) => m.type === "init")).toHaveLength(2); + // … but the expensive scan ran exactly once: one pending, one batch set, + // one done. A second scan would double these. + expect(calls.filter((m) => m.type === "selection-pending")).toHaveLength(1); + expect(calls.filter((m) => m.type === "selection-batch")).toHaveLength(1); + expect(calls.filter((m) => m.type === "selection-done")).toHaveLength(1); + }); + + it("with no selection: config-first init, an (empty) scan, done total 0", async () => { + const figma = await loadMain([], "figma", []); + + await figma.ui.onmessage?.({ type: "ui-ready" }); + + const calls = figma.ui.postMessage.mock.calls.map((c) => c[0] as Posted); + const init = calls.find((m) => m.type === "init") as Posted; + expect(init.hasUserSelection).toBe(false); + expect(init.selectedNodes).toEqual([]); + // No nodes to stream → no batch, and done reports an empty selection. + expect(calls.filter((m) => m.type === "selection-batch")).toHaveLength(0); + const done = calls.find((m) => m.type === "selection-done") as Posted; + expect(done.total).toBe(0); + expect(done.hasUserSelection).toBe(false); + }); +}); + +/** + * The supersede mechanism behind `selectionchange` — flagged as untested in + * review, and the exact place the race below lived. + * + * `getSelectionInfo` is mocked so a scan's completion can be held open across + * a second selection event; the real one resolves too fast to interleave. + */ +describe("main.ts selection supersede", () => { + /** A scan whose completion the test controls. */ + function deferredScan() { + const pending: Array<{ + resolve: () => void; + isStale: () => boolean; + emit: (nodes: unknown[], first: boolean) => void; + }> = []; + const getSelectionInfo = vi.fn( + (isStale: () => boolean, onBatch: (nodes: unknown[], first: boolean) => void) => + new Promise((resolve) => { + pending.push({ resolve, isStale, emit: onBatch }); + }), + ); + return { pending, getSelectionInfo }; + } + + it("drops a scan superseded by a newer selection before it finishes", async () => { + const scan = deferredScan(); + vi.doMock("$main/nodes/selection", async () => { + const actual = await vi.importActual( + "$main/nodes/selection", + ); + return { ...actual, getSelectionInfo: scan.getSelectionInfo }; + }); + + const figma = await loadMain([]); + const selectionChange = figma.on.mock.calls.find((c) => c[0] === "selectionchange")?.[1] as + | (() => void) + | undefined; + expect(selectionChange).toBeTypeOf("function"); + + // Selection A — starts a scan and leaves it hanging. + selectionChange?.(); + await vi.waitFor(() => expect(scan.pending).toHaveLength(1)); + figma.ui.postMessage.mockClear(); + + // Selection B arrives while A is still in flight. + selectionChange?.(); + + // A now finishes. It must be recognised as stale: no batch, no done. + const first = scan.pending[0]; + first?.emit([{ id: "stale-node" }], true); + first?.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + const types = figma.ui.postMessage.mock.calls.map((c) => (c[0] as { type: string }).type); + expect(first?.isStale()).toBe(true); + expect(types).not.toContain("selection-batch"); + expect(types).not.toContain("selection-done"); + // The UI must still be told a scan is under way, or it would sit on the + // previous result with no loader. + expect(types).toContain("selection-pending"); + + vi.doUnmock("$main/nodes/selection"); + }); +}) diff --git a/src/main/bus.ts b/src/main/bus.ts new file mode 100644 index 00000000..014f4e7b --- /dev/null +++ b/src/main/bus.ts @@ -0,0 +1,65 @@ +import type { MainToUi, UiToMain } from "$shared/messages"; +import { DEV_ALLOWED_IMPACTS, MESSAGE_IMPACT } from "$shared/messagePolicy"; + +type Handlers = { + [K in UiToMain["type"]]?: (msg: Extract) => void | Promise; +}; + +const handlers: Handlers = {}; + +export function send(msg: MainToUi): void { + figma.ui.postMessage(msg); +} + +export function on( + type: K, + handler: (msg: Extract) => void | Promise, +): void { + handlers[type] = handler as Handlers[K]; +} + +export function attachBus(): void { + figma.ui.onmessage = async (msg: UiToMain) => { + // Dev Mode is inspect-only: hard-block anything classified as a canvas + // write (see $shared/messagePolicy — the exhaustive impact map is the + // single defence layer, not per-call-site conditions). The UI hides the + // affordances that send these, so reaching this guard is an anomaly + // worth surfacing to the user, not a silent drop. + if (figma.editorType === "dev" && !DEV_ALLOWED_IMPACTS.has(MESSAGE_IMPACT[msg.type])) { + figma.notify("Not available in Dev Mode"); + console.warn("[tolgee:main] blocked canvas message in Dev Mode:", msg.type); + return; + } + const handler = handlers[msg.type] as ((m: UiToMain) => void | Promise) | undefined; + if (handler) { + try { + await handler(msg); + } catch (err) { + // Figma drops an unhandled rejection from `onmessage` without a trace, + // so an unwrapped throw meant the UI simply never got an answer: no + // response, no error, and whatever pending/progress state it armed for + // this request stayed armed until the user force-quit the plugin. + // Individual handlers had started growing their own try/catch to work + // around this (see `request-copy-staleness` in main.ts); the boundary + // belongs here, once. + console.error("[tolgee:main] handler failed", msg.type, err); + figma.notify("Something went wrong. Please try again."); + send({ + type: "handler-error", + forType: msg.type, + // Correlated requests can settle exactly the round-trip that died; + // uncorrelated ones (e.g. the bulk-write progress bar) still get a + // signal to clear on. + correlationId: + "correlationId" in msg && typeof msg.correlationId === "string" + ? msg.correlationId + : undefined, + }); + } + } else { + // Log only the type, never the whole message — some UI→main messages + // carry the API key (e.g. save-config), which must not leak to the console. + console.warn("[tolgee:main] unhandled message", msg.type); + } + }; +} diff --git a/src/main/endpoints/clearPrefilledKeys.ts b/src/main/endpoints/clearPrefilledKeys.ts deleted file mode 100644 index d4317300..00000000 --- a/src/main/endpoints/clearPrefilledKeys.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { TOLGEE_NODE_INFO } from "@/constants"; -import { createEndpoint } from "../utils/createEndpoint"; - -const walkTextNodes = ( - nodes: readonly SceneNode[], - visit: (node: TextNode) => void, -) => { - for (const node of nodes) { - if (node.type === "TEXT") { - visit(node); - } - // @ts-ignore - not all SceneNodes have children, but the check guards us - if (node.children) { - // @ts-ignore - walkTextNodes(node.children as SceneNode[], visit); - } - } -}; - -export const clearPrefilledKeysEndpoint = createEndpoint( - "CLEAR_PREFILLED_KEYS", - () => { - for (const page of figma.root.children) { - if (page.type !== "PAGE") continue; - walkTextNodes(page.children, (node) => { - const raw = node.getPluginData(TOLGEE_NODE_INFO); - if (!raw) return; - let data: Record; - try { - data = JSON.parse(raw); - } catch { - return; - } - if (data.connected) return; - if (!data.key) return; - node.setPluginData( - TOLGEE_NODE_INFO, - JSON.stringify({ ...data, key: "" }), - ); - }); - } - }, -); diff --git a/src/main/endpoints/copyPage.ts b/src/main/endpoints/copyPage.ts deleted file mode 100644 index 75a99fa5..00000000 --- a/src/main/endpoints/copyPage.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { NodeInfo } from "@/types"; -import { createEndpoint } from "../utils/createEndpoint"; -import { getAllPages, getPageData, setPageData } from "../utils/pages"; -import { findTextNodes, getNodeInfo } from "../utils/nodeTools"; -import { loadFontsAsync } from "@create-figma-plugin/utilities"; -import { compareNs } from "@/tools/compareNs"; - -type Props = { - language: string; - nodes: NodeInfo[]; -}; - -export const copyPageEndpoint = createEndpoint( - "COPY_PAGE", - async (data) => { - const newPage = figma.currentPage.clone(); - const name = `${figma.currentPage.name} - ${ - data?.language ? data.language : "keys" - }`; - getAllPages().forEach((page) => { - if (page.name === name && getPageData(page).pageCopy === true) { - page.remove(); - } - }); - newPage.name = name; - setPageData( - { pageCopy: true, pageInfo: true, language: data?.language }, - newPage, - ); - const textNodes = findTextNodes(newPage.children).filter( - (n) => getNodeInfo(n).key, - ); - - try { - await loadFontsAsync(textNodes); - } catch (e) { - console.error(e); - } - for (const node of textNodes) { - const { key, ns } = getNodeInfo(node); - - if (node.hasMissingFont) { - continue; - } - - if (data?.language) { - const newText = data.nodes.find( - (n) => n.key === key && compareNs(n.ns, ns), - )?.characters; - - if (newText) { - node.characters = newText; - } - } else { - node.characters = key; - } - } - - figma.notify(`Created page "${name}"`); - }, -); diff --git a/src/main/endpoints/editorType.ts b/src/main/endpoints/editorType.ts deleted file mode 100644 index 63f47cc6..00000000 --- a/src/main/endpoints/editorType.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createEndpoint } from "../utils/createEndpoint"; - -type EditorType = (typeof figma)["editorType"]; - -export const editorTypeEndpoint = createEndpoint( - "EDITOR_TYPE", - () => { - return figma.editorType; - }, -); diff --git a/src/main/endpoints/formatText.ts b/src/main/endpoints/formatText.ts deleted file mode 100644 index 2c46b7a9..00000000 --- a/src/main/endpoints/formatText.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { NodeInfo } from "../../types"; -import { createEndpoint } from "../utils/createEndpoint"; - -// Cached across all formatText calls in a session. listAvailableFontsAsync is -// one of the most expensive Figma API calls; calling it once per node during -// a multi-node pull would multiply the cost unnecessarily. -let availableFontsCache: Font[] | null = null; -// In-flight promise prevents concurrent callers from each issuing the request. -let availableFontsPromise: Promise | null = null; - -export type FormatTextEndpointArgs = { - /** The text that is to be displayed in the textNode. Can contain some basic html tags that will be replaced */ - formatted: string; - nodeInfo: NodeInfo; -}; - -/** - * Takes a node and the formatted (meaning parameters and plural values have been interpolated) text for this node - * and replaces the text in the node with the formatted text. It also applies bold, italic and underline styles to the text - */ -export const formatText = async ({ - nodeInfo, - formatted, -}: FormatTextEndpointArgs) => { - const textNode = figma.getNodeById(nodeInfo.id) as TextNode; - textNode.autoRename = false; - - const strippedAwayTags = ["strong", "b", "em", "i", "u"]; - - const fontNames = textNode.getRangeAllFontNames( - 0, - textNode.characters.length, - ); - - for (const font of fontNames) { - await figma.loadFontAsync(font); - } - - const breaks = /\s*<\/br>||<\/br>/gi; - - const formattedWithoutBreaks = formatted.replace(breaks, "\n"); - - const plainText = formattedWithoutBreaks.replace(/<[^>]*>/g, ""); - textNode.characters = plainText; - - const findRanges = ( - html: string, - tag: string, - ): { start: number; end: number }[] => { - const ranges: { start: number; end: number }[] = []; - const regex = new RegExp(`<${tag}>(.*?)`, "g"); - let match: RegExpExecArray | null; - - while ((match = regex.exec(html)) !== null) { - let offset = 0; - let tempString = html.substring(0, match.index); - strippedAwayTags.forEach((tag) => { - let regex = new RegExp(``, "g"); - let match; - while ((match = regex.exec(tempString)) !== null) { - offset += match[0].length; - tempString = tempString.replace(match[0], ""); - regex = new RegExp(``, "g"); - } - }); - - const start = match.index - offset; - const end = start + match[1].length; - ranges.push({ start, end }); - } - return ranges; - }; - - const getAvailableFonts = async () => { - if (availableFontsCache) return availableFontsCache; - if (!availableFontsPromise) { - availableFontsPromise = figma.listAvailableFontsAsync(); - } - availableFontsCache = await availableFontsPromise; - return availableFontsCache; - }; - - const getFontsOfFamily = async (font: FontName) => - (await getAvailableFonts()).filter( - (f) => f.fontName.family === font.family, - ); - - const findBestBoldStyle = async ( - family: string, - availableFonts: Font[], - ): Promise => { - /** Some common names for bold styles */ - const boldStyles = [ - "Bold", - "Semibold", - "Semi Bold", - "Medium", - "Extra Bold", - "Heavy", - "Black", - "Ultra Bold", - ]; - - for (const style of boldStyles) { - const fontExists = availableFonts.some( - (font) => - font.fontName.family === family && font.fontName.style === style, - ); - if (fontExists) { - try { - await figma.loadFontAsync({ family, style }); - return style; - } catch (error) { - console.log(`Failed to load font: ${error}`); - } - } - } - - /** First font where "bold" is included */ - const fallbackFont = availableFonts.find((f) => - f.fontName.style.toLowerCase().includes("bold"), - ); - - if (fallbackFont) { - try { - await figma.loadFontAsync(fallbackFont.fontName); - return fallbackFont.fontName.style; - } catch (error) { - console.error(`Failed to load fallback font: ${error}`); - } - } - return null; - }; - - const findBestItalicStyle = async ( - family: string, - availableFonts: Font[], - ): Promise => { - /** List of common italic styles */ - const italicStyles = [ - "Italic", - "Regular Italic", - "Medium Italic", - "Light Italic", - "Semi Bold Italic", - "Semibold Italic", - "Bold Italic", - "Oblique", - "Regular Oblique", - ]; - - for (const style of italicStyles) { - const fontExists = availableFonts.some( - (font) => - font.fontName.family === family && font.fontName.style === style, - ); - if (fontExists) { - try { - await figma.loadFontAsync({ family, style }); - return style; - } catch (error) { - console.error(`Failed to load font: ${error}`); - } - } - } - - /** First font with italic/oblique in it's name */ - const fallbackFont = availableFonts.find( - (f) => - f.fontName.style.toLowerCase().includes("italic") || - f.fontName.style.toLowerCase().includes("oblique"), - ); - - if (fallbackFont) { - try { - await figma.loadFontAsync(fallbackFont.fontName); - return fallbackFont.fontName.style; - } catch (error) { - console.error(`Failed to load fallback font: ${error}`); - } - } - return null; - }; - - const applyStyles = async ( - ranges: { start: number; end: number }[], - style: Partial | undefined, - decoration?: TextDecoration, - ) => { - for (const range of ranges) { - if (range.start > range.end) { - continue; - } - if (style && style.family && style.style) { - textNode.setRangeFontName( - range.start, - Math.min(textNode.characters.length, range.end), - { - family: style.family, - style: style.style, - }, - ); - } - if (decoration) { - textNode.setRangeTextDecoration( - range.start, - Math.min(textNode.characters.length, range.end), - decoration, - ); - } - } - }; - - const getFontForRange = (range: { start: number; end: number }) => { - const font = textNode.getRangeFontName( - range.start, - Math.min(textNode.characters.length, range.end), - ); - - if (font === figma.mixed) { - return textNode.getRangeFontName( - range.start, - range.start + 1, - ) as FontName; - } - return font as FontName; - }; - - const boldRanges = [ - ...findRanges(formattedWithoutBreaks, "strong"), - ...findRanges(formattedWithoutBreaks, "b"), - ]; - const italicRanges = [ - ...findRanges(formattedWithoutBreaks, "em"), - ...findRanges(formattedWithoutBreaks, "i"), - ]; - const underlineRanges = findRanges(formattedWithoutBreaks, "u"); - - const defaultRanges: { start: number; end: number }[] = []; - - // Track continuous ranges of default text - let currentRange: { start: number; end: number } | null = null; - - const formattedRanges = [...boldRanges, ...italicRanges]; - for (let i = 0; i < textNode.characters.length; i++) { - const isBoldOrItalic = formattedRanges.some( - (range) => i >= range.start && i <= range.end, - ); - - if (!isBoldOrItalic) { - if (currentRange === null) { - currentRange = { start: i, end: i + 1 }; - } else { - currentRange.end = i + 1; - } - } else if (currentRange) { - defaultRanges.push(currentRange); - currentRange = null; - } - } - - if (currentRange) { - defaultRanges.push(currentRange); - } - - for (const range of defaultRanges) { - const font = getFontForRange(range); - - if (font.family) { - await figma.loadFontAsync({ - family: font.family, - style: font.style, - }); - await applyStyles([range], { - family: font.family, - style: font.style, - }); - } - } - - for (const range of boldRanges) { - const font = getFontForRange(range); - - if (font.family) { - const availableFonts = await getFontsOfFamily(font); - const boldStyle = await findBestBoldStyle(font.family, availableFonts); - - if (boldStyle) { - await applyStyles([range], { family: font.family, style: boldStyle }); - } else { - // Fallback to original style if no bold variant found - await figma.loadFontAsync({ family: font.family, style: font.style }); - await applyStyles([range], { family: font.family, style: font.style }); - } - } - } - - for (const range of italicRanges) { - const font = getFontForRange(range); - - if (font.family) { - const availableFonts = await getFontsOfFamily(font); - const italicStyle = await findBestItalicStyle( - font.family, - availableFonts, - ); - - if (italicStyle) { - await applyStyles([range], { family: font.family, style: italicStyle }); - } else { - // Fallback to original style if no italic variant found - await figma.loadFontAsync({ family: font.family, style: font.style }); - await applyStyles([range], { family: font.family, style: font.style }); - } - } - } - - for (const range of underlineRanges) { - await applyStyles([range], undefined, "UNDERLINE"); - } -}; - -export const formatTextEndpoint = createEndpoint( - "UPDATE_NODE_TEXT", - async (args) => await formatText(args), -); diff --git a/src/main/endpoints/getConnectedNodes.ts b/src/main/endpoints/getConnectedNodes.ts deleted file mode 100644 index a3c22538..00000000 --- a/src/main/endpoints/getConnectedNodes.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { TOLGEE_NODE_INFO } from "@/constants"; -import { NodeInfo } from "@/types"; -import { createEndpoint } from "../utils/createEndpoint"; -import { findTextNodesInfo, getNodeInfo } from "../utils/nodeTools"; - -export type ConnectedNodesProps = { - ignoreSelection: boolean; -}; - -export const getConnectedNodesEndpoint = createEndpoint< - ConnectedNodesProps, - { items: NodeInfo[]; basedOnSelection: boolean } ->("GET_CONNECTED_NODES", async ({ ignoreSelection }) => { - const basedOnSelection = - !ignoreSelection && figma.currentPage.selection.length > 0; - - if (basedOnSelection) { - // Selection path: still walk manually since selection may be a subtree of - // any depth and `findAllWithCriteria` only operates on the whole page. - return { - items: findTextNodesInfo(figma.currentPage.selection).filter( - ({ key }) => key, - ), - basedOnSelection: true, - }; - } - - // Page-wide path: let the Figma runtime filter by plugin-data key natively. - // This avoids recursing through every node in JS and calling getPluginData - // on every TextNode — O(all nodes) → O(connected nodes). - await figma.currentPage.loadAsync(); - const connected = figma.currentPage.findAllWithCriteria({ - types: ["TEXT"], - pluginData: { keys: [TOLGEE_NODE_INFO] }, - }) as TextNode[]; - - return { - items: connected.map(getNodeInfo), - basedOnSelection: false, - }; -}); diff --git a/src/main/endpoints/getScreenshots.ts b/src/main/endpoints/getScreenshots.ts deleted file mode 100644 index 351fcce0..00000000 --- a/src/main/endpoints/getScreenshots.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { FrameScreenshot } from "@/types"; -import { createEndpoint } from "../utils/createEndpoint"; -import { - findLastParentFrame, - findTextNodes, - getNodeInfo, -} from "../utils/nodeTools"; - -export const getScreenshotsEndpoint = createEndpoint( - "GET_SCREENSHOTS", - async () => { - const connectedNodes = findTextNodes( - figma.currentPage.selection.length - ? figma.currentPage.selection - : figma.currentPage.children, - ).filter((node) => getNodeInfo(node).key); - - const frameToNodes = new Map(); - - // find frames in parents and group text nodes by frame - connectedNodes.forEach((connectedNode) => { - const frame = findLastParentFrame(connectedNode); - if (frame) { - let nodes = frameToNodes.get(frame); - if (!nodes) { - nodes = []; - frameToNodes.set(frame, nodes); - } - nodes.push(connectedNode); - } - }); - - const data: FrameScreenshot[] = await Promise.all( - Array.from(frameToNodes.entries()).map(async ([frame, textNodes]) => { - const framePosition = frame.absoluteBoundingBox!; - return { - image: await frame.exportAsync({ format: "PNG" }), - info: { - id: frame.id, - name: frame.name, - width: framePosition.width, - height: framePosition.height, - }, - keys: textNodes.map((node) => { - const nodePosition = node.absoluteBoundingBox!; - return { - ...getNodeInfo(node), - x: nodePosition.x - framePosition.x, - y: nodePosition.y - framePosition.y, - width: nodePosition.width, - height: nodePosition.height, - }; - }), - }; - }), - ); - return data; - }, -); diff --git a/src/main/endpoints/getSelectedNodes.ts b/src/main/endpoints/getSelectedNodes.ts deleted file mode 100644 index 9aecb4f9..00000000 --- a/src/main/endpoints/getSelectedNodes.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NodeInfo } from "@/types"; -import { createEndpoint } from "../utils/createEndpoint"; -import { findTextNodesInfo } from "../utils/nodeTools"; - -export const getSelectedNodesEndpoint = createEndpoint< - void, - { items: NodeInfo[]; somethingSelected: boolean } ->("GET_SELECTED_NODES", () => { - const somethingSelected = figma.currentPage.selection.length > 0; - return { - items: findTextNodesInfo(figma.currentPage.selection), - somethingSelected, - }; -}); diff --git a/src/main/endpoints/highlightNode.ts b/src/main/endpoints/highlightNode.ts deleted file mode 100644 index 2ec482fd..00000000 --- a/src/main/endpoints/highlightNode.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createEndpoint } from "../utils/createEndpoint"; - -const HIGHLIGHT_COLOR: SolidPaint = { - type: "SOLID", - color: { r: 1, g: 0.41, b: 0.58 }, -}; - -export type HighlightNodeProps = { - id: string; -}; - -const highlitedNodes: Map = new Map(); - -export const highlightNodeEndpoint = createEndpoint( - "HIGHLIGHT_NODE", - async ({ id }: HighlightNodeProps) => { - const node = figma.getNodeById(id) as TextNode | null; - - if (node) { - figma.viewport.scrollAndZoomIntoView([node]); - } - - if (node && !highlitedNodes.has(id) && figma.editorType === "figma") { - highlitedNodes.set(id, node.fills); - const original = node.fills; - node.fills = [HIGHLIGHT_COLOR]; - - setTimeout(() => { - node.fills = original; - highlitedNodes.delete(id); - }, 500); - } - }, -); - -export const cleanUp = () => { - highlitedNodes.forEach((value, id) => { - const node = figma.getNodeById(id) as TextNode | null; - if (node) { - node.fills = value; - highlitedNodes.delete(id); - } - }); -}; diff --git a/src/main/endpoints/notify.ts b/src/main/endpoints/notify.ts deleted file mode 100644 index 2038587a..00000000 --- a/src/main/endpoints/notify.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createEndpoint } from "../utils/createEndpoint"; - -export const notifyEndpoint = createEndpoint("NOTIFY", (arg) => { - figma.notify(arg); -}); diff --git a/src/main/endpoints/preformatKey.ts b/src/main/endpoints/preformatKey.ts deleted file mode 100644 index c2de9dd9..00000000 --- a/src/main/endpoints/preformatKey.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { createEndpoint } from "../utils/createEndpoint"; -import { TOLGEE_KEY_FORMAT_PLACEHOLDERS } from "@/constants"; -import { getAllParents } from "../utils/nodeParents"; -import { formatString } from "@/utilities"; -import { TolgeeConfig } from "@/types"; - -export type PreformatKeyEndpointArgs = { - nodeId: string; - keyFormat: string; - variableCasing: TolgeeConfig["variableCasing"]; -}; - -function escapeRegExp(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -function replacePlaceholder( - originalFormat: string, - result: string, - placeholder: string, - value: string, - variableCasing: TolgeeConfig["variableCasing"], -) { - let newFormat = result; - const replaceString = formatString(value, variableCasing); - - if (replaceString === "") { - // If the value is empty, we need to remove the placeholder and the separator before it - const textBeforePlaceholder = originalFormat.slice( - 0, - originalFormat.indexOf(placeholder), - ); - - const separatorBeforePlaceholder = textBeforePlaceholder.slice( - textBeforePlaceholder.lastIndexOf("}") + 1, - ); - - newFormat = newFormat.replace( - new RegExp( - `${escapeRegExp(separatorBeforePlaceholder)}${escapeRegExp(placeholder)}`, - "g", - ), - "", - ); - } else { - // If the value is not empty, we need to replace the placeholder with the value - newFormat = newFormat.replace( - new RegExp(`${escapeRegExp(placeholder)}`, "g"), - replaceString, - ); - } - return newFormat; -} - -export const preformatKeyEndpoint = createEndpoint< - PreformatKeyEndpointArgs, - string ->("FORMAT_KEY", ({ nodeId, keyFormat, variableCasing }) => { - const parents = getAllParents(nodeId); - - let result = keyFormat; - for (const placeholder of Object.values(TOLGEE_KEY_FORMAT_PLACEHOLDERS)) { - switch (placeholder) { - case "{artboard}": - result = replacePlaceholder( - keyFormat, - result, - placeholder, - parents.artboard?.name ?? "", - variableCasing, - ); - break; - case "{frame}": - result = replacePlaceholder( - keyFormat, - result, - placeholder, - parents.frame?.name ?? "", - variableCasing, - ); - break; - case "{elementName}": - result = replacePlaceholder( - keyFormat, - result, - placeholder, - parents.element?.name ?? "", - variableCasing, - ); - break; - case "{elementText}": - result = replacePlaceholder( - keyFormat, - result, - placeholder, - parents.element?.type === "TEXT" ? parents.element.characters : "", - variableCasing, - ); - break; - case "{component}": - result = replacePlaceholder( - keyFormat, - result, - placeholder, - parents.component?.name ?? "", - variableCasing, - ); - break; - case "{section}": - result = replacePlaceholder( - keyFormat, - result, - placeholder, - parents.section?.name ?? "", - variableCasing, - ); - break; - case "{group}": - result = replacePlaceholder( - keyFormat, - result, - placeholder, - parents.group?.name ?? "", - variableCasing, - ); - break; - default: - break; - } - } - return result; -}); diff --git a/src/main/endpoints/setNodesData.ts b/src/main/endpoints/setNodesData.ts deleted file mode 100644 index 4eec0afa..00000000 --- a/src/main/endpoints/setNodesData.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NodeInfo } from "@/types"; -import { createEndpoint } from "../utils/createEndpoint"; -import { TOLGEE_NODE_INFO } from "@/constants"; - -export type SetNodesDataProps = { - nodes: NodeInfo[]; -}; - -export const setNodesDataEndpoint = createEndpoint( - "SET_NODES_DATA", - async ({ nodes }) => { - for (const nodeInfo of nodes) { - const node = figma.getNodeById(nodeInfo.id); - node?.setPluginData( - TOLGEE_NODE_INFO, - JSON.stringify({ - key: nodeInfo.key, - isPlural: nodeInfo.isPlural, - pluralParamValue: nodeInfo.pluralParamValue, - translation: nodeInfo.translation, - paramsValues: nodeInfo.paramsValues, - ns: nodeInfo.ns, - connected: nodeInfo.connected, - }), - ); - } - }, -); diff --git a/src/main/endpoints/updateNodes.ts b/src/main/endpoints/updateNodes.ts deleted file mode 100644 index e8ac3ef5..00000000 --- a/src/main/endpoints/updateNodes.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { NodeInfo, SelectionChangeHandler } from "@/types"; -import { createEndpoint } from "../utils/createEndpoint"; -import { emit, loadFontsAsync } from "@create-figma-plugin/utilities"; -import { formatText } from "./formatText"; - -export type UpdateNodeProps = { - nodes: (NodeInfo & { formatted: string })[]; -}; - -export const updateNodesEndpoint = createEndpoint( - "UPDATE_NODES", - async ({ nodes }) => { - const textNodes = nodes.map((n) => figma.getNodeById(n.id) as TextNode); - - try { - await loadFontsAsync(textNodes); - } catch (e) { - console.error(e); - } - const promises = nodes.map((nodeInfo) => { - const node = textNodes.find((nod) => nod.id === nodeInfo.id)!; - if (node.hasMissingFont) { - return; - } - - return formatText({ - formatted: nodeInfo.formatted, - nodeInfo, - }); - }); - await Promise.allSettled(promises); - figma.notify("Document translations updated"); - - // update selection - emit("SELECTION_CHANGE"); - }, -); diff --git a/src/main/handlers/__tests__/createCopy.test.ts b/src/main/handlers/__tests__/createCopy.test.ts new file mode 100644 index 00000000..c7035a7f --- /dev/null +++ b/src/main/handlers/__tests__/createCopy.test.ts @@ -0,0 +1,825 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { TOLGEE_NODE_INFO, TOLGEE_PLUGIN_CONFIG_NAME } from "$shared/constants"; +import { checkCopyStaleness, createCopy, removeExistingCopyPages } from "../createCopy"; + +type FakeTextNode = { + type: "TEXT"; + id: string; + name: string; + characters: string; + visible: boolean; + getPluginData: (key: string) => string; +}; + +let nextNodeId = 0; + +function textNode(info: { key?: string; ns?: string; connected?: boolean } = {}): FakeTextNode { + nextNodeId++; + const data = JSON.stringify({ + key: info.key ?? "", + ns: info.ns, + connected: info.connected ?? true, + }); + return { + type: "TEXT", + id: `node-${nextNodeId}`, + name: "Layer", + characters: "", + visible: true, + getPluginData: (k: string) => (k === TOLGEE_NODE_INFO ? data : ""), + }; +} + +type FakePage = { + type: "PAGE"; + id: string; + name: string; + loadAsync: () => Promise; + getPluginData: (key: string) => string; + setPluginData: (key: string, value: string) => void; + remove: () => void; + findAllWithCriteria: () => FakeTextNode[]; +}; + +function page( + name: string, + pageData?: Record, + opts: { id?: string; textNodes?: FakeTextNode[] } = {}, +): FakePage { + let raw = pageData ? JSON.stringify(pageData) : ""; + const textNodes = opts.textNodes ?? []; + return { + type: "PAGE", + id: opts.id ?? name, + name, + loadAsync: vi.fn(async () => {}), + getPluginData: (key: string) => (key === TOLGEE_PLUGIN_CONFIG_NAME ? raw : ""), + setPluginData: (key: string, value: string) => { + if (key === TOLGEE_PLUGIN_CONFIG_NAME) raw = value; + }, + remove: vi.fn(), + findAllWithCriteria: () => textNodes, + }; +} + +function setPages(pages: FakePage[], currentPage?: FakePage) { + const figma = { + root: { children: pages }, + currentPage: currentPage ?? pages[0], + getNodeByIdAsync: async (id: string) => pages.find((p) => p.id === id) ?? null, + // The sync `figma.currentPage` SETTER throws under dynamic-page access — + // production code must only ever switch via this async API. + setCurrentPageAsync: async (page: FakePage) => { + figma.currentPage = page; + }, + mixed: Symbol("mixed"), + }; + (globalThis as unknown as { figma: unknown }).figma = figma; +} + +afterEach(() => { + (globalThis as unknown as { figma?: unknown }).figma = undefined; +}); + +describe("removeExistingCopyPages", () => { + it("removes a previous copy with the same name", async () => { + const oldCopy = page("Home — en", { pageCopy: true }); + setPages([page("Home"), oldCopy]); + + await removeExistingCopyPages({ name: "Home — en", sourcePageId: "src" }); + + expect(oldCopy.remove).toHaveBeenCalledTimes(1); + }); + + it("does NOT remove a user's own same-named page (not marked pageCopy)", async () => { + const userPage = page("Home — en"); // no pageCopy marker + setPages([userPage]); + + await removeExistingCopyPages({ name: "Home — en", sourcePageId: "src" }); + + expect(userPage.remove).not.toHaveBeenCalled(); + }); + + it("does NOT remove a copy with a different name", async () => { + const otherCopy = page("Home — de", { pageCopy: true }); + setPages([otherCopy]); + + await removeExistingCopyPages({ name: "Home — en", sourcePageId: "src" }); + + expect(otherCopy.remove).not.toHaveBeenCalled(); + }); + + describe("when the page being removed is the currently active one", () => { + it("steps onto the preferred fallback first, then removes it (and reports it)", async () => { + const source = page("Home", undefined, { id: "src" }); + const activeCopy = page("Home — en", { pageCopy: true }, { id: "copy" }); + setPages([source, activeCopy], activeCopy); + + const result = await removeExistingCopyPages( + { name: "Home — en", sourcePageId: "src" }, + source as never, + ); + + expect(result.switchedAwayFromCurrent).toBe(true); + expect( + (globalThis as unknown as { figma: { currentPage: FakePage } }).figma.currentPage, + ).toBe(source); + expect(activeCopy.remove).toHaveBeenCalledTimes(1); + }); + + it("doesn't touch the current page when the copy being removed isn't it", async () => { + const source = page("Home", undefined, { id: "src" }); + const oldCopy = page("Home — en", { pageCopy: true }, { id: "copy" }); + setPages([source, oldCopy], source); + + const result = await removeExistingCopyPages( + { name: "Home — en", sourcePageId: "src" }, + source as never, + ); + + expect(result.switchedAwayFromCurrent).toBe(false); + expect( + (globalThis as unknown as { figma: { currentPage: FakePage } }).figma.currentPage, + ).toBe(source); + expect(oldCopy.remove).toHaveBeenCalledTimes(1); + }); + }); +}); + +describe("checkCopyStaleness", () => { + it("reports 0 missing when the copy already has every key the source has", async () => { + const source = page("Home", undefined, { + id: "src", + textNodes: [textNode({ key: "a" }), textNode({ key: "b" })], + }); + const copyPage = page( + "Home — en", + { pageCopy: true, sourcePageId: "src", language: "en" }, + { id: "copy", textNodes: [textNode({ key: "a" }), textNode({ key: "b" })] }, + ); + setPages([source, copyPage], copyPage); + + await expect(checkCopyStaleness(copyPage as never)).resolves.toEqual({ + ok: true, + missingCount: 0, + removedCount: 0, + }); + }); + + it("counts source keys the copy doesn't have yet", async () => { + const source = page("Home", undefined, { + id: "src", + textNodes: [textNode({ key: "a" }), textNode({ key: "b" }), textNode({ key: "c" })], + }); + const copyPage = page( + "Home — en", + { pageCopy: true, sourcePageId: "src", language: "en" }, + { id: "copy", textNodes: [textNode({ key: "a" })] }, + ); + setPages([source, copyPage], copyPage); + + await expect(checkCopyStaleness(copyPage as never)).resolves.toEqual({ + ok: true, + missingCount: 2, + removedCount: 0, + }); + }); + + it("ignores nodes that aren't actually connected on either side", async () => { + const source = page("Home", undefined, { + id: "src", + textNodes: [textNode({ key: "a" }), textNode({ key: "unconnected", connected: false })], + }); + const copyPage = page( + "Home — en", + { pageCopy: true, sourcePageId: "src", language: "en" }, + { id: "copy", textNodes: [textNode({ key: "a" })] }, + ); + setPages([source, copyPage], copyPage); + + await expect(checkCopyStaleness(copyPage as never)).resolves.toEqual({ + ok: true, + missingCount: 0, + removedCount: 0, + }); + }); + + it("counts an ADDITIONAL node connected to an already-connected key (set-diff regression)", async () => { + // Real-world repro: the source already had key "a" connected on one node + // when the copy was made; the user then connected a SECOND node to that + // same key. A set-based diff sees no new key and stays silent — but the + // copy's clone of that second node predates the connection, so Download + // will never touch it. Counts must flag it. + const source = page("Home", undefined, { + id: "src", + textNodes: [textNode({ key: "a" }), textNode({ key: "a" })], + }); + const copyPage = page( + "Home — en", + { pageCopy: true, sourcePageId: "src", language: "en" }, + { id: "copy", textNodes: [textNode({ key: "a" })] }, + ); + setPages([source, copyPage], copyPage); + + await expect(checkCopyStaleness(copyPage as never)).resolves.toEqual({ + ok: true, + missingCount: 1, + removedCount: 0, + }); + }); + + it("reports strings the source LOST as removedCount, never as negative missing", async () => { + // The copy has a surplus on key "a" (the source lost a node since the + // copy was made). That surplus must surface as `removedCount` — and per- + // key maxes keep it from cancelling genuine missing counts elsewhere. + const source = page("Home", undefined, { + id: "src", + textNodes: [textNode({ key: "a" }), textNode({ key: "b" })], + }); + const copyPage = page( + "Home — en", + { pageCopy: true, sourcePageId: "src", language: "en" }, + { + id: "copy", + textNodes: [textNode({ key: "a" }), textNode({ key: "a" }), textNode({ key: "b" })], + }, + ); + setPages([source, copyPage], copyPage); + + await expect(checkCopyStaleness(copyPage as never)).resolves.toEqual({ + ok: true, + missingCount: 0, + removedCount: 1, + }); + }); + + it("counts both directions independently in one check", async () => { + // Source gained key "c" AND lost one "a" node; neither direction may + // cancel the other. + const source = page("Home", undefined, { + id: "src", + textNodes: [textNode({ key: "a" }), textNode({ key: "c" })], + }); + const copyPage = page( + "Home — en", + { pageCopy: true, sourcePageId: "src", language: "en" }, + { id: "copy", textNodes: [textNode({ key: "a" }), textNode({ key: "a" })] }, + ); + setPages([source, copyPage], copyPage); + + await expect(checkCopyStaleness(copyPage as never)).resolves.toEqual({ + ok: true, + missingCount: 1, + removedCount: 1, + }); + }); + + it("fails gracefully when the copy has no sourcePageId recorded (older copy / production)", async () => { + const copyPage = page("Home — en", { pageCopy: true }, { id: "copy" }); + setPages([copyPage], copyPage); + + const result = await checkCopyStaleness(copyPage as never); + expect(result.ok).toBe(false); + expect(result.error).toBeTruthy(); + }); + + it("fails gracefully when the recorded source page no longer exists", async () => { + const copyPage = page( + "Home — en", + { pageCopy: true, sourcePageId: "gone" }, + { id: "copy" }, + ); + setPages([copyPage], copyPage); + + const result = await checkCopyStaleness(copyPage as never); + expect(result.ok).toBe(false); + expect(result.error).toBeTruthy(); + }); +}); + +describe("createCopy — ignore settings", () => { + /** Minimal TEXT-node stand-in that also supports the plain-text write path + * (`writeTextSafely` → `applyRichText` with `plainOnly: true`), unlike the + * read-only `textNode()` helper above. */ + function writableTextNode(props: { + id: string; + characters: string; + key?: string; + ns?: string; + connected?: boolean; + visible?: boolean; + name?: string; + }) { + const pluginData = new Map(); + pluginData.set( + TOLGEE_NODE_INFO, + JSON.stringify({ key: props.key ?? "", ns: props.ns, connected: props.connected ?? true }), + ); + return { + type: "TEXT" as const, + id: props.id, + name: props.name ?? "Layer", + characters: props.characters, + visible: props.visible ?? true, + autoRename: true, + fontName: { family: "Inter", style: "Regular" }, + getRangeAllFontNames: () => [{ family: "Inter", style: "Regular" }], + getPluginData: (k: string) => pluginData.get(k) ?? "", + setPluginData: (k: string, v: string) => pluginData.set(k, v), + }; + } + type WritableTextNode = ReturnType; + + /** A source page whose `.clone()` returns a second page backed by the SAME + * node list — good enough to exercise the per-node filter without + * modelling Figma's actual clone semantics. */ + function cloneableSourcePage(name: string, textNodes: WritableTextNode[]) { + const clone = { + type: "PAGE" as const, + id: `${name}-clone`, + name, + loadAsync: vi.fn(async () => {}), + getPluginData: () => "", + setPluginData: vi.fn(), + findAllWithCriteria: () => textNodes, + }; + return { + type: "PAGE" as const, + id: name, + name, + loadAsync: vi.fn(async () => {}), + getPluginData: () => "", + setPluginData: vi.fn(), + findAllWithCriteria: () => textNodes, + clone: () => clone, + }; + } + + function installFigmaForCreateCopy(sourcePage: ReturnType) { + (globalThis as unknown as { figma: unknown }).figma = { + currentPage: sourcePage, + root: { children: [sourcePage], appendChild: vi.fn() }, + getNodeByIdAsync: async (id: string) => (id === sourcePage.id ? sourcePage : null), + setCurrentPageAsync: async () => {}, + loadFontAsync: async () => {}, + mixed: Symbol("mixed"), + }; + } + + it("does not overwrite hidden / numeric nodes in keys mode (default settings)", async () => { + const visible = writableTextNode({ id: "n1", characters: "Hello", key: "greeting" }); + const hidden = writableTextNode({ + id: "n2", + characters: "Hidden", + key: "hiddenKey", + visible: false, + }); + const numeric = writableTextNode({ id: "n3", characters: "42", key: "numKey" }); + const src = cloneableSourcePage("Home", [visible, hidden, numeric]); + installFigmaForCreateCopy(src); + + const result = await createCopy({ mode: "keys", correlationId: "c1" }, {}); + + expect(result.ok).toBe(true); + expect(visible.characters).toBe("greeting"); + expect(hidden.characters).toBe("Hidden"); + expect(numeric.characters).toBe("42"); + }); + + it("writes hidden nodes too once ignoreHiddenLayers is turned off", async () => { + const hidden = writableTextNode({ + id: "n1", + characters: "Hidden", + key: "hiddenKey", + visible: false, + }); + const src = cloneableSourcePage("Home", [hidden]); + installFigmaForCreateCopy(src); + + await createCopy({ mode: "keys", correlationId: "c2" }, { ignoreHiddenLayers: false }); + + expect(hidden.characters).toBe("hiddenKey"); + }); + + it("excludes ignored nodes from the collected node list in languages mode", async () => { + const visible = writableTextNode({ id: "n1", characters: "Hello", key: "greeting" }); + const numeric = writableTextNode({ id: "n2", characters: "42", key: "numKey" }); + const src = cloneableSourcePage("Home", [visible, numeric]); + installFigmaForCreateCopy(src); + + const result = await createCopy( + { mode: "languages", correlationId: "c3", languages: ["cs"] }, + {}, + ); + + expect(result.ok).toBe(true); + expect(result.pages?.[0]?.nodes.map((n) => n.id)).toEqual(["n1"]); + }); + + it("keys mode: writes the key onto a MIXED-font node (advanced string with bold/italic ranges)", async () => { + // Regression: an advanced string rendered with / ranges has + // `fontName === figma.mixed`, and writeTextSafely used to bail on that — + // leaving the keys copy showing the original text instead of the key for + // exactly those nodes. applyRichText pre-loads every range font, so the + // write is safe; the original plugin wrote keys onto mixed nodes too. + const rich = writableTextNode({ id: "n1", characters: "bold italic text", key: "rich.key" }); + const src = cloneableSourcePage("Home", [rich]); + installFigmaForCreateCopy(src); + // Same sentinel the fake figma env exposes — assigned after install. + rich.fontName = (globalThis as unknown as { figma: { mixed: unknown } }).figma + .mixed as never; + rich.getRangeAllFontNames = () => [ + { family: "Inter", style: "Regular" }, + { family: "Inter", style: "Bold Italic" }, + ]; + + const result = await createCopy({ mode: "keys", correlationId: "c6" }, {}); + + expect(result.ok).toBe(true); + expect(rich.characters).toBe("rich.key"); + }); + + it("keys mode: prefixes the label with the namespace when namespacesEnabled is true", async () => { + const namespaced = writableTextNode({ + id: "n1", + characters: "Hello", + key: "greeting", + ns: "marketing", + }); + const src = cloneableSourcePage("Home", [namespaced]); + installFigmaForCreateCopy(src); + + await createCopy({ mode: "keys", correlationId: "c4", namespacesEnabled: true }, {}); + + expect(namespaced.characters).toBe("marketing.greeting"); + }); + + it("keys mode: writes the plain key when namespacesEnabled is false, even if the node has an ns", async () => { + // A node can carry a real `ns` (e.g. legacy data, or namespaces toggled + // off after some keys were connected) even while the project-wide + // feature is disabled — the label must match what Pull/CopyView/ + // StringDetails show for that same node (namespacedKeyLabel's gate). + const namespaced = writableTextNode({ + id: "n1", + characters: "Hello", + key: "greeting", + ns: "marketing", + }); + const src = cloneableSourcePage("Home", [namespaced]); + installFigmaForCreateCopy(src); + + await createCopy({ mode: "keys", correlationId: "c5", namespacesEnabled: false }, {}); + + expect(namespaced.characters).toBe("greeting"); + }); +}); + +// NOTE: the per-node render logic (formerly `resolveCopyNodeText` here) moved +// to the UI — `$ui/lib/logic/copyApply`'s `buildCopyUpdates` — because ICU +// rendering needs `Intl`, which Figma's main-thread sandbox doesn't provide. +// Its regression tests live in `src/ui/lib/logic/__tests__/copyApply.test.ts`. + +describe("removeExistingCopyPages — identity fallback", () => { + it("replaces a copy of the same (source, language) whose NAME no longer matches", () => { + // The reported case: this PR aligned copy naming with production, so a + // copy made by an earlier v2 build carries the old name. Matching on the + // name alone left it unfound, and Recreate appended a duplicate beside it. + const renamed = page( + "Home – en", // en dash: an earlier build's separator + { pageCopy: true, sourcePageId: "src", copyLanguage: "en" }, + { id: "old" }, + ); + setPages([page("Home", undefined, { id: "src" }), renamed]); + + return removeExistingCopyPages({ + name: "Home - en", + sourcePageId: "src", + language: "en", + }).then(() => { + expect(renamed.remove).toHaveBeenCalledTimes(1); + }); + }); + + it("does NOT confuse a keys copy with a language copy of the same source", async () => { + // Identity is (source page, language) — a keys copy carries no language, + // so recreating it must leave the language copies alone. + const keysCopy = page( + "Home - keys", + { pageCopy: true, sourcePageId: "src" }, + { id: "keys" }, + ); + const enCopy = page( + "Home - en", + { pageCopy: true, sourcePageId: "src", copyLanguage: "en" }, + { id: "en" }, + ); + setPages([page("Home", undefined, { id: "src" }), keysCopy, enCopy]); + + await removeExistingCopyPages({ name: "Home - keys", sourcePageId: "src" }); + + expect(keysCopy.remove).toHaveBeenCalledTimes(1); + expect(enCopy.remove).not.toHaveBeenCalled(); + }); + + it("does NOT remove a copy belonging to a DIFFERENT source page", async () => { + const otherSourceCopy = page( + "Other - en", + { pageCopy: true, sourcePageId: "other", copyLanguage: "en" }, + { id: "other-copy" }, + ); + setPages([page("Home", undefined, { id: "src" }), otherSourceCopy]); + + await removeExistingCopyPages({ + name: "Home - en", + sourcePageId: "src", + language: "en", + }); + + expect(otherSourceCopy.remove).not.toHaveBeenCalled(); + }); + + it("leaves a pre-tracking copy (no sourcePageId) alone unless its name matches", async () => { + // Copies made by the published plugin have no `sourcePageId`; the name is + // their only handle, and guessing beyond it could delete a page the user + // still wants. + const legacy = page("Something else", { pageCopy: true }, { id: "legacy" }); + setPages([page("Home", undefined, { id: "src" }), legacy]); + + await removeExistingCopyPages({ + name: "Home - en", + sourcePageId: "src", + language: "en", + }); + + expect(legacy.remove).not.toHaveBeenCalled(); + }); + + it("stops after the name match, so the usual path never loads other pages", async () => { + // Performance guard: reading a copy marker requires loading the page, and + // a document can have many. The identity pass exists for the migration + // case only and must not make every ordinary recreate load the document. + const namedCopy = page("Home - en", { pageCopy: true, sourcePageId: "src" }, { id: "c" }); + const unrelated = page("Sketches", undefined, { id: "u" }); + setPages([page("Home", undefined, { id: "src" }), namedCopy, unrelated]); + + await removeExistingCopyPages({ name: "Home - en", sourcePageId: "src" }); + + expect(namedCopy.remove).toHaveBeenCalledTimes(1); + expect(unrelated.loadAsync).not.toHaveBeenCalled(); + }); +}); + +describe("createCopy — rollback of a failed run", () => { + /** A clone that records whether the failed run cleaned it up. */ + function failingSetup(opts: { cloneIsCurrent?: boolean } = {}) { + const exploding = { + type: "TEXT" as const, + id: "boom", + name: "Layer", + visible: true, + get characters(): string { + throw new Error("node write blew up"); + }, + getPluginData: () => JSON.stringify({ key: "k", connected: true }), + setPluginData: () => {}, + }; + const clone = { + type: "PAGE" as const, + id: "clone-id", + name: "Home - keys", + loadAsync: vi.fn(async () => {}), + getPluginData: () => "", + setPluginData: vi.fn(), + findAllWithCriteria: () => [exploding], + remove: vi.fn(), + }; + const source = { + type: "PAGE" as const, + id: "src", + name: "Home", + loadAsync: vi.fn(async () => {}), + getPluginData: () => "", + setPluginData: vi.fn(), + findAllWithCriteria: () => [], + clone: () => clone, + }; + const figma = { + currentPage: opts.cloneIsCurrent ? clone : source, + root: { children: [source], appendChild: vi.fn() }, + getNodeByIdAsync: async (id: string) => + id === clone.id ? clone : id === source.id ? source : null, + setCurrentPageAsync: vi.fn(async (p: unknown) => { + figma.currentPage = p as typeof source; + }), + loadFontAsync: async () => {}, + mixed: Symbol("mixed"), + }; + (globalThis as unknown as { figma: unknown }).figma = figma; + return { clone, source, figma }; + } + + it("removes the half-written clone instead of leaving an orphan", async () => { + // The clone is stamped by `markPageAsCopy` only AFTER the write loop, so a + // page created before a throw carries no marker — invisible to + // `removeExistingCopyPages` forever, and every retry would add another. + const { clone } = failingSetup(); + + const result = await createCopy({ mode: "keys", correlationId: "c-fail" }, {}); + + expect(result.ok).toBe(false); + expect(clone.remove).toHaveBeenCalledTimes(1); + }); + + it("reports no created pages once they have been rolled back", async () => { + // The caller shouldn't be handed ids that no longer exist. + failingSetup(); + + const result = await createCopy({ mode: "keys", correlationId: "c-fail2" }, {}); + + expect(result.createdPageIds).toEqual([]); + expect(result.error).toContain("node write blew up"); + }); + + it("steps off an earlier clone before removing it when the run landed on it", async () => { + // Reachable in languages mode: recreating from inside a copy makes the run + // step onto each finished clone (`switchedAwayFromCurrent`), so when a + // LATER language throws, the rollback is deleting the page the user is + // standing on. Figma forbids removing the active page, so without the + // step-off the rollback itself would throw and leave both orphans behind. + const exploding = { + type: "TEXT" as const, + id: "boom", + name: "Layer", + visible: true, + get characters(): string { + throw new Error("scan blew up"); + }, + getPluginData: () => JSON.stringify({ key: "k", connected: true }), + setPluginData: () => {}, + }; + const makeClone = (id: string, nodes: unknown[]) => ({ + type: "PAGE" as const, + id, + name: id, + loadAsync: vi.fn(async () => {}), + getPluginData: () => "", + setPluginData: vi.fn(), + findAllWithCriteria: () => nodes, + remove: vi.fn(), + }); + const enClone = makeClone("clone-en", []); // succeeds + const deClone = makeClone("clone-de", [exploding]); // throws + const clones = [enClone, deClone]; + let cloneIndex = 0; + + const source = { + type: "PAGE" as const, + id: "src", + name: "Home", + loadAsync: vi.fn(async () => {}), + getPluginData: () => "", + setPluginData: vi.fn(), + findAllWithCriteria: () => [], + clone: () => clones[cloneIndex++], + }; + // The copy the user is standing on when they hit Recreate. + const activeOldCopy = page( + "Home - en", + { pageCopy: true, sourcePageId: "src", copyLanguage: "en" }, + { id: "old-en" }, + ); + const figma = { + currentPage: activeOldCopy as unknown, + root: { children: [source, activeOldCopy], appendChild: vi.fn() }, + getNodeByIdAsync: async (id: string) => + [source, enClone, deClone, activeOldCopy].find((p) => p.id === id) ?? null, + setCurrentPageAsync: vi.fn(async (p: unknown) => { + figma.currentPage = p; + }), + loadFontAsync: async () => {}, + mixed: Symbol("mixed"), + }; + (globalThis as unknown as { figma: unknown }).figma = figma; + + const result = await createCopy( + { mode: "languages", languages: ["en", "de"], correlationId: "c-fail3", sourcePageId: "src" }, + {}, + ); + + expect(result.ok).toBe(false); + // The run had stepped onto the finished `en` clone; rollback must move off + // it before removing, and remove BOTH clones it created. + expect(figma.setCurrentPageAsync).toHaveBeenCalledWith(source); + expect(enClone.remove).toHaveBeenCalledTimes(1); + expect(deClone.remove).toHaveBeenCalledTimes(1); + expect(result.createdPageIds).toEqual([]); + }); +}) + +describe("createCopy — layers a missing font blocked", () => { + it("reports skipped layers instead of leaving the copy silently incomplete", async () => { + // A node with a missing font can't be written, so a keys copy keeps its + // ORIGINAL translated text on that layer instead of the key label. Nothing + // surfaced that, so the copy looked complete and the layer looked like a + // bug in the plugin. + const writable = { + type: "TEXT" as const, + id: "ok", + name: "Good layer", + characters: "Hello", + visible: true, + autoRename: true, + hasMissingFont: false, + fontName: { family: "Inter", style: "Regular" }, + getRangeAllFontNames: () => [{ family: "Inter", style: "Regular" }], + getPluginData: (k: string) => + k === TOLGEE_NODE_INFO ? JSON.stringify({ key: "greeting", connected: true }) : "", + setPluginData: () => {}, + }; + const blocked = { ...writable, id: "bad", name: "Broken layer", hasMissingFont: true }; + + const clone = { + type: "PAGE" as const, + id: "clone", + name: "Home - keys", + loadAsync: vi.fn(async () => {}), + getPluginData: () => "", + setPluginData: vi.fn(), + findAllWithCriteria: () => [writable, blocked], + }; + const source = { + type: "PAGE" as const, + id: "src", + name: "Home", + loadAsync: vi.fn(async () => {}), + getPluginData: () => "", + setPluginData: vi.fn(), + findAllWithCriteria: () => [], + clone: () => clone, + }; + (globalThis as unknown as { figma: unknown }).figma = { + currentPage: source, + root: { children: [source], appendChild: vi.fn() }, + getNodeByIdAsync: async (id: string) => (id === "src" ? source : null), + setCurrentPageAsync: async () => {}, + loadFontAsync: async () => {}, + mixed: Symbol("mixed"), + }; + + const result = await createCopy({ mode: "keys", correlationId: "c-font" }, {}); + + expect(result.ok).toBe(true); + expect(result.skippedMissingFont).toEqual(["Broken layer"]); + // The writable one still got its key label — one bad layer must not stop + // the rest. + expect(writable.characters).toBe("greeting"); + }); +}) + +describe("copy staleness counts key a `|` cannot collide in", () => { + it("sees two distinct keys where a `|` join would merge them", async () => { + // The main thread had its own `${ns}|${key}` join, missed by the first + // sweep because the shared helper lived UI-side, where main can't import + // from. Staleness compares per-key COUNTS, so a collision doesn't just + // mislabel a key — it cancels a real difference out. + // + // Source holds (ns "a|b", key "c"); the copy holds (ns "a", key "b|c"). + // Different keys, but `a|b` + `|` + `c` and `a` + `|` + `b|c` are the same + // string — so both sides counted 1 for one merged key and the copy read as + // up to date, hiding both a gained and a lost string. + const source = page("Home", undefined, { + id: "src", + textNodes: [textNode({ key: "c", ns: "a|b" })], + }); + const copy = page( + "Home - keys", + { pageCopy: true, sourcePageId: "src" }, + { id: "copy", textNodes: [textNode({ key: "b|c", ns: "a" })] }, + ); + setPages([source, copy], copy); + + const result = await checkCopyStaleness(copy as never); + + expect(result.ok).toBe(true); + expect(result.missingCount).toBe(1); // the source's key the copy lacks + expect(result.removedCount).toBe(1); // the copy's key the source lacks + }); + + it("still reports a genuinely up-to-date copy as up to date", async () => { + const source = page("Home", undefined, { + id: "src2", + textNodes: [textNode({ key: "greeting", ns: "web" })], + }); + const copy = page( + "Home - keys", + { pageCopy: true, sourcePageId: "src2" }, + { id: "copy2", textNodes: [textNode({ key: "greeting", ns: "web" })] }, + ); + setPages([source, copy], copy); + + const result = await checkCopyStaleness(copy as never); + + expect(result.missingCount).toBe(0); + expect(result.removedCount).toBe(0); + }); +}); diff --git a/src/main/handlers/auth.ts b/src/main/handlers/auth.ts new file mode 100644 index 00000000..66c0c08c --- /dev/null +++ b/src/main/handlers/auth.ts @@ -0,0 +1,11 @@ +// API-key validation lives in the design-mode UI bundle (see +// `src/ui/lib/api/auth.ts`) because it depends on `openapi-fetch` and the +// generated OpenAPI types — neither of which we want to drag into the main +// (sandbox) bundle. The UI relays the resolved `projectId` back here via the +// `persist-project-id` message so we can persist it at the document scope and +// the inspect (Dev Mode) UI can build project-aware deep links without doing +// its own API call. See the `on("persist-project-id", ...)` handler in +// `src/main/main.ts`. + +// Intentionally no exports: this file documents the design decision above. +export {}; diff --git a/src/main/handlers/createCopy.ts b/src/main/handlers/createCopy.ts new file mode 100644 index 00000000..a26abbb6 --- /dev/null +++ b/src/main/handlers/createCopy.ts @@ -0,0 +1,580 @@ +import { TOLGEE_NODE_INFO, TOLGEE_PLUGIN_CONFIG_NAME } from "$shared/constants"; +import type { NodeInfo } from "$shared/types"; + +import { send } from "$main/bus"; +import { type IgnoreSettings, shouldIgnoreNode } from "$main/nodes/filter"; +import { getNodeInfo } from "$main/nodes/getNodeInfo"; +import { computeAncestorHidden } from "$main/nodes/scan"; +import { applyRichText } from "$main/text/applyRichText"; +import { nsKeyIndex } from "$shared/keyIndex"; + +/** + * Options for the `createCopy` handler. The two modes have different payloads + * but share a `correlationId` so the UI can pair its `create-copy` request + * with the `create-copy-result` it receives back. + * + * `sourcePageId` is only needed for "Recreate copy" (triggered FROM inside an + * existing copy — `figma.currentPage` is the copy itself there, not the page + * to clone from). Omitted for the normal Index-invoked "Create page" flow, + * which defaults to `figma.currentPage` exactly as before. + */ +export type CreateCopyOptions = + | { + mode: "keys"; + correlationId: string; + sourcePageId?: string; + /** See `UiToMain`'s `create-copy.namespacesEnabled` doc. */ + namespacesEnabled?: boolean; + } + | { + mode: "languages"; + correlationId: string; + languages: string[]; + sourcePageId?: string; + }; + +/** One freshly cloned language page + its connected nodes, handed back to the + * UI so IT can render + apply the translations (see `CreateCopyResult`). */ +export type CreatedCopyPage = { + pageId: string; + language: string; + nodes: NodeInfo[]; +}; + +export type CreateCopyResult = { + ok: boolean; + createdPageIds: string[]; + /** + * Names of layers a missing font prevented writing. Keys mode only in + * practice — such a layer keeps its ORIGINAL text instead of the key label, + * which reads as the copy having quietly not worked there, so the count has + * to reach the user rather than stopping at a dev-only console line. + */ + skippedMissingFont?: string[]; + /** + * Languages mode only. This handler deliberately does NOT write translated + * text itself: ICU rendering (plurals, params) needs `Intl`, which doesn't + * exist in Figma's main-thread sandbox — every render of a `{...}` string + * silently failed here, leaving copies half in the source language. The UI + * renders these nodes exactly like the Download flow and writes them back + * via the ordinary `apply-translations` request, which also persists the + * raw `translation` into plugin data — so a fresh copy is by construction + * identical to clone + "Download all". + */ + pages?: CreatedCopyPage[]; + error?: string; +}; + +/** + * How often to emit a progress message + yield to the event loop while + * iterating large text-node lists. Keeps the plugin sandbox responsive + * during long-running operations on big pages. + */ +const PROGRESS_INTERVAL = 10; + +/** + * Clone the current page and rewrite every connected text node: + * + * - `mode: "keys"` replaces each connected text node's `characters` with the + * Tolgee key (prefixed with the namespace when one is set) so the page can + * be used as a debug overlay. + * - `mode: "languages"` clones the page once per language and writes the + * provided translation (looked up by `nsKeyIndex`) into every connected + * text node. Falls back to the persisted `translation` for any miss so the + * page never ends up empty. + * + * Every cloned page is marked with `pageCopy: true` in plugin data so the UI + * routes to the read-only `CopyView` when the user navigates to it. Before each + * clone, a previous copy with the SAME name (and only one marked `pageCopy`) is + * removed so repeated copies replace rather than pile up — matching the original + * plugin. + * + * `settings` (hidden layers, digit-only strings, prefixed layer names, …) is + * applied exactly like the with-selection and page-wide scans — production's + * `copyPageEndpoint` runs the same filter (`findTextNodes` → `shouldIncludeNode`) + * before touching a node, so a node the user asked to ignore was never part of + * the copy in the first place. This handler used to skip that filter entirely + * (only `findAllWithCriteria`'s pluginData match + `connected && key`), so an + * ignored node's text got overwritten with its key/translation same as any + * other connected node — a regression against both production and this app's + * own Download/Pull, which already respect the setting. + * + * Progress is reported via `create-copy-progress` messages keyed by the + * caller's `correlationId`. + */ +export async function createCopy( + options: CreateCopyOptions, + settings: Partial = {}, +): Promise { + const sourcePage = await resolveSourcePage(options.sourcePageId); + if (!sourcePage) { + return { ok: false, createdPageIds: [], error: "The original page no longer exists." }; + } + await sourcePage.loadAsync(); + + // Matches the guard `request-page-connected-nodes` uses: the ancestor-chain + // walk only matters (and is only worth its cost) when BOTH the base + // "hidden layers" filter and its "including children" opt-in are on. + const needsAncestorHidden = Boolean( + (settings.ignoreHiddenLayers ?? true) && settings.ignoreHiddenLayersIncludingChildren, + ); + + const createdPageIds: string[] = []; + // Layer names a missing font kept us from writing — surfaced in the result + // rather than dropped, so "Copy created" doesn't overstate what happened. + const skippedMissingFont: string[] = []; + // Set when a removed copy turned out to be `figma.currentPage` itself (the + // "Recreate copy" flow, triggered from inside the copy being replaced) — + // Figma forbids deleting the active page, so we had to step onto + // `sourcePage` first. Land back on the fresh replacement afterwards + // instead of stranding the user on the source page. + let switchedAwayFromCurrent = false; + + try { + if (options.mode === "keys") { + // Matches production's naming exactly (`${name} - keys`, hyphen + + // lowercase) — `removeExistingCopyPages` below replaces a previous copy + // by EXACT name match, so a mismatched format (e.g. an em dash or + // capitalized "Keys") would never find/replace a copy the production + // plugin created, and vice versa, letting copies pile up instead. + const targetName = `${sourcePage.name} - keys`; + switchedAwayFromCurrent ||= ( + await removeExistingCopyPages( + { name: targetName, sourcePageId: sourcePage.id }, + sourcePage, + ) + ).switchedAwayFromCurrent; + const targetPage = sourcePage.clone(); + targetPage.name = targetName; + figma.root.appendChild(targetPage); + createdPageIds.push(targetPage.id); + + await targetPage.loadAsync(); + // pluginData filter: only Tolgee-tagged nodes come back, so untagged + // text (usually most of the page) never pays the 5-bridge-call + // `getNodeInfo` just to be skipped. + const textNodes = targetPage.findAllWithCriteria({ + types: ["TEXT"], + pluginData: { keys: [TOLGEE_NODE_INFO] }, + }); + const total = textNodes.length; + let processed = 0; + + for (const node of textNodes) { + const characters = node.characters; + const ancestorHidden = needsAncestorHidden ? computeAncestorHidden(node) : false; + if (!shouldIgnoreNode(node, ancestorHidden, settings, characters)) { + const info = getNodeInfo(node, characters); + if (info.connected && info.key) { + // Matches `namespacedKeyLabel`'s gate (Pull/CopyView/StringDetails): + // a node can carry a real `ns` even when namespaces are toggled + // off in Settings (e.g. legacy data, or the project disabled the + // feature after some keys were already namespaced) — without this + // check the copy would show "ns.key" while every other screen + // shows plain "key" for that exact node. + const label = + info.ns && options.namespacesEnabled ? `${info.ns}.${info.key}` : info.key; + const written = await writeTextSafely(node, label, { plainOnly: true }); + if (!written) skippedMissingFont.push(node.name); + } + } + processed++; + if (processed % PROGRESS_INTERVAL === 0) { + send({ + type: "create-copy-progress", + correlationId: options.correlationId, + current: processed, + total, + phase: "writing-keys", + }); + await yieldToEventLoop(); + } + } + + markPageAsCopy(targetPage, sourcePage.id); + if (switchedAwayFromCurrent) await figma.setCurrentPageAsync(targetPage); + } else { + // mode === "languages" + const totalPages = options.languages.length; + const pages: CreatedCopyPage[] = []; + + for (let i = 0; i < totalPages; i++) { + const lang = options.languages[i]; + if (!lang) continue; + + // Matches production's naming exactly — see the "keys" branch above. + const targetName = `${sourcePage.name} - ${lang}`; + switchedAwayFromCurrent ||= ( + await removeExistingCopyPages( + { name: targetName, sourcePageId: sourcePage.id, language: lang }, + sourcePage, + ) + ).switchedAwayFromCurrent; + const targetPage = sourcePage.clone(); + targetPage.name = targetName; + figma.root.appendChild(targetPage); + createdPageIds.push(targetPage.id); + + await targetPage.loadAsync(); + const textNodes = targetPage.findAllWithCriteria({ + types: ["TEXT"], + pluginData: { keys: [TOLGEE_NODE_INFO] }, + }); + + // No text is written here — see `CreateCopyResult.pages`: the UI + // renders + applies the translations, this side only collects the + // clone's connected nodes for it. + const nodes: NodeInfo[] = []; + const total = textNodes.length; + let processed = 0; + + for (const node of textNodes) { + const characters = node.characters; + const ancestorHidden = needsAncestorHidden ? computeAncestorHidden(node) : false; + if (!shouldIgnoreNode(node, ancestorHidden, settings, characters)) { + const info = getNodeInfo(node, characters); + if (info.connected && info.key) { + nodes.push(info); + } + } + processed++; + if (processed % PROGRESS_INTERVAL === 0) { + const overall = i * 100 + Math.round((processed / Math.max(total, 1)) * 100); + send({ + type: "create-copy-progress", + correlationId: options.correlationId, + current: overall, + total: totalPages * 100, + phase: `scanning-${lang}`, + }); + await yieldToEventLoop(); + } + } + + markPageAsCopy(targetPage, sourcePage.id, lang); + pages.push({ pageId: targetPage.id, language: lang, nodes }); + if (switchedAwayFromCurrent) await figma.setCurrentPageAsync(targetPage); + } + + return { ok: true, createdPageIds, pages, skippedMissingFont }; + } + + return { ok: true, createdPageIds, skippedMissingFont }; + } catch (err) { + const error = err instanceof Error ? err.message : String(err); + // Roll back the clones this run appended. A page is only stamped by + // `markPageAsCopy` AFTER its write loop finishes, so one created before the + // throw carries no marker at all — which makes it invisible to + // `removeExistingCopyPages` forever. Left in place, every retry would add + // another half-written orphan the user has to hunt down by hand, and no + // later run could ever clean them up. + const orphans = await removeCreatedPages(createdPageIds, sourcePage); + return { ok: false, createdPageIds: orphans, error }; + } +} + +/** + * Delete pages created by a failed run. Returns the ids it could NOT remove, + * so the caller reports what actually survived rather than what it attempted. + * + * Removal is best-effort per page: one page refusing to go must not strand the + * rest, and the original error is what the user needs to see, not a rollback + * failure on top of it. + */ +async function removeCreatedPages(ids: string[], fallback: PageNode): Promise { + const remaining: string[] = []; + for (const id of ids) { + try { + const node = await figma.getNodeByIdAsync(id); + if (node?.type !== "PAGE") continue; + const page = node as PageNode; + // Figma forbids removing the active page — step onto the source first. + if (page === figma.currentPage) { + await fallback.loadAsync(); + await figma.setCurrentPageAsync(fallback); + } + page.remove(); + } catch { + remaining.push(id); + } + } + return remaining; +} + +/** `sourcePageId` set (the "Recreate copy" flow) resolves that specific page; + * otherwise defaults to `figma.currentPage` — the normal Index-invoked flow, + * unchanged from before this existed. */ +async function resolveSourcePage(sourcePageId?: string): Promise { + if (!sourcePageId) return figma.currentPage; + const node = await figma.getNodeByIdAsync(sourcePageId); + return node?.type === "PAGE" ? node : null; +} + +/** The copy marker written by `markPageAsCopy`, or `null` for any page that + * isn't a previously-generated Tolgee copy. */ +function readCopyMarker( + page: PageNode, +): { sourcePageId?: string; language?: string } | null { + const raw = page.getPluginData(TOLGEE_PLUGIN_CONFIG_NAME); + if (!raw) return null; + try { + const parsed = JSON.parse(raw) as { + pageCopy?: boolean; + sourcePageId?: string; + copyLanguage?: string; + language?: string; + }; + if (parsed.pageCopy !== true) return null; + return { + sourcePageId: parsed.sourcePageId, + // `copyLanguage` is the immutable marker; `language` shares the page + // scope with the selectable Push/Pull language and can be repointed. + language: parsed.copyLanguage ?? parsed.language, + }; + } catch { + return null; + } +} + +/** + * Identifies the copy a fresh one REPLACES. + * + * A copy's identity is `(source page, language)` — a keys copy carries no + * language, each language copy carries its own — not its name. Matching on the + * name alone (which is all this used to do) breaks the moment the naming + * format changes: this PR aligned the separator with production, so a copy + * made by an earlier v2 build no longer matched, Recreate appended a SECOND + * page instead of replacing it, `switchedAwayFromCurrent` never fired, and the + * user was left looking at the stale one under a "Copy recreated." toast. + * + * The name is still accepted as a fallback, because copies made by the + * published plugin predate `sourcePageId` and have nothing else to match on. + */ +function matchesCopyIdentity(page: PageNode, target: CopyTarget): boolean { + const marker = readCopyMarker(page); + if (!marker?.sourcePageId) return false; // pre-tracking copy: name is its only handle + return marker.sourcePageId === target.sourcePageId && marker.language === target.language; +} + +/** The copy a `createCopy` run is about to write, for replacement lookup. */ +export type CopyTarget = { + name: string; + sourcePageId: string; + /** Absent for a keys copy; the language tag for a language copy. */ + language?: string; +}; + +/** + * Remove any existing copy page with the given name, so a repeated copy + * replaces the old one instead of accumulating duplicates. Only pages marked + * `pageCopy: true` are removed — a user's own same-named page is left alone + * (matches the original plugin). Only pages whose name matches are loaded, so + * this doesn't force-load the whole document under dynamic-page access. + * + * Figma forbids removing the CURRENTLY ACTIVE page — which happens when + * "Recreate copy" is run from inside the very copy being replaced. When that's + * the page about to be removed, we step onto `preferredFallback` (the source + * page, already loaded by the caller) first and report it via + * `switchedAwayFromCurrent` so the caller can land on the fresh replacement + * once it exists, instead of stranding the user on the fallback. + */ +export async function removeExistingCopyPages( + target: CopyTarget, + preferredFallback?: PageNode, +): Promise<{ switchedAwayFromCurrent: boolean }> { + let switchedAwayFromCurrent = false; + let removedAny = false; + + const removePage = async (page: PageNode): Promise => { + if (page === figma.currentPage) { + const fallback = + preferredFallback ?? figma.root.children.find((p) => p !== page && p.type === "PAGE"); + if (fallback) { + await fallback.loadAsync(); + // The sync `figma.currentPage` setter throws under + // `documentAccess: "dynamic-page"` — only the async API is allowed. + await figma.setCurrentPageAsync(fallback as PageNode); + switchedAwayFromCurrent = true; + } + } + page.remove(); + removedAny = true; + }; + + // Pass 1 — by name. Comparing a name needs no page load, so this stays cheap + // on a document with many pages, and it is the ONLY handle on copies made by + // the published plugin (they predate `sourcePageId`). Snapshot the children: + // `remove()` mutates the live array. + const others: PageNode[] = []; + for (const page of [...figma.root.children]) { + if (page.type !== "PAGE") continue; + if (page.name !== target.name) { + others.push(page); + continue; + } + await page.loadAsync(); + if (readCopyMarker(page) === null) continue; // a user's own same-named page + await removePage(page); + } + if (removedAny) return { switchedAwayFromCurrent }; + + // Pass 2 — by identity `(source page, language)`, for a copy whose NAME no + // longer matches: this PR aligned the naming with production, so a copy made + // by an earlier v2 build would otherwise go unfound and Recreate would append + // a duplicate beside it. Reading the marker requires loading each page, which + // is why this runs only when pass 1 came up empty — the usual case never pays + // for it. + for (const page of others) { + await page.loadAsync(); + if (!matchesCopyIdentity(page, target)) continue; + await removePage(page); + } + + return { switchedAwayFromCurrent }; +} + +/** + * Write plugin data onto a freshly cloned page so the UI shows the read-only + * `CopyView`. Skips `writeConfig` so we don't leak the marker into the + * global/document scopes. `sourcePageId` lets a later `checkCopyStaleness` + * find the original page again to compare connected keys. + */ +function markPageAsCopy(page: PageNode, sourcePageId: string, language?: string): void { + const payload: Record = { + pageCopy: true, + pageInfo: true, + sourcePageId, + }; + if (language) { + payload.language = language; + // Immutable copy-language marker: `language` shares the page scope with the + // selectable Push/Pull language and can get repointed, which made Recreate + // (and Download) fall back to the source/default language. `copyLanguage` + // is written once here and read first by CopyView so the copy always + // Recreates/Downloads in the language it was actually made in. + payload.copyLanguage = language; + } + page.setPluginData(TOLGEE_PLUGIN_CONFIG_NAME, JSON.stringify(payload)); +} + +/** + * Write `text` into `node` via the shared rich-text applier (bold/italic/ + * underline from inline HTML tags, same as the regular Pull/apply- + * translations path) — bails out silently when the node has missing or + * mixed fonts, which are very rare on connected nodes and would otherwise + * abort the whole copy. + */ +async function writeTextSafely( + node: TextNode, + text: string, + options?: { plainOnly?: boolean }, +): Promise { + // A node whose font isn't available can't be written at all. Report it so + // the caller can tell the user: on a keys copy such a layer silently keeps + // its original translated text instead of the key label, which looks like + // the copy simply didn't work on those layers. + if (node.hasMissingFont) return false; + // NO bail on `fontName === figma.mixed`: an advanced string rendered with + // bold/italic ranges IS mixed, and skipping it left the keys copy showing + // the original text instead of the key for exactly those nodes (the + // original plugin wrote keys onto mixed nodes too). `applyRichText` + // handles mixed safely — it pre-loads every font on the node via + // `getRangeAllFontNames` before assigning `characters`. + await applyRichText(node, text, options); + return true; +} + +function yieldToEventLoop(): Promise { + return new Promise((resolve) => setTimeout(resolve, 0)); +} + +export type CopyStalenessResult = { + ok: boolean; + /** Connected strings on the source page that this copy doesn't have yet. */ + missingCount?: number; + /** Connected strings the source page LOST since the copy was made (the + copy still shows them). */ + removedCount?: number; + error?: string; +}; + +/** + * Compares `copyPage`'s connected keys against its recorded SOURCE page's + * current connected keys, so `CopyView` can warn when the original gained new + * connections since the copy was made. Download only refreshes text for keys + * the copy already tracks — it has no way to discover new ones on its own, + * so a growing gap here would otherwise go unnoticed indefinitely. + * + * Fails gracefully (an explanatory `error`, not a throw) whenever the + * comparison isn't possible: no `sourcePageId` recorded (a copy from before + * this existed, or from production) or the source page was since deleted. + */ +export async function checkCopyStaleness(copyPage: PageNode): Promise { + const raw = copyPage.getPluginData(TOLGEE_PLUGIN_CONFIG_NAME); + let sourcePageId: string | undefined; + try { + sourcePageId = raw ? (JSON.parse(raw) as { sourcePageId?: string }).sourcePageId : undefined; + } catch { + return { ok: false, error: "Could not read this copy's plugin data." }; + } + if (!sourcePageId) { + return { ok: false, error: "This copy predates staleness tracking." }; + } + + const sourcePage = await figma.getNodeByIdAsync(sourcePageId); + if (!sourcePage || sourcePage.type !== "PAGE") { + return { ok: false, error: "The original page no longer exists." }; + } + await sourcePage.loadAsync(); + await copyPage.loadAsync(); + + // Both directions from the same two scans — deletions cost nothing extra. + // `missing` = connected strings the source gained since the copy was made; + // `removed` = connected strings the source lost (the copy still shows + // them). Per-key maxes, so a surplus on one key never cancels a genuine + // gap on another. + const sourceCounts = await collectConnectedKeyCounts(sourcePage); + const copyCounts = await collectConnectedKeyCounts(copyPage); + let missingCount = 0; + for (const [key, srcCount] of sourceCounts) { + missingCount += Math.max(0, srcCount - (copyCounts.get(key) ?? 0)); + } + let removedCount = 0; + for (const [key, copyCount] of copyCounts) { + removedCount += Math.max(0, copyCount - (sourceCounts.get(key) ?? 0)); + } + return { ok: true, missingCount, removedCount }; +} + +/** + * Connected string COUNT per `(ns, key)` on `page` — counts, not a set. + * Node ids differ between a source page and its clone, so nodes can't be + * matched individually; per-key counts are the closest stable proxy. A set + * comparison missed the real-world case that surfaced this: connecting an + * ADDITIONAL node to an already-connected key changes no key set, but the + * copy's clone of that node predates the connection, so Download will never + * touch it — the copy is stale all the same. + */ +async function collectConnectedKeyCounts(page: PageNode): Promise> { + const textNodes = page.findAllWithCriteria({ + types: ["TEXT"], + pluginData: { keys: [TOLGEE_NODE_INFO] }, + }); + const counts = new Map(); + let scanned = 0; + for (const node of textNodes) { + const info = getNodeInfo(node); + if (info.connected && info.key) { + const key = nsKeyIndex(info.ns, info.key); + counts.set(key, (counts.get(key) ?? 0) + 1); + } + // The staleness check runs TWO of these scans back-to-back on every + // CopyView open/page switch — unbroken, that's a visible canvas freeze + // on a large page (`getNodeInfo` is ~5 bridge reads per node). + scanned++; + if (scanned % 100 === 0) { + await yieldToEventLoop(); + } + } + return counts; +} diff --git a/src/main/handlers/screenshots.ts b/src/main/handlers/screenshots.ts new file mode 100644 index 00000000..2791edd6 --- /dev/null +++ b/src/main/handlers/screenshots.ts @@ -0,0 +1 @@ +export { captureScreenshots } from "../screenshots/capture"; diff --git a/src/main/handlers/settings.ts b/src/main/handlers/settings.ts new file mode 100644 index 00000000..27dfce2a --- /dev/null +++ b/src/main/handlers/settings.ts @@ -0,0 +1 @@ +export { readMergedConfig, resetConfig, writeConfig } from "../settings/index"; diff --git a/src/main/main.ts b/src/main/main.ts index 6f0d1156..be41863b 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,133 +1,475 @@ -import { showUI } from "@create-figma-plugin/utilities"; -import { on, emit } from "@/utilities"; - +import { attachBus, on, send } from "$main/bus"; import { - ConfigChangeHandler, - DocumentChangeHandler, - ResetHandler, - ResizeHandler, - SelectionChangeHandler, - SetBranchHandler, - SetLanguageHandler, - SetupHandle, - SyncCompleteHandler, -} from "../types"; -import { getScreenshotsEndpoint } from "./endpoints/getScreenshots"; -import { getSelectedNodesEndpoint } from "./endpoints/getSelectedNodes"; -import { getConnectedNodesEndpoint } from "./endpoints/getConnectedNodes"; -import { copyPageEndpoint } from "./endpoints/copyPage"; -import { updateNodesEndpoint } from "./endpoints/updateNodes"; -import { setNodesDataEndpoint } from "./endpoints/setNodesData"; + checkCopyStaleness, + type CopyStalenessResult, + createCopy, +} from "$main/handlers/createCopy"; +import { applyTranslations } from "$main/nodes/selection"; +import { cleanUpHighlights, highlightNode } from "$main/nodes/highlight"; +import { clearPrefilledKeys } from "$main/nodes/clearPrefilled"; +import { buildConnectedNodesInfo, scanConnectedNodes } from "$main/nodes/scan"; +import { type MainComponentNameCache, resolveParentNames } from "$main/nodes/nodeParents"; +import { type KeyParentNames, keyFormatUsesParents } from "$shared/keyFormat"; +import { getSelectionInfo, setNodesData } from "$main/nodes/selection"; +import { captureScreenshots } from "$main/screenshots/capture"; import { - deleteDocumentData, - deleteGlobalSettings, - deletePageData, - getPluginData, - setPluginData, -} from "./utils/settingsTools"; -import { cleanUp, highlightNodeEndpoint } from "./endpoints/highlightNode"; -import { DEFAULT_SIZE } from "@/ui/state/sizes"; -import { formatTextEndpoint } from "./endpoints/formatText"; -import { editorTypeEndpoint } from "./endpoints/editorType"; -import { notifyEndpoint } from "./endpoints/notify"; -import { preformatKeyEndpoint } from "./endpoints/preformatKey"; -import { clearPrefilledKeysEndpoint } from "./endpoints/clearPrefilledKeys"; - -const getAllPages = () => { - const document = figma.root; - - return document.children.filter((node) => node.type === "PAGE"); -}; - -const isOnlyProperty = (e: NodeChangeEvent, property: string) => { - return e.nodeChanges.every((ch) => { - return ( - ch.type === "PROPERTY_CHANGE" && - ch.properties.every((p) => p === property) - ); - }); -}; + invalidateConfigCache, + readMergedConfig, + resetConfig, + writeConfig, +} from "$main/settings"; +import { UI_SIZES } from "$shared/constants"; -export default async function () { - figma.on("selectionchange", () => { - emit("SELECTION_CHANGE"); - }); +// When the manifest's `ui` is a string, Figma injects `__html__`. When it's +// an object (per-editor UIs), Figma injects `__uiFiles__` instead with one +// entry per editor key. We picked the object form (figma + dev) so we have +// to read from `__uiFiles__` here. `__html__` is only used as a fallback if +// neither global is available (shouldn't happen but keeps TypeScript calm). +declare const __uiFiles__: { figma?: string; dev?: string } | undefined; +declare const __html__: string | undefined; - figma.currentPage.on("nodechange", (e) => { - if (isOnlyProperty(e, "pluginData") || isOnlyProperty(e, "fills")) { - return; - } +const uiHtml = + figma.editorType === "dev" + ? (__uiFiles__?.dev ?? __uiFiles__?.figma ?? __html__ ?? "") + : (__uiFiles__?.figma ?? __html__ ?? ""); - emit("DOCUMENT_CHANGE"); - }); +figma.skipInvisibleInstanceChildren = true; + +figma.showUI(uiHtml, { + width: UI_SIZES.DEFAULT.width, + height: UI_SIZES.DEFAULT.height, + themeColors: true, +}); +attachBus(); - figma.on("close", () => { - cleanUp(); +// Monotonic token: each new scan invalidates every scan still in flight, so a +// slow older scan can never overwrite a newer selection in the UI. +let scanGeneration = 0; + +// Whether the first `ui-ready` has already kicked off the startup selection +// scan. The UI retries `ui-ready` every 400ms until it receives `init` (to +// survive the race where this main-side listener isn't attached yet), so this +// handler can fire more than once — but the (potentially multi-second) scan +// must run AT MOST ONCE. A repeat `ui-ready` just re-sends the cheap +// config-first `init`; it never launches another scan. +let initHandled = false; + +async function emitSelection(sendPending = true): Promise { + const generation = ++scanGeneration; + const isStale = () => generation !== scanGeneration; + // Tell the UI a scan is starting so it can show a loader during the + // (potentially slow) getSelectionInfo() below, not just after it resolves. + if (sendPending) send({ type: "selection-pending" }); + // One REAL breather before the heavy work: everything up to the first + // engine search resolves in microtasks (cached config, loaded page), so + // without this the scan blocks the canvas straight from inside the + // selectionchange callback — the pending message never flushes to the UI + // (no loader) and pending input (a deselect click) never gets processed. + await new Promise((resolve) => setTimeout(resolve, 0)); + if (isStale()) return; + // STREAMED delivery: each completed batch goes straight to the UI, so the + // list starts rendering after the first ~100 nodes instead of after the + // whole (potentially many-second) scan. Stale batches are suppressed at + // send time; the terminal `selection-done` closes the stream. + let total = 0; + try { + await getSelectionInfo(isStale, (nodes, first) => { + if (isStale()) return; + total += nodes.length; + send({ type: "selection-batch", nodes, first }); + }); + } catch (err) { + // A failed scan MUST still close the stream — otherwise the UI's + // delayed spinner (armed by `selection-pending`) stays up forever. Fall + // through to `selection-done`; the next selection change recovers. + console.warn("[tolgee:main] selection scan failed", err); + } + if (generation !== scanGeneration) return; // superseded by a newer scan + send({ + type: "selection-done", + hasUserSelection: figma.currentPage.selection.length > 0, + total, }); +} + +// Selection events come in two shapes: a single deliberate click, and rapid +// bursts (arrow-keying / drag-selecting through layers fires one event per +// step). A deliberate click scans IMMEDIATELY — a fixed delay on every click +// reads as sluggishness against the previous plugin. Only events that follow +// each other closely (a burst) are coalesced with a short trailing debounce. +const SELECTION_BURST_MS = 150; +const SELECTION_DEBOUNCE_MS = 60; +let selectionDebounceTimer: ReturnType | null = null; +let lastSelectionChangeAt = 0; + +function scheduleEmitSelection(): void { + // Invalidate any in-flight scan NOW, not whenever the debounced call gets + // around to running. `emitSelection` bumps the token itself, but during the + // debounce window it hasn't run yet — so an older scan finishing in that gap + // still passed its own staleness check and sent `selection-batch` + + // `selection-done` for the PREVIOUS selection. The UI took that as the final + // answer and cleared its loading state, then the debounced scan started with + // `sendPending = false` and nothing put the loader back: the old selection's + // result sat there looking authoritative, and a bulk action fired in that + // window would have targeted those nodes instead of the current ones. + scanGeneration++; + send({ type: "selection-pending" }); + const now = Date.now(); + const inBurst = now - lastSelectionChangeAt < SELECTION_BURST_MS; + lastSelectionChangeAt = now; + if (selectionDebounceTimer) clearTimeout(selectionDebounceTimer); + if (!inBurst) { + void emitSelection(false); + return; + } + selectionDebounceTimer = setTimeout(() => { + selectionDebounceTimer = null; + void emitSelection(false); + }, SELECTION_DEBOUNCE_MS); +} + +async function emitPageChange(): Promise { + const config = await readMergedConfig(); + send({ type: "page-changed", config, pageName: figma.currentPage.name }); + await emitSelection(); +} + +// --- Figma event subscriptions ---------------------------------------------- + +figma.on("selectionchange", () => { + scheduleEmitSelection(); +}); + +figma.on("currentpagechange", () => { + void (async () => { + // Page scope contributes to the merged config (language lives per page). + invalidateConfigCache(); + await emitPageChange(); + })(); +}); - figma.on("currentpagechange", async () => { - emit("SELECTION_CHANGE"); - emit("DOCUMENT_CHANGE"); - emit("CONFIG_CHANGE", await getPluginData()); +// In `documentAccess: "dynamic-page"` mode, Figma rejects `documentchange` +// registration unless we first call `figma.loadAllPagesAsync()`, which is +// expensive on large files. We deliberately skip it — no code here needs +// document-wide change notifications; selection-driven scans and explicit +// writes cover everything the plugin does. + +figma.on("close", () => { + // Restore fills on any node still mid-highlight — an unexpected close + // would otherwise leave layers stuck in the pink pulse colour. + cleanUpHighlights(); +}); + +// --- UI -> main message handlers -------------------------------------------- + +on("ui-ready", async () => { + const config = await readMergedConfig(); + + // Config-FIRST init: reply immediately with everything cheap to read and + // `selectedNodes: []`. The real selection streams in right after via the + // generation-tokened `selection-pending`/`selection-batch`/`selection-done` + // path (same as `selectionchange`). Because this reply is instant, the UI's + // `ui-ready` retry stops within a frame — so a slow scan can no longer pile + // up one full scan per 400ms retry, and a late `init` from an earlier retry + // can no longer overwrite a newer streamed selection. + send({ + type: "init", + config, + selectedNodes: [], + hasUserSelection: figma.currentPage.selection.length > 0, + editorType: figma.editorType as "figma" | "dev", + pageName: figma.currentPage.name, }); - on("SETUP", async (config) => { - await setPluginData(config); - figma.notify("Tolgee settings saved."); + // A retry landed after the first init already ran the scan — re-sending the + // (cheap) init above is enough; never launch a second scan. + if (initHandled) return; + initHandled = true; + + // Stream the initial selection with a generation token, exactly like a + // `selectionchange` — the UI already renders `selection-pending`/`-batch`/ + // `-done` incrementally, so a large startup selection paints progressively + // instead of blocking `init` on a full scan. + await emitSelection(); +}); + +on("resize", (msg) => { + figma.ui.resize(msg.size.width, msg.size.height); +}); + +on("close", () => { + figma.closePlugin(); +}); + +on("notify", (msg) => { + figma.notify(msg.text, { error: msg.error }); +}); + +on("open-external", (msg) => { + figma.openExternal(msg.url); +}); + +// Settings submits its WHOLE form every time, so deciding what a save +// actually changed must compare VALUES, not submitted keys (a save that only +// touched the API key used to trigger a full re-scan of the selection — +// seconds of canvas work on large selections, for nothing). +const IGNORE_RULE_KEYS = [ + "ignoreNumbers", + "ignoreFormattedNumbers", + "ignorePrefix", + "ignoreTextLayers", + "ignoreHiddenLayers", + "ignoreHiddenLayersIncludingChildren", +] as const; +const PREFILL_KEYS = ["prefillKeyFormat", "keyFormat", "variableCasing"] as const; + +on("save-config", async (msg) => { + const before = await readMergedConfig(); + // Production always stamps these two scope markers on every settings save + // (see settingsTools.ts in the reference plugin). Writing them here restores + // parity: `documentInfo` lets App.svelte's PageSetup gate for newly added + // pages fire correctly, and `pageInfo` records that this page's language was + // configured. It also keeps rollback-safe: if a document this plugin + // configured is later reopened with the ORIGINAL production plugin, + // production's own "documentInfo missing" gate won't spuriously trigger. + await writeConfig({ ...msg.config, documentInfo: true, pageInfo: true }); + const merged = await readMergedConfig(); + send({ type: "config-changed", config: merged }); + const ignoreRulesChanged = IGNORE_RULE_KEYS.some((key) => before[key] !== merged[key]); + const prefillChanged = PREFILL_KEYS.some((key) => before[key] !== merged[key]); + // Prefill turned OFF: drop the auto-generated keys the prefill persisted to + // node pluginData while it was on, so they don't linger in the list or get + // re-offered on push. Matches the published plugin's `clearPrefilledKeys` + // (which triggers on its per-toggle save); we do it on our form Save. + const prefillTurnedOff = Boolean(before.prefillKeyFormat) && !merged.prefillKeyFormat; + if (prefillTurnedOff) { + await clearPrefilledKeys(); + } + // Re-scan only when it changes what the scan RETURNS: different ignore + // filtering, a prefill format that needs freshly resolved parent names + // ({frame}/{component}/…), or a prefill-off clear (so the current selection + // reflects the wiped keys). Key regeneration itself happens in the UI from + // data it already has — re-scanning for a plain format change made a save + // repaint the list three times (scan overlay + scan result + regeneration + // patch) instead of once. + const needsRescan = + ignoreRulesChanged || + prefillTurnedOff || + (prefillChanged && + Boolean(merged.prefillKeyFormat) && + keyFormatUsesParents(merged.keyFormat)); + if (needsRescan) { + await emitSelection(); + } +}); + +// API-key validation lives in the design-mode UI (depends on `openapi-fetch`). +// The UI relays the resolved `projectId` back here so we can persist it at +// the document scope; the inspect (Dev Mode) UI reads it from config to +// build project-aware deep links into the Tolgee web app. +on("persist-project-id", async (msg) => { + await writeConfig({ projectId: msg.projectId }); + const merged = await readMergedConfig(); + send({ type: "config-changed", config: merged }); +}); + +on("reset", async () => { + await resetConfig(); + send({ type: "config-changed", config: {} }); +}); + +on("set-language", async (msg) => { + await writeConfig({ language: msg.language }); + send({ type: "config-changed", config: await readMergedConfig() }); +}); + +on("set-branch", async (msg) => { + await writeConfig({ branch: msg.branch }); + send({ type: "config-changed", config: await readMergedConfig() }); +}); + +on("request-page-connected-nodes", async (msg) => { + const nodes = await scanConnectedNodes(); + // Same ignore settings (hidden layers, digit-only strings, prefixed layer + // names, …) the with-selection scan already applies — a page-wide + // "Download all" should skip the same nodes a selection scan would, not + // touch everything regardless of those settings. + const config = await readMergedConfig(); + const needsAncestorHidden = Boolean( + (config.ignoreHiddenLayers ?? true) && config.ignoreHiddenLayersIncludingChildren, + ); + // Chunked — `getNodeInfo` is ~5 bridge reads (incl. a full `characters` + // copy) per node, and a page-wide Pull can hit thousands of connected nodes. + // `buildConnectedNodesInfo` reports progress (guarded to `total > 100`) so + // the UI's watchdog can tell a slow-but-alive scan from a hung one. + const infos = await buildConnectedNodesInfo(nodes, config, needsAncestorHidden, (done, total) => { + send({ + type: "page-connected-nodes-progress", + correlationId: msg.correlationId, + done, + total, + }); }); + send({ + type: "page-connected-nodes-result", + correlationId: msg.correlationId, + nodes: infos, + }); +}); - on("RESET", async () => { - await deleteGlobalSettings(); - deleteDocumentData(); - getAllPages().forEach((page) => { - deletePageData(page); +on("set-nodes-data", async (msg) => { + const result = await setNodesData(msg.nodes, (done, total) => { + send({ + type: "nodes-set-progress", + correlationId: msg.correlationId, + done, + total, }); - emit("CONFIG_CHANGE", await getPluginData()); }); + // The result carries fresh snapshots of the written nodes so the UI can + // patch its selection in place. We deliberately do NOT re-scan the whole + // selection here — with large selections that full re-scan per write is + // what saturated the canvas thread and froze Figma. + send({ + type: "nodes-set-result", + correlationId: msg.correlationId, + ok: result.ok, + nodes: result.nodes, + }); +}); - on("SET_LANGUAGE", async (language: string) => { - const pluginData = await getPluginData(); - const data = { ...pluginData, language }; - await setPluginData(data); +on("resolve-parent-names", async (msg) => { + // Walk each node's ancestors on demand (only the requested nodes, not the + // whole page) so the bulk "Generate key names" action can resolve parent + // placeholders for a template that differs from the saved key format. + // Sequential with periodic yields — the walks are bridge-call heavy, and a + // parallel batch over a large selection blocks the canvas until it's done. + const mainComponentNames: MainComponentNameCache = new Map(); + const entries: [string, KeyParentNames][] = []; + let processed = 0; + for (const id of msg.nodeIds) { + const node = await figma.getNodeByIdAsync(id); + entries.push([id, node ? await resolveParentNames(node, mainComponentNames) : {}]); + processed++; + if (processed % 25 === 0) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + send({ + type: "parent-names-result", + correlationId: msg.correlationId, + parents: Object.fromEntries(entries), }); +}); - on("SET_BRANCH", async (branch: string) => { - const pluginData = await getPluginData(); - const data = { ...pluginData, branch }; - await setPluginData(data); +on("request-screenshots", async (msg) => { + // Streamed: one message per exported frame, then a terminal marker. Keeps + // peak memory at one PNG and avoids serializing tens of MB in one go on + // the canvas thread. + let index = 0; + const { delivered, failed } = await captureScreenshots(msg.nodeIds, (screenshot) => { + send({ + type: "screenshot-frame", + correlationId: msg.correlationId, + screenshot, + index: index++, + }); + }); + send({ + type: "screenshots-done", + correlationId: msg.correlationId, + total: delivered, + failed, }); +}); + +on("scroll-to-node", async (msg) => { + // `highlightNode` already calls `scrollAndZoomIntoView` and adds the + // 500ms pink pulse on top, but it bails on dev-mode editors and on text + // nodes with mixed fills. Falling back to a plain scroll keeps the click + // useful in those branches. + await highlightNode(msg.id); + if (figma.editorType === "dev") { + const node = await figma.getNodeByIdAsync(msg.id); + if (node && "x" in node) { + figma.viewport.scrollAndZoomIntoView([node]); + } + } +}); - on("RESIZE", (size) => { - figma.ui.resize(size.width, size.height); +on("apply-translations", async (msg) => { + const { ok, errors, nodes } = await applyTranslations(msg.updates, (done, total) => { + send({ + type: "apply-translations-progress", + correlationId: msg.correlationId, + done, + total, + }); + }); + // Fresh post-write snapshots ride along for in-place patching — see the + // matching comment in the `set-nodes-data` handler. + send({ + type: "apply-translations-result", + correlationId: msg.correlationId, + ok, + errors, + nodes, }); +}); - on("SYNC_COMPLETE", () => { - figma.notify("Synchronization completed."); - figma.closePlugin(); +on("create-copy", async (msg) => { + // Same ignore settings (hidden layers, digit-only strings, prefixed layer + // names, …) every other scan/write path already applies — a node the user + // asked to ignore shouldn't have its text overwritten just because it's + // being copied instead of downloaded to. + const settings = await readMergedConfig(); + const result = + msg.mode === "keys" + ? await createCopy( + { + mode: "keys", + correlationId: msg.correlationId, + sourcePageId: msg.sourcePageId, + namespacesEnabled: msg.namespacesEnabled, + }, + settings, + ) + : await createCopy( + { + mode: "languages", + correlationId: msg.correlationId, + languages: msg.languages ?? [], + sourcePageId: msg.sourcePageId, + }, + settings, + ); + send({ + type: "create-copy-result", + correlationId: msg.correlationId, + ok: result.ok, + createdPageIds: result.createdPageIds, + pages: result.pages, + error: result.error, + skippedMissingFont: result.skippedMissingFont, }); +}); - getScreenshotsEndpoint.register(); - getSelectedNodesEndpoint.register(); - getConnectedNodesEndpoint.register(); - formatTextEndpoint.register(); - copyPageEndpoint.register(); - updateNodesEndpoint.register(); - setNodesDataEndpoint.register(); - highlightNodeEndpoint.register(); - editorTypeEndpoint.register(); - notifyEndpoint.register(); - preformatKeyEndpoint.register(); - clearPrefilledKeysEndpoint.register(); - - const config = await getPluginData(); - - showUI( - { - title: "Tolgee", - ...DEFAULT_SIZE, - }, - { - config, - }, - ); -} +on("request-copy-staleness", async (msg) => { + // A throw here would otherwise vanish (the main bus doesn't wrap handlers) + // and the UI would wait forever with no banner and no trace — the check + // must ALWAYS answer, even if only with the failure reason. + let result: CopyStalenessResult; + try { + result = await checkCopyStaleness(figma.currentPage); + } catch (err) { + result = { ok: false, error: err instanceof Error ? err.message : String(err) }; + } + send({ + type: "copy-staleness-result", + correlationId: msg.correlationId, + ok: result.ok, + missingCount: result.missingCount, + removedCount: result.removedCount, + error: result.error, + }); +}); diff --git a/src/main/nodes/__tests__/clearPrefilled.test.ts b/src/main/nodes/__tests__/clearPrefilled.test.ts new file mode 100644 index 00000000..d61077c3 --- /dev/null +++ b/src/main/nodes/__tests__/clearPrefilled.test.ts @@ -0,0 +1,139 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { TOLGEE_NODE_INFO } from "$shared/constants"; +import { clearPrefilledKeys } from "$main/nodes/clearPrefilled"; + +/** A TEXT-node stand-in whose pluginData is a live map, so a `setPluginData` + * from `clearPrefilledKeys` is observable via `getPluginData` afterward. */ +function makeTextNode(id: string, info: Record | null) { + const store = new Map(); + if (info) store.set(TOLGEE_NODE_INFO, JSON.stringify(info)); + return { + id, + type: "TEXT" as const, + getPluginData: (key: string) => store.get(key) ?? "", + setPluginData: (key: string, value: string) => { + store.set(key, value); + }, + /** Test helper (not part of the Figma API) — read back the parsed data. */ + _data: () => { + const raw = store.get(TOLGEE_NODE_INFO); + return raw ? (JSON.parse(raw) as Record) : null; + }, + }; +} + +type FakeNode = ReturnType; + +/** Installs a fake `figma` whose pages return `nodes` from the pluginData- + * filtered `findAllWithCriteria` (the same filter the real code relies on). */ +function installFigma(pages: FakeNode[][]): void { + const figma = { + root: { + children: pages.map((nodes) => ({ + type: "PAGE" as const, + loadAsync: async () => {}, + findAllWithCriteria: (_criteria: unknown) => nodes, + })), + }, + }; + (globalThis as unknown as { figma: unknown }).figma = figma; +} + +afterEach(() => { + (globalThis as unknown as { figma?: unknown }).figma = undefined; + vi.restoreAllMocks(); +}); + +describe("clearPrefilledKeys", () => { + it("clears an UNTOUCHED auto key (key === prefilledKey), dropping the marker", () => { + const node = makeTextNode("1:1", { + key: "auto.generated", + prefilledKey: "auto.generated", + connected: false, + translation: "Hello", + ns: "web", + }); + installFigma([[node]]); + + return clearPrefilledKeys().then((cleared) => { + expect(cleared).toBe(1); + // Key wiped, marker dropped, other fields untouched. + expect(node._data()).toEqual({ + key: "", + connected: false, + translation: "Hello", + ns: "web", + }); + }); + }); + + it("PRESERVES an edited key (key !== prefilledKey) — the reported bug", async () => { + // Prefill generated "auto.generated", the user edited it to "my.custom.key". + const node = makeTextNode("1:1", { + key: "my.custom.key", + prefilledKey: "auto.generated", + connected: false, + }); + installFigma([[node]]); + + const cleared = await clearPrefilledKeys(); + expect(cleared).toBe(0); + expect(node._data()?.key).toBe("my.custom.key"); + }); + + it("PRESERVES a manually-typed key that was never auto (no marker)", async () => { + const node = makeTextNode("1:1", { key: "hand.typed", connected: false }); + installFigma([[node]]); + + const cleared = await clearPrefilledKeys(); + expect(cleared).toBe(0); + expect(node._data()?.key).toBe("hand.typed"); + }); + + it("never touches CONNECTED nodes, even if key === prefilledKey", async () => { + const node = makeTextNode("1:1", { + key: "linked", + prefilledKey: "linked", + connected: true, + }); + installFigma([[node]]); + + const cleared = await clearPrefilledKeys(); + expect(cleared).toBe(0); + expect(node._data()?.key).toBe("linked"); + }); + + it("skips unconnected nodes that already have no key", async () => { + const node = makeTextNode("1:1", { key: "", connected: false }); + installFigma([[node]]); + + expect(await clearPrefilledKeys()).toBe(0); + }); + + it("clears matching auto keys across every page, keeping edited and connected ones", async () => { + const autoP1 = makeTextNode("1:1", { key: "a", prefilledKey: "a", connected: false }); + const editedP2 = makeTextNode("2:1", { key: "b.edited", prefilledKey: "b", connected: false }); + const autoP2 = makeTextNode("2:2", { key: "c", prefilledKey: "c", connected: false }); + const connectedP2 = makeTextNode("2:3", { key: "d", prefilledKey: "d", connected: true }); + installFigma([[autoP1], [editedP2, autoP2, connectedP2]]); + + const cleared = await clearPrefilledKeys(); + expect(cleared).toBe(2); + expect(autoP1._data()?.key).toBe(""); + expect(autoP2._data()?.key).toBe(""); + expect(editedP2._data()?.key).toBe("b.edited"); // edited → kept + expect(connectedP2._data()?.key).toBe("d"); // connected → kept + }); + + it("ignores nodes whose pluginData is unparseable, without throwing", async () => { + const bad = makeTextNode("1:1", null); + bad.setPluginData(TOLGEE_NODE_INFO, "{not json"); + const good = makeTextNode("1:2", { key: "x", prefilledKey: "x", connected: false }); + installFigma([[bad, good]]); + + const cleared = await clearPrefilledKeys(); + expect(cleared).toBe(1); + expect(good._data()?.key).toBe(""); + }); +}); diff --git a/src/main/nodes/__tests__/filter.test.ts b/src/main/nodes/__tests__/filter.test.ts new file mode 100644 index 00000000..6354bf3a --- /dev/null +++ b/src/main/nodes/__tests__/filter.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from "vitest"; +import { shouldIgnoreNode } from "../filter"; + +function node( + overrides: Partial<{ characters: string; name: string; visible: boolean }> = {}, +): TextNode { + return { + characters: overrides.characters ?? "Hello", + name: overrides.name ?? "Layer", + visible: overrides.visible ?? true, + parent: null, + } as unknown as TextNode; +} + +describe("shouldIgnoreNode", () => { + it("always ignores blank / whitespace, even with Numbers off", () => { + expect(shouldIgnoreNode(node({ characters: "" }), false, { ignoreNumbers: false })).toBe(true); + expect(shouldIgnoreNode(node({ characters: " " }), false, { ignoreNumbers: false })).toBe(true); + }); + + it("ignores pure integers when Numbers is on (default)", () => { + expect(shouldIgnoreNode(node({ characters: "100" }), false, {})).toBe(true); + expect(shouldIgnoreNode(node({ characters: "42" }), false, { ignoreNumbers: true })).toBe(true); + }); + + it("KEEPS formatted numbers by default (base is pure-integer only)", () => { + for (const c of ["1,234.00", "3.14", "+420", "-5", "1 000"]) { + expect(shouldIgnoreNode(node({ characters: c }), false, { ignoreNumbers: true })).toBe(false); + } + }); + + it("ignores formatted numbers only when opted in", () => { + for (const c of ["1,234.00", "3.14", "+420", "-5"]) { + expect( + shouldIgnoreNode(node({ characters: c }), false, { + ignoreNumbers: true, + ignoreFormattedNumbers: true, + }), + ).toBe(true); + } + }); + + it("keeps strings with letters regardless of the numeric toggles", () => { + expect( + shouldIgnoreNode(node({ characters: "12 apples" }), false, { + ignoreNumbers: true, + ignoreFormattedNumbers: true, + }), + ).toBe(false); + }); + + it("does not ignore numbers when Numbers is off", () => { + expect(shouldIgnoreNode(node({ characters: "100" }), false, { ignoreNumbers: false })).toBe(false); + }); + + it("defaults the ignore prefix to '_' when the text-layer filter is on without a prefix", () => { + expect( + shouldIgnoreNode(node({ name: "_helper", characters: "x" }), false, { + ignoreTextLayers: true, + }), + ).toBe(true); + expect( + shouldIgnoreNode(node({ name: "Title", characters: "x" }), false, { + ignoreTextLayers: true, + }), + ).toBe(false); + }); + + it("respects an explicit prefix", () => { + expect( + shouldIgnoreNode(node({ name: "//skip", characters: "x" }), false, { + ignoreTextLayers: true, + ignorePrefix: "//", + }), + ).toBe(true); + }); + + it("prefix filter is inert unless ignoreTextLayers is enabled", () => { + expect(shouldIgnoreNode(node({ name: "_helper", characters: "x" }), false, {})).toBe(false); + }); +}); + +describe("shouldIgnoreNode — hidden layers", () => { + // `ancestorHidden` is the 2nd arg — precomputed during the scan (like the + // original's threaded flag) rather than by walking parents here. + it("ignores a self-hidden node when Hidden layers is on (default)", () => { + expect(shouldIgnoreNode(node({ visible: false }), false, {})).toBe(true); + }); + + it("keeps a self-hidden node when Hidden layers is off", () => { + expect(shouldIgnoreNode(node({ visible: false }), false, { ignoreHiddenLayers: false })).toBe( + false, + ); + }); + + it("keeps a VISIBLE text inside a hidden subtree unless 'including children' is on", () => { + expect(shouldIgnoreNode(node(), true, { ignoreHiddenLayers: true })).toBe(false); + expect( + shouldIgnoreNode(node(), true, { + ignoreHiddenLayers: true, + ignoreHiddenLayersIncludingChildren: true, + }), + ).toBe(true); + }); + + it("does NOT apply 'including children' when Hidden layers is off (gated, like the original)", () => { + expect( + shouldIgnoreNode(node(), true, { + ignoreHiddenLayers: false, + ignoreHiddenLayersIncludingChildren: true, + }), + ).toBe(false); + }); + + it("keeps a visible text that is not in a hidden subtree", () => { + expect( + shouldIgnoreNode(node(), false, { + ignoreHiddenLayers: true, + ignoreHiddenLayersIncludingChildren: true, + }), + ).toBe(false); + }); +}); diff --git a/src/main/nodes/__tests__/getNodeInfo.test.ts b/src/main/nodes/__tests__/getNodeInfo.test.ts new file mode 100644 index 00000000..dbe5ef60 --- /dev/null +++ b/src/main/nodes/__tests__/getNodeInfo.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, it } from "vitest"; + +import { TOLGEE_NODE_INFO } from "$shared/constants"; +import { getNodeInfo, setNodeInfo } from "$main/nodes/getNodeInfo"; + +/** Minimal TEXT-node stand-in with live pluginData. */ +function makeTextNode(id = "1:1") { + const store = new Map(); + return { + id, + name: `Layer ${id}`, + characters: "Hello", + visible: true, + getPluginData: (key: string) => store.get(key) ?? "", + setPluginData: (key: string, value: string) => { + store.set(key, value); + }, + } as unknown as TextNode; +} + +describe("getNodeInfo / setNodeInfo — prefilledKey provenance marker", () => { + it("persists prefilledKey and reads it back", () => { + const node = makeTextNode(); + setNodeInfo(node, { key: "auto.key", prefilledKey: "auto.key", connected: false }); + expect(getNodeInfo(node).prefilledKey).toBe("auto.key"); + expect(getNodeInfo(node).key).toBe("auto.key"); + }); + + it("a manual key edit keeps the STALE marker (key diverges from prefilledKey)", () => { + const node = makeTextNode(); + // Prefill wrote an auto key + marker… + setNodeInfo(node, { key: "auto.key", prefilledKey: "auto.key", connected: false }); + // …then a manual edit changes only the key (no prefilledKey in the patch). + setNodeInfo(node, { key: "my.custom.key" }); + + const info = getNodeInfo(node); + expect(info.key).toBe("my.custom.key"); + // Marker unchanged → key !== prefilledKey → clearPrefilledKeys preserves it. + expect(info.prefilledKey).toBe("auto.key"); + }); + + it("a format regeneration updates both key and marker together", () => { + const node = makeTextNode(); + setNodeInfo(node, { key: "old", prefilledKey: "old", connected: false }); + // Prefill regenerates under a new format: writes the new value to both. + setNodeInfo(node, { key: "new", prefilledKey: "new" }); + + const info = getNodeInfo(node); + expect(info.key).toBe("new"); + expect(info.prefilledKey).toBe("new"); // stays in sync → still clearable + }); + + it("leaves prefilledKey undefined for a plain manual node", () => { + const node = makeTextNode(); + setNodeInfo(node, { key: "hand.typed", connected: false }); + expect(getNodeInfo(node).prefilledKey).toBeUndefined(); + }); +}); + +describe("setNodeInfo — merge semantics", () => { + it("preserves persisted fields that the patch omits", () => { + const node = makeTextNode(); + setNodeInfo(node, { key: "k", ns: "common", translation: "Hi {name}" }); + // A patch that touches only the key must not wipe ns / translation. + setNodeInfo(node, { key: "k2" }); + + const info = getNodeInfo(node); + expect(info.key).toBe("k2"); + expect(info.ns).toBe("common"); + expect(info.translation).toBe("Hi {name}"); + }); + + it("clears `ns` only on an explicit `ns: undefined`, not on omission", () => { + const node = makeTextNode(); + setNodeInfo(node, { key: "k", ns: "common" }); + + setNodeInfo(node, { key: "k" }); // ns omitted → kept + expect(getNodeInfo(node).ns).toBe("common"); + + setNodeInfo(node, { ns: undefined }); // ns present-but-undefined → cleared + expect(getNodeInfo(node).ns).toBeUndefined(); + }); + + it("uses presence (not nullishness) for paramsValues and pluralParamValue", () => { + const node = makeTextNode(); + setNodeInfo(node, { paramsValues: { name: "World" }, pluralParamValue: "3" }); + + setNodeInfo(node, { key: "k" }); // both omitted → kept + let info = getNodeInfo(node); + expect(info.paramsValues).toEqual({ name: "World" }); + expect(info.pluralParamValue).toBe("3"); + + setNodeInfo(node, { paramsValues: undefined, pluralParamValue: undefined }); // cleared + info = getNodeInfo(node); + expect(info.paramsValues).toBeUndefined(); + expect(info.pluralParamValue).toBeUndefined(); + }); + + it("can clear the key to an empty string", () => { + const node = makeTextNode(); + setNodeInfo(node, { key: "was.set" }); + setNodeInfo(node, { key: "" }); + expect(getNodeInfo(node).key).toBe(""); + }); + + it("coerces connected / isPlural to booleans and defaults them to false", () => { + const node = makeTextNode(); + const info = setNodeInfo(node, { key: "k" }); + expect(info.connected).toBe(false); + expect(info.isPlural).toBe(false); + + const info2 = setNodeInfo(node, { connected: true, isPlural: true }); + expect(info2.connected).toBe(true); + expect(info2.isPlural).toBe(true); + }); + + it("strips undefined values so the stored payload round-trips cleanly", () => { + const node = makeTextNode(); + setNodeInfo(node, { key: "k", ns: undefined, pluralParamValue: undefined }); + const raw = node.getPluginData(TOLGEE_NODE_INFO); + const parsed = JSON.parse(raw) as Record; + expect("ns" in parsed).toBe(false); + expect("pluralParamValue" in parsed).toBe(false); + expect(parsed.key).toBe("k"); + }); +}); + +describe("getNodeInfo — defensive read", () => { + it("reads shape-stable defaults from invalid plugin-data JSON", () => { + const node = makeTextNode(); + node.setPluginData(TOLGEE_NODE_INFO, "{ not valid json"); + + const info = getNodeInfo(node); + expect(info.key).toBe(""); + expect(info.translation).toBe(""); + expect(info.connected).toBe(false); + expect(info.isPlural).toBe(false); + expect(info.ns).toBeUndefined(); + }); +}); diff --git a/src/main/nodes/__tests__/highlight.test.ts b/src/main/nodes/__tests__/highlight.test.ts new file mode 100644 index 00000000..7d4cd52e --- /dev/null +++ b/src/main/nodes/__tests__/highlight.test.ts @@ -0,0 +1,83 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { cleanUpHighlights, highlightNode } from "$main/nodes/highlight"; + +/** Minimal TEXT-node stand-in for the fills read/write the pulse touches. */ +function makeTextNode(id: string, fills: Paint[] = [{ type: "SOLID", color: { r: 0, g: 0, b: 0 } }]) { + return { + id, + type: "TEXT" as const, + fills, + removed: false, + }; +} + +type FakeNode = ReturnType; + +function installFigma(nodes: FakeNode[], editorType: "figma" | "dev" = "figma") { + const byId = new Map(nodes.map((n) => [n.id, n])); + (globalThis as unknown as { figma: unknown }).figma = { + editorType, + getNodeByIdAsync: async (id: string) => byId.get(id) ?? null, + // Deliberately NOT provided: a sync `getNodeById` — this is the whole + // point of the fix. If cleanup ever calls it again, the test blows up + // with "figma.getNodeById is not a function" instead of silently + // swallowing a thrown error like the try/catch used to. + viewport: { scrollAndZoomIntoView: () => {} }, + }; +} + +beforeEach(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + vi.useRealTimers(); + (globalThis as unknown as { figma?: unknown }).figma = undefined; +}); + +describe("cleanUpHighlights", () => { + it("restores original fills using the captured node reference, without figma.getNodeById", async () => { + const original: Paint[] = [{ type: "SOLID", color: { r: 0, g: 0, b: 0 } }]; + const node = makeTextNode("1:1", original); + installFigma([node]); + + await highlightNode("1:1"); + // Highlight applied, pulse still pending (500ms timer not yet fired). + expect(node.fills).not.toEqual(original); + + cleanUpHighlights(); + + expect(node.fills).toEqual(original); + }); + + it("is a no-op when nothing is pulsing", () => { + installFigma([]); + expect(() => cleanUpHighlights()).not.toThrow(); + }); + + it("skips a node that was removed while pulsing, without throwing", async () => { + const node = makeTextNode("1:1"); + installFigma([node]); + + await highlightNode("1:1"); + node.removed = true; + + expect(() => cleanUpHighlights()).not.toThrow(); + }); + + it("clears the pending timer so it can't fire after cleanup", async () => { + const original: Paint[] = [{ type: "SOLID", color: { r: 0, g: 0, b: 0 } }]; + const node = makeTextNode("1:1", original); + installFigma([node]); + + await highlightNode("1:1"); + cleanUpHighlights(); + node.fills = [{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }]; + + vi.advanceTimersByTime(1000); + + // The (cleared) pulse timer must not overwrite this later change. + expect(node.fills).toEqual([{ type: "SOLID", color: { r: 1, g: 1, b: 1 } }]); + }); +}); diff --git a/src/main/nodes/__tests__/nodeParents.test.ts b/src/main/nodes/__tests__/nodeParents.test.ts new file mode 100644 index 00000000..6606c46b --- /dev/null +++ b/src/main/nodes/__tests__/nodeParents.test.ts @@ -0,0 +1,123 @@ +import { type MainComponentNameCache, resolveParentNames } from "$main/nodes/nodeParents"; +import { describe, expect, it } from "vitest"; + +/** + * Minimal live-node stand-in: only `type`, `name`, `id` and the `parent` link + * are read by the ancestor walk. Built root→leaf so `parent` is set at creation. + */ +type FakeNode = { + type: string; + name: string; + id: string; + parent: FakeNode | null; + getMainComponentAsync?: () => Promise; +}; + +function n(type: string, name: string, parent: FakeNode | null = null): FakeNode { + return { type, name, id: name, parent }; +} + +/** An INSTANCE whose `getMainComponentAsync` resolves to `main`. */ +function inst( + name: string, + main: unknown, + parent: FakeNode | null = null, +): FakeNode { + return { type: "INSTANCE", name, id: name, parent, getMainComponentAsync: async () => main }; +} + +const resolve = (node: FakeNode, cache?: MainComponentNameCache) => + resolveParentNames(node as unknown as BaseNode, cache); + +describe("resolveParentNames", () => { + it("takes the nearest FRAME as `frame` and the topmost FRAME as `artboard`", async () => { + const outer = n("FRAME", "Outer"); + const inner = n("FRAME", "Inner", outer); + const text = n("TEXT", "t", inner); + + const r = await resolve(text); + expect(r.frame).toBe("Inner"); + expect(r.artboard).toBe("Outer"); + }); + + it("uses the same frame for both when there is only one", async () => { + const frame = n("FRAME", "Only"); + const r = await resolve(n("TEXT", "t", frame)); + expect(r.frame).toBe("Only"); + expect(r.artboard).toBe("Only"); + }); + + it("reads a real COMPONENT ancestor into `component`", async () => { + const card = n("COMPONENT", "Card"); + const r = await resolve(n("TEXT", "t", card)); + expect(r.component).toBe("Card"); + }); + + it("fills `instance` from the instance layer and `component` from its main", async () => { + const instance = inst("card-1", { name: "Card" }); + const r = await resolve(n("TEXT", "t", instance)); + expect(r.instance).toBe("card-1"); + expect(r.component).toBe("Card"); + }); + + it("uses the COMPONENT_SET name when the instance's main is a variant", async () => { + const instance = inst("btn", { + name: "State=Hover", + parent: { type: "COMPONENT_SET", name: "Button" }, + }); + const r = await resolve(n("TEXT", "t", instance)); + expect(r.component).toBe("Button"); + }); + + it("leaves `component` undefined when the instance has no main", async () => { + const instance = inst("orphan", null); + const r = await resolve(n("TEXT", "t", instance)); + expect(r.instance).toBe("orphan"); + expect(r.component).toBeUndefined(); + }); + + it("prefers a nearer real COMPONENT over an outer instance's main", async () => { + const instance = inst("inst", { name: "MainOfInstance" }); + const real = n("COMPONENT", "RealComponent", instance); + const r = await resolve(n("TEXT", "t", real)); + expect(r.component).toBe("RealComponent"); // nearest COMPONENT wins + expect(r.instance).toBe("inst"); // instance name still captured + }); + + it("captures SECTION and GROUP ancestors", async () => { + const section = n("SECTION", "Hero"); + const group = n("GROUP", "Buttons", section); + const r = await resolve(n("TEXT", "t", group)); + expect(r.section).toBe("Hero"); + expect(r.group).toBe("Buttons"); + }); + + it("returns undefined placeholders when there are no relevant ancestors", async () => { + const r = await resolve(n("TEXT", "t", null)); + expect(r).toEqual({ + frame: undefined, + artboard: undefined, + }); + }); + + it("resolves each instance's main once per shared batch cache", async () => { + let calls = 0; + const instance: FakeNode = { + type: "INSTANCE", + name: "shared", + id: "shared", + parent: null, + getMainComponentAsync: async () => { + calls++; + return { name: "Card" }; + }, + }; + const cache: MainComponentNameCache = new Map(); + const a = await resolve(n("TEXT", "a", instance), cache); + const b = await resolve(n("TEXT", "b", instance), cache); + + expect(a.component).toBe("Card"); + expect(b.component).toBe("Card"); + expect(calls).toBe(1); // second lookup served from the batch cache + }); +}); diff --git a/src/main/nodes/__tests__/scan.test.ts b/src/main/nodes/__tests__/scan.test.ts new file mode 100644 index 00000000..1e19acb4 --- /dev/null +++ b/src/main/nodes/__tests__/scan.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, it, vi } from "vitest"; + +import { buildConnectedNodesInfo, scanSelectedTextNodes } from "$main/nodes/scan"; + +/** Minimal TEXT-node stand-in — just enough for `getNodeInfo`/`shouldIgnoreNode` + * to read. `parent` defaults to a bare PAGE stand-in (no hidden ancestors). */ +function makeTextNode( + id: string, + overrides: Partial> & { parent?: unknown } = {}, +): TextNode { + return { + id, + name: `Layer ${id}`, + characters: `text ${id}`, + visible: true, + parent: { type: "PAGE" }, + getPluginData: () => "", + ...overrides, + } as unknown as TextNode; +} + +describe("buildConnectedNodesInfo", () => { + it("reports progress every 50 nodes when total > 100, ending at done === total", async () => { + const nodes = Array.from({ length: 250 }, (_, i) => makeTextNode(`1:${i}`)); + const onProgress = vi.fn(); + + const infos = await buildConnectedNodesInfo(nodes, {}, false, onProgress); + + expect(infos).toHaveLength(250); + expect(onProgress).toHaveBeenCalledTimes(5); + expect(onProgress.mock.calls).toEqual([ + [50, 250], + [100, 250], + [150, 250], + [200, 250], + [250, 250], + ]); + const last = onProgress.mock.calls.at(-1); + expect(last?.[0]).toBe(last?.[1]); + }); + + it("sends no progress messages when total <= 100", async () => { + const nodes = Array.from({ length: 100 }, (_, i) => makeTextNode(`1:${i}`)); + const onProgress = vi.fn(); + + const infos = await buildConnectedNodesInfo(nodes, {}, false, onProgress); + + expect(infos).toHaveLength(100); + expect(onProgress).not.toHaveBeenCalled(); + }); + + it("builds correct NodeInfo entries regardless of progress reporting", async () => { + const nodes = [makeTextNode("1:1"), makeTextNode("1:2")]; + + const infos = await buildConnectedNodesInfo(nodes, {}, false); + + expect(infos.map((i) => i.id)).toEqual(["1:1", "1:2"]); + expect(infos.map((i) => i.characters)).toEqual(["text 1:1", "text 1:2"]); + }); + + it("skips nodes the ignore settings would exclude, same as a with-selection scan", async () => { + const nodes = [ + makeTextNode("1:1"), + makeTextNode("1:2", { characters: "42" }), // pure digits, ignoreNumbers default on + makeTextNode("1:3", { visible: false }), // self-hidden, ignoreHiddenLayers default on + ]; + + const infos = await buildConnectedNodesInfo(nodes, {}, false); + + expect(infos.map((i) => i.id)).toEqual(["1:1"]); + }); + + it("does not check ancestor visibility when needsAncestorHidden is false", async () => { + const hiddenParent = { type: "FRAME", visible: false, parent: { type: "PAGE" } }; + const nodes = [makeTextNode("1:1", { parent: hiddenParent })]; + + const infos = await buildConnectedNodesInfo(nodes, { ignoreHiddenLayersIncludingChildren: true }, false); + + expect(infos.map((i) => i.id)).toEqual(["1:1"]); + }); + + it("skips nodes under a hidden ancestor when needsAncestorHidden is true", async () => { + const hiddenParent = { type: "FRAME", visible: false, parent: { type: "PAGE" } }; + const nodes = [ + makeTextNode("1:1", { parent: hiddenParent }), + makeTextNode("1:2"), // parent is the plain PAGE stand-in, not hidden + ]; + + const infos = await buildConnectedNodesInfo( + nodes, + { ignoreHiddenLayersIncludingChildren: true }, + true, + ); + + expect(infos.map((i) => i.id)).toEqual(["1:2"]); + }); +}); + +describe("scanSelectedTextNodes — hidden ancestors above the selected root", () => { + /** Installs a page whose selection is `selection`. */ + function installFigma(selection: unknown[]) { + (globalThis as unknown as { figma: unknown }).figma = { + currentPage: { loadAsync: async () => {}, selection }, + }; + } + + it("honours a hidden parent ABOVE the node picked in the Layers panel", async () => { + // Selecting a visible text node (or nested frame) through the Layers panel + // while one of its parents is hidden used to start the walk with + // `ancestorHidden = false` — the walk only ever looks downward, so nothing + // above the selected root was ever examined. The text then showed up and + // could be synchronised, while a page-wide scan correctly excluded it. + const page = { type: "PAGE" }; + const hiddenFrame = { type: "FRAME", visible: false, parent: page }; + const node = makeTextNode("t1", { type: "TEXT", parent: hiddenFrame }); + installFigma([node]); + + const scanned = await scanSelectedTextNodes(true); + + expect(scanned).toHaveLength(1); + expect(scanned[0]?.ancestorHidden).toBe(true); + }); + + it("leaves a node with only visible ancestors alone", async () => { + const page = { type: "PAGE" }; + const frame = { type: "FRAME", visible: true, parent: page }; + const node = makeTextNode("t2", { type: "TEXT", parent: frame }); + installFigma([node]); + + const scanned = await scanSelectedTextNodes(true); + + expect(scanned[0]?.ancestorHidden).toBe(false); + }); + + it("reports no ancestor-hidden state when the filter is off", async () => { + // The fast path deliberately skips the walk entirely — the filter isn't + // enabled, so nothing should be marked hidden. + const page = { type: "PAGE" }; + const hiddenFrame = { type: "FRAME", visible: false, parent: page }; + const node = makeTextNode("t3", { type: "TEXT", parent: hiddenFrame }); + installFigma([node]); + + const scanned = await scanSelectedTextNodes(false); + + expect(scanned[0]?.ancestorHidden).toBe(false); + }); +}) diff --git a/src/main/nodes/__tests__/selection.test.ts b/src/main/nodes/__tests__/selection.test.ts new file mode 100644 index 00000000..34bf5f94 --- /dev/null +++ b/src/main/nodes/__tests__/selection.test.ts @@ -0,0 +1,167 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { TOLGEE_NODE_INFO } from "$shared/constants"; +import { applyTranslations, setNodesData } from "$main/nodes/selection"; + +/** + * Minimal TEXT-node stand-in for the write paths: pluginData round-trip plus + * the properties `getNodeInfo` / `applyRichText` touch. + */ +function makeTextNode(id: string, characters: string) { + const pluginData = new Map(); + return { + id, + type: "TEXT" as const, + name: `Layer ${id}`, + characters, + visible: true, + autoRename: true, + getPluginData: (key: string) => pluginData.get(key) ?? "", + setPluginData: (key: string, value: string) => { + pluginData.set(key, value); + }, + getRangeAllFontNames: () => [{ family: "Inter", style: "Regular" }], + }; +} + +type FakeNode = ReturnType; + +function installFigma(nodes: FakeNode[]) { + const byId = new Map(nodes.map((n) => [n.id, n])); + (globalThis as unknown as { figma: unknown }).figma = { + getNodeByIdAsync: async (id: string) => byId.get(id) ?? null, + loadFontAsync: async () => {}, + }; +} + +beforeEach(() => { + installFigma([]); +}); + +afterEach(() => { + (globalThis as unknown as { figma?: unknown }).figma = undefined; +}); + +describe("setNodesData", () => { + it("returns fresh post-write snapshots of the updated nodes", async () => { + const a = makeTextNode("1:1", "Hello"); + const b = makeTextNode("1:2", "World"); + installFigma([a, b]); + + const result = await setNodesData([ + { id: "1:1", info: { key: "greeting", connected: false } }, + { id: "1:2", info: { key: "world", ns: "app", connected: true } }, + ]); + + expect(result.ok).toBe(true); + expect(result.nodes).toEqual([ + expect.objectContaining({ id: "1:1", key: "greeting", characters: "Hello" }), + expect.objectContaining({ id: "1:2", key: "world", ns: "app", connected: true }), + ]); + // And the snapshot reflects what was actually persisted. + expect(JSON.parse(a.getPluginData(TOLGEE_NODE_INFO))).toMatchObject({ key: "greeting" }); + }); + + it("skips missing / non-text nodes without failing the batch", async () => { + const a = makeTextNode("1:1", "Hello"); + installFigma([a]); + + const result = await setNodesData([ + { id: "1:1", info: { key: "kept" } }, + { id: "9:9", info: { key: "ghost" } }, + ]); + + expect(result.ok).toBe(true); + expect(result.nodes.map((n) => n.id)).toEqual(["1:1"]); + }); + + it("reports progress every 50 updates when total > 100, ending at done === total", async () => { + const nodes = Array.from({ length: 250 }, (_, i) => makeTextNode(`1:${i}`, `text ${i}`)); + installFigma(nodes); + const onProgress = vi.fn(); + + const updates = nodes.map((n) => ({ id: n.id, info: { key: `k${n.id}` } })); + const result = await setNodesData(updates, onProgress); + + expect(result.ok).toBe(true); + expect(onProgress).toHaveBeenCalledTimes(5); + expect(onProgress.mock.calls[0]).toEqual([50, 250]); + const last = onProgress.mock.calls.at(-1); + expect(last).toEqual([250, 250]); + }); + + it("sends no progress messages when total <= 100", async () => { + const nodes = Array.from({ length: 100 }, (_, i) => makeTextNode(`1:${i}`, `text ${i}`)); + installFigma(nodes); + const onProgress = vi.fn(); + + const updates = nodes.map((n) => ({ id: n.id, info: { key: `k${n.id}` } })); + const result = await setNodesData(updates, onProgress); + + expect(result.ok).toBe(true); + expect(onProgress).not.toHaveBeenCalled(); + }); +}); + +describe("applyTranslations", () => { + it("returns snapshots carrying the new text and translation", async () => { + const a = makeTextNode("1:1", "old text"); + installFigma([a]); + + const result = await applyTranslations([ + { id: "1:1", text: "new text", translation: "new text", key: "k", connected: true }, + ]); + + expect(result.ok).toBe(true); + expect(result.errors).toEqual([]); + expect(result.nodes).toEqual([ + expect.objectContaining({ + id: "1:1", + characters: "new text", + translation: "new text", + connected: true, + }), + ]); + }); + + it("collects per-node errors without aborting the batch", async () => { + const a = makeTextNode("1:1", "ok"); + installFigma([a]); + + const result = await applyTranslations([ + { id: "9:9", text: "x", translation: "x" }, + { id: "1:1", text: "fine", translation: "fine" }, + ]); + + expect(result.ok).toBe(false); + expect(result.errors).toHaveLength(1); + expect(result.nodes.map((n) => n.id)).toEqual(["1:1"]); + }); + + it("reports progress every 10 updates when total > 100, ending at done === total", async () => { + const nodes = Array.from({ length: 250 }, (_, i) => makeTextNode(`1:${i}`, `old ${i}`)); + installFigma(nodes); + const onProgress = vi.fn(); + + const updates = nodes.map((n) => ({ id: n.id, text: "new", translation: "new" })); + const result = await applyTranslations(updates, onProgress); + + expect(result.ok).toBe(true); + expect(onProgress).toHaveBeenCalledTimes(25); + expect(onProgress.mock.calls[0]).toEqual([10, 250]); + const last = onProgress.mock.calls.at(-1); + expect(last).toEqual([250, 250]); + }); + + it("sends no progress messages when total <= 100", async () => { + const nodes = Array.from({ length: 100 }, (_, i) => makeTextNode(`1:${i}`, `old ${i}`)); + installFigma(nodes); + const onProgress = vi.fn(); + + const updates = nodes.map((n) => ({ id: n.id, text: "new", translation: "new" })); + const result = await applyTranslations(updates, onProgress); + + expect(result.ok).toBe(true); + expect(onProgress).not.toHaveBeenCalled(); + }); +}); diff --git a/src/main/nodes/clearPrefilled.ts b/src/main/nodes/clearPrefilled.ts new file mode 100644 index 00000000..6d643c05 --- /dev/null +++ b/src/main/nodes/clearPrefilled.ts @@ -0,0 +1,71 @@ +import { TOLGEE_NODE_INFO } from "$shared/constants"; + +/** Yield to the event loop after this many examined nodes so a heavily + * prefilled document can't freeze the canvas mid-clear. The candidate set is + * already engine-filtered (only nodes carrying our pluginData). */ +const YIELD_EVERY = 200; + +/** + * Undo auto-generated key names when the user turns "Prefill key format" OFF: + * clear the persisted key of unconnected text nodes across the document — but + * ONLY where the key is still exactly the value the prefill generated for it, + * recorded in the `prefilledKey` provenance marker (written by the prefill + * effect in Index.svelte). + * + * key === prefilledKey → untouched auto value → cleared + * key !== prefilledKey → hand-typed or edited → preserved + * no prefilledKey → never auto (manual) → preserved + * + * The marker is self-invalidating: any manual key change diverges it from the + * key, and a format change re-generates both together, so it stays correct even + * across format changes — which a "regenerate and compare" approach could not. + * + * Fast by construction: each page uses the engine-side `findAllWithCriteria` + * pluginData filter (like `scanConnectedNodes`), so only nodes carrying our + * data are examined — never a full JS walk of every layer. The per-node work is + * a parse plus a string compare; no regeneration, no ancestor walks. Each page + * is `loadAsync`-ed first (dynamic-page), mirroring `resetConfig`. + * + * Returns the number of nodes cleared. + */ +export async function clearPrefilledKeys(): Promise { + let cleared = 0; + let examined = 0; + for (const page of figma.root.children) { + if (page.type !== "PAGE") continue; + await page.loadAsync(); + const nodes = page.findAllWithCriteria({ + types: ["TEXT"], + pluginData: { keys: [TOLGEE_NODE_INFO] }, + }); + for (const node of nodes) { + const raw = node.getPluginData(TOLGEE_NODE_INFO); + if (!raw) continue; + let data: Record; + try { + data = JSON.parse(raw) as Record; + } catch { + continue; + } + // Keep real Tolgee links, and skip nodes with nothing to clear. + if (data.connected) continue; + const key = data.key; + if (typeof key !== "string" || key === "") continue; + + examined++; + if (examined % YIELD_EVERY === 0) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + + // Only an UNTOUCHED auto key still equals its provenance marker. A manual + // edit / hand-typed key (or one with no marker) diverges → preserve it. + if (data.prefilledKey !== key) continue; + + // Drop both the key and its now-meaningless marker. + const { prefilledKey: _drop, ...rest } = data; + node.setPluginData(TOLGEE_NODE_INFO, JSON.stringify({ ...rest, key: "" })); + cleared++; + } + } + return cleared; +} diff --git a/src/main/nodes/filter.ts b/src/main/nodes/filter.ts new file mode 100644 index 00000000..0e0a708b --- /dev/null +++ b/src/main/nodes/filter.ts @@ -0,0 +1,79 @@ +import type { GlobalSettings } from "$shared/types"; + +export type IgnoreSettings = Pick< + GlobalSettings, + | "ignorePrefix" + | "ignoreNumbers" + | "ignoreFormattedNumbers" + | "ignoreHiddenLayers" + | "ignoreHiddenLayersIncludingChildren" + | "ignoreTextLayers" +>; + +/** Pure integers only — "100", "42" (matches the original plugin's `/^\d+$/`). */ +const PURE_DIGITS = /^\d+$/; +/** Formatted numbers too — decimals, thousands separators, signs: "1,234.00", + * "3.14", "+420", "-5". Only used when `ignoreFormattedNumbers` is opted in. */ +const FORMATTED_NUMERIC = /^[\d\s.,+\-]+$/; + +/** + * Returns `true` when the given text node should be skipped by the Tolgee sync + * flow. Mirrors the original `shouldIncludeNode` (inverted): blank strings, + * numbers, self-hidden layers, and — with "including children" — layers inside + * a hidden subtree are ignored. + * + * @param node The text node under consideration. + * @param ancestorHidden Whether an ancestor within the scanned selection is + * hidden — computed once during the scan traversal + * (see `scanSelectedTextNodes`), so we don't walk each + * node's parent chain here (the original's approach). + * @param settings Subset of `GlobalSettings` describing the active filters. + */ +export const shouldIgnoreNode = ( + node: TextNode, + ancestorHidden: boolean, + settings: Partial, + // Callers that already read `characters` (the selection scan reads it for + // `getNodeInfo` too) pass it in — every access copies the whole string + // across the plugin bridge, and this runs per node of a large selection. + characters: string = node.characters, +): boolean => { + + // Blank / whitespace-only nodes are never translatable — always skip them, + // regardless of any toggle (matches the original `characters.trim() === ""`). + if (characters.trim().length === 0) { + return true; + } + + if (settings.ignoreNumbers ?? true) { + // Base "Numbers" = pure integers (original behaviour). Opt in to + // `ignoreFormattedNumbers` to also skip decimals / separators / signs. + const re = settings.ignoreFormattedNumbers ? FORMATTED_NUMERIC : PURE_DIGITS; + if (re.test(characters)) return true; + } + + // Default the prefix to "_" when the "text layers with prefix" filter is on + // but no prefix was set — matches the original plugin's read-time default. + const prefix = settings.ignorePrefix ?? "_"; + if (settings.ignoreTextLayers && prefix && node.name.startsWith(prefix)) { + return true; + } + + // Both hidden-layer rules are gated on `ignoreHiddenLayers` (like the + // original `respectVisibility`): "including children" does nothing on its own + // when visibility isn't being respected. + const respectVisibility = settings.ignoreHiddenLayers ?? true; + if (respectVisibility && !node.visible) { + return true; + } + + if ( + respectVisibility && + settings.ignoreHiddenLayersIncludingChildren && + ancestorHidden + ) { + return true; + } + + return false; +}; diff --git a/src/main/nodes/getNodeInfo.ts b/src/main/nodes/getNodeInfo.ts new file mode 100644 index 00000000..e1bb240a --- /dev/null +++ b/src/main/nodes/getNodeInfo.ts @@ -0,0 +1,103 @@ +import { TOLGEE_NODE_INFO } from "$shared/constants"; +import type { NodeInfo } from "$shared/types"; + +/** + * The subset of `NodeInfo` fields that we persist into `setPluginData`. + * + * `id`, `name`, `characters`, `visible` and the resolved parent names + * (`component`/`frame`/`artboard`/`section`/`group`) come from the live Figma + * node/tree and must NOT be written into plugin data — they would only ever + * drift. + */ +type PersistedNodeInfo = Omit< + NodeInfo, + | "id" + | "name" + | "characters" + | "visible" + | "component" + | "frame" + | "artboard" + | "section" + | "group" +>; + +const readPersisted = (node: TextNode): Partial => { + const raw = node.getPluginData(TOLGEE_NODE_INFO); + if (!raw) { + return {}; + } + try { + return JSON.parse(raw) as Partial; + } catch { + return {}; + } +}; + +/** + * Read the Tolgee `NodeInfo` for a Figma text node. Plugin data is parsed + * defensively (an invalid JSON payload yields an "empty" record), and every + * persisted field is normalised to its safe default so the UI can rely on + * shape-stable records. + */ +export const getNodeInfo = (node: TextNode, characters?: string): NodeInfo => { + const pluginData = readPersisted(node); + return { + id: node.id, + name: node.name, + // Optional passthrough: the selection scan already copied the string + // across the bridge for the ignore filter — don't copy it twice. + characters: characters ?? node.characters, + visible: node.visible, + key: pluginData.key ?? "", + ns: pluginData.ns, + translation: pluginData.translation ?? "", + isPlural: Boolean(pluginData.isPlural), + pluralParamValue: pluginData.pluralParamValue, + paramsValues: pluginData.paramsValues, + connected: Boolean(pluginData.connected), + prefilledKey: pluginData.prefilledKey, + }; +}; + +/** + * Merge `partial` into the persisted plugin data of `node` and return the + * resulting `NodeInfo`. Node-derived fields (`id`, `name`, `characters`, + * `visible`) are never written to plugin data. + */ +export const setNodeInfo = (node: TextNode, partial: Partial): NodeInfo => { + const current = readPersisted(node); + + const merged: PersistedNodeInfo = { + key: partial.key ?? current.key ?? "", + ns: "ns" in partial ? partial.ns : current.ns, + translation: partial.translation ?? current.translation ?? "", + isPlural: Boolean(partial.isPlural ?? current.isPlural), + pluralParamValue: + "pluralParamValue" in partial ? partial.pluralParamValue : current.pluralParamValue, + paramsValues: "paramsValues" in partial ? partial.paramsValues : current.paramsValues, + connected: Boolean(partial.connected ?? current.connected), + // Only the prefill write sets this; a manual key edit omits it, leaving the + // stale marker so `key !== prefilledKey` and the key is preserved on clear. + prefilledKey: "prefilledKey" in partial ? partial.prefilledKey : current.prefilledKey, + }; + + // Strip explicit `undefined` values so the serialized form stays compact and + // round-trips cleanly through `JSON.parse`. + const serializable: Record = {}; + for (const [k, v] of Object.entries(merged)) { + if (v !== undefined) { + serializable[k] = v; + } + } + + node.setPluginData(TOLGEE_NODE_INFO, JSON.stringify(serializable)); + + return { + id: node.id, + name: node.name, + characters: node.characters, + visible: node.visible, + ...merged, + }; +}; diff --git a/src/main/nodes/highlight.ts b/src/main/nodes/highlight.ts new file mode 100644 index 00000000..1b99f620 --- /dev/null +++ b/src/main/nodes/highlight.ts @@ -0,0 +1,92 @@ +/** + * Briefly recolors a TextNode pink so the user can spot it after a + * `scrollAndZoomIntoView`. Tiny strings in large frames vanish even at full + * zoom; the pulse is a 500 ms affordance that survived the rewrite as a + * regression and is restored here. + * + * `node.fills` may be `figma.mixed` when the user has manually styled ranges + * differently. In that case we leave fills untouched — overwriting them with + * a single SolidPaint would silently flatten the user's work, which is much + * worse than skipping the pulse. + * + * The original-fill snapshot is kept in `pending` so: + * - Repeated highlights of the same node coalesce (no growing chain of + * pending restores). + * - `cleanUp()` can restore everything if the plugin closes mid-pulse. + */ + +const HIGHLIGHT_FILL: SolidPaint = { + type: "SOLID", + color: { r: 1, g: 0.41, b: 0.58 }, +}; + +const PULSE_MS = 500; + +type Pending = { + /** The node reference itself — cleanup must reuse this, not look it up by + * id: `figma.getNodeById` is sync and throws under `documentAccess: + * "dynamic-page"` (this manifest's setting), which would silently abort + * the close-time restore and leave the layer stuck pink. */ + node: TextNode; + /** The fills array we captured before applying the highlight. */ + originalFills: ReadonlyArray; + /** Timer id so we can clear it on re-pulse / cleanup. */ + timer: ReturnType; +}; + +const pending = new Map(); + +export async function highlightNode(id: string): Promise { + if (figma.editorType !== "figma") return; + const node = await figma.getNodeByIdAsync(id); + if (!node || node.type !== "TEXT") return; + + figma.viewport.scrollAndZoomIntoView([node]); + + const fills = node.fills; + if (fills === figma.mixed) { + // Range-specific fills: don't risk flattening them. + return; + } + + // If a previous pulse is still active for this node, reuse its captured + // original fills (they pre-date OUR overwrite) and reset the timer. + const previous = pending.get(id); + const originalFills = previous?.originalFills ?? (fills as ReadonlyArray); + if (previous) clearTimeout(previous.timer); + + node.fills = [HIGHLIGHT_FILL]; + const timer = setTimeout(() => { + const entry = pending.get(id); + if (!entry) return; + pending.delete(id); + // The node may have been deleted while the timer was waiting. Guard + // every property access so the restore can't throw. + try { + if (node.removed) return; + node.fills = entry.originalFills as Paint[]; + } catch { + /* node went away or fills are no longer writable — give up silently */ + } + }, PULSE_MS); + + pending.set(id, { node, originalFills, timer }); +} + +/** + * Restore every still-pulsing node's original fills. Called from the plugin's + * `close` hook so an unexpected exit doesn't leave layers stuck pink. + */ +export function cleanUpHighlights(): void { + for (const [, entry] of pending) { + clearTimeout(entry.timer); + try { + if (!entry.node.removed) { + entry.node.fills = entry.originalFills as Paint[]; + } + } catch { + /* swallow — best-effort cleanup */ + } + } + pending.clear(); +} diff --git a/src/main/nodes/nodeParents.ts b/src/main/nodes/nodeParents.ts new file mode 100644 index 00000000..6dd6692d --- /dev/null +++ b/src/main/nodes/nodeParents.ts @@ -0,0 +1,89 @@ +/** + * MAIN-THREAD — ported from the v2 plugin's `src/main/utils/nodeParents.ts` + * (`getAllParents`), adapted for this plugin's `documentAccess: "dynamic-page"` + * manifest: + * - traverses the LIVE `node.parent` chain instead of `figma.getNodeById(id)` + * (the sync lookup is unavailable under dynamic-page — and we already hold + * the node during the scan), and + * - resolves an INSTANCE's main component via the async + * `getMainComponentAsync()` (the sync `.mainComponent` getter is deprecated + * under dynamic-page). + * + * NOTE FOR THE COLLEAGUE (main-thread owner): this is new main code. It's + * additive — a read-only ancestor walk used only to fill the parent key-format + * placeholders; please review the dynamic-page assumptions above. + */ + +import type { KeyParentNames } from "$shared/keyFormat"; + +export type NodeParentNames = KeyParentNames; + +/** + * Cache for `getMainComponentAsync` lookups within one batch (one selection + * scan / one bulk generate). Keyed by the INSTANCE node's id, holding the + * resolved component name promise — text nodes sharing an instance ancestor + * would otherwise each pay the same async round-trip. Scoped to the batch + * (not module-level) so component renames are picked up by the next scan. + */ +export type MainComponentNameCache = Map>; + +async function resolveMainComponentName( + instance: InstanceNode, + cache?: MainComponentNameCache, +): Promise { + const cached = cache?.get(instance.id); + if (cached) return cached; + const promise = instance.getMainComponentAsync().then((main) => { + if (!main) return undefined; + return main.parent?.type === "COMPONENT_SET" ? main.parent.name : main.name; + }); + cache?.set(instance.id, promise); + return promise; +} + +/** + * Single upward traversal collecting the names of the node's relevant + * ancestors for key placeholders. `frame` = nearest FRAME, `artboard` = topmost + * FRAME; `component` prefers a real COMPONENT/COMPONENT_SET, and for an INSTANCE + * resolves the main component it was created from (using the whole COMPONENT_SET + * name when the main is a variant, so the key reads "Button" rather than + * "State=Hover"). `!== undefined` guards keep the NEAREST match of each kind. + */ +export async function resolveParentNames( + node: BaseNode, + cache?: MainComponentNameCache, +): Promise { + const result: NodeParentNames = {}; + let frame: string | undefined; + let artboard: string | undefined; + + let parent = node.parent; + while (parent) { + if (parent.type === "FRAME") { + if (frame === undefined) frame = parent.name; + artboard = parent.name; + } else if ( + (parent.type === "COMPONENT" || parent.type === "COMPONENT_SET") && + result.component === undefined + ) { + result.component = parent.name; + } else if (parent.type === "INSTANCE") { + // The instance's OWN layer name feeds `{instance}`; its main component + // (resolved async, cached per batch) feeds `{component}`. Both keep the + // nearest match. + if (result.instance === undefined) result.instance = parent.name; + if (result.component === undefined) { + result.component = await resolveMainComponentName(parent, cache); + } + } else if (parent.type === "SECTION" && result.section === undefined) { + result.section = parent.name; + } else if (parent.type === "GROUP" && result.group === undefined) { + result.group = parent.name; + } + parent = parent.parent; + } + + result.frame = frame; + result.artboard = artboard; + return result; +} diff --git a/src/main/nodes/scan.ts b/src/main/nodes/scan.ts new file mode 100644 index 00000000..8b7352f0 --- /dev/null +++ b/src/main/nodes/scan.ts @@ -0,0 +1,208 @@ +import { TOLGEE_NODE_INFO } from "$shared/constants"; +import type { NodeInfo } from "$shared/types"; +import { type IgnoreSettings, shouldIgnoreNode } from "./filter"; +import { getNodeInfo } from "./getNodeInfo"; + +/** + * Return only the text nodes on the current page that carry our plugin data + * key. The `pluginData.keys` filter is a 2024 addition to the Plugin API and + * works with `api: 1.0.0` + `documentAccess: "dynamic-page"`, which is what + * this plugin targets. + */ +export const scanConnectedNodes = async (): Promise => { + await figma.currentPage.loadAsync(); + // TODO: yield periodically for huge documents once measurement exists. + return figma.currentPage.findAllWithCriteria({ + types: ["TEXT"], + pluginData: { keys: [TOLGEE_NODE_INFO] }, + }); +}; + +/** Yield to the event loop after this many nodes' `NodeInfo` has been built — + * matches the interval `request-page-connected-nodes` used before progress + * reporting existed. */ +const INFO_YIELD_EVERY = 50; + +/** Progress is only reported (and the interim `page-connected-nodes-progress` + * message only sent) once a scan is large enough that the UI actually needs + * live feedback — small pages resolve near-instantly and the extra chatter + * would just be noise. Matches the guard used for other bulk operations + * (`nodes-set-progress`, `apply-translations-progress`). */ +const PROGRESS_MIN_TOTAL = 100; + +/** Walk up from a connected text node to the page root to find out whether + * any CONTAINER between it and the page is hidden — the same "ancestor + * hidden" concept `scanSelectedTextNodes` threads down during its recursive + * walk, needed for the opt-in "ignore hidden layers including children" + * filter. Unlike a full-document walk, this stays cheap here: it only ever + * runs once per node `findAllWithCriteria` already matched (i.e. bounded to + * the page's CONNECTED nodes), not once per node on the page. */ +export function computeAncestorHidden(node: SceneNode): boolean { + let parent = node.parent; + while (parent && parent.type !== "PAGE") { + if ("visible" in parent && parent.visible === false) return true; + parent = parent.parent; + } + return false; +} + +/** + * Build a `NodeInfo` for every given text node, yielding to the event loop + * every `INFO_YIELD_EVERY` nodes so a page-wide scan of thousands of connected + * nodes doesn't freeze the canvas thread. `getNodeInfo` costs ~5 bridge reads + * (incl. a full `characters` copy) per node. + * + * Nodes the user's ignore settings would exclude (hidden layers, digit-only + * strings, prefixed layer names, …) are filtered out here too — a page-wide + * "Download all" should skip exactly what a with-selection scan would skip, + * rather than touching everything regardless of those settings. + * + * `onProgress` is called at each yield point with the running (scanned, not + * kept) count, but only when `nodes.length > PROGRESS_MIN_TOTAL` — see that + * constant's doc. + */ +export async function buildConnectedNodesInfo( + nodes: TextNode[], + settings: Partial, + needsAncestorHidden: boolean, + onProgress?: (done: number, total: number) => void, +): Promise { + const total = nodes.length; + const infos: NodeInfo[] = []; + let scanned = 0; + for (const node of nodes) { + const characters = node.characters; + const ancestorHidden = needsAncestorHidden ? computeAncestorHidden(node) : false; + if (!shouldIgnoreNode(node, ancestorHidden, settings, characters)) { + infos.push(getNodeInfo(node, characters)); + } + scanned++; + if (scanned % INFO_YIELD_EVERY === 0) { + if (total > PROGRESS_MIN_TOTAL) { + onProgress?.(scanned, total); + } + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + return infos; +} + +/** A scanned text node plus whether any ANCESTOR within the scanned selection + * is hidden — computed once during traversal (threaded down), so the ignore + * filter never has to walk each node's parent chain (matches the original + * plugin's `ancestorHidden` approach; the per-node walk was "expensive"). */ +export type ScannedTextNode = { node: TextNode; ancestorHidden: boolean }; + +// The plugin sandbox and the Figma canvas share scheduling — a long +// uninterrupted walk over a huge subtree freezes the whole editor. Yielding +// to the event loop every couple hundred visited nodes keeps Figma responsive +// while costing nothing on small selections (no yield ever fires). +const YIELD_EVERY = 200; + +const collectTextNodes = async ( + node: SceneNode, + out: ScannedTextNode[], + ancestorHidden: boolean, + progress: { visited: number }, + isStale: () => boolean, +): Promise => { + progress.visited++; + if (progress.visited % YIELD_EVERY === 0) { + await new Promise((resolve) => setTimeout(resolve, 0)); + if (isStale()) return; + } + if (node.type === "TEXT") { + out.push({ node, ancestorHidden }); + return; + } + if ("children" in node) { + // A container's own hidden state makes its whole subtree "ancestor-hidden". + const childAncestorHidden = ancestorHidden || node.visible === false; + for (const child of node.children) { + if (isStale()) return; + await collectTextNodes(child, out, childAncestorHidden, progress, isStale); + } + } +}; + +/** + * Walk the current selection and return every reachable `TextNode`, each tagged + * with whether an ancestor inside the selection is hidden. Containers (frames, + * groups, components, instances, …) are descended into so the user can select + * an arbitrary subtree and still get all its text nodes back. + * + * Two strategies: + * - FAST (default): `findAllWithCriteria` per selection root. The traversal + * runs inside the Figma engine — the JS side never touches the (often tens + * of thousands of) vector/shape nodes a design section contains, so a + * 100-key section costs ~100 bridge crossings instead of ~100k. Usable + * whenever `ancestorHidden` isn't needed. + * - RECURSIVE: JS walk that threads `ancestorHidden` down — required only for + * the opt-in "ignore hidden layers including children" filter, which needs + * to know whether any ancestor within the selection is hidden. + */ +export const scanSelectedTextNodes = async ( + needsAncestorHidden: boolean, + // Superseded-scan check, polled at every yield. A newer selection makes the + // running scan WORTHLESS — without bailing out it would keep burning canvas + // time in the background, and rapid selection switches after a large + // selection queued up "zombie" scans the new one had to wait behind. + isStale: () => boolean = () => false, +): Promise => { + // `figma.currentPage` is implicitly loaded — selection access requires it — + // but calling `loadAsync` again is cheap and keeps the contract explicit. + await figma.currentPage.loadAsync(); + const out: ScannedTextNode[] = []; + + if (!needsAncestorHidden) { + let roots = 0; + for (const node of figma.currentPage.selection) { + if (isStale()) return out; + if (node.type === "TEXT") { + out.push({ node, ancestorHidden: false }); + } else if ("children" in node) { + // Search per CHILD, not per root. One atomic `findAllWithCriteria` + // on a whole design-system wall (tens of thousands of layers) blocks + // the canvas for its entire duration — Figma can't even process a + // deselect click, and the pending signal never flushes to the UI. + // A wall's children are individual screens: each per-child search is + // small and fast, and the breathers between them keep Figma + // interactive AND give a superseding selection a place to abort. + let children = 0; + for (const child of node.children) { + if (isStale()) return out; + if (child.type === "TEXT") { + out.push({ node: child, ancestorHidden: false }); + } else if ("findAllWithCriteria" in child) { + const found = child.findAllWithCriteria({ types: ["TEXT"] }); + for (const text of found) { + out.push({ node: text, ancestorHidden: false }); + } + } + children++; + if (children % 10 === 0) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + } + roots++; + if (roots % 25 === 0) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + return out; + } + + const progress = { visited: 0 }; + for (const node of figma.currentPage.selection) { + if (isStale()) return out; + // Seed with the SELECTED ROOT's own ancestor state, not `false`. The walk + // only ever looks downward, so selecting a visible text node or nested + // frame from the Layers panel while one of its parents is hidden used to + // start the traversal as if nothing above it were hidden — the text then + // showed up and could be synchronised, while a page-wide scan correctly + // excluded it via the same `computeAncestorHidden`. + await collectTextNodes(node, out, computeAncestorHidden(node), progress, isStale); + } + return out; +}; diff --git a/src/main/nodes/selection.ts b/src/main/nodes/selection.ts new file mode 100644 index 00000000..2664bcaf --- /dev/null +++ b/src/main/nodes/selection.ts @@ -0,0 +1,273 @@ +import { keyFormatUsesParents } from "$shared/keyFormat"; +import type { NodeInfo } from "$shared/types"; + +import { readMergedConfig } from "$main/settings"; +import { applyRichText } from "$main/text/applyRichText"; +import { shouldIgnoreNode } from "./filter"; +import { getNodeInfo, setNodeInfo } from "./getNodeInfo"; +import { type MainComponentNameCache, resolveParentNames } from "./nodeParents"; +import { scanSelectedTextNodes } from "./scan"; + +/** + * Replace the rendered text of a `TextNode`, applying Tolgee's inline + * formatting tags (`` / `` / `` / `
`) to the corresponding + * character ranges. See `$main/text/applyRichText` for the parsing details. + * + * Disables `autoRename` (via `applyRichText`) so the layer name doesn't + * follow the new text — the Tolgee key is the source of truth. + */ +export async function writeTextNode(node: TextNode, newText: string): Promise { + await applyRichText(node, newText); +} + +/** + * Returns the text nodes the UI should currently focus on — always strictly + * the user's selection: + * + * - If the user has a selection, we return the text nodes inside it (possibly + * an empty list, e.g. a frame with no text). Each is run through + * `shouldIgnoreNode` so the "ignore numbers / hidden layers / prefixed layer + * names" settings prune what shows up. The filter applies UNIFORMLY — a + * connected string in a hidden layer is hidden too, matching the original + * plugin (`findTextNodesInfo` runs the same `shouldIncludeNode` on every node, + * with no exception for connected ones). + * - If nothing is selected, we return an empty list — we deliberately do NOT + * scan the whole page. On large files that page-wide `findAllWithCriteria` + * scan is a real performance hit, and silently listing the entire document + * is rarely what the user wants. The UI shows a "select something" empty + * state instead. Page-wide work still happens on demand in the Download/Pull + * flow via the dedicated `request-page-connected-nodes` path. + */ +/** Nodes per streamed `onBatch` delivery. Large enough that per-batch + * overhead (parent resolve + message) amortises, small enough that the + * first rows reach the UI within a fraction of a second. */ +const SELECTION_BATCH_SIZE = 100; + +export const getSelectionInfo = async ( + // Superseded-scan probe (see `scanSelectedTextNodes`): checked at every + // yield so an outdated scan stops burning canvas time as soon as a newer + // selection arrives. The caller discards the partial result. + isStale: () => boolean = () => false, + // Streamed delivery: called with each completed batch of NodeInfos (parent + // placeholders already resolved for the batch). Building info for + // thousands of nodes costs many seconds of bridge work — without + // streaming the UI showed NOTHING until the whole selection finished. + onBatch?: (nodes: NodeInfo[], first: boolean) => void, +): Promise<{ + nodes: NodeInfo[]; + basedOnSelection: boolean; +}> => { + const hasUserSelection = figma.currentPage.selection.length > 0; + if (!hasUserSelection) { + return { nodes: [], basedOnSelection: false }; + } + + // Config first (cached) — it decides the scan strategy: `ancestorHidden` + // is only consumed by the opt-in "including children" hidden filter, and + // skipping it unlocks the engine-side `findAllWithCriteria` fast path. + const config = await readMergedConfig(); + const needsAncestorHidden = Boolean( + (config.ignoreHiddenLayers ?? true) && config.ignoreHiddenLayersIncludingChildren, + ); + // Parent key-format placeholders ({component}/{frame}/…) are resolved only + // when prefill is on AND the format actually uses one — a plain key format + // adds zero traversal cost. + const needsParents = Boolean(config.prefillKeyFormat && keyFormatUsesParents(config.keyFormat)); + // Shared per-scan cache: text nodes under the same INSTANCE ancestor + // resolve its main component once instead of once each. + const mainComponentNames: MainComponentNameCache = new Map(); + + const selected = await scanSelectedTextNodes(needsAncestorHidden, isStale); + if (isStale()) return { nodes: [], basedOnSelection: true }; + + const all: NodeInfo[] = []; + let batch: { node: TextNode; info: NodeInfo }[] = []; + let firstBatch = true; + + // Complete a batch: resolve its parent placeholders (sequential with + // yields — a parallel burst of ancestor walks blocks the canvas), then + // hand it to the caller. Returns false when superseded mid-way. + const flushBatch = async (): Promise => { + if (batch.length === 0) return true; + if (needsParents) { + let resolved = 0; + for (const { node, info } of batch) { + const parents = await resolveParentNames(node, mainComponentNames); + info.component = parents.component; + info.instance = parents.instance; + info.frame = parents.frame; + info.artboard = parents.artboard; + info.section = parents.section; + info.group = parents.group; + resolved++; + if (resolved % 25 === 0) { + await new Promise((resolve) => setTimeout(resolve, 0)); + if (isStale()) return false; + } + } + } + const infos = batch.map(({ info }) => info); + all.push(...infos); + batch = []; + onBatch?.(infos, firstBatch); + firstBatch = false; + return true; + }; + + // Filter BEFORE building NodeInfo — `getNodeInfo` costs ~5 bridge calls per + // node (pluginData + name + …), which ignored nodes must not pay. One loop + // so `characters` crosses the bridge once per node (filter + info share it), + // with periodic yields — thousands of kept nodes are tens of thousands of + // bridge calls, and doing them unbroken visibly froze the canvas. + let scanned = 0; + for (const { node, ancestorHidden } of selected) { + const characters = node.characters; + if (!shouldIgnoreNode(node, ancestorHidden, config, characters)) { + batch.push({ node, info: getNodeInfo(node, characters) }); + } + scanned++; + if (scanned % 50 === 0) { + await new Promise((resolve) => setTimeout(resolve, 0)); + if (isStale()) return { nodes: all, basedOnSelection: true }; + } + if (batch.length >= SELECTION_BATCH_SIZE) { + if (!(await flushBatch())) return { nodes: all, basedOnSelection: true }; + } + } + if (!(await flushBatch())) return { nodes: all, basedOnSelection: true }; + + return { nodes: all, basedOnSelection: true }; +}; + +export type NodeUpdate = { + id: string; + info: Partial; +}; + +/** + * Apply a batch of partial `NodeInfo` updates. Each lookup goes through + * `getNodeByIdAsync` because `documentAccess: "dynamic-page"` rules out the + * synchronous variant. Non-text or missing nodes are skipped silently and do + * not flip the result to `ok: false` — callers receive `ok: false` only when + * an unexpected error escapes a single update. + * + * Returns the fresh post-write `NodeInfo` snapshots (we already hold the node + * reference, so this costs no extra lookups). The UI patches its selection + * from these instead of the main thread re-scanning the whole selection. + * + * `onProgress` is invoked at each yield point with the running count, but + * only when `updates.length > 100` — same guard as `applyTranslations` / + * `buildConnectedNodesInfo` (small batches finish fast enough that progress + * messages would just be noise, and the UI's bulk action bar shouldn't flash + * busy for an instant write). + */ +export const setNodesData = async ( + updates: NodeUpdate[], + onProgress?: (done: number, total: number) => void, +): Promise<{ ok: boolean; nodes: NodeInfo[] }> => { + let ok = true; + const nodes: NodeInfo[] = []; + const total = updates.length; + let processed = 0; + for (const update of updates) { + try { + const node = await figma.getNodeByIdAsync(update.id); + if (node && node.type === "TEXT") { + // `setNodeInfo` already returns the exact post-write snapshot — no + // second pluginData read needed. + nodes.push(setNodeInfo(node, update.info)); + } + } catch { + ok = false; + } + processed++; + // Yield every ~50 updates so a bulk write over a large selection doesn't + // starve the canvas thread (pluginData-only writes are cheap). + if (processed % 50 === 0) { + if (total > 100) { + onProgress?.(processed, total); + } + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + return { ok, nodes }; +}; + +export type ApplyTranslationUpdate = { + id: string; + /** Final, ICU-formatted string to write into `TextNode.characters`. */ + text: string; + /** Raw translation source to persist into plugin data. */ + translation: string; + /** Optional plural flag; falls back to existing plugin data when omitted. */ + isPlural?: boolean; + pluralParamValue?: string; + paramsValues?: Record; + key?: string; + ns?: string; + connected?: boolean; +}; + +/** + * Pull-side counterpart to `setNodesData`. For each update we: + * 1. resolve the node via the async API (dynamic-page safe), + * 2. load every font present in the existing text and write `text`, + * 3. persist the raw `translation` (and optional `isPlural`) into plugin + * data so future diffs work off the canonical source. + * + * The function never throws — per-node failures are collected into `errors` + * so the UI can report them without aborting the whole batch. + * + * `onProgress` is invoked at each yield point with the running count, but + * only when `updates.length > 100` — same guard as `buildConnectedNodesInfo` + * (small batches finish fast enough that progress messages would just be + * noise). It doubles as the UI watchdog's "still alive" signal. + */ +export const applyTranslations = async ( + updates: ApplyTranslationUpdate[], + onProgress?: (done: number, total: number) => void, +): Promise<{ ok: boolean; errors: string[]; nodes: NodeInfo[] }> => { + const errors: string[] = []; + const nodes: NodeInfo[] = []; + const total = updates.length; + let processed = 0; + + for (const update of updates) { + try { + const node = await figma.getNodeByIdAsync(update.id); + if (!node || node.type !== "TEXT") { + errors.push(`Node ${update.id} is not a text node`); + continue; + } + await writeTextNode(node, update.text); + const partial: Partial = { translation: update.translation }; + if (update.isPlural !== undefined) partial.isPlural = update.isPlural; + if (update.pluralParamValue !== undefined) { + partial.pluralParamValue = update.pluralParamValue; + } + if (update.paramsValues !== undefined) { + partial.paramsValues = update.paramsValues; + } + if (update.key !== undefined) partial.key = update.key; + if (update.ns !== undefined) partial.ns = update.ns; + if (update.connected !== undefined) partial.connected = update.connected; + nodes.push(setNodeInfo(node, partial)); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + errors.push(`Node ${update.id}: ${msg}`); + } + + processed++; + // Each update is a real canvas mutation (font loads + `characters` + // relayout), so breathe every few nodes — 50 unbroken text rewrites was + // hundreds of ms of canvas-thread blocking on real frames. + if (processed % 10 === 0) { + if (total > 100) { + onProgress?.(processed, total); + } + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + + return { ok: errors.length === 0, errors, nodes }; +}; diff --git a/src/main/screenshots/__tests__/capture.test.ts b/src/main/screenshots/__tests__/capture.test.ts new file mode 100644 index 00000000..8daaed4b --- /dev/null +++ b/src/main/screenshots/__tests__/capture.test.ts @@ -0,0 +1,152 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { captureScreenshots } from "$main/screenshots/capture"; +import type { FrameScreenshot } from "$shared/types"; + +/** + * Minimal fakes for the parent-chain walks (`findParentPage`, + * `findOutermostFrame`) and the handful of node properties `capture.ts` + * touches (`absoluteBoundingBox`, `exportAsync`, plugin data get/set). + */ +function makePage(id: string) { + return { id, type: "PAGE" as const, parent: null, loadAsync: async () => {} }; +} + +function makeFrame(id: string, parent: unknown) { + return { + id, + name: `Frame ${id}`, + type: "FRAME" as const, + parent, + absoluteBoundingBox: { x: 0, y: 0, width: 100, height: 100 }, + exportAsync: vi.fn(async () => new Uint8Array([1, 2, 3])), + }; +} + +function makeTextNode(id: string, parent: unknown) { + const pluginData = new Map(); + return { + id, + name: `Text ${id}`, + type: "TEXT" as const, + parent, + characters: "Hello", + visible: true, + absoluteBoundingBox: { x: 10, y: 10, width: 50, height: 20 }, + getPluginData: (key: string) => pluginData.get(key) ?? "", + setPluginData: (key: string, value: string) => { + pluginData.set(key, value); + }, + }; +} + +function installFigma(getNodeByIdAsync: (id: string) => Promise) { + (globalThis as unknown as { figma: unknown }).figma = { getNodeByIdAsync }; +} + +afterEach(() => { + (globalThis as unknown as { figma?: unknown }).figma = undefined; + vi.restoreAllMocks(); +}); + +describe("captureScreenshots", () => { + it("resolves node ids, skips missing ones, and delivers one screenshot per frame", async () => { + const page = makePage("page-1"); + const frame = makeFrame("f-1", page); + const t1 = makeTextNode("t-1", frame); + const t2 = makeTextNode("t-2", frame); + const nodesById = new Map([ + ["f-1", frame], + ["t-1", t1], + ["t-2", t2], + ]); + installFigma(async (id) => nodesById.get(id) ?? null); + + const delivered: FrameScreenshot[] = []; + // "missing-id" resolves to null and must be skipped, not crash the batch. + const { delivered: count } = await captureScreenshots(["t-1", "t-2", "missing-id"], (s) => + delivered.push(s), + ); + + expect(count).toBe(1); + expect(delivered).toHaveLength(1); + expect(delivered[0]?.info.id).toBe("f-1"); + expect(delivered[0]?.keys.map((k) => k.id)).toEqual(["t-1", "t-2"]); + }); + + it("resolves ids sequentially with periodic yields instead of one concurrent Promise.all burst", async () => { + const total = 120; + const nodesById = new Map(); + // None of these are TEXT nodes, so `frameToNodes` stays empty and the + // frame-export loop never runs -- any yields observed come solely from + // the id-resolve step under test. + for (let i = 0; i < total; i++) { + nodesById.set(`n-${i}`, { id: `n-${i}`, type: "RECTANGLE", parent: null }); + } + + let inFlight = 0; + let maxInFlight = 0; + installFigma(async (id) => { + inFlight++; + maxInFlight = Math.max(maxInFlight, inFlight); + // Simulate a real bridge round-trip (a microtask gap) so a concurrent + // burst would actually show overlapping in-flight calls. + await Promise.resolve(); + const result = nodesById.get(id) ?? null; + inFlight--; + return result; + }); + + const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); + + const ids = Array.from({ length: total }, (_, i) => `n-${i}`); + const { delivered: count } = await captureScreenshots(ids, () => {}); + + expect(count).toBe(0); + // Sequential resolution: at most one lookup in flight at any time, never + // a 120-way concurrent burst. + expect(maxInFlight).toBe(1); + // Yields at 50 and 100 resolved ids (120 isn't itself a multiple of 50). + expect(setTimeoutSpy).toHaveBeenCalledTimes(2); + }); +}); + +describe("captureScreenshots — failed frames", () => { + it("reports frames whose export threw instead of dropping them silently", async () => { + // A failed export was console.warn'd (dev-only, invisible to users) and + // vanished, so the push summary presented the reduced count as the whole + // set: "N screenshot(s) uploaded" with no hint that M never made it. + vi.spyOn(console, "warn").mockImplementation(() => {}); + const page = makePage("page-1"); + const okFrame = makeFrame("f-ok", page); + const badFrame = makeFrame("f-bad", page); + badFrame.exportAsync = vi.fn(async () => { + throw new Error("export blew up"); + }) as never; + const tOk = makeTextNode("t-ok", okFrame); + const tBad = makeTextNode("t-bad", badFrame); + const nodesById = new Map([ + ["t-ok", tOk], + ["t-bad", tBad], + ]); + installFigma(async (id) => nodesById.get(id) ?? null); + + const delivered: FrameScreenshot[] = []; + const result = await captureScreenshots(["t-ok", "t-bad"], (s) => delivered.push(s)); + + expect(result.delivered).toBe(1); + expect(result.failed).toBe(1); + expect(delivered).toHaveLength(1); + }); + + it("reports zero failures when every frame exports", async () => { + const page = makePage("page-1"); + const frame = makeFrame("f-1", page); + const t1 = makeTextNode("t-1", frame); + installFigma(async (id) => (id === "t-1" ? t1 : null)); + + const result = await captureScreenshots(["t-1"], () => {}); + + expect(result).toEqual({ delivered: 1, failed: 0 }); + }); +}); diff --git a/src/main/screenshots/__tests__/groupByFrame.test.ts b/src/main/screenshots/__tests__/groupByFrame.test.ts new file mode 100644 index 00000000..e6f59909 --- /dev/null +++ b/src/main/screenshots/__tests__/groupByFrame.test.ts @@ -0,0 +1,125 @@ +import { findOutermostFrame, groupNodesByFrame } from "$main/screenshots/groupByFrame"; +import { describe, expect, it } from "vitest"; + +/** + * Minimal subset of `BaseNode` we need to assemble fake parent chains. + * We never touch real Figma globals — everything is plain objects cast + * through `unknown` to the relevant Figma node interface. + */ +type FakeBase = { + id: string; + name: string; + type: string; + parent: FakeBase | null; +}; + +function frame(id: string, parent: FakeBase | null = null): FakeBase { + return { id, name: id, type: "FRAME", parent }; +} + +function section(id: string, parent: FakeBase | null = null): FakeBase { + return { id, name: id, type: "SECTION", parent }; +} + +function page(id: string): FakeBase { + return { id, name: id, type: "PAGE", parent: null }; +} + +function textNode(id: string, parent: FakeBase): FakeBase { + return { id, name: id, type: "TEXT", parent }; +} + +describe("findOutermostFrame", () => { + it("returns the parent frame when the text node sits directly inside it", () => { + const pageNode = page("page-1"); + const f = frame("frame-1", pageNode); + const text = textNode("text-1", f); + + const result = findOutermostFrame(text as unknown as SceneNode); + expect(result).toBe(f as unknown as FrameNode); + }); + + it("returns the frame when the text is wrapped in a section inside a frame", () => { + const pageNode = page("page-1"); + const f = frame("frame-1", pageNode); + const sec = section("section-1", f); + const text = textNode("text-1", sec); + + const result = findOutermostFrame(text as unknown as SceneNode); + expect(result).toBe(f as unknown as FrameNode); + }); + + it("returns the OUTERMOST frame when frames are nested", () => { + const pageNode = page("page-1"); + const outer = frame("outer", pageNode); + const inner = frame("inner", outer); + const text = textNode("text-1", inner); + + const result = findOutermostFrame(text as unknown as SceneNode); + expect(result).toBe(outer as unknown as FrameNode); + expect(result).not.toBe(inner as unknown as FrameNode); + }); + + it("returns the outermost frame across a Frame > Section > Frame chain", () => { + const pageNode = page("page-1"); + const outer = frame("outer", pageNode); + const sec = section("section", outer); + const inner = frame("inner", sec); + const text = textNode("text-1", inner); + + const result = findOutermostFrame(text as unknown as SceneNode); + expect(result).toBe(outer as unknown as FrameNode); + }); + + it("returns null when there is no frame parent at all", () => { + const pageNode = page("page-1"); + const sec = section("section", pageNode); + const text = textNode("text-1", sec); + + const result = findOutermostFrame(text as unknown as SceneNode); + expect(result).toBeNull(); + }); + + it("returns null when the text node has no parent", () => { + const text: FakeBase = { + id: "orphan", + name: "orphan", + type: "TEXT", + parent: null, + }; + const result = findOutermostFrame(text as unknown as SceneNode); + expect(result).toBeNull(); + }); +}); + +describe("groupNodesByFrame", () => { + it("groups text nodes by their outermost enclosing frame", () => { + const pageNode = page("page-1"); + const frameA = frame("a", pageNode); + const frameB = frame("b", pageNode); + + const tA1 = textNode("t-a-1", frameA); + const tA2 = textNode("t-a-2", frameA); + const tB = textNode("t-b", frameB); + + const grouped = groupNodesByFrame([ + tA1 as unknown as TextNode, + tA2 as unknown as TextNode, + tB as unknown as TextNode, + ]); + + expect(grouped.size).toBe(2); + expect(grouped.get(frameA as unknown as FrameNode)).toEqual([ + tA1 as unknown as TextNode, + tA2 as unknown as TextNode, + ]); + expect(grouped.get(frameB as unknown as FrameNode)).toEqual([tB as unknown as TextNode]); + }); + + it("drops text nodes that have no frame ancestor", () => { + const pageNode = page("page-1"); + const orphan = textNode("orphan", pageNode); + const grouped = groupNodesByFrame([orphan as unknown as TextNode]); + expect(grouped.size).toBe(0); + }); +}); diff --git a/src/main/screenshots/capture.ts b/src/main/screenshots/capture.ts new file mode 100644 index 00000000..ca68a4ec --- /dev/null +++ b/src/main/screenshots/capture.ts @@ -0,0 +1,166 @@ +import { getNodeInfo } from "$main/nodes/getNodeInfo"; +import type { FrameInfo, FrameScreenshot, NodeInfo } from "$shared/types"; +import { groupNodesByFrame } from "./groupByFrame"; +import { getNodeRelativePosition } from "./position"; + +// Tolgee renders position overlays against the exported image at 1x. The node +// x/y/width/height we send come from `absoluteBoundingBox` (in design-space +// pixels) so the screenshot must match — exporting at 2x while keeping +// positions at 1x makes the overlay boxes appear half-size and offset. The +// legacy plugin exported at 1x for the same reason. +const EXPORT_SCALE = 1; + +/** + * How many frames we may export back-to-back before yielding to the event + * loop. The Figma plugin host runs JS on the same thread as the canvas, so + * a tight `await Promise.all` over hundreds of frames can starve UI input. + */ +const YIELD_EVERY_N_FRAMES = 5; + +/** + * How many node ids we may resolve back-to-back (via `getNodeByIdAsync`) + * before yielding to the event loop. A push touching thousands of connected + * nodes previously fired all lookups in a single `Promise.all` burst — this + * keeps the same total work but paced, matching the yield cadence already + * used for bulk node reads elsewhere (`scan.ts`, `selection.ts`). + */ +const YIELD_EVERY_N_NODE_RESOLVES = 50; + +const yieldToEventLoop = (): Promise => new Promise((resolve) => setTimeout(resolve, 0)); + +/** + * Walk up from any node until we find the enclosing `PageNode`. Returns + * `null` if the node is detached from the document tree. + */ +function findParentPage(node: BaseNode): PageNode | null { + let current: BaseNode | null = node; + while (current) { + if (current.type === "PAGE") { + return current; + } + current = current.parent; + } + return null; +} + +/** + * Ensure the page containing `node` is loaded. Required when the plugin runs + * with `documentAccess: "dynamic-page"` — exporting or reading geometry on a + * node whose page hasn't been loaded throws at runtime. + */ +async function ensurePageLoaded(node: BaseNode): Promise { + const page = findParentPage(node); + if (page) { + await page.loadAsync(); + } +} + +/** + * Export a single frame to a PNG byte buffer at `EXPORT_SCALE`. The caller is + * responsible for guaranteeing the frame's page has been loaded — we still + * defensively load it here so the helper is safe to use in isolation. + */ +export async function exportFrame(frame: FrameNode): Promise { + await ensurePageLoaded(frame); + return frame.exportAsync({ + format: "PNG", + constraint: { type: "SCALE", value: EXPORT_SCALE }, + }); +} + +/** + * Capture screenshots for the supplied node ids, delivering each frame to + * `onFrame` AS SOON as it is exported. Streaming keeps at most one PNG buffer + * alive at a time — accumulating them meant peak memory of the whole set plus + * one giant postMessage at the end. Returns how many frames were delivered AND + * how many failed to export — a per-frame failure skips that frame rather than + * killing the capture, so the caller has to be told about the gap or it will + * report the reduced count as if it were the whole set. + * + * Each id is resolved (via `getNodeByIdAsync` to respect dynamic-page access), + * filtered down to `TextNode`s, then grouped by closest enclosing frame. Each + * frame is exported once as a PNG and the contained text nodes contribute + * positioned `NodeInfo` entries. + */ +export type CaptureResult = { + /** Frames handed to `onFrame`. */ + delivered: number; + /** Frames whose export threw and were skipped. */ + failed: number; +}; + +export async function captureScreenshots( + nodeIds: string[], + onFrame: (screenshot: FrameScreenshot) => void, +): Promise { + // 1. Resolve ids → nodes. Missing nodes are skipped silently; the UI is + // free to pass a stale selection without crashing the export. Sequential + // (rather than one giant `Promise.all`) so a push touching thousands of + // nodes doesn't fire that many concurrent bridge calls in a single burst + // — yield periodically to keep the canvas thread responsive. + const resolved: (BaseNode | null)[] = []; + for (const id of nodeIds) { + resolved.push(await figma.getNodeByIdAsync(id)); + if (resolved.length % YIELD_EVERY_N_NODE_RESOLVES === 0) { + await yieldToEventLoop(); + } + } + + // 2. Keep only text nodes — screenshots are anchored to translatable text. + const textNodes: TextNode[] = []; + for (const node of resolved) { + if (node && node.type === "TEXT") { + textNodes.push(node); + } + } + + // 3. Group by closest frame so each frame is exported at most once. + const frameToNodes = groupNodesByFrame(textNodes); + + // 4. Export each frame and hand it off immediately. Sequential (rather than + // Promise.all) so we can yield to the event loop and keep the canvas + // responsive; per-frame failures skip that frame instead of killing the + // whole capture. + let delivered = 0; + let failed = 0; + let processed = 0; + for (const [frame, nodes] of frameToNodes) { + processed += 1; + if (processed % YIELD_EVERY_N_FRAMES === 0) { + await yieldToEventLoop(); + } + try { + await ensurePageLoaded(frame); + + const frameBox = frame.absoluteBoundingBox; + const image = await exportFrame(frame); + + const info: FrameInfo = { + id: frame.id, + name: frame.name, + width: frameBox?.width ?? 0, + height: frameBox?.height ?? 0, + }; + + const keys: FrameScreenshot["keys"] = nodes.map((node) => { + const position = getNodeRelativePosition(node, frame); + const nodeInfo: NodeInfo = getNodeInfo(node); + return { + ...nodeInfo, + ...position, + }; + }); + + onFrame({ image, info, keys }); + delivered += 1; + } catch (err) { + failed += 1; + console.warn( + `[tolgee:main] screenshot export failed for frame "${frame.name}":`, + err instanceof Error ? err.message : err, + ); + } + } + + return { delivered, failed }; +} diff --git a/src/main/screenshots/groupByFrame.ts b/src/main/screenshots/groupByFrame.ts new file mode 100644 index 00000000..fe2831f0 --- /dev/null +++ b/src/main/screenshots/groupByFrame.ts @@ -0,0 +1,41 @@ +/** + * Walk the parent chain of `node` and return the outermost FrameNode. + * + * The legacy plugin (src/main/utils/nodeTools.ts#findLastParentFrame) walks all + * the way to the root and returns the last frame seen, i.e. the top-level + * frame on the page, not the innermost wrapper. Screenshots are produced for + * that outermost frame so the captured area matches the artboard the designer + * sees rather than a tightly cropped sub-frame. Components and instances are + * intentionally ignored — only true FrameNodes count. + */ +export function findOutermostFrame(node: SceneNode): FrameNode | null { + let current: BaseNode | null = node.parent; + let outermost: FrameNode | null = null; + while (current) { + if (current.type === "FRAME") { + outermost = current; + } + current = current.parent; + } + return outermost; +} + +/** + * Group text nodes by their outermost enclosing frame. Nodes without any + * parent frame are silently dropped — they cannot be screenshotted in + * isolation. + */ +export function groupNodesByFrame(nodes: TextNode[]): Map { + const result = new Map(); + for (const node of nodes) { + const frame = findOutermostFrame(node); + if (!frame) continue; + const existing = result.get(frame); + if (existing) { + existing.push(node); + } else { + result.set(frame, [node]); + } + } + return result; +} diff --git a/src/main/screenshots/position.ts b/src/main/screenshots/position.ts new file mode 100644 index 00000000..c6917b7e --- /dev/null +++ b/src/main/screenshots/position.ts @@ -0,0 +1,25 @@ +/** + * Compute the position of `node` relative to the top-left corner of `frame`. + * + * Both nodes expose `absoluteBoundingBox` in document space; subtracting the + * frame origin yields coordinates suitable for overlaying the key marker on + * the exported PNG. If either bounding box is unavailable (rare — e.g. nodes + * that have just been detached) we fall back to a zero-position so callers + * never have to special-case null. + */ +export function getNodeRelativePosition( + node: SceneNode, + frame: FrameNode, +): { x: number; y: number; width: number; height: number } { + const nodeBox = node.absoluteBoundingBox; + const frameBox = frame.absoluteBoundingBox; + if (!nodeBox || !frameBox) { + return { x: 0, y: 0, width: 0, height: 0 }; + } + return { + x: nodeBox.x - frameBox.x, + y: nodeBox.y - frameBox.y, + width: nodeBox.width, + height: nodeBox.height, + }; +} diff --git a/src/main/settings/__tests__/index.test.ts b/src/main/settings/__tests__/index.test.ts new file mode 100644 index 00000000..5ec5cba9 --- /dev/null +++ b/src/main/settings/__tests__/index.test.ts @@ -0,0 +1,184 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { TOLGEE_PLUGIN_CONFIG_NAME } from "$shared/constants"; +import { invalidateConfigCache, readMergedConfig, writeConfig } from "$main/settings"; +import { readGlobalSettings, writeGlobalSettings } from "$main/settings/storage"; + +/** + * Minimal fake covering exactly what `writeConfig` touches: clientStorage + * (global scope), `figma.root` pluginData (document scope), and + * `figma.currentPage` pluginData (page scope) — including the + * `loadAsync()` the dynamic-page-access path awaits before touching it. + */ +function installFigma() { + const clientStorage = new Map(); + const rootPluginData = new Map(); + const pagePluginData = new Map(); + return installFigmaWith(clientStorage, rootPluginData, pagePluginData); +} + +function installFigmaWith( + clientStorage: Map, + rootPluginData: Map, + pagePluginData: Map, +) { + + (globalThis as unknown as { figma: unknown }).figma = { + clientStorage: { + getAsync: async (key: string) => clientStorage.get(key), + setAsync: async (key: string, value: unknown) => { + clientStorage.set(key, value); + }, + deleteAsync: async (key: string) => { + clientStorage.delete(key); + }, + }, + root: { + getPluginData: (key: string) => rootPluginData.get(key) ?? "", + setPluginData: (key: string, value: string) => { + rootPluginData.set(key, value); + }, + }, + currentPage: { + loadAsync: async () => {}, + getPluginData: (key: string) => pagePluginData.get(key) ?? "", + setPluginData: (key: string, value: string) => { + pagePluginData.set(key, value); + }, + }, + }; + + return { clientStorage, rootPluginData, pagePluginData }; +} + +beforeEach(() => { + installFigma(); +}); + +afterEach(() => { + (globalThis as unknown as { figma?: unknown }).figma = undefined; +}); + +describe("writeConfig", () => { + it("persists documentInfo:true to document scope and pageInfo:true to page scope", async () => { + const { rootPluginData, pagePluginData } = installFigma(); + + // Mirrors what the `save-config` handler in main.ts now passes: the UI's + // submitted config plus the two production-parity scope markers. + await writeConfig({ + apiUrl: "https://app.tolgee.io", + documentInfo: true, + pageInfo: true, + }); + + const doc = JSON.parse(rootPluginData.get(TOLGEE_PLUGIN_CONFIG_NAME) ?? "{}"); + const page = JSON.parse(pagePluginData.get(TOLGEE_PLUGIN_CONFIG_NAME) ?? "{}"); + + expect(doc.documentInfo).toBe(true); + expect(page.pageInfo).toBe(true); + // And they land in the right scope only — not cross-leaked. + expect(page.documentInfo).toBeUndefined(); + expect(doc.pageInfo).toBeUndefined(); + }); +}); + +describe("global settings clientStorage format (rollback compatibility)", () => { + it("persists a JSON STRING the published plugin's unguarded JSON.parse can read", async () => { + const { clientStorage } = installFigma(); + + await writeGlobalSettings({ apiUrl: "https://app.tolgee.io", apiKey: "k" }); + + const stored = clientStorage.get(TOLGEE_PLUGIN_CONFIG_NAME); + // The PUBLISHED plugin reads this value with a bare `JSON.parse(value)` + // at startup (settingsTools.ts) — a raw object here would brick it after + // a rollback. This assertion IS that reader. + expect(typeof stored).toBe("string"); + expect(JSON.parse(stored as string)).toEqual({ + apiUrl: "https://app.tolgee.io", + apiKey: "k", + }); + }); + + it("still reads back both legacy-string and (historical) object shapes", async () => { + const { clientStorage } = installFigma(); + + clientStorage.set(TOLGEE_PLUGIN_CONFIG_NAME, JSON.stringify({ apiKey: "s" })); + expect(await readGlobalSettings()).toEqual({ apiKey: "s" }); + + // Docs touched by the brief window where we stored raw objects must not + // lose their settings either. + clientStorage.set(TOLGEE_PLUGIN_CONFIG_NAME, { apiKey: "o" }); + expect(await readGlobalSettings()).toEqual({ apiKey: "o" }); + }); +}); + +describe("config cache around a save", () => { + beforeEach(() => { + invalidateConfigCache(); + }); + + it("does not keep a pre-write config that was cached DURING the save", async () => { + // `writeConfig` invalidates on entry, then awaits several times before the + // writes land. Anything that reads in that window (a `selectionchange` + // scan, say) caches the OLD state — and without a second invalidation that + // stale promise outlives the save, reverting the form in the UI and + // keeping the old ignore/prefill behaviour. + const { clientStorage } = installFigma(); + clientStorage.set(TOLGEE_PLUGIN_CONFIG_NAME, JSON.stringify({ apiUrl: "https://old.example" })); + + // Force a read to land mid-save by making the first storage await yield. + const figma = (globalThis as unknown as { figma: { clientStorage: { getAsync: unknown } } }) + .figma; + const original = figma.clientStorage.getAsync as (k: string) => Promise; + let concurrentRead: Promise | undefined; + figma.clientStorage.getAsync = vi.fn(async (key: string) => { + // Read once, from inside the save's own await. + concurrentRead ??= readMergedConfig(); + return original(key); + }) as never; + + await writeConfig({ apiUrl: "https://new.example" }); + await concurrentRead; + figma.clientStorage.getAsync = original as never; + + await expect(readMergedConfig()).resolves.toMatchObject({ + apiUrl: "https://new.example", + }); + }); + + it("serves the persisted config once the save has settled", async () => { + installFigma(); + + await writeConfig({ apiUrl: "https://one.example" }); + await expect(readMergedConfig()).resolves.toMatchObject({ apiUrl: "https://one.example" }); + + await writeConfig({ apiUrl: "https://two.example" }); + await expect(readMergedConfig()).resolves.toMatchObject({ apiUrl: "https://two.example" }); + }); +}); + +describe("safeParseObject logging", () => { + it("logs instead of silently resetting when stored settings are unreadable", async () => { + // A silent `{}` here wipes the API key, language, ignore rules and key + // format with nothing in the console to explain it. + const { clientStorage } = installFigma(); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + clientStorage.set(TOLGEE_PLUGIN_CONFIG_NAME, "{not json"); + + expect(await readGlobalSettings()).toEqual({}); + expect(warn).toHaveBeenCalledTimes(1); + expect(String(warn.mock.calls[0]?.[0])).toContain("global"); + + warn.mockRestore(); + }); + + it("stays quiet for an absent value — a fresh document is not an anomaly", async () => { + installFigma(); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + expect(await readGlobalSettings()).toEqual({}); + expect(warn).not.toHaveBeenCalled(); + + warn.mockRestore(); + }); +}); diff --git a/src/main/settings/__tests__/merge.test.ts b/src/main/settings/__tests__/merge.test.ts new file mode 100644 index 00000000..6e8108d4 --- /dev/null +++ b/src/main/settings/__tests__/merge.test.ts @@ -0,0 +1,211 @@ +import { mergeConfig, splitConfig } from "$main/settings/merge"; +import type { + CurrentDocumentSettings, + CurrentPageSettings, + GlobalSettings, + NodeInfo, + TolgeeConfig, +} from "$shared/types"; +import { describe, expect, it } from "vitest"; + +describe("mergeConfig", () => { + it("page wins over doc wins over global (right-wins precedence)", () => { + const merged = mergeConfig( + { apiUrl: "a" } as Partial, + { apiUrl: "b" } as Partial, + {} as Partial, + ); + expect(merged.apiUrl).toBe("b"); + }); + + it("includes fields from all three scopes when keys don't collide", () => { + const merged = mergeConfig( + { apiKey: "x" } as Partial, + {} as Partial, + { language: "en" } as Partial, + ); + expect(merged.apiKey).toBe("x"); + expect(merged.language).toBe("en"); + }); + + it("page overrides global on the same key", () => { + const merged = mergeConfig( + { apiUrl: "fromGlobal" } as Partial, + {} as Partial, + // `apiUrl` isn't part of CurrentPageSettings but the merge is structural + // — cast through unknown to drive the right-wins behaviour intentionally. + { apiUrl: "fromPage" } as unknown as Partial, + ); + expect(merged.apiUrl).toBe("fromPage"); + }); +}); + +describe("splitConfig", () => { + it("routes page-only fields to `page`", () => { + const nodeInfo: NodeInfo = { + id: "n1", + name: "node", + characters: "hi", + translation: "hi", + isPlural: false, + key: "k", + ns: undefined, + connected: true, + }; + const split = splitConfig({ + language: "en", + pageInfo: true, + pageCopy: false, + nodeInfo, + } as Partial); + + expect(split.page.language).toBe("en"); + expect(split.page.pageInfo).toBe(true); + expect(split.page.pageCopy).toBe(false); + expect(split.page.nodeInfo).toBe(nodeInfo); + // `language` is page-only — it should never leak onto `doc` or `global`. + // Access through Record so TS doesn't complain about the missing key. + expect((split.doc as Record).language).toBeUndefined(); + expect((split.global as Record).language).toBeUndefined(); + }); + + it("routes the immutable `copyLanguage` marker to `page` only (survives writeConfig, no doc leak)", () => { + // A repointed selectable `language` must not drag the copy's true language + // with it — `copyLanguage` stays page-scoped so a later config write echoes + // it straight back onto the copy page instead of leaking onto the document. + const split = splitConfig({ + language: "en", + copyLanguage: "cs", + pageCopy: true, + sourcePageId: "1:2", + } as Partial); + + expect(split.page.copyLanguage).toBe("cs"); + expect((split.doc as Record).copyLanguage).toBeUndefined(); + expect((split.global as Record).copyLanguage).toBeUndefined(); + // Round-trip: it comes back out of the merged view unchanged. + expect(mergeConfig(split.global, split.doc, split.page).copyLanguage).toBe("cs"); + }); + + it("routes doc-only fields (namespace, branch, documentInfo) to `doc`", () => { + const split = splitConfig({ + namespace: "ns", + branch: "main", + documentInfo: true, + } as Partial); + + expect(split.doc.namespace).toBe("ns"); + expect(split.doc.branch).toBe("main"); + expect(split.doc.documentInfo).toBe(true); + // `namespace` is doc-only — verify it doesn't leak onto `global` or `page`. + expect((split.global as Record).namespace).toBeUndefined(); + expect((split.page as Record).namespace).toBeUndefined(); + }); + + it("mirrors the global preferences (apiKey/apiUrl/ignorePrefix/ignoreNumbers) to both `global` and `doc`", () => { + const split = splitConfig({ + apiUrl: "https://app.tolgee.io", + apiKey: "tg-abc", + ignorePrefix: "//", + ignoreNumbers: true, + } as Partial); + + for (const scope of [split.global, split.doc] as Record[]) { + expect(scope.apiUrl).toBe("https://app.tolgee.io"); + expect(scope.apiKey).toBe("tg-abc"); + expect(scope.ignorePrefix).toBe("//"); + expect(scope.ignoreNumbers).toBe(true); + } + expect((split.page as Record).apiUrl).toBeUndefined(); + }); + + it("keeps document-level settings (keyFormat, variableCasing, tags) OUT of global — no cross-document leak", () => { + const split = splitConfig({ + keyFormat: "{frame}.{elementName}", + variableCasing: "snake_case", + tags: ["a", "b"], + addTags: true, + ignoreHiddenLayers: true, + } as Partial); + + // They belong to the document only… + expect(split.doc.keyFormat).toBe("{frame}.{elementName}"); + expect(split.doc.variableCasing).toBe("snake_case"); + expect(split.doc.tags).toEqual(["a", "b"]); + expect(split.doc.addTags).toBe(true); + expect(split.doc.ignoreHiddenLayers).toBe(true); + + // …never on global (would otherwise pre-fill other documents). + const g = split.global as Record; + expect(g.keyFormat).toBeUndefined(); + expect(g.variableCasing).toBeUndefined(); + expect(g.tags).toBeUndefined(); + expect(g.addTags).toBeUndefined(); + expect(g.ignoreHiddenLayers).toBeUndefined(); + }); + + it("skips undefined values during split", () => { + const split = splitConfig({ + apiUrl: undefined, + apiKey: "keep", + } as Partial); + + expect("apiUrl" in split.global).toBe(false); + expect("apiUrl" in split.doc).toBe(false); + expect(split.global.apiKey).toBe("keep"); + expect(split.doc.apiKey).toBe("keep"); + }); + + it("round-trip: splitConfig(mergeConfig(g, d, p)) preserves all fields", () => { + const global: Partial = { + apiUrl: "https://app.tolgee.io", + apiKey: "tg-key", + ignorePrefix: "//", + ignoreNumbers: true, + }; + const doc: Partial = { + namespace: "myns", + branch: "feat/foo", + documentInfo: true, + }; + const page: Partial = { + language: "de", + pageInfo: true, + pageCopy: false, + pageStringDetails: true, + }; + + const merged = mergeConfig(global, doc, page); + const split = splitConfig(merged); + + // Global keys land in both `global` and `doc` after split. + expect(split.global.apiUrl).toBe(global.apiUrl); + expect(split.global.apiKey).toBe(global.apiKey); + expect(split.doc.apiUrl).toBe(global.apiUrl); + expect(split.doc.apiKey).toBe(global.apiKey); + + // Doc-only keys stay on `doc`. + expect(split.doc.namespace).toBe(doc.namespace); + expect(split.doc.branch).toBe(doc.branch); + expect(split.doc.documentInfo).toBe(doc.documentInfo); + + // Page-only keys stay on `page`. + expect(split.page.language).toBe(page.language); + expect(split.page.pageInfo).toBe(page.pageInfo); + expect(split.page.pageCopy).toBe(page.pageCopy); + expect(split.page.pageStringDetails).toBe(page.pageStringDetails); + + // Re-merging the split must yield an equivalent merged view. + const remerged = mergeConfig(split.global, split.doc, split.page); + expect(remerged).toEqual(merged); + }); + + it("routes unknown keys to `doc` only (self-contained, no global leak), not dropped", () => { + const config = { someLegacyKey: 42 } as unknown as Partial; + const split = splitConfig(config); + + expect((split.doc as Record).someLegacyKey).toBe(42); + expect((split.global as Record).someLegacyKey).toBeUndefined(); + expect((split.page as Record).someLegacyKey).toBeUndefined(); + }); +}); diff --git a/src/main/settings/index.ts b/src/main/settings/index.ts new file mode 100644 index 00000000..7556c421 --- /dev/null +++ b/src/main/settings/index.ts @@ -0,0 +1,126 @@ +import type { TolgeeConfig } from "$shared/types"; + +import { mergeConfig, pickGlobalSettings, splitConfig } from "./merge"; +import { + deleteGlobalSettings, + readDocumentSettings, + readGlobalSettings, + readPageSettings, + writeDocumentSettings, + writeGlobalSettings, + writePageSettings, +} from "./storage"; + +/** + * This plugin's manifest ships with `documentAccess: "dynamic-page"` (see + * scripts/build-manifest.mjs), so pages must be `loadAsync()`-ed before their + * pluginData is touched. A constant — the previous runtime probe read + * `figma.documentAccess`, which does not exist in the Plugin API, so it was + * always `false` and silently disabled every dynamic-page branch below. + */ +const DYNAMIC_PAGE_ACCESS = true; + +/** + * Merged-config cache. The config is read on EVERY selection scan (each + * debounced selectionchange), and a miss costs a clientStorage async hop plus + * three pluginData reads + JSON parses. All main-thread writes funnel through + * `writeConfig`/`resetConfig`, and page scope changes with the current page — + * those three events invalidate. (A second Figma window editing the shared + * clientStorage mid-session is the one accepted miss.) + */ +let cachedConfig: Promise> | null = null; + +export function invalidateConfigCache(): void { + cachedConfig = null; +} + +/** + * Reads and merges settings from all three storage scopes (global -> + * document -> page, right-wins). + * + * Under `documentAccess: "dynamic-page"`, the current page must be explicitly + * loaded before its pluginData is safe to read. + */ +export async function readMergedConfig(): Promise> { + if (!cachedConfig) { + cachedConfig = readMergedConfigUncached(); + // Don't cache a failed read — the next caller retries. + cachedConfig.catch(() => { + cachedConfig = null; + }); + } + return cachedConfig; +} + +async function readMergedConfigUncached(): Promise> { + if (DYNAMIC_PAGE_ACCESS) { + await figma.currentPage.loadAsync(); + } + // Filter global to its legit keys so any document-level settings leaked into + // global by an earlier build don't pre-fill this document. + const global = pickGlobalSettings(await readGlobalSettings()); + const doc = readDocumentSettings(); + const page = readPageSettings(figma.currentPage); + return mergeConfig(global, doc, page); +} + +/** + * Persists a partial config across the three storage scopes. + * + * The input is split by scope, merged onto the current persisted state of + * each scope (right-wins so callers can supply partial updates), and the + * combined result is written back. No notification is emitted from here — + * the calling handler decides whether/how to notify subscribers. + */ +export async function writeConfig(partial: Partial): Promise { + invalidateConfigCache(); + const split = splitConfig(partial); + + if (DYNAMIC_PAGE_ACCESS) { + await figma.currentPage.loadAsync(); + } + + const currentGlobal = await readGlobalSettings(); + const currentDoc = readDocumentSettings(); + const currentPage = readPageSettings(figma.currentPage); + + // `pickGlobalSettings` also drops any leaked document-level keys a previous + // build may have written into global, cleaning them up on this save. + const nextGlobal = pickGlobalSettings({ ...currentGlobal, ...split.global }); + const nextDoc = { ...currentDoc, ...split.doc }; + const nextPage = { ...currentPage, ...split.page }; + + await writeGlobalSettings(nextGlobal); + writeDocumentSettings(nextDoc); + writePageSettings(figma.currentPage, nextPage); + + // Invalidate AGAIN, now that the writes have landed. The invalidation at the + // top of this function only clears what was cached before it; every `await` + // since is a window in which another event (a `selectionchange` scan, say) + // can call `readMergedConfig` and cache the pre-write state. That stale + // promise would then survive the save — reverting the form in the UI and + // keeping the old ignore/prefill behaviour until something else happened to + // invalidate. + invalidateConfigCache(); +} + +/** + * Clears persisted state across all three scopes, including every page in the + * current document. Does not emit any events — callers handle notifications. + */ +export async function resetConfig(): Promise { + invalidateConfigCache(); + await deleteGlobalSettings(); + // Empty-object writes delete the underlying pluginData entry (see + // `./storage.ts`). + writeDocumentSettings({}); + + // Iterate every page so settings written under "documentAccess: full" + // (or by older versions of the plugin) don't get left behind. Each page + // must be loaded before its pluginData is mutated under dynamic-page. + for (const page of figma.root.children) { + if (page.type !== "PAGE") continue; + await page.loadAsync(); + writePageSettings(page, {}); + } +} diff --git a/src/main/settings/merge.ts b/src/main/settings/merge.ts new file mode 100644 index 00000000..97639ee8 --- /dev/null +++ b/src/main/settings/merge.ts @@ -0,0 +1,114 @@ +import type { + CurrentDocumentSettings, + CurrentPageSettings, + GlobalSettings, + TolgeeConfig, +} from "$shared/types"; + +/** + * Field allocation across the three storage scopes. Mirrors the split that + * `setPluginData` performed in the legacy `settingsTools.ts`, generalised so + * `splitConfig` can route any field to the correct scope. + * + * Right-wins precedence on read: global < document < page. + */ +const PAGE_KEYS = [ + "language", + "pageInfo", + "pageCopy", + "pageStringDetails", + "nodeInfo", + // Written by `markPageAsCopy` directly (bypassing splitConfig), but listed + // here so any future writeConfig echoing it back routes it to the PAGE + // scope — doc-scoped it would leak onto every other page's merged config. + "sourcePageId", + "copyLanguage", +] as const satisfies ReadonlyArray; + +/** + * The ONLY keys mirrored into global (per-machine) clientStorage — user-level + * credentials + number-ignoring preferences. Everything else is document-scoped + * so it never leaks into other documents (e.g. `keyFormat` / `tags` set in doc + * A must not pre-fill a fresh doc B). Matches the original `setGlobalSettings` + * split exactly (plus `ignoreFormattedNumbers`, the sub-option of ignoreNumbers). + */ +const GLOBAL_KEYS = [ + "apiKey", + "apiUrl", + "ignorePrefix", + "ignoreNumbers", + "ignoreFormattedNumbers", +] as const satisfies ReadonlyArray; + +type PageKey = (typeof PAGE_KEYS)[number]; + +const PAGE_KEY_SET: ReadonlySet = new Set(PAGE_KEYS); +const GLOBAL_KEY_SET: ReadonlySet = new Set(GLOBAL_KEYS); + +/** + * Flat right-wins merge of the three storage levels into a single config view. + * Page values override document values, which override global values. + */ +export function mergeConfig( + global: Partial, + doc: Partial, + page: Partial, +): Partial { + return { ...global, ...doc, ...page }; +} + +/** + * Splits a merged config back into the three storage scopes so each field + * lands in the right place: + * + * - page-only fields (`PAGE_KEYS`) → `page`; + * - the few global preferences (`GLOBAL_KEYS`) → BOTH `global` (cross-machine + * reuse) AND `doc` (so the document stays self-contained); + * - EVERYTHING ELSE → `doc` only, so document-level settings like `keyFormat`, + * `variableCasing`, `tags`, ignore rules etc. never leak into other + * documents via the global merge. Matches the original `setPluginData`. + */ +export function splitConfig(config: Partial): { + global: Partial; + doc: Partial; + page: Partial; +} { + const global: Partial = {}; + const doc: Partial = {}; + const page: Partial = {}; + + // Iterate the input object (not a fixed key list) so unknown/legacy keys are + // still routed — to `doc` (self-contained, no cross-document leak). + for (const key of Object.keys(config) as Array) { + const value = (config as Record)[key]; + if (value === undefined) continue; + + if (PAGE_KEY_SET.has(key)) { + (page as Record)[key as PageKey] = value; + } else if (GLOBAL_KEY_SET.has(key)) { + (global as Record)[key] = value; + (doc as Record)[key] = value; + } else { + (doc as Record)[key] = value; + } + } + + return { global, doc, page }; +} + +/** + * Keep only the fields that legitimately belong in global (per-machine) + * clientStorage. Used on read AND write so any document-level keys a buggy + * earlier build leaked into global are ignored on read and dropped on the next + * save — no stale `keyFormat`/`tags`/… pre-filling fresh documents. + */ +export function pickGlobalSettings( + settings: Partial, +): Partial { + const out: Partial = {}; + for (const key of GLOBAL_KEYS) { + const value = (settings as Record)[key]; + if (value !== undefined) (out as Record)[key] = value; + } + return out; +} diff --git a/src/main/settings/storage.ts b/src/main/settings/storage.ts new file mode 100644 index 00000000..bbbb4a32 --- /dev/null +++ b/src/main/settings/storage.ts @@ -0,0 +1,105 @@ +import { TOLGEE_PLUGIN_CONFIG_NAME } from "$shared/constants"; +import type { CurrentDocumentSettings, CurrentPageSettings, GlobalSettings } from "$shared/types"; + +/** + * Defensive JSON parsing: returns the parsed value if it is a non-null object, + * otherwise an empty object. Never throws. + * + * Falling back to `{}` means the caller silently resets to defaults — API key, + * language, ignore rules and key format all gone. An ABSENT value is the + * ordinary case (a fresh document) and stays quiet; anything present but + * unreadable is a real anomaly and gets logged, so a corrupted or truncated + * `clientStorage`/pluginData entry leaves a trace to explain the reset instead + * of looking like the user's own settings vanishing for no reason. + */ +function safeParseObject(raw: unknown, scope: string): Partial { + if (raw === null || raw === undefined || raw === "") { + return {}; + } + if (typeof raw === "object") { + return raw as Partial; + } + if (typeof raw !== "string") { + console.warn(`[tolgee] ignoring non-string ${scope} settings of type ${typeof raw}`); + return {}; + } + try { + const parsed = JSON.parse(raw) as unknown; + if (parsed && typeof parsed === "object") { + return parsed as Partial; + } + console.warn(`[tolgee] ${scope} settings parsed to a non-object, resetting to defaults`); + return {}; + } catch (err) { + console.warn(`[tolgee] failed to parse stored ${scope} settings, resetting to defaults`, err); + return {}; + } +} + +/** + * Returns `true` when an object has no own enumerable keys. Used to decide + * whether to delete plugin data rather than persist an empty `"{}"` blob. + */ +function isEmptyObject(value: Record): boolean { + for (const _key in value) { + return false; + } + return true; +} + +// --- Global (clientStorage) --------------------------------------------------- + +export async function readGlobalSettings(): Promise> { + // clientStorage may have been written by the legacy plugin as a JSON string, + // or by this code path as a plain object. Handle both transparently. + const raw: unknown = await figma.clientStorage.getAsync(TOLGEE_PLUGIN_CONFIG_NAME); + return safeParseObject(raw, "global"); +} + +export async function writeGlobalSettings(settings: Partial): Promise { + if (isEmptyObject(settings as Record)) { + await figma.clientStorage.deleteAsync(TOLGEE_PLUGIN_CONFIG_NAME); + return; + } + // MUST be a JSON string, not the raw object: the published plugin's reader + // is an unguarded `JSON.parse(value)` at startup (settingsTools.ts), so a + // plain object persisted here would make a ROLLBACK to that version throw + // on launch — on every document, with no way out short of clearing plugin + // storage. Our own reader accepts both shapes; production's does not. + await figma.clientStorage.setAsync(TOLGEE_PLUGIN_CONFIG_NAME, JSON.stringify(settings)); +} + +export async function deleteGlobalSettings(): Promise { + await figma.clientStorage.deleteAsync(TOLGEE_PLUGIN_CONFIG_NAME); +} + +// --- Document (figma.root pluginData) ---------------------------------------- + +export function readDocumentSettings(): Partial { + const raw = figma.root.getPluginData(TOLGEE_PLUGIN_CONFIG_NAME); + return safeParseObject(raw, "document"); +} + +export function writeDocumentSettings(settings: Partial): void { + if (isEmptyObject(settings as Record)) { + // Figma's documented "delete" pattern for pluginData is to write "". + figma.root.setPluginData(TOLGEE_PLUGIN_CONFIG_NAME, ""); + return; + } + figma.root.setPluginData(TOLGEE_PLUGIN_CONFIG_NAME, JSON.stringify(settings)); +} + +// --- Page (PageNode pluginData) ---------------------------------------------- + +export function readPageSettings(page: PageNode): Partial { + const raw = page.getPluginData(TOLGEE_PLUGIN_CONFIG_NAME); + return safeParseObject(raw, "page"); +} + +export function writePageSettings(page: PageNode, settings: Partial): void { + if (isEmptyObject(settings as Record)) { + page.setPluginData(TOLGEE_PLUGIN_CONFIG_NAME, ""); + return; + } + page.setPluginData(TOLGEE_PLUGIN_CONFIG_NAME, JSON.stringify(settings)); +} diff --git a/src/main/text/__tests__/applyRichText.test.ts b/src/main/text/__tests__/applyRichText.test.ts new file mode 100644 index 00000000..94638ed6 --- /dev/null +++ b/src/main/text/__tests__/applyRichText.test.ts @@ -0,0 +1,90 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { __test__, applyRichText } from "../applyRichText"; + +const { findRanges, BR_REGEX } = __test__; + +function makeNode(characters: string, fontName: unknown) { + return { + characters, + autoRename: true, + fontName, + getRangeAllFontNames: vi.fn(() => { + throw new Error("must not read a font range on an empty node"); + }), + }; +} + +afterEach(() => { + (globalThis as unknown as { figma?: unknown }).figma = undefined; +}); + +describe("findRanges", () => { + it("returns empty when the tag is absent", () => { + expect(findRanges("plain text", "b")).toEqual([]); + }); + + it("finds a single bold range and normalises to plain-text offsets", () => { + // "Hello world!" — after stripping `` and ``, the inner + // "world" lives at index 6..11 of the plain text "Hello world!". + expect(findRanges("Hello world!", "b")).toEqual([{ start: 6, end: 11 }]); + }); + + it("finds multiple ranges on the same kind with correct plain-text offsets", () => { + // "bold and bolder" → plain "bold and bolder". + const html = "bold and bolder"; + expect(findRanges(html, "b")).toEqual([ + { start: 0, end: 4 }, + { start: 9, end: 15 }, + ]); + }); + + it("normalises offsets across previously stripped sibling tags", () => { + // "x y" → plain "x y". `` starts at HTML index 9; after + // stripping the prior (3 chars) and (4 chars), the bold range + // sits at plain index 2..3. + const html = "x y"; + expect(findRanges(html, "b")).toEqual([{ start: 2, end: 3 }]); + }); +}); + +describe("BR_REGEX", () => { + it.each([ + ["a
b", "a\nb"], + ["a
b", "a\nb"], + ["a
b", "a\nb"], + ["a
b", "a\nb"], + ["a

b", "a\nb"], + ])("normalises %s", (input, expected) => { + expect(input.replace(BR_REGEX, "\n")).toBe(expected); + }); +}); + +describe("applyRichText — empty node", () => { + it("loads the node's own fontName directly instead of reading a font range", async () => { + const font = { family: "EmptyNodeTestFont", style: "Regular" }; + const loadFontAsync = vi.fn(async () => {}); + (globalThis as unknown as { figma: unknown }).figma = { mixed: Symbol("mixed"), loadFontAsync }; + + const node = makeNode("", font); + await applyRichText(node as never, "Hello"); + + expect(node.getRangeAllFontNames).not.toHaveBeenCalled(); + expect(loadFontAsync).toHaveBeenCalledWith(font); + expect(node.characters).toBe("Hello"); + }); + + it("skips loading entirely when an empty node reports a mixed font", async () => { + const MIXED = Symbol("mixed"); + const loadFontAsync = vi.fn(async () => {}); + (globalThis as unknown as { figma: unknown }).figma = { mixed: MIXED, loadFontAsync }; + + const node = makeNode("", MIXED); + await applyRichText(node as never, "Hello"); + + expect(node.getRangeAllFontNames).not.toHaveBeenCalled(); + expect(loadFontAsync).not.toHaveBeenCalled(); + // Still writes the plain text even though no font could be pre-loaded. + expect(node.characters).toBe("Hello"); + }); +}); diff --git a/src/main/text/applyRichText.ts b/src/main/text/applyRichText.ts new file mode 100644 index 00000000..690bd15a --- /dev/null +++ b/src/main/text/applyRichText.ts @@ -0,0 +1,295 @@ +/** + * Inline rich-text rendering for translated strings. + * + * Tolgee stores formatting hints inline as a tiny subset of HTML: + * + * - `` / `` → bold range + * - `` / `` → italic range + * - `` → underline range + * - `
` / `
` / `
` → newline + * + * When the UI writes the formatted ICU result into a `TextNode`, we need to: + * + * 1. parse out the tag positions, + * 2. write the *plain* text (HTML tags stripped), + * 3. find suitable bold/italic font variants for each range's existing + * family and apply them via `setRangeFontName`, + * 4. apply `setRangeTextDecoration("UNDERLINE")` for `` ranges. + * + * The legacy plugin shipped this in `src/main/endpoints/formatText.ts`. The + * v2 rewrite dropped it, which made every `…` end up literal on the + * canvas after a Pull — a regression for any project that uses inline HTML + * (which is Tolgee's default plural / rich-text convention). + */ + +import { LINE_BREAK_TAG_REGEX, stripMarkupTags } from "$shared/richText"; + +const STRIPPED_TAGS = ["strong", "b", "em", "i", "u"] as const; + +type Range = { start: number; end: number }; + +/** + * Walk every `<${tag}>...` occurrence in `html` and return the + * start/end indices of the *inner content* once every tag listed in + * `STRIPPED_TAGS` has been removed. This is what makes the resulting indices + * line up with `TextNode.characters` after we strip the tags. + */ +function findRanges(html: string, tag: string): Range[] { + const ranges: Range[] = []; + const regex = new RegExp(`<${tag}>(.*?)`, "g"); + + for (const match of html.matchAll(regex)) { + let offset = 0; + let prefix = html.substring(0, match.index); + for (const stripped of STRIPPED_TAGS) { + const r = new RegExp(``, "g"); + for (const m of prefix.matchAll(r)) { + offset += m[0].length; + } + prefix = prefix.replace(r, ""); + } + const inner = match[1] ?? ""; + const start = match.index - offset; + const end = start + inner.length; + ranges.push({ start, end }); + } + return ranges; +} + +const BOLD_STYLE_CANDIDATES = [ + "Bold", + "Semibold", + "Semi Bold", + "Medium", + "Extra Bold", + "Heavy", + "Black", + "Ultra Bold", +] as const; + +const ITALIC_STYLE_CANDIDATES = [ + "Italic", + "Regular Italic", + "Medium Italic", + "Light Italic", + "Semi Bold Italic", + "Semibold Italic", + "Bold Italic", + "Oblique", + "Regular Oblique", +] as const; + +// `listAvailableFontsAsync` serialises the entire system font list across the +// plugin bridge — one of the most expensive Figma API calls. The list can't +// change while the plugin is running, so one in-flight-deduped fetch serves +// every node of a bulk pull instead of one call per formatted node. +let availableFontsPromise: Promise | null = null; + +function loadAvailableFontsOnce(): Promise { + if (!availableFontsPromise) { + availableFontsPromise = figma.listAvailableFontsAsync(); + // Never cache a rejection — one transient failure must not poison every + // formatted-text apply for the rest of the session. + availableFontsPromise.catch(() => { + availableFontsPromise = null; + }); + } + return availableFontsPromise; +} + +// Fonts stay loaded for the plugin's lifetime, so skip the async hop for any +// (family, style) we've already loaded — a 150-node pull typically reuses a +// handful of fonts across every node. Shared with `createCopy`, whose +// language pages rewrite the same font set page after page. +const loadedFonts = new Set(); + +async function loadFontCached(font: FontName): Promise { + const key = `${font.family}\0${font.style}`; + if (loadedFonts.has(key)) return; + await figma.loadFontAsync(font); + loadedFonts.add(key); +} + +/** + * Pick the closest "bold" variant available for `family` and load it. Returns + * the matched style name, or `null` if nothing suitable was found. + */ +async function findBestBoldStyle(family: string, available: Font[]): Promise { + for (const style of BOLD_STYLE_CANDIDATES) { + const exists = available.some( + (f) => f.fontName.family === family && f.fontName.style === style, + ); + if (!exists) continue; + try { + await loadFontCached({ family, style }); + return style; + } catch { + // Some fonts list a style we can't actually load (licensing, missing + // weight file). Try the next candidate. + } + } + const fallback = available.find( + (f) => f.fontName.family === family && f.fontName.style.toLowerCase().includes("bold"), + ); + if (fallback) { + try { + await loadFontCached(fallback.fontName); + return fallback.fontName.style; + } catch { + return null; + } + } + return null; +} + +async function findBestItalicStyle(family: string, available: Font[]): Promise { + for (const style of ITALIC_STYLE_CANDIDATES) { + const exists = available.some( + (f) => f.fontName.family === family && f.fontName.style === style, + ); + if (!exists) continue; + try { + await loadFontCached({ family, style }); + return style; + } catch { + // skip + } + } + const fallback = available.find( + (f) => + f.fontName.family === family && + (f.fontName.style.toLowerCase().includes("italic") || + f.fontName.style.toLowerCase().includes("oblique")), + ); + if (fallback) { + try { + await loadFontCached(fallback.fontName); + return fallback.fontName.style; + } catch { + return null; + } + } + return null; +} + +/** Take the font of `range.start`, normalising over the "mixed" sentinel. */ +function getFontAtRange(node: TextNode, range: Range): FontName | null { + const len = node.characters.length; + if (range.start >= len) return null; + const probe = node.getRangeFontName(range.start, Math.min(len, range.start + 1)); + if (probe === figma.mixed) return null; + return probe as FontName; +} + +function applyFontRange(node: TextNode, range: Range, font: FontName): void { + const end = Math.min(node.characters.length, range.end); + if (range.start >= end) return; + node.setRangeFontName(range.start, end, font); +} + +function applyUnderlineRange(node: TextNode, range: Range): void { + const end = Math.min(node.characters.length, range.end); + if (range.start >= end) return; + node.setRangeTextDecoration(range.start, end, "UNDERLINE"); +} + +export type ApplyRichTextOptions = { + /** + * Skip rich-text styling and just write plain characters. Useful for tests + * that mock a minimal `figma` global without `setRangeFontName` etc. + */ + plainOnly?: boolean; +}; + +/** + * Parse `formatted` (Tolgee's tagged ICU result), write the plain text into + * `node.characters`, and apply bold/italic/underline ranges to mirror the + * source markup. + * + * Pre-loads every distinct font that already lives on the node so that + * assigning `characters` doesn't throw, then walks the per-tag ranges and + * resolves the closest bold/italic variant of each range's family. Underlines + * are applied last so they stack cleanly with the font-name updates. + */ +export async function applyRichText( + node: TextNode, + formatted: string, + options: ApplyRichTextOptions = {}, +): Promise { + node.autoRename = false; + + // Pre-load every font already used on the node — required before we can + // assign `characters` or call any of the `setRange*` methods. The range + // APIs throw on an empty node ((0,0) is out of bounds, same as (0,1)), so + // a never-rendered node loads its single `fontName` directly instead. + const length = node.characters.length; + if (length === 0) { + const font = node.fontName; + if (font !== figma.mixed) await loadFontCached(font); + } else { + const existingFonts = node.getRangeAllFontNames(0, length); + await Promise.all(existingFonts.map((f) => loadFontCached(f))); + } + + // The BR replace + tag strip MUST stay the shared `plainCanvasText` + // transform (see $shared/richText) — pullDiff compares canvas characters + // against it to detect drift. `withoutBreaks` is kept separately because + // the range finder below needs the tags still in place. + const withoutBreaks = formatted.replace(LINE_BREAK_TAG_REGEX, "\n"); + const plainText = stripMarkupTags(withoutBreaks); + node.characters = plainText; + + if (options.plainOnly) return; + + const boldRanges = [...findRanges(withoutBreaks, "strong"), ...findRanges(withoutBreaks, "b")]; + const italicRanges = [...findRanges(withoutBreaks, "em"), ...findRanges(withoutBreaks, "i")]; + const underlineRanges = findRanges(withoutBreaks, "u"); + + // Fast path: nothing to format, all done. + if (boldRanges.length === 0 && italicRanges.length === 0 && underlineRanges.length === 0) { + return; + } + + const available = await loadAvailableFontsOnce(); + + // Resolve every distinct (family, kind) pair we'll need up front so we + // never call `loadFontAsync` for the same target twice — large pluralised + // strings can re-use the same family across many ranges. + const boldStyleByFamily = new Map(); + const italicStyleByFamily = new Map(); + + for (const range of boldRanges) { + const font = getFontAtRange(node, range); + if (!font || boldStyleByFamily.has(font.family)) continue; + boldStyleByFamily.set(font.family, await findBestBoldStyle(font.family, available)); + } + for (const range of italicRanges) { + const font = getFontAtRange(node, range); + if (!font || italicStyleByFamily.has(font.family)) continue; + italicStyleByFamily.set(font.family, await findBestItalicStyle(font.family, available)); + } + + for (const range of boldRanges) { + const font = getFontAtRange(node, range); + if (!font) continue; + const style = boldStyleByFamily.get(font.family); + if (style) { + applyFontRange(node, range, { family: font.family, style }); + } + // No bold variant: leave the existing font in place. + } + for (const range of italicRanges) { + const font = getFontAtRange(node, range); + if (!font) continue; + const style = italicStyleByFamily.get(font.family); + if (style) { + applyFontRange(node, range, { family: font.family, style }); + } + } + for (const range of underlineRanges) { + applyUnderlineRange(node, range); + } +} + +// Exported for tests. +export const __test__ = { findRanges, BR_REGEX: LINE_BREAK_TAG_REGEX }; diff --git a/src/main/utils/createEndpoint.ts b/src/main/utils/createEndpoint.ts deleted file mode 100644 index 16c950d0..00000000 --- a/src/main/utils/createEndpoint.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { emit, on, once } from "@/utilities"; - -export const createEndpoint = ( - name: string, - fn: ((input: I) => Promise) | ((input: I) => O), -) => { - let endpointRequestCounter = 0; - let implementation = fn; - const self = { - register: () => { - on(`${name}_IN`, async (data) => { - const result = await implementation(data); - emit(`${name}_${data._requestId}_OUT`, result); - }); - }, - call: (input: I) => { - const _requestId = endpointRequestCounter++; - const result = new Promise((resolve) => { - once(`${name}_${_requestId}_OUT`, resolve); - }); - emit(`${name}_IN`, { ...input, _requestId }); - return result; - }, - mock: (fn: ((input: I) => Promise) | ((input: I) => O)) => { - implementation = fn; - self.register(); - return self; - }, - name, - }; - return self; -}; diff --git a/src/main/utils/delayed.ts b/src/main/utils/delayed.ts deleted file mode 100644 index c221bb7a..00000000 --- a/src/main/utils/delayed.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const delayed = - (promise: (props: P) => Promise) => - (props: P) => - new Promise((resolve, reject) => - setTimeout(() => promise(props).then(resolve).catch(reject), 10), - ); diff --git a/src/main/utils/nodeParents.ts b/src/main/utils/nodeParents.ts deleted file mode 100644 index bc77dd4c..00000000 --- a/src/main/utils/nodeParents.ts +++ /dev/null @@ -1,96 +0,0 @@ -export type NodeParents = { - element: BaseNode | undefined; - frame: FrameNode | undefined; - artboard: FrameNode | undefined; - component: ComponentNode | ComponentSetNode | undefined; - section: SectionNode | undefined; - group: GroupNode | undefined; -}; - -/** - * Single traversal to collect all relevant parent types at once. - */ -export function getAllParents(nodeId: string): NodeParents { - const result: NodeParents = { - element: undefined, - frame: undefined, - artboard: undefined, - component: undefined, - section: undefined, - group: undefined, - }; - - const realNode = figma.getNodeById(nodeId); - if (!realNode) return result; - - result.element = realNode; - - let parent = realNode.parent; - while (parent) { - if (parent.type === "FRAME") { - if (!result.frame) result.frame = parent as FrameNode; - result.artboard = parent as FrameNode; - } else if ( - (parent.type === "COMPONENT" || parent.type === "COMPONENT_SET") && - !result.component - ) { - result.component = parent; - } else if (parent.type === "INSTANCE" && !result.component) { - // For an instance, resolve the main component it was created from. - // If that component is a variant inside a component set, use the set - // so the key gets the recognizable component name (e.g. "Button"). - const mainComponent = parent.mainComponent; - if (mainComponent) { - result.component = - mainComponent.parent?.type === "COMPONENT_SET" - ? (mainComponent.parent as ComponentSetNode) - : mainComponent; - } - } else if (parent.type === "SECTION" && !result.section) { - result.section = parent; - } else if (parent.type === "GROUP" && !result.group) { - result.group = parent as GroupNode; - } - parent = parent.parent; - } - - return result; -} - -/** - * Returns the nearest frame to the node. - */ -export function getFrame(nodeId: string): FrameNode | undefined { - return getAllParents(nodeId).frame; -} -/** - * Returns the topmost frame in the node's hierarchy. - */ -export function getArtboard(nodeId: string): FrameNode | undefined { - return getAllParents(nodeId).artboard; -} - -/** - * Returns the component name if the node is inside a component. - */ -export function getComponent( - nodeId: string, -): ComponentNode | ComponentSetNode | undefined { - return getAllParents(nodeId).component; -} - -/** - * Returns the section name based on parent group/frame naming. - */ -export function getSection(nodeId: string): SectionNode | undefined { - return getAllParents(nodeId).section; -} - -export function getGroup(nodeId: string): GroupNode | undefined { - return getAllParents(nodeId).group; -} - -export function getElement(nodeId: string): BaseNode | undefined { - const realNode = figma.getNodeById(nodeId); - return realNode ?? undefined; -} diff --git a/src/main/utils/nodeTools.ts b/src/main/utils/nodeTools.ts deleted file mode 100644 index c0b3169e..00000000 --- a/src/main/utils/nodeTools.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { TOLGEE_NODE_INFO } from "@/constants"; -import { CurrentDocumentSettings, NodeInfo } from "@/types"; -import { getDocumentData } from "./settingsTools"; - -export const getNodeInfo = (node: TextNode): NodeInfo => { - const pluginData = JSON.parse( - node.getPluginData(TOLGEE_NODE_INFO) || "{}", - ) as Partial; - return { - id: node.id, - name: node.name, - characters: node.characters, - key: pluginData.key || "", - isPlural: Boolean(pluginData.isPlural), - pluralParamValue: pluginData.pluralParamValue, - translation: pluginData.translation || "", - paramsValues: pluginData.paramsValues, - ns: pluginData.ns, - connected: Boolean(pluginData.connected), - visible: Boolean(pluginData.visible), - }; -}; - -function shouldIncludeNode( - node: TextNode, - settings: Partial, -) { - if ( - (settings.ignoreNumbers || typeof settings.ignoreNumbers === "undefined") && - /^\d+$/.test(node.characters) - ) { - return false; - } - if ( - (settings.ignoreHiddenLayers || - typeof settings.ignoreHiddenLayers === "undefined") && - !node.visible - ) { - return false; - } - if ( - settings.ignoreTextLayers && - settings.ignorePrefix && - node.name.startsWith(settings.ignorePrefix) - ) { - return false; - } - if (node.characters.trim().length === 0) { - return false; - } - return true; -} - -// `settings` and `ancestorHidden` are threaded through recursion so we read -// document data once and skip hidden subtrees up front, instead of walking -// each text node's parents via the (expensive) plugin bridge. -export const findTextNodes = ( - nodes: readonly SceneNode[], - settings: Partial = getDocumentData(), - ancestorHidden = false, -): TextNode[] => { - const respectVisibility = - settings.ignoreHiddenLayers || - typeof settings.ignoreHiddenLayers === "undefined"; - const skipHiddenSubtrees = - respectVisibility && !!settings.ignoreHiddenLayersIncludingChildren; - - const result: TextNode[] = []; - for (const node of nodes) { - const nodeHidden = - respectVisibility && "visible" in node && !(node as SceneNode).visible; - const subtreeHidden = ancestorHidden || nodeHidden; - - if (skipHiddenSubtrees && subtreeHidden) { - continue; - } - - if (node.type === "TEXT") { - if (shouldIncludeNode(node, settings)) { - result.push(node); - } - } - // @ts-ignore - if (node.children) { - result.push( - // @ts-ignore - ...findTextNodes(node.children as SceneNode[], settings, subtreeHidden), - ); - } - } - return result; -}; - -export const findTextNodesInfo = (nodes: readonly SceneNode[]): NodeInfo[] => { - return findTextNodes(nodes).map(getNodeInfo); -}; - -export const findLastParentFrame = (subject: BaseNode) => { - let node = subject as (BaseNode & ChildrenMixin) | null; - let lastFrame: FrameNode | undefined; - while (node) { - if (node.type === "FRAME") { - lastFrame = node; - } - node = node.parent; - } - return lastFrame; -}; diff --git a/src/main/utils/pages.ts b/src/main/utils/pages.ts deleted file mode 100644 index f0834167..00000000 --- a/src/main/utils/pages.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { TOLGEE_PLUGIN_CONFIG_NAME } from "@/constants"; -import { CurrentPageSettings } from "@/types"; - -export const getAllPages = () => { - const document = figma.root; - return document.children.filter((node) => node.type === "PAGE"); -}; - -export const getPageData = (page = figma.currentPage) => { - const pluginData = page.getPluginData(TOLGEE_PLUGIN_CONFIG_NAME); - return pluginData - ? (JSON.parse(pluginData) as Partial) - : {}; -}; - -export const setPageData = ( - data: Partial, - page = figma.currentPage, -) => { - page.setPluginData(TOLGEE_PLUGIN_CONFIG_NAME, JSON.stringify(data)); -}; diff --git a/src/main/utils/settingsTools.ts b/src/main/utils/settingsTools.ts deleted file mode 100644 index d00be799..00000000 --- a/src/main/utils/settingsTools.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { TOLGEE_PLUGIN_CONFIG_NAME } from "@/constants"; -import { - ConfigChangeHandler, - CurrentDocumentSettings, - CurrentPageSettings, - GlobalSettings, - TolgeeConfig, -} from "@/types"; -import { emit } from "@create-figma-plugin/utilities"; -import { setPageData } from "./pages"; - -const getGlobalSettings = async (): Promise> => { - const pluginData = await figma.clientStorage.getAsync( - TOLGEE_PLUGIN_CONFIG_NAME, - ); - if (!pluginData) return {}; - // clientStorage may return an already-parsed object (Figma auto-parses JSON - // strings, or old data was stored without JSON.stringify). Handle both. - if (typeof pluginData === "string") { - try { - return JSON.parse(pluginData) as Partial; - } catch { - return {}; - } - } - return pluginData as Partial; -}; - -const setGlobalSettings = async (data: Partial) => { - await figma.clientStorage.setAsync(TOLGEE_PLUGIN_CONFIG_NAME, data); -}; - -export const deleteGlobalSettings = async () => { - await figma.clientStorage.deleteAsync(TOLGEE_PLUGIN_CONFIG_NAME); -}; - -export const getDocumentData = (): Partial => { - const pluginData = figma.root.getPluginData(TOLGEE_PLUGIN_CONFIG_NAME); - const result = pluginData - ? (JSON.parse(pluginData) as Partial) - : {}; - - return { - ignorePrefix: "_", - ignoreNumbers: true, - ...result, - }; -}; - -const setDocumentData = (data: Partial) => { - figma.root.setPluginData(TOLGEE_PLUGIN_CONFIG_NAME, JSON.stringify(data)); -}; - -export const deleteDocumentData = () => { - figma.root.setPluginData(TOLGEE_PLUGIN_CONFIG_NAME, ""); -}; - -const getPageData = (page = figma.currentPage) => { - const pluginData = page.getPluginData(TOLGEE_PLUGIN_CONFIG_NAME); - return pluginData - ? (JSON.parse(pluginData) as Partial) - : {}; -}; - -export const deletePageData = (page = figma.currentPage) => { - page.setPluginData(TOLGEE_PLUGIN_CONFIG_NAME, ""); -}; - -export const getPluginData = async () => { - return { - ...(await getGlobalSettings()), - ...getDocumentData(), - ...getPageData(), - }; -}; - -export const setPluginData = async (data: Partial) => { - const { - addTags, - apiKey, - apiUrl, - branch, - ignoreHiddenLayers, - ignoreHiddenLayersIncludingChildren, - ignoreNumbers, - ignorePrefix, - ignoreTextLayers, - keyFormat, - language, - namespace, - prefillKeyFormat, - tags, - updateScreenshots, - variableCasing, - } = data; - await setGlobalSettings({ apiKey, apiUrl, ignorePrefix, ignoreNumbers }); - setDocumentData({ - addTags, - apiKey, - apiUrl, - branch, - documentInfo: true, - ignoreHiddenLayers, - ignoreHiddenLayersIncludingChildren, - ignoreNumbers, - ignorePrefix, - ignoreTextLayers, - keyFormat, - namespace, - prefillKeyFormat, - tags, - updateScreenshots, - variableCasing, - }); - setPageData({ language, pageInfo: true }); - emit("CONFIG_CHANGE", await getPluginData()); -}; diff --git a/src/main/utils/textFormattingTools.ts b/src/main/utils/textFormattingTools.ts deleted file mode 100644 index 619cd42e..00000000 --- a/src/main/utils/textFormattingTools.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Replaces newlines and html tags so that the text of a figma node can be compared to a translation. - */ -export const stringFormatter = ( - str: string, - args: { - replaceHtmlTags: boolean; - replaceNewlines: boolean; - } = { - replaceHtmlTags: true, - replaceNewlines: true, - }, -) => { - let newString = str; - if (args.replaceHtmlTags) { - newString = newString.replace(/<[^>]*>/g, ""); - } - if (args.replaceNewlines) { - newString = newString.replace(/\u2028|\u2029/g, "\n\n"); - } - return newString; -}; diff --git a/src/shared/__tests__/icu.test.ts b/src/shared/__tests__/icu.test.ts new file mode 100644 index 00000000..ff7c6a6b --- /dev/null +++ b/src/shared/__tests__/icu.test.ts @@ -0,0 +1,77 @@ +import { formatIcuMessage } from "$shared/icu"; +import { describe, expect, it } from "vitest"; + +describe("formatIcuMessage", () => { + it("returns plain text unchanged when there are no placeholders and no params", () => { + const out = formatIcuMessage("Hello world", {}, "en"); + expect(out.result).toBe("Hello world"); + expect(out.error).toBeNull(); + }); + + it("still formats a message containing braces even with empty params (H10 fast-path fix)", () => { + // A message with real, unfilled placeholders and no params can't be + // formatted — this is a genuine caller error, not the "plain ICU-escaped + // text" case the guard is for. It must fail LOUD (error populated), not + // silently short-circuit like the old fast path did. + const out = formatIcuMessage("{name} likes {fruit}", {}, "en"); + expect(out.result).toBe("{name} likes {fruit}"); + expect(out.error).not.toBeNull(); + }); + + it("unescapes Tolgee's own ICU escaping even with no params (H10 regression)", () => { + // Tolgee escapes literal braces/apostrophes as `'{'`/`'}'`/`''` in the + // stored source. The old fast path (short-circuit on empty params) + // returned this escaped source untouched, writing it verbatim to the + // canvas instead of the intended literal text. + const out = formatIcuMessage("It''s a '{'test'}'", {}, "en"); + expect(out.result).toBe("It's a {test}"); + expect(out.error).toBeNull(); + }); + + it("returns plain text with a brace unchanged in effect after unescaping", () => { + const out = formatIcuMessage("100 '{'", {}, "en"); + expect(out.result).toBe("100 {"); + expect(out.error).toBeNull(); + }); + + it("skips the parser entirely for text with no apostrophe or brace (guard fast path)", () => { + const out = formatIcuMessage("Hello world", {}, "en"); + expect(out.result).toBe("Hello world"); + expect(out.error).toBeNull(); + }); + + it("substitutes a single {name} placeholder", () => { + const out = formatIcuMessage("Hello, {name}!", { name: "Alice" }, "en"); + expect(out.result).toBe("Hello, Alice!"); + expect(out.error).toBeNull(); + }); + + it("formats an ICU plural with count=5 to '5 items'", () => { + const out = formatIcuMessage( + "{count, plural, one {1 item} other {# items}}", + { count: "5" }, + "en", + ); + expect(out.result).toBe("5 items"); + expect(out.error).toBeNull(); + }); + + it("formats an ICU plural with count=1 to '1 item'", () => { + const out = formatIcuMessage( + "{count, plural, one {1 item} other {# items}}", + { count: "1" }, + "en", + ); + expect(out.result).toBe("1 item"); + expect(out.error).toBeNull(); + }); + + it("falls back to the raw message and sets error on malformed ICU", () => { + // Missing closing brace — intl-messageformat throws on parse. + const malformed = "Hello {name"; + const out = formatIcuMessage(malformed, { name: "Alice" }, "en"); + expect(out.result).toBe(malformed); + expect(out.error).not.toBeNull(); + expect(out.error).toBeInstanceOf(Error); + }); +}); diff --git a/src/shared/__tests__/interpolate.test.ts b/src/shared/__tests__/interpolate.test.ts new file mode 100644 index 00000000..3bad18c5 --- /dev/null +++ b/src/shared/__tests__/interpolate.test.ts @@ -0,0 +1,209 @@ +import type { NodeInfo } from "$shared/types"; +import { describe, expect, it } from "vitest"; +import { + inferPluralCount, + interpolatedText, + isSimpleNode, + renderIcuForNode, + renderParams, + translationDiffersFromNode, +} from "$shared/interpolate"; + +const PLURAL = "{count, plural, one {# item} other {# items}}"; + +function makeNode(overrides: Partial = {}): NodeInfo { + return { + id: "n", + name: "Layer", + characters: "", + translation: "", + isPlural: false, + key: "k", + ns: undefined, + connected: true, + ...overrides, + } as NodeInfo; +} + +describe("renderIcuForNode", () => { + it("renders a plural with the count from a numeric pluralParamValue", () => { + const node = makeNode({ isPlural: true, pluralParamValue: "10" }); + expect(renderIcuForNode(PLURAL, node, "en").text).toBe("10 items"); + }); + + it("prefers a named sample in paramsValues over pluralParamValue", () => { + const node = makeNode({ isPlural: true, pluralParamValue: "10", paramsValues: { count: "1" } }); + expect(renderIcuForNode(PLURAL, node, "en").text).toBe("1 item"); + }); + + it("defaults the count to 1 and never injects a non-numeric value", () => { + const node = makeNode({ isPlural: true, pluralParamValue: "value" }); + expect(renderIcuForNode(PLURAL, node, "en").text).toBe("1 item"); + }); + + it("returns the raw text and an error on malformed ICU", () => { + const node = makeNode({ paramsValues: { name: "x" } }); + const out = renderIcuForNode("Hello {name", node, "en"); + expect(out.text).toBe("Hello {name"); + expect(out.error).toBeInstanceOf(Error); + }); + + it("overrides a NON-NUMERIC plural sample instead of rendering NaN", () => { + // Old node data can carry the plural arg's NAME as its own sample + // (paramsValues: {value: "value"}) — IntlMessageFormat doesn't throw on + // that, it silently coerces to NaN ("Mám NaN jablek" on canvas). The + // numeric fallback chain must kick in: pluralParamValue, then "1". + const CS = + "{value, plural, one {Mám # jablko} few {Mám # jablka} many {Mám # jablka} other {Mám # jablek}}"; + const poisoned = makeNode({ paramsValues: { value: "value" }, pluralParamValue: "9" }); + expect(renderIcuForNode(CS, poisoned, "cs").text).toBe("Mám 9 jablek"); + + const emptySample = makeNode({ paramsValues: { value: "" } }); + expect(renderIcuForNode(CS, emptySample, "cs").text).toBe("Mám 1 jablko"); + }); + + it("renders a plural even when isPlural is false — the ICU's own structure wins", () => { + // Real-world case: Tolgee's key-level "isPlural" setting disagreed with + // the translation's actual ICU shape (typed as a plural without the key + // ever being marked plural). Trusting the flag meant no sample was ever + // seeded, IntlMessageFormat threw on the missing argument, and the whole + // raw pattern landed on the canvas verbatim instead of rendering. + const node = makeNode({ isPlural: false, pluralParamValue: "9" }); + const out = renderIcuForNode( + "{value, plural, one {J'ai # pomme} many {J'ai # pommes} other {J'ai # pommes}}", + node, + "fr", + ); + expect(out.error).toBeUndefined(); + expect(out.text).toBe("J'ai 9 pommes"); + }); +}); + +describe("isSimpleNode", () => { + it("is true for a plain string with no params/markup", () => { + expect(isSimpleNode(makeNode({ translation: "Hello" }))).toBe(true); + }); + it("is false for a plural / params / markup", () => { + expect(isSimpleNode(makeNode({ isPlural: true }))).toBe(false); + expect(isSimpleNode(makeNode({ paramsValues: { a: "1" } }))).toBe(false); + expect(isSimpleNode(makeNode({ translation: "Hi" }))).toBe(false); + }); +}); + +describe("translationDiffersFromNode (manual change)", () => { + it("flags a plural whose canvas was edited away from its rendered form", () => { + const node = makeNode({ + isPlural: true, + translation: PLURAL, + pluralParamValue: "10", + characters: "10 kittens", // edited on canvas, should be "10 items" + }); + expect(translationDiffersFromNode(node, "en")).toBe(true); + }); + + it("does NOT flag a plural whose canvas matches its rendered form", () => { + const node = makeNode({ + isPlural: true, + translation: PLURAL, + pluralParamValue: "10", + characters: "10 items", + }); + expect(translationDiffersFromNode(node, "en")).toBe(false); + }); + + it("does NOT flag a simple (plain) string", () => { + const node = makeNode({ translation: "Hello", characters: "Hello, world (edited)" }); + expect(translationDiffersFromNode(node, "en")).toBe(false); + }); + + it("ignores inline markup when comparing", () => { + const node = makeNode({ translation: "Hi", characters: "Hi" }); + expect(translationDiffersFromNode(node, "en")).toBe(false); + }); +}); + +describe("inferPluralCount", () => { + it("finds the count that renders the canvas form", () => { + expect(inferPluralCount(PLURAL, "5 items", "en")).toBe("5"); + expect(inferPluralCount(PLURAL, "1 item", "en")).toBe("1"); + }); + it("returns undefined when nothing matches or it isn't a plural", () => { + expect(inferPluralCount(PLURAL, "totally different", "en")).toBeUndefined(); + expect(inferPluralCount("Just a string", "Just a string", "en")).toBeUndefined(); + }); +}); + +describe("interpolatedText", () => { + it("renders the stored translation for an advanced node", () => { + const node = makeNode({ isPlural: true, translation: PLURAL, pluralParamValue: "2" }); + expect(interpolatedText(node, "en").text).toBe("2 items"); + }); + it("uses raw characters for a simple node", () => { + const node = makeNode({ translation: "Hello", characters: "Hello" }); + expect(interpolatedText(node, "en").text).toBe("Hello"); + }); +}); + +describe("plural embedded in surrounding text", () => { + // Regression: the plural NAME used to come from `getTolgeeFormat`, which only + // recognises a plural that is the WHOLE string. A plural with any literal + // text around it — a very ordinary shape — left `count` unseeded, so + // `IntlMessageFormat` threw MISSING_VALUE: the node was dropped in + // `copyApply` and Pull kept the stale canvas text. + const TRAILING = "{count, plural, one {# day} other {# days}} left"; + const LEADING = "Only {count, plural, one {# day} other {# days}}"; + + it("renders a plural followed by literal text", () => { + const node = makeNode({ isPlural: true, pluralParamValue: "10" }); + const out = renderIcuForNode(TRAILING, node, "en"); + expect(out.error).toBeUndefined(); + expect(out.text).toBe("10 days left"); + }); + + it("renders a plural preceded by literal text", () => { + const node = makeNode({ isPlural: true, pluralParamValue: "1" }); + const out = renderIcuForNode(LEADING, node, "en"); + expect(out.error).toBeUndefined(); + expect(out.text).toBe("Only 1 day"); + }); + + it("renders a plural nested inside a select — BOTH arguments get seeded", () => { + // This used to be asserted on the params alone, because the outer + // `select`'s own `gender` argument was invisible to the placeholder regex + // (no bounded `[^{}]*` tail when the body holds braces) and the full + // render therefore threw MISSING_VALUE on it. Both are seeded now. + const icu = "{gender, select, other {{count, plural, one {# reply} other {# replies}}}}"; + const node = makeNode({ isPlural: true, pluralParamValue: "5" }); + + const params = renderParams(icu, node); + expect(params.count).toBe("5"); + expect(params.gender).toBe("gender"); + + const out = renderIcuForNode(icu, node, "en"); + expect(out.error).toBeUndefined(); + expect(out.text).toBe("5 replies"); + }); + + it("seeds a plain select so it renders instead of throwing", () => { + const icu = "{gender, select, female {her} male {his} other {their}} bag"; + const node = makeNode({ paramsValues: { gender: "female" } }); + + const out = renderIcuForNode(icu, node, "en"); + expect(out.error).toBeUndefined(); + expect(out.text).toBe("her bag"); + }); + + it("does not treat an ICU-escaped brace as a plural argument", () => { + // `'{'count, plural…` is literal text, not an argument — seeding a sample + // for it would be inventing a parameter that isn't there. + const node = makeNode({ isPlural: false }); + const out = renderIcuForNode("literal '{'count, plural'}' here", node, "en"); + expect(out.error).toBeUndefined(); + expect(out.text).toBe("literal {count, plural} here"); + }); + + it("infers the sample count for an embedded plural too", () => { + expect(inferPluralCount(TRAILING, "3 days left", "en")).toBe("3"); + expect(inferPluralCount(TRAILING, "1 day left", "en")).toBe("1"); + }); +}); diff --git a/src/shared/__tests__/keyFormat.test.ts b/src/shared/__tests__/keyFormat.test.ts new file mode 100644 index 00000000..5882e8d5 --- /dev/null +++ b/src/shared/__tests__/keyFormat.test.ts @@ -0,0 +1,248 @@ +import { applyCasing, formatKey } from "$shared/keyFormat"; +import type { GlobalSettings } from "$shared/types"; +import { describe, expect, it } from "vitest"; + +type Casing = NonNullable; + +describe("applyCasing", () => { + it("produces snake_case", () => { + expect(applyCasing("My Frame Title", "snake_case")).toBe("my_frame_title"); + }); + + it("produces snake_case_capitalized (capitalises every word, joined by '_')", () => { + // The implementation uppercases the first char of every word and keeps + // the rest of the word as-is — it does NOT lowercase the tail. + expect(applyCasing("my frame title", "snake_case_capitalized")).toBe("My_Frame_Title"); + // Already-mixed casing in the tail is preserved. + expect(applyCasing("hello WORLD", "snake_case_capitalized")).toBe("Hello_WORLD"); + }); + + it("produces camelCase", () => { + expect(applyCasing("My Frame Title", "camelCase")).toBe("myFrameTitle"); + }); + + it("produces PascalCase", () => { + expect(applyCasing("my frame title", "PascalCase")).toBe("MyFrameTitle"); + }); + + it("produces noSpaces (preserves casing, strips whitespace only)", () => { + expect(applyCasing("My Frame Title", "noSpaces")).toBe("MyFrameTitle"); + }); + + it("keeps the original format for \"\" / undefined (no transformation)", () => { + // "keep original format" — the default, matching the original plugin. Must + // NOT snake_case, or configs that kept the original would be transformed. + expect(applyCasing("My Frame Title", "")).toBe("My Frame Title"); + expect(applyCasing("My Frame Title", undefined)).toBe("My Frame Title"); + }); + + it("returns empty string for empty input", () => { + const casings: Casing[] = [ + "snake_case", + "snake_case_capitalized", + "camelCase", + "PascalCase", + "noSpaces", + ]; + for (const casing of casings) { + expect(applyCasing("", casing)).toBe(""); + } + }); + + it("preserves special chars like '/', '.', '-' inside a word", () => { + expect(applyCasing("foo/bar.baz-qux", "snake_case")).toBe("foo/bar.baz-qux"); + expect(applyCasing("foo/bar.baz-qux", "camelCase")).toBe("foo/bar.baz-qux"); + // PascalCase uppercases the first char; the rest is preserved. + expect(applyCasing("foo/bar.baz-qux", "PascalCase")).toBe("Foo/bar.baz-qux"); + }); + + it("handles multi-word input with special chars", () => { + expect(applyCasing("My/Frame Element-1", "snake_case")).toBe("my/frame_element-1"); + expect(applyCasing("My/Frame Element-1", "camelCase")).toBe("my/frameElement-1"); + }); +}); + +describe("formatKey", () => { + it("substitutes {frame}_{elementName} for snake_case", () => { + expect( + formatKey( + "{frame}_{elementName}", + { frame: "My Frame", elementName: "my element" }, + "snake_case", + ), + ).toBe("my_frame_my_element"); + }); + + it("substitutes {frame}_{elementName} for snake_case_capitalized", () => { + // Every word's first char is uppercased; the rest is preserved verbatim. + expect( + formatKey( + "{frame}_{elementName}", + { frame: "My Frame", elementName: "my element" }, + "snake_case_capitalized", + ), + ).toBe("My_Frame_My_Element"); + }); + + it("substitutes {frame}_{elementName} for camelCase", () => { + expect( + formatKey( + "{frame}_{elementName}", + { frame: "My Frame", elementName: "my element" }, + "camelCase", + ), + ).toBe("myFrame_myElement"); + }); + + it("substitutes {frame}_{elementName} for PascalCase", () => { + expect( + formatKey( + "{frame}_{elementName}", + { frame: "My Frame", elementName: "my element" }, + "PascalCase", + ), + ).toBe("MyFrame_MyElement"); + }); + + it("substitutes {frame}_{elementName} for noSpaces", () => { + expect( + formatKey( + "{frame}_{elementName}", + { frame: "My Frame", elementName: "my element" }, + "noSpaces", + ), + ).toBe("MyFrame_myelement"); + }); + + it("returns empty result for empty template", () => { + expect(formatKey("", { frame: "Anything", elementName: "Whatever" }, "snake_case")).toBe(""); + }); + + it("collapses to empty when every placeholder value is missing", () => { + // Both empty → the placeholders AND the separator between them are removed, + // rather than leaving a bare "_". + expect(formatKey("{frame}_{elementName}", {}, "snake_case")).toBe(""); + }); + + it("drops the separator BEFORE an empty middle placeholder", () => { + expect( + formatKey( + "{frame}.{component}.{elementName}", + { frame: "Hero", elementName: "title" }, + "snake_case", + ), + ).toBe("hero.title"); + }); + + it("drops the separator AFTER a leading empty placeholder", () => { + // No component → no dangling leading dot. + expect( + formatKey("{component}.{elementName}", { elementName: "log in" }, "snake_case"), + ).toBe("log_in"); + }); + + it("drops the separator BEFORE a trailing empty placeholder", () => { + expect( + formatKey("{elementName}.{component}", { elementName: "log in" }, "snake_case"), + ).toBe("log_in"); + }); + + it("collapses consecutive empty placeholders and their separators", () => { + expect( + formatKey( + "{component}.{instance}.{elementName}", + { elementName: "title" }, + "snake_case", + ), + ).toBe("title"); + }); + + it("keeps a literal prefix when the following placeholder is empty", () => { + expect( + formatKey("app.{component}.{elementName}", { elementName: "title" }, "snake_case"), + ).toBe("app.title"); + }); + + it("substitutes the {instance} placeholder", () => { + expect( + formatKey( + "{component}.{instance}.{elementName}", + { component: "Button", instance: "Primary Button", elementName: "label" }, + "snake_case", + ), + ).toBe("button.primary_button.label"); + }); + + it("preserves special chars '/', '.', '-' in placeholder values", () => { + expect( + formatKey( + "{frame}/{elementName}", + { frame: "foo.bar", elementName: "baz-qux" }, + "snake_case", + ), + ).toBe("foo.bar/baz-qux"); + }); + + it("substitutes only the placeholders present in the template", () => { + expect( + formatKey("{section}.{group}", { section: "Header", group: "Buttons" }, "PascalCase"), + ).toBe("Header.Buttons"); + }); + + it("leaves unknown placeholders untouched", () => { + // The implementation only knows the documented placeholders; an unknown + // `{unknown}` must pass through verbatim. + expect(formatKey("{unknown}_{frame}", { frame: "Hero" }, "snake_case")).toBe("{unknown}_hero"); + }); +}); + +describe("formatKey — an empty placeholder trims a separator, not a literal", () => { + it("keeps fixed text that sits between two placeholders", () => { + // The reported bug: the whole adjacent literal was dropped, so `bar` + // vanished from a key that then got persisted and uploaded. Only the + // separator should go — and `bar` isn't one. + expect( + formatKey("foo{component}bar{elementName}", { elementName: "Name" }, "snake_case"), + ).toBe("foobarname"); + }); + + it("keeps a fixed prefix ahead of the empty placeholder", () => { + // `app.` is the user's own prefix, not a separator to be eaten. + expect( + formatKey("app.{component}.{elementName}", { elementName: "Title" }, "snake_case"), + ).toBe("app.title"); + }); + + it("still drops a leading separator so the key can't start with one", () => { + expect( + formatKey("{component}.{elementName}", { elementName: "my element" }, "snake_case"), + ).toBe("my_element"); + }); + + it("still collapses a separator between two present placeholders", () => { + expect( + formatKey( + "{frame}.{component}.{elementName}", + { frame: "Web", elementName: "Title" }, + "snake_case", + ), + ).toBe("web.title"); + }); + + it("treats an underscore as a separator, not as word content", () => { + // `\w` would keep `_`, which is exactly the separator most templates use. + expect(formatKey("{component}_{elementName}", { elementName: "Title" }, "snake_case")).toBe( + "title", + ); + }); + + it("handles consecutive empty placeholders without eating extra characters", () => { + expect( + formatKey("{frame}.{component}.{elementName}", { elementName: "Title" }, "snake_case"), + ).toBe("title"); + }); + + it("leaves a trailing empty placeholder's separator off the end", () => { + expect(formatKey("{frame}.{component}", { frame: "Home" }, "snake_case")).toBe("home"); + }); +}); diff --git a/src/shared/__tests__/messagePolicy.test.ts b/src/shared/__tests__/messagePolicy.test.ts new file mode 100644 index 00000000..8a15ba5b --- /dev/null +++ b/src/shared/__tests__/messagePolicy.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { DEV_ALLOWED_IMPACTS, MESSAGE_IMPACT } from "../messagePolicy"; + +/** + * Exhaustiveness is enforced by the compiler (the `Record` over + * `UiToMain["type"]`); these tests are the RECLASSIFICATION guard — a + * refactor that quietly moves a canvas write out of the blocked class (or + * widens what Dev Mode may process) must fail here, visibly. + */ +describe("MESSAGE_IMPACT", () => { + it("classifies exactly the two canvas-writing messages as canvas", () => { + const canvas = Object.entries(MESSAGE_IMPACT) + .filter(([, impact]) => impact === "canvas") + .map(([type]) => type) + .sort(); + expect(canvas).toEqual(["apply-translations", "create-copy"]); + }); + + it("classifies the staleness check (added after the original 17 types) as read", () => { + expect(MESSAGE_IMPACT["request-copy-staleness"]).toBe("read"); + }); + + it("allows everything except canvas in Dev Mode", () => { + expect(DEV_ALLOWED_IMPACTS.has("canvas")).toBe(false); + expect(DEV_ALLOWED_IMPACTS.has("read")).toBe(true); + expect(DEV_ALLOWED_IMPACTS.has("metadata")).toBe(true); + expect(DEV_ALLOWED_IMPACTS.has("ui")).toBe(true); + }); +}); diff --git a/src/shared/__tests__/tolgeeFormat.test.ts b/src/shared/__tests__/tolgeeFormat.test.ts new file mode 100644 index 00000000..2731955f --- /dev/null +++ b/src/shared/__tests__/tolgeeFormat.test.ts @@ -0,0 +1,374 @@ +import { + findArgumentNames, + findPluralParameters, + getTolgeeFormat, + hasIcuArgument, + tolgeeFormatGenerateIcu, +} from "$shared/tolgeeFormat"; +import IntlMessageFormat from "intl-messageformat"; +import { describe, expect, it } from "vitest"; + +/** + * The whole point of `tolgeeFormatGenerateIcu`'s bug fixes is that the ICU it + * produces must be ACCEPTED (parsed and formatted) by `intl-messageformat` — + * the same library every render path in this app uses (`src/shared/icu.ts`). + * Assert that directly instead of just checking the string shape. + */ +function expectValidIcu(icu: string): void { + expect(() => { + if (icu === "") return; // empty string is a valid "no message" sentinel, not ICU + const formatter = new IntlMessageFormat(icu, "en", undefined, { ignoreTag: true }); + formatter.format({ value: 1, p: 1, count: 1, name: "x" }); + }).not.toThrow(); +} + +describe("getTolgeeFormat (non-plural)", () => { + it("wraps plain text as the `other` variant", () => { + expect(getTolgeeFormat("Hello", false, false)).toEqual({ + variants: { other: "Hello" }, + }); + }); + + it("does not parse ICU when plural=false, even if the text looks like one", () => { + const input = "{count, plural, one {one item} other {# items}}"; + expect(getTolgeeFormat(input, false, false)).toEqual({ + variants: { other: input }, + }); + }); + + it("preserves empty strings", () => { + expect(getTolgeeFormat("", false, false)).toEqual({ + variants: { other: "" }, + }); + }); +}); + +describe("getTolgeeFormat (plural)", () => { + it("parses a basic plural form", () => { + const result = getTolgeeFormat("{count, plural, one {one item} other {# items}}", true, false); + expect(result.parameter).toBe("count"); + expect(result.variants.one).toBe("one item"); + expect(result.variants.other).toBe("# items"); + }); + + it("parses `=0` style match keys", () => { + const result = getTolgeeFormat( + "{count, plural, =0 {no items} one {one item} other {many}}", + true, + false, + ); + expect(result.parameter).toBe("count"); + expect(result.variants["=0"]).toBe("no items"); + expect(result.variants.one).toBe("one item"); + expect(result.variants.other).toBe("many"); + }); + + it("handles whitespace between the param, keyword, and variants", () => { + const result = getTolgeeFormat( + "{ value , plural , other {# tests} one {# test} }", + true, + false, + ); + expect(result.parameter).toBe("value"); + expect(result.variants.other).toBe("# tests"); + expect(result.variants.one).toBe("# test"); + }); + + it("respects ICU `'` escaped braces inside variant bodies", () => { + // The variant body contains a literal `{` via `'{'`. The closing brace of + // the variant should be the one AFTER the literal, not the one inside. + const input = "{variable, plural, one {'{'} other {}}"; + const result = getTolgeeFormat(input, true, false); + expect(result.parameter).toBe("variable"); + // Body returned RAW (no unescape). + expect(result.variants.one).toBe("'{'"); + expect(result.variants.other).toBe(""); + }); + + it("falls back to no-plural when the input is malformed", () => { + const malformed = "not an icu plural"; + expect(getTolgeeFormat(malformed, true, false)).toEqual({ + variants: { other: malformed }, + }); + }); + + it("falls back when the select function is not `plural`", () => { + const input = "{gender, select, female {she} male {he} other {they}}"; + const result = getTolgeeFormat(input, true, false); + expect(result.variants.other).toBe(input); + expect(result.parameter).toBeUndefined(); + }); + + it("falls back when input has trailing content past the outer brace", () => { + const input = "{count, plural, other {x}} trailing"; + const result = getTolgeeFormat(input, true, false); + expect(result.variants.other).toBe(input); + expect(result.parameter).toBeUndefined(); + }); + + it("falls back when a variant name is empty", () => { + const input = "{count, plural, {x}}"; + const result = getTolgeeFormat(input, true, false); + expect(result.variants.other).toBe(input); + expect(result.parameter).toBeUndefined(); + }); + + it("normalizes equivalent ICU forms to the same shape (whitespace agnostic)", () => { + const a = getTolgeeFormat("{count, plural, one {x} other {y}}", true, false); + const b = getTolgeeFormat("{ count ,plural,one {x} other {y} }", true, false); + expect(JSON.stringify(a)).toBe(JSON.stringify(b)); + }); + + it("falls back when a variant name repeats", () => { + const input = "{count, plural, one {x} one {y} other {z}}"; + const result = getTolgeeFormat(input, true, false); + expect(result.variants.other).toBe(input); + expect(result.parameter).toBeUndefined(); + }); +}); + +describe("tolgeeFormatGenerateIcu", () => { + it("keeps the normal two-category case byte-identical (golden case)", () => { + const icu = tolgeeFormatGenerateIcu( + { parameter: "value", variants: { one: "# day", other: "# days" } }, + false, + ); + expect(icu).toBe("{value, plural, one {# day} other {# days}}"); + expectValidIcu(icu); + }); + + it("escapes a literal `{` in a variant body and still parses", () => { + const icu = tolgeeFormatGenerateIcu( + { parameter: "value", variants: { one: "co{unt", other: "items" } }, + false, + ); + // Exact reference output from the real `@tginternal/editor@1.15.2` package. + expect(icu).toBe("{value, plural, one {co'{'unt} other {items}}"); + expectValidIcu(icu); + }); + + it("escapes a literal `}` in a variant body and still parses", () => { + const icu = tolgeeFormatGenerateIcu( + { parameter: "value", variants: { one: "wrong}brace", other: "ok" } }, + false, + ); + expect(icu).toBe("{value, plural, one {wrong'}'brace} other {ok}}"); + expectValidIcu(icu); + }); + + it("force-appends `other {}` when `other` is absent, and the result parses", () => { + const icu = tolgeeFormatGenerateIcu({ parameter: "value", variants: { one: "# day" } }, false); + // Exact reference output from the real `@tginternal/editor@1.15.2` package. + expect(icu).toBe("{value, plural, one {# day} other {}}"); + expectValidIcu(icu); + }); + + it("mirrors `makePluralFromNumber`'s single-category auto-plural (StringDetails.svelte) and it must parse", () => { + // This is exactly the shape `makePluralFromNumber()` builds: one detected + // CLDR category, no `other`. Before the fix this produced ICU with no + // `other` fallback, which `IntlMessageFormat` rejects outright. + const icu = tolgeeFormatGenerateIcu({ parameter: "value", variants: { one: "# item" } }, false); + expect(icu).toContain("other {}"); + expectValidIcu(icu); + }); + + it("returns \"\" when every variant body is empty", () => { + expect(tolgeeFormatGenerateIcu({ parameter: "value", variants: { one: "", other: "" } }, false)).toBe( + "", + ); + expect(tolgeeFormatGenerateIcu({ parameter: "value", variants: {} }, false)).toBe(""); + }); + + it("does NOT collapse to \"\" when a variant body is whitespace-only (matches reference: only exactly-empty strings count)", () => { + const icu = tolgeeFormatGenerateIcu( + { parameter: "value", variants: { one: " ", other: "" } }, + false, + ); + // Exact reference output from the real `@tginternal/editor@1.15.2` package. + expect(icu).toBe("{value, plural, one { } other {}}"); + expectValidIcu(icu); + }); + + it("leaves the non-plural passthrough (no parameter) untouched", () => { + expect(tolgeeFormatGenerateIcu({ variants: { other: "plain text" } }, false)).toBe("plain text"); + expect(tolgeeFormatGenerateIcu({ variants: {} }, false)).toBe(""); + }); +}); + +describe("tolgeeFormatGenerateIcu — literal vs. argument braces", () => { + it("keeps a nested `{name}` argument interpolating instead of escaping it to text", () => { + // Regression: the generator used to escape EVERY brace, so a legitimate + // nested argument became literal text — and, because Push regenerates the + // whole ICU through this path when merging plural layers, it was uploaded + // in that corrupted form. + const icu = tolgeeFormatGenerateIcu( + { parameter: "count", variants: { one: "One for {name}", other: "# for {name}" } }, + false, + ); + expect(icu).toBe("{count, plural, one {One for {name}} other {# for {name}}}"); + expectValidIcu(icu); + + const rendered = new IntlMessageFormat(icu, "en", undefined, { ignoreTag: true }).format({ + count: 1, + name: "Zuzka", + }); + expect(rendered).toBe("One for Zuzka"); + }); + + it("does not re-escape a brace that is already ICU-escaped", () => { + // Regression: `a '{' b` used to round-trip to `a ''{'' b` — a literal + // apostrophe followed by an unclosed argument, which throws downstream. + const icu = tolgeeFormatGenerateIcu( + { parameter: "value", variants: { one: "a '{' b", other: "ok" } }, + false, + ); + expect(icu).toBe("{value, plural, one {a '{' b} other {ok}}"); + expectValidIcu(icu); + + const rendered = new IntlMessageFormat(icu, "en", undefined, { ignoreTag: true }).format({ + value: 1, + }); + expect(rendered).toBe("a { b"); + }); + + it("still escapes prose that merely sits between braces", () => { + // `{not an argument}` is not a valid ICU argument (the name would have to + // be one unbroken token), so it must keep being escaped to literal text. + const icu = tolgeeFormatGenerateIcu( + { parameter: "value", variants: { one: "{not an argument}", other: "ok" } }, + false, + ); + expect(icu).toBe("{value, plural, one {'{'not an argument'}'} other {ok}}"); + expectValidIcu(icu); + }); + + it("round-trips a variant body through parse → generate unchanged", () => { + // The property that actually matters for Push: parsing an ICU string and + // regenerating it must not alter it, or a no-op edit reads as a change. + const original = "{count, plural, one {One for {name}} other {# for {name}}}"; + const parsed = getTolgeeFormat(original, true, false); + expect(tolgeeFormatGenerateIcu(parsed, false)).toBe(original); + }); +}); + +describe("findPluralParameters", () => { + it("finds a plural that is the whole string", () => { + expect(findPluralParameters("{count, plural, one {# day} other {# days}}")).toEqual(["count"]); + }); + + it("finds a plural surrounded by literal text", () => { + expect(findPluralParameters("{count, plural, one {# day} other {# days}} left")).toEqual([ + "count", + ]); + expect(findPluralParameters("Only {n, plural, other {# x}}!")).toEqual(["n"]); + }); + + it("finds a plural nested inside another argument", () => { + expect( + findPluralParameters("{gender, select, other {{count, plural, other {# replies}}}}"), + ).toEqual(["count"]); + }); + + it("ignores a non-plural argument and an escaped literal brace", () => { + expect(findPluralParameters("Hello, {name}!")).toEqual([]); + expect(findPluralParameters("literal '{'count, plural'}'")).toEqual([]); + }); + + it("leaves getTolgeeFormat's stricter whole-string semantics alone", () => { + // The two answer different questions on purpose: push diffing needs "is + // this string ONE plural form?", which must stay false here, while the + // render path needs "is there a plural anywhere in it?". + const embedded = "{count, plural, one {# day} other {# days}} left"; + expect(getTolgeeFormat(embedded, true, false).parameter).toBeUndefined(); + expect(findPluralParameters(embedded)).toEqual(["count"]); + }); +}); + +describe("hasIcuArgument", () => { + it("is true for a real argument", () => { + expect(hasIcuArgument("Hello, {name}!")).toBe(true); + expect(hasIcuArgument("{count, plural, one {# day} other {# days}}")).toBe(true); + expect(hasIcuArgument("{count, plural, other {# days}} left")).toBe(true); + }); + + it("is false for ICU-escaped literal braces", () => { + expect(hasIcuArgument("Use '{'braces'}' here")).toBe(false); + expect(hasIcuArgument("a '{' b")).toBe(false); + }); + + it("is false for stray or prose braces", () => { + expect(hasIcuArgument("co{unt")).toBe(false); + expect(hasIcuArgument("wrong}brace")).toBe(false); + expect(hasIcuArgument("{not an argument}")).toBe(false); + }); + + it("is false for plain text", () => { + expect(hasIcuArgument("")).toBe(false); + expect(hasIcuArgument("just words")).toBe(false); + }); +}); + +describe("round-trip of ICU written by the PUBLISHED plugin", () => { + // The property that decides whether a user migrating from the published + // plugin gets phantom "changed" keys: parsing what that plugin stored and + // regenerating it must return the identical string, or Push reports a + // difference nobody made. `mergePluralForms` regenerates on every plural key + // with more than one layer, so this is not a rare path. + // + // Values below are the real output of `@tginternal/editor@1.15.2`, captured + // by running the package (it is deliberately not a dependency here — it + // drags in ~500 kB of CodeMirror). To refresh them: + // npm i @tginternal/editor@1.15.2 # in a scratch dir + // node -e 'const {tolgeeFormatGenerateIcu}=require("/dist/tolgee-editor.cjs"); …' + // The `exports` map blocks both bare-name ESM import and subpath require, so + // the absolute path to the dist file is required. + const WRITTEN_BY_THE_ORIGINAL = [ + "{count, plural, one {# day} other {x}}", + "{count, plural, one {One for {name}} other {x}}", + "{count, plural, one {a '{' b} other {x}}", + "{count, plural, one {'{not an argument}'} other {x}}", + "{count, plural, one {co'{'unt} other {x}}", + "{count, plural, one {wrong'}'brace} other {x}}", + "{count, plural, one {Hi '{name} and co{'unt} other {x}}", + "{count, plural, one {100% '{'sure'}'} other {x}}", + "{count, plural, one {# item — {a}, {b}} other {x}}", + ]; + + for (const icu of WRITTEN_BY_THE_ORIGINAL) { + it(`survives parse → generate unchanged: ${icu}`, () => { + expect(tolgeeFormatGenerateIcu(getTolgeeFormat(icu, true, false), false)).toBe(icu); + }); + } +}); + +describe("findArgumentNames", () => { + it("finds an argument whose own body contains braces", () => { + // The gap a regex can't close: `{gender, select, …}` has no bounded + // `[^{}]*` tail, so it was never seen — and an unseeded argument makes + // IntlMessageFormat throw MISSING_VALUE. + expect(findArgumentNames("{gender, select, other {x}}")).toEqual(["gender"]); + }); + + it("finds both the outer argument and one nested inside it", () => { + expect( + findArgumentNames("{gender, select, other {{count, plural, other {# replies}}}}"), + ).toEqual(["gender", "count"]); + }); + + it("finds an argument inside a plural variant body", () => { + expect(findArgumentNames("{count, plural, one {Hello {name}} other {hi}}")).toEqual([ + "count", + "name", + ]); + }); + + it("finds a plain argument and dedupes repeats", () => { + expect(findArgumentNames("Hi {name}, bye {name}")).toEqual(["name"]); + }); + + it("ignores escaped braces and prose between braces", () => { + expect(findArgumentNames("literal '{'name'}' here")).toEqual([]); + expect(findArgumentNames("{not an argument}")).toEqual([]); + expect(findArgumentNames("plain text")).toEqual([]); + }); +}); diff --git a/src/constants.ts b/src/shared/constants.ts similarity index 75% rename from src/constants.ts rename to src/shared/constants.ts index 4a0fb46c..34088109 100644 --- a/src/constants.ts +++ b/src/shared/constants.ts @@ -7,6 +7,7 @@ export const TOLGEE_KEY_FORMAT_PLACEHOLDERS = { elementText: "{elementText}", group: "{group}", component: "{component}", + instance: "{instance}", frame: "{frame}", artboard: "{artboard}", section: "{section}", @@ -19,7 +20,14 @@ export const TOLGEE_KEY_FORMAT_PLACEHOLDERS_EXAMPLES: { elementText: "My element text", group: "My group", component: "My Component", + instance: "My Instance", section: "My Section", artboard: "My Artboard", frame: "My Frame", } as const; + +export const UI_SIZES = { + MINIMUM: { width: 400, height: 160 }, + DEFAULT: { width: 500, height: 600 }, + EXPANDED: { width: 500, height: 800 }, +} as const; diff --git a/src/shared/iconSizes.ts b/src/shared/iconSizes.ts new file mode 100644 index 00000000..2a631852 --- /dev/null +++ b/src/shared/iconSizes.ts @@ -0,0 +1,22 @@ +/** + * Central icon-size scale (in px), tuned to pair with the plugin's text sizes. + * + * Rule of thumb: an inline icon reads balanced at roughly font-size + 2–4px. + * Changing a value here resizes every icon in that tier across the whole UI — + * this is the single source of truth, so prefer `ICON.*` over hard-coded sizes. + * + * - `badge` — small glyphs inside badges / pills (sits by ~10px badge text). + * - `marker` — standalone status markers next to a row's text (conflict flags). + * - `inline` — sits next to body text (11–12px) or inside small markers. + * - `action` — buttons, header actions, toggles. + * - `hero` — large feature glyphs in empty states. + */ +export const ICON = { + badge: 12, + marker: 14, + inline: 16, + action: 18, + hero: 32, +} as const; + +export type IconSize = (typeof ICON)[keyof typeof ICON]; diff --git a/src/shared/icu.ts b/src/shared/icu.ts new file mode 100644 index 00000000..db3b7351 --- /dev/null +++ b/src/shared/icu.ts @@ -0,0 +1,71 @@ +import IntlMessageFormat from "intl-messageformat"; + +export type IcuFormatResult = { + result: string; + error: Error | null; +}; + +// Constructing an `IntlMessageFormat` parses the ICU string and loads locale +// data — by far the costliest step here. The same (locale, message) pairs recur +// constantly (e.g. the Index list rebuilds its manual-change set on every +// selection re-emit, re-formatting unchanged advanced strings), so we memoize +// the parsed formatter. Bounded with simple FIFO eviction so a long-lived main +// thread can't grow it without limit. +const FORMATTER_CACHE_MAX = 500; +const formatterCache = new Map(); + +function getFormatter(message: string, locale: string): IntlMessageFormat { + const key = `${locale}\0${message}`; + const cached = formatterCache.get(key); + if (cached) return cached; + // `ignoreTag: true` keeps inline markup (``, ``, …) as LITERAL text + // instead of parsing it as a rich-text tag that would require a `b`/`i`/… + // handler in the values (intl-messageformat throws "variable 'b' was not + // provided" otherwise). We never pass tag handlers — the preview renders the + // markup as HTML and the canvas writer re-applies it — so literals are exactly + // what we want, and params still fill normally. + const formatter = new IntlMessageFormat(message, locale, undefined, { + ignoreTag: true, + }); + if (formatterCache.size >= FORMATTER_CACHE_MAX) { + const oldest = formatterCache.keys().next().value; + if (oldest !== undefined) formatterCache.delete(oldest); + } + formatterCache.set(key, formatter); + return formatter; +} + +/** + * Formats an ICU message with the given parameters and locale. + * + * - Plain text with no `'` (ICU escape marker) and no `{` (start of any + * placeholder/plural/tag) can't need unescaping or formatting, so it's + * returned untouched without invoking the parser — the common case, kept + * cheap. Any other message ALWAYS goes through the formatter, even with + * empty `params`: Tolgee itself emits ICU-escaped literals (`'{'`, `''`, + * …) for plain text containing braces/apostrophes, and skipping the + * parser for those wrote the escaped source straight to the canvas + * instead of the intended literal text (H10). + * - When parsing or formatting fails (e.g. real placeholders left unfilled + * with empty `params`), returns the original `message` with `error` + * populated so callers can surface diagnostics without breaking the UI. + */ +export function formatIcuMessage( + message: string, + params: Record, + locale: string, +): IcuFormatResult { + if (!message.includes("'") && !message.includes("{")) { + return { result: message, error: null }; + } + + try { + const formatter = getFormatter(message, locale); + const formatted = formatter.format(params ?? {}); + const result = typeof formatted === "string" ? formatted : String(formatted); + return { result, error: null }; + } catch (err) { + const error = err instanceof Error ? err : new Error(String(err)); + return { result: message, error }; + } +} diff --git a/src/shared/interpolate.ts b/src/shared/interpolate.ts new file mode 100644 index 00000000..f7c22a36 --- /dev/null +++ b/src/shared/interpolate.ts @@ -0,0 +1,191 @@ +import { formatIcuMessage } from "$shared/icu"; +import type { NodeInfo } from "$shared/types"; +import { findArgumentNames, findPluralParameters } from "$shared/tolgeeFormat"; + +/** + * Port of the published plugin's render/diff core + * (`src/ui/hooks/useInterpolatedTranslation.ts`). ONE place that turns a node's + * stored ICU into the string shown on the Figma canvas, so the pull render, the + * live preview and the manual-change check all agree — as they did upstream. + * + * Plural handling matches the original: the plural variable's NAME comes from + * the ICU itself, and its sample COUNT is a NUMBER. To also read files written + * by the original (which stored the count in `pluralParamValue`) AND by this + * rewrite (which edits it as a named sample in `paramsValues`), the count is + * sourced tolerantly: `paramsValues[name]` first, then a numeric + * `pluralParamValue`, then "1". + */ + +type RenderNode = Pick< + NodeInfo, + "isPlural" | "translation" | "characters" | "paramsValues" | "pluralParamValue" +>; + +/** A plain digits-only string, or undefined — so a plural arg NAME accidentally + * left in `pluralParamValue` is never used as a count. */ +function numericCount(value: string | undefined): string | undefined { + return value != null && /^\d+$/.test(value.trim()) ? value.trim() : undefined; +} + +/** Named `{param}` placeholders in an ICU string (excludes `#` and positional + * numeric args). + * + * Uses the brace-aware scan rather than a regex: the previous + * `\{\s*(\w+)\s*(?:,[^{}]*)?\}` couldn't match an argument whose own body + * contains braces, so a `select` — or a plural wrapped in one — was never + * seen and never seeded, and the render threw `MISSING_VALUE`. */ +function namedPlaceholders(icu: string): string[] { + return findArgumentNames(icu).filter((name) => name !== "#" && !/^\d+$/.test(name)); +} + +/** + * ICU params for rendering `icu` with this node's samples: the named + * `paramsValues`, plus the plural variable seeded with its sample count when the + * ICU is a plural and no sample is already provided for it. + * + * Whether `icu` HOLDS a plural is decided by its own STRUCTURE + * (`findPluralParameters` scans for a `{name, plural, ...}` argument anywhere + * in it), not by the node's `isPlural` flag — that flag + * mirrors Tolgee's KEY setting, which can be stale or simply wrong for a key + * whose translation was typed as ICU plural syntax without the key itself + * ever being marked plural. Trusting the flag there meant a correctly-shaped + * plural silently rendered as its own raw ICU pattern (no sample ever seeded, + * so `IntlMessageFormat` throws on the missing argument) whenever the two + * disagreed — this makes the render succeed regardless of which side lied. + * + * The scan is used instead of `getTolgeeFormat(...).parameter` because that one + * only recognises a plural that is the ENTIRE string; `"{count, plural, …} left"` + * — a plural with literal text around it, a very ordinary shape — went unseeded + * and threw `MISSING_VALUE`, dropping the node in `copyApply` and leaving stale + * canvas text on Pull. + */ +export function renderParams(icu: string, node: RenderNode): Record { + const params: Record = { ...(node.paramsValues ?? {}) }; + const pluralNames = findPluralParameters(icu); + for (const pluralName of pluralNames) { + // The plural argument MUST be numeric — IntlMessageFormat doesn't throw + // on a non-numeric value, it silently coerces it to NaN ("Mám NaN + // jablek" on the canvas). Old node data can carry the arg NAME (or "") + // as its own sample, so a non-numeric `paramsValues` entry falls + // through to `pluralParamValue`, then to "1". + params[pluralName] = + numericCount(params[pluralName]) ?? numericCount(node.pluralParamValue) ?? "1"; + } + // Seed any other named placeholder with its OWN name so it renders as e.g. + // "Hello, name!" instead of a literal "{name}" or an ICU throw — the original + // pull behaviour. A real sample in `paramsValues` always wins. + for (const name of namedPlaceholders(icu)) { + if (!pluralNames.includes(name) && !(name in params)) params[name] = name; + } + return params; +} + +/** + * Render an explicit ICU string with a node's samples. Used for pulled text + * (which may differ from the node's stored translation). Returns the raw ICU + * plus the captured `Error` on any formatting failure so callers can warn. + */ +export function renderIcuForNode( + icu: string, + node: RenderNode, + language: string, +): { text: string; error?: Error } { + const { result, error } = formatIcuMessage(icu, renderParams(icu, node), language || "en"); + return error ? { text: icu, error } : { text: result }; +} + +/** + * A node with no ICU interpolation to do — not plural, no sample params, no + * inline markup. Mirrors the original `isSimpleNode`: such a node's canvas text + * IS its value, so it's never treated as a manual-change divergence. + */ +export function isSimpleNode(node: RenderNode): boolean { + return ( + !node.isPlural && + Object.keys(node.paramsValues ?? {}).length === 0 && + !/<[^>]*>/.test(node.translation ?? "") + ); +} + +/** The string that should sit on the canvas for this node: the render of its + * stored translation (or the raw characters for a simple node). */ +export function interpolatedText( + node: RenderNode, + language: string, +): { text: string; error?: Error } { + const icu = isSimpleNode(node) + ? node.characters || node.translation || "" + : node.translation || node.characters || ""; + return renderIcuForNode(icu, node, language); +} + +/** + * The canvas `characters` no longer equal the rendered stored translation — an + * advanced string edited directly in Figma (formatting lost). Mirrors the + * original `translationDiffersFromNode`, comparing with inline tags stripped + * from both sides. + */ +export function translationDiffersFromNode(node: RenderNode, language: string): boolean { + if (isSimpleNode(node)) return false; + const out = interpolatedText(node, language); + // If the ICU can't be rendered we can't tell whether it diverged — don't + // claim a manual change (avoids a false warning on a broken translation). + if (out.error) return false; + const rendered = out.text.replace(/<[^>]*>/g, ""); + const canvas = (node.characters ?? "").replace(/<[^>]*>/g, ""); + return rendered.trim() !== canvas.trim(); +} + +// The diff result is pure in the node's render inputs, but the render itself +// (ICU parse + IntlMessageFormat) is the priciest per-node work the Index list +// does — and it used to re-run for EVERY advanced node on every selection +// update. Cache by the exact inputs; entries for unchanged nodes survive the +// in-place patches that follow writes, so only edited nodes re-render. +const diffCache = new Map(); +const DIFF_CACHE_MAX = 2000; + +function diffCacheKey(node: RenderNode, language: string): string { + return JSON.stringify([ + language, + node.isPlural ?? false, + node.translation ?? "", + node.characters ?? "", + node.pluralParamValue ?? "", + node.paramsValues ?? {}, + ]); +} + +/** Memoised `translationDiffersFromNode` for hot list paths. */ +export function translationDiffersFromNodeCached(node: RenderNode, language: string): boolean { + const key = diffCacheKey(node, language); + const hit = diffCache.get(key); + if (hit !== undefined) return hit; + const result = translationDiffersFromNode(node, language); + // Unbounded growth guard; a full reset is fine, the next pass repopulates. + if (diffCache.size >= DIFF_CACHE_MAX) diffCache.clear(); + diffCache.set(key, result); + return result; +} + +/** + * Best sample count for `icu` whose plural render matches `canvas`, so a + * freshly connected plural layer keeps the form it already shows instead of + * snapping to a default. Returns undefined when `icu` isn't a plural or nothing + * matches (caller falls back to "1"). The original stored the arg NAME here + * instead — a latent bug — so inferring the count is the intended behaviour. + */ +export function inferPluralCount( + icu: string, + canvas: string, + language: string, +): string | undefined { + const name = findPluralParameters(icu)[0]; + if (!name) return undefined; + const target = canvas.replace(/<[^>]*>/g, "").trim(); + if (!target) return undefined; + for (const n of [0, 1, 2, 3, 4, 5, 6, 10, 11, 21, 100]) { + const { result, error } = formatIcuMessage(icu, { [name]: String(n) }, language || "en"); + if (!error && result.replace(/<[^>]*>/g, "").trim() === target) return String(n); + } + return undefined; +} diff --git a/src/shared/keyFormat.ts b/src/shared/keyFormat.ts new file mode 100644 index 00000000..9d246b9d --- /dev/null +++ b/src/shared/keyFormat.ts @@ -0,0 +1,169 @@ +import type { GlobalSettings } from "./types"; + +/** + * Context values used to substitute placeholders in a key-format template. + * + * Mirrors the placeholders defined in `constants.ts` / + * `TOLGEE_KEY_FORMAT_PLACEHOLDERS`. + */ +export type KeyFormatContext = { + artboard?: string; + frame?: string; + elementName?: string; + elementText?: string; + component?: string; + /** Layer name of the nearest component INSTANCE (distinct from `component`, + * which is the name of the main component it was created from). */ + instance?: string; + section?: string; + group?: string; +}; + +/** + * The subset of placeholders that must be resolved by walking the Figma tree + * (as opposed to `elementName`/`elementText`, which a node already carries). + */ +export type KeyParentNames = Pick< + KeyFormatContext, + "component" | "instance" | "frame" | "artboard" | "section" | "group" +>; + +/** + * Whether a key-format template references any parent-derived placeholder. + * Resolving parents means an upward tree walk (main-thread only), so callers + * on BOTH sides gate that work behind this check. + */ +export function keyFormatUsesParents(keyFormat: string | undefined): boolean { + return /\{(component|instance|frame|artboard|section|group)\}/.test(keyFormat ?? ""); +} + +/** + * Applies the requested casing transformation to a free-form string. + * + * Reference implementation: `src/utilities.ts#formatString`. + */ +export function applyCasing(input: string, casing: GlobalSettings["variableCasing"]): string { + const str = input ?? ""; + switch (casing) { + case "camelCase": + return str + .split(/\s+/) + .filter(Boolean) + .map((word, index) => + index > 0 + ? word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + : word.toLowerCase(), + ) + .join(""); + case "PascalCase": + return str + .split(/\s+/) + .filter(Boolean) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(""); + case "snake_case": + return str + .split(/\s+/) + .filter(Boolean) + .map((word) => word.toLowerCase()) + .join("_"); + case "snake_case_capitalized": + return str + .split(/\s+/) + .filter(Boolean) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join("_"); + case "noSpaces": + return str.replace(/\s/g, ""); + default: + // "keep original format": "" / undefined / anything unrecognised → return + // the string untouched. Matches the original plugin's `formatString` + // fallthrough; must NOT snake_case, or configs that kept the original + // format would be silently transformed. + return str; + } +} + +/** A template split into literal text and (resolved) placeholder parts. */ +type KeyPart = + | { ph: false; value: string } + | { ph: true; value: string; empty: boolean }; + +const PLACEHOLDER = + /\{(elementName|elementText|component|instance|frame|artboard|section|group)\}/g; + +/** + * Formats a Tolgee key from a template string and a context object. + * + * Reference implementation: `src/main/endpoints/preformatKey.ts`. + * + * An empty placeholder value is removed together with ONE adjacent separator, + * so the result never carries a leading, trailing, or doubled separator — e.g. + * `{component}.{elementName}` with no component yields `element_name`, not + * `.element_name`. + */ +export function formatKey( + template: string, + context: KeyFormatContext, + casing: GlobalSettings["variableCasing"], +): string { + // Split into literal + placeholder parts, resolving each placeholder to its + // casing-applied value; literals (separators, fixed text) pass through as-is. + const parts: KeyPart[] = []; + let last = 0; + for (const m of template.matchAll(PLACEHOLDER)) { + const idx = m.index ?? 0; + if (idx > last) parts.push({ ph: false, value: template.slice(last, idx) }); + const value = applyCasing(context[m[1] as keyof KeyFormatContext] ?? "", casing); + parts.push({ ph: true, value, empty: value === "" }); + last = idx + m[0].length; + } + if (last < template.length) parts.push({ ph: false, value: template.slice(last) }); + + // Drop each empty placeholder plus ONE adjacent separator CHARACTER: prefer + // the literal immediately BEFORE it, or the one AFTER when nothing precedes + // it (so a leading empty placeholder doesn't leave a dangling separator). + // + // One character, not the whole literal. A literal can carry fixed text as + // well as a separator — `app.{component}.{elementName}` holds `app.` — and + // removing all of it silently drops text the user put in their template, + // straight into a key name that then gets persisted and uploaded. + const drop = new Set(); + let contentBefore = false; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (!part?.ph) continue; + if (!part.empty) { + contentBefore = true; + continue; + } + drop.add(i); + const prev = parts[i - 1]; + const next = parts[i + 1]; + if (contentBefore && prev && !prev.ph) { + prev.value = trimTrailingSeparator(prev.value); + } else if (next && !next.ph) { + next.value = trimLeadingSeparator(next.value); + } + } + + return parts + .filter((_, i) => !drop.has(i)) + .map((p) => p.value) + .join(""); +} + +/** Anything that isn't a letter or a digit reads as a separator here — `.`, + * `-`, `/` and `_` are all in common use in key templates, so `\w` (which + * keeps `_`) would be the wrong test. */ +const SEPARATOR = /[^\p{L}\p{N}]/u; + +function trimTrailingSeparator(value: string): string { + const last = value.slice(-1); + return last && SEPARATOR.test(last) ? value.slice(0, -1) : value; +} + +function trimLeadingSeparator(value: string): string { + const first = value.slice(0, 1); + return first && SEPARATOR.test(first) ? value.slice(1) : value; +} diff --git a/src/shared/keyIndex.ts b/src/shared/keyIndex.ts new file mode 100644 index 00000000..3b465f30 --- /dev/null +++ b/src/shared/keyIndex.ts @@ -0,0 +1,25 @@ +/** + * Composite lookup key for a `(namespace, key)` pair. + * + * Joins on NUL rather than `|`, which several maps in this codebase used: `|` + * is a legal character in both a namespace and a key name, and neither the + * inline key input nor the namespace input forbids it — so `(ns "a|b", key + * "c")` and `(ns "a", key "b|c")` collapsed onto the same index `a|b|c`. The + * later entry overwrote the earlier one, and in the copy flow that meant one + * translation being written onto BOTH cloned nodes. + * + * NUL cannot occur in a Tolgee key or namespace, and is the separator the + * published plugin uses for its own composite keys. + * + * Lives in `$shared` because BOTH threads key maps this way — the main + * thread's copy-staleness counts as much as the UI's translation lookups. + * Keeping it UI-side is exactly what left the main thread on the old separator + * after the first sweep. + * + * Every map keyed this way MUST build and read through this function: the copy + * flow builds its index in one file and reads it in another, so a separator + * changed in only one of them silently finds nothing. + */ +export function nsKeyIndex(ns: string | undefined, key: string): string { + return `${ns ?? ""}\u0000${key}`; +} diff --git a/src/shared/messagePolicy.ts b/src/shared/messagePolicy.ts new file mode 100644 index 00000000..c805c23f --- /dev/null +++ b/src/shared/messagePolicy.ts @@ -0,0 +1,69 @@ +import type { UiToMain } from "./messages"; + +/** + * The single source of truth for what each UI→main message DOES to the + * document — the central safety layer for Dev Mode (Figma's inspect-only + * editor), where the plugin must never change anything a designer can see. + * + * Every message type MUST be classified here: the `Record` over + * `UiToMain["type"]` is exhaustive, so adding a new message without + * classifying it is a compile error, not a silently-unguarded hole. The + * main-thread bus derives its Dev-Mode blocking from this map (see + * `$main/bus.ts`), and the UI derives visibility from the same value — no + * scattered `if (editorType === "dev")` conditions as the only defence. + * + * How to classify a NEW message: + * - `canvas` — changes something VISIBLE on the design (text, layers, + * pages). Blocked outright in Dev Mode. When unsure between + * `canvas` and `metadata`, pick `canvas` — a reviewer can + * relax it with evidence, but a leaked canvas write in Dev + * Mode is the exact bug this map exists to prevent. + * - `metadata` — writes only invisible state: pluginData, plugin config, + * clientStorage. Safe everywhere; production's Dev Mode + * allows the same (its Push flow writes pluginData). + * - `read` — reads the document (or moves the viewport) without + * changing it. `scroll-to-node` sits here: the highlight + * flash is already skipped on non-design editors inside + * `$main/nodes/highlight.ts`, the scroll itself is a + * viewport move, not a document change. + * - `ui` — iframe housekeeping (resize, close, toasts, opening + * external URLs). Never touches the document at all. + */ +export type MessageImpact = "canvas" | "metadata" | "read" | "ui"; + +export const MESSAGE_IMPACT: Record = { + // canvas — rewrites TextNode.characters ("apply-translations") or clones/ + // deletes whole pages ("create-copy"). The two ways this plugin can change + // what a designer sees. + "apply-translations": "canvas", + "create-copy": "canvas", + + // metadata — pluginData / config / clientStorage writes only. + "save-config": "metadata", + "persist-project-id": "metadata", + reset: "metadata", + "set-language": "metadata", + "set-branch": "metadata", + "set-nodes-data": "metadata", + + // read — document reads (scans, screenshots' exportAsync) + viewport moves. + "request-page-connected-nodes": "read", + "resolve-parent-names": "read", + "request-screenshots": "read", + "scroll-to-node": "read", + "request-copy-staleness": "read", + + // ui — iframe housekeeping. + "ui-ready": "ui", + resize: "ui", + close: "ui", + notify: "ui", + "open-external": "ui", +}; + +/** Impacts the Dev-Mode editor may process — everything except `canvas`. */ +export const DEV_ALLOWED_IMPACTS: ReadonlySet = new Set([ + "read", + "metadata", + "ui", +]); diff --git a/src/shared/messages.ts b/src/shared/messages.ts new file mode 100644 index 00000000..af488fdf --- /dev/null +++ b/src/shared/messages.ts @@ -0,0 +1,296 @@ +import type { KeyParentNames } from "./keyFormat"; +import type { FrameScreenshot, NodeInfo, TolgeeConfig, WindowSize } from "./types"; + +/** + * Messages sent from the main thread (Plugin sandbox) to the UI iframe. + */ +export type MainToUi = + | { + /** + * A main-thread handler threw. Sent by the bus's error boundary so the + * UI can settle whatever it armed for this request instead of waiting + * out a multi-minute idle timeout (or, where there is no watchdog, + * forever). + */ + type: "handler-error"; + /** The UI→main message type whose handler failed. */ + forType: string; + /** Present when the failed request carried one. */ + correlationId?: string; + } + | { + type: "init"; + config: Partial | null; + selectedNodes: NodeInfo[]; + /** See `selection-changed.hasUserSelection`. */ + hasUserSelection: boolean; + editorType: "figma" | "dev"; + /** `figma.currentPage.name` — CopyView's header shows it directly + (the page name already carries the language, e.g. "Home - cs"), + so the UI never needs a page-content round trip just for a title. */ + pageName: string; + /** Optional: navigate to this route immediately after init (used by E2E tests). */ + initialRoute?: string; + } + | { + type: "selection-changed"; + nodes: NodeInfo[]; + /** + * `true` iff the user has at least one node selected on the current + * page. When `false`, `nodes` holds the page-wide connected-node + * fallback and a "you have a selection but … will be applied to all" + * hint would be misleading. + */ + hasUserSelection: boolean; + } + | { + /** + * Emitted the instant `selectionchange` fires, BEFORE the (potentially + * slow) selection scan. Lets the UI show a loader during the scan instead + * of only after `selection-changed` arrives. Carries no data — the UI + * just flips into a "scanning" state until the matching + * `selection-changed` lands. + */ + type: "selection-pending"; + } + | { + /** One chunk of a STREAMED selection scan. Large selections used to + arrive as a single `selection-changed` only after every node's info + was built — thousands of nodes meant many seconds of built-up work + before the UI showed anything. Batches let the list render within + the first ~100 nodes and fill in progressively; a superseding + selection simply stops the remaining batches. `first: true` + replaces the list, subsequent batches append. Closed by + `selection-done`. (`selection-changed` remains for non-streamed + senders: empty selections, init, and the e2e host.) */ + type: "selection-batch"; + nodes: NodeInfo[]; + first: boolean; + } + | { + /** Terminal marker of a streamed selection scan. `total` lets the UI + clear the list when the scan yielded zero usable text nodes (no + `selection-batch` was sent at all). */ + type: "selection-done"; + hasUserSelection: boolean; + total: number; + } + | { type: "page-changed"; config: Partial; pageName: string } + | { type: "config-changed"; config: Partial } + | { + /** One exported frame. Screenshots stream one message per frame — a + single message carrying every PNG serialized tens of MB on the + canvas thread and held all buffers in memory at once. */ + type: "screenshot-frame"; + correlationId: string; + screenshot: FrameScreenshot; + index: number; + } + | { + /** Terminal marker for a `request-screenshots` stream. `total` is the + number of `screenshot-frame` messages that were sent. */ + type: "screenshots-done"; + correlationId: string; + /** Frames actually delivered. */ + total: number; + /** Frames whose export failed and were skipped, so the push summary can + * report the gap instead of presenting `total` as the whole set. */ + failed: number; + } + | { + /** Progress for an in-flight `set-nodes-data` write. Same `total > 100` + guard as `page-connected-nodes-progress`/`apply-translations-progress` + — see `setNodesData` in `selection.ts`. The UI intentionally does NOT + pair this by `correlationId`: the write-progress bar represents ANY + in-flight large write (bulk actions in Index, auto-connect, and the + save-queue's prefill/regen flush alike), not one specific request. */ + type: "nodes-set-progress"; + correlationId: string; + done: number; + total: number; + } + | { + type: "nodes-set-result"; + correlationId: string; + ok: boolean; + /** Fresh post-write snapshots of the updated nodes. The UI patches its + selection in place from these — the main thread deliberately does + NOT re-scan the whole selection after a write (that full re-scan + per write is what froze large selections). */ + nodes: NodeInfo[]; + } + | { + /** Parent placeholder names resolved on demand — see the matching + `resolve-parent-names` request. Keyed by node id; a missing id (or + missing field) means the node has no such ancestor. */ + type: "parent-names-result"; + correlationId: string; + parents: Record; + } + | { + /** Progress for a `request-page-connected-nodes` scan. Only sent when + `total > 100` (small pages resolve fast enough that the message + traffic would just be noise) — see `getNodeInfo` loop in + `scan.ts:buildConnectedNodesInfo`. Also doubles as an idle-timeout + "still alive" signal for `pageNodes.ts`'s watchdog. */ + type: "page-connected-nodes-progress"; + correlationId: string; + done: number; + total: number; + } + | { + type: "page-connected-nodes-result"; + correlationId: string; + nodes: NodeInfo[]; + } + | { + /** Progress for an in-flight `apply-translations` write. Same `total > + 100` guard and idle-timeout role as `page-connected-nodes-progress` + — see `applyTranslations` in `selection.ts`. */ + type: "apply-translations-progress"; + correlationId: string; + done: number; + total: number; + } + | { + type: "apply-translations-result"; + correlationId: string; + ok: boolean; + errors: string[]; + /** See `nodes-set-result.nodes` — post-write snapshots for in-place + patching instead of a full selection re-scan. */ + nodes: NodeInfo[]; + } + | { + type: "create-copy-progress"; + correlationId: string; + current: number; + total: number; + phase: string; + } + | { + type: "create-copy-result"; + correlationId: string; + ok: boolean; + createdPageIds: string[]; + /** Layer names a missing font prevented writing — they keep their + * ORIGINAL text instead of the key label, so the count is reported + * rather than left to a dev-only console warning. */ + skippedMissingFont?: string[]; + /** + * Languages mode only: the connected nodes of each freshly cloned page. + * The main thread deliberately does NOT write translated text itself — + * ICU rendering needs `Intl` (plural rules), which doesn't exist in + * Figma's main-thread sandbox; every `{param}`/plural render silently + * failed there and the copy kept the source-language text. The UI takes + * these nodes, renders each one exactly like the Download flow, and + * writes them back via the ordinary `apply-translations` request — so a + * fresh copy is BY CONSTRUCTION identical to clone + Download all. + */ + pages?: Array<{ pageId: string; language: string; nodes: NodeInfo[] }>; + error?: string; + } + | { + type: "copy-staleness-result"; + correlationId: string; + ok: boolean; + /** Connected strings on the source page this copy doesn't have yet. */ + missingCount?: number; + /** Connected strings the source page lost since the copy was made. */ + removedCount?: number; + error?: string; + }; + +/** + * Messages sent from the UI iframe back to the main thread (Plugin sandbox). + */ +export type UiToMain = + | { type: "ui-ready" } + | { type: "resize"; size: WindowSize } + | { type: "close" } + | { type: "notify"; text: string; error?: boolean } + | { type: "open-external"; url: string } + | { type: "save-config"; config: Partial } + /** + * Persist a project id resolved from the API key during a successful + * `Test Connection` in the design-mode UI. The main thread writes this + * into the document-scoped config so the inspect (Dev Mode) UI can build + * project-aware deep links without performing its own API validation. + */ + | { type: "persist-project-id"; projectId: number } + | { type: "reset" } + | { type: "set-language"; language: string } + | { type: "set-branch"; branch: string } + /** + * Request every connected text node on the current page, independent of + * the user's current selection. Used by Pull when the language changes so + * the new translations land on the whole page, not just selected layers. + */ + | { type: "request-page-connected-nodes"; correlationId: string } + | { + type: "set-nodes-data"; + correlationId: string; + nodes: Array<{ id: string; info: Partial }>; + } + /** + * Resolve the parent placeholder names ({component}/{frame}/…) for specific + * nodes on demand. The selection scan only fills these when the SAVED format + * uses them; the bulk "Generate key names" action lets the user type an + * ad-hoc template, so it asks for them here right before formatting. + */ + | { type: "resolve-parent-names"; correlationId: string; nodeIds: string[] } + | { + type: "apply-translations"; + correlationId: string; + updates: Array<{ + id: string; + /** Final, ICU-formatted text to write into the TextNode. */ + text: string; + /** Raw translation source to persist into plugin data. */ + translation: string; + /** Optional plural flag to update along with the translation. */ + isPlural?: boolean; + /** Optional plural parameter name when isPlural === true. */ + pluralParamValue?: string; + /** Optional sample parameter values for ICU preview. */ + paramsValues?: Record; + /** Optional key updates so a single round-trip can both label and + re-render the node (used by StringDetails save). */ + key?: string; + ns?: string; + connected?: boolean; + }>; + } + | { + type: "request-screenshots"; + correlationId: string; + nodeIds: string[]; + } + | { type: "scroll-to-node"; id: string } + | { + type: "create-copy"; + correlationId: string; + mode: "keys" | "languages"; + /** Required when `mode === "languages"`. List of language tags to copy. + * The translations themselves never cross the bridge: the main thread + * only clones and returns the clones' connected nodes (see + * `create-copy-result.pages`); the UI renders and applies the text via + * `apply-translations` — ICU rendering needs `Intl`, which Figma's + * main-thread sandbox doesn't have. */ + languages?: string[]; + /** + * Only set by CopyView's "Recreate copy" — `figma.currentPage` there is + * the copy itself, not the page to clone from. Omitted for the normal + * Index-invoked "Create page" flow (defaults to `figma.currentPage`). + */ + sourcePageId?: string; + /** + * `mode: "keys"` only — whether to prefix the written key label with its + * namespace (`ns.key` vs plain `key`), matching `namespacedKeyLabel`'s + * gate everywhere else in the app. The main thread has no access to + * `auth.value.namespacesEnabled` (a UI-side, API-derived flag, not a + * persisted setting `readMergedConfig` can see), so the UI sends it. + */ + namespacesEnabled?: boolean; + } + | { type: "request-copy-staleness"; correlationId: string }; diff --git a/src/shared/richText.ts b/src/shared/richText.ts new file mode 100644 index 00000000..4e674e1c --- /dev/null +++ b/src/shared/richText.ts @@ -0,0 +1,31 @@ +/** + * The plain-text side of Tolgee's inline markup, shared between the two + * places that must agree on it EXACTLY: + * + * - `$main/text/applyRichText` — derives the string written into + * `TextNode.characters` (tags become font ranges, `
` becomes "\n"), + * - `$ui/lib/logic/pullDiff` — decides whether a canvas has "drifted" from + * its translation by comparing `characters` against what apply WOULD write. + * + * If these two transforms diverge, every formatted string looks permanently + * "drifted" and re-downloads forever — that's the bug that motivated + * extracting this module, not hypothetical reuse. + */ + +/** Every `
` spelling Tolgee's editor can produce (also the malformed + * `

` pair some tooling emits) — replaced with a real newline + * BEFORE generic tag stripping, which would otherwise just delete it. */ +export const LINE_BREAK_TAG_REGEX = /\s*<\/br>||<\/br>/gi; + +/** Remove every remaining tag. Deliberately generic (``), matching + * what `applyRichText` has always done — unknown tags disappear from the + * canvas rather than showing up literally. */ +export function stripMarkupTags(text: string): string { + return text.replace(/<[^>]*>/g, ""); +} + +/** The exact string `applyRichText` will put into `TextNode.characters` for + * a rendered translation: line-break tags → newlines, all other tags gone. */ +export function plainCanvasText(formatted: string): string { + return stripMarkupTags(formatted.replace(LINE_BREAK_TAG_REGEX, "\n")); +} diff --git a/src/shared/tolgeeFormat.ts b/src/shared/tolgeeFormat.ts new file mode 100644 index 00000000..e6f7f365 --- /dev/null +++ b/src/shared/tolgeeFormat.ts @@ -0,0 +1,662 @@ +/** + * Local, dependency-free re-implementation of the helpers from + * `@tginternal/editor` that the plugin needs — without dragging in + * `@codemirror/{state,view}` (~500 kB raw / ~125 kB gzip). + * + * Exports: + * - `getTolgeeFormat` — ICU → TolgeeFormat (for push diffing) + * - `tolgeeFormatGenerateIcu` — TolgeeFormat → ICU string + * - `tokenize` — ICU → Token[] for syntax highlighting + * - `getPluralVariants` — CLDR plural categories for a locale + * - `getVariantExample` — example number for a plural category + * + * Behavior we mirror for `getTolgeeFormat`: + * + * - `plural=false` → `{ variants: { other: input } }` (verbatim, no parsing) + * - `plural=true` and the input parses as a top-level ICU plural form + * `{paramName, plural, variant1 {body1} variant2 {body2} ...}` → + * `{ parameter, variants: { variant1: body1, ... } }` + * - any parse failure → falls back to `{ variants: { other: input } }` + * + * NOTE: the result is intended to be compared via `JSON.stringify`, so the + * exact key ordering of `variants` doesn't matter as long as both sides go + * through this same parser. + */ + +export type TolgeeFormat = { + parameter?: string; + variants: Record; +}; + +const PLURAL_KEYWORD = "plural"; + +export function getTolgeeFormat(input: string, plural: boolean, _raw: boolean): TolgeeFormat { + if (!plural) { + return { variants: { other: input } }; + } + + const parsed = parsePlural(input); + if (!parsed) { + return { variants: { other: input } }; + } + return parsed; +} + +/** + * Parse exactly one top-level ICU plural expression. Returns `null` for any + * input that isn't a single, well-formed plural expression. + */ +function parsePlural(input: string): TolgeeFormat | null { + // Find outermost `{ ... }`. We require the whole input (after trimming + // surrounding whitespace) to be ONE plural form — anything else falls back + // to "no plural", matching the upstream lezer parser behavior. + const trimmed = input.trim(); + if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null; + + // Walk respecting ICU `'` escapes so we don't get tripped by literal braces. + const closeIdx = findMatchingBrace(trimmed, 0); + if (closeIdx !== trimmed.length - 1) return null; + + // Inside the outermost braces: `paramName, plural, variant {body} ...` + const inner = trimmed.slice(1, -1); + + // Find first comma (paramName boundary). `paramName` doesn't contain braces + // or commas in a well-formed plural form. + const firstComma = inner.indexOf(","); + if (firstComma < 0) return null; + const parameter = inner.slice(0, firstComma).trim(); + if (!parameter) return null; + + // Next token must be `plural`. + const afterParam = inner.slice(firstComma + 1).trimStart(); + if (!afterParam.startsWith(PLURAL_KEYWORD)) return null; + const afterKeyword = afterParam.slice(PLURAL_KEYWORD.length); + // Must be followed by whitespace + `,`. + const ws = afterKeyword.match(/^\s*,/); + if (!ws) return null; + let rest = afterKeyword.slice(ws[0].length); + + // Now parse zero or more `variantName {body}` chunks. + const variants: Record = {}; + while (true) { + rest = rest.replace(/^\s+/, ""); + if (rest.length === 0) break; + + // Variant name: any non-whitespace, non-`{` run. ICU allows `=0`, `=1`, + // `zero`, `one`, etc. — we don't validate the alphabet, we just split on + // the first `{`. + const braceAt = rest.indexOf("{"); + if (braceAt < 0) return null; + const variantName = rest.slice(0, braceAt).trim(); + if (!variantName) return null; + // Disallow further commas in variant header (would mean malformed input + // we shouldn't try to interpret as plural). + if (variantName.includes(",")) return null; + + const variantClose = findMatchingBrace(rest, braceAt); + if (variantClose < 0) return null; + const body = rest.slice(braceAt + 1, variantClose); + + // Duplicate variant — treat as malformed (mirrors upstream lezer + // behavior of returning "no plural" on `nodes[Expression].nextSibling`). + if (Object.prototype.hasOwnProperty.call(variants, variantName)) { + return null; + } + + variants[variantName] = body; + rest = rest.slice(variantClose + 1); + } + + if (Object.keys(variants).length === 0) return null; + + return { parameter, variants }; +} + +/** + * Returns the index of the `}` that matches the `{` at `start`, accounting + * for ICU `'`-escapes (e.g. `'{'` is a literal open brace and must be + * skipped). Returns `-1` if no match is found. + * + * Exported so the tokenizer can use it outside `parsePlural`. + */ +function findMatchingBrace(input: string, start: number): number { + if (input[start] !== "{") return -1; + let depth = 0; + let i = start; + while (i < input.length) { + const ch = input[i]; + if (ch === "'") { + // ICU escape: `''` is a literal `'`. `'{`, `'}` or `'#` starts a + // literal run that continues until the next `'`. + const next = input[i + 1]; + if (next === "'") { + i += 2; + continue; + } + if (next === "{" || next === "}" || next === "#") { + // Skip the literal run until the closing `'` (or end of input). + const close = input.indexOf("'", i + 2); + if (close < 0) { + // unterminated — treat the rest as literal + return -1; + } + i = close + 1; + continue; + } + // Lone `'` not followed by an escapable — just advance. + i += 1; + continue; + } + if (ch === "{") { + depth += 1; + } else if (ch === "}") { + depth -= 1; + if (depth === 0) return i; + if (depth < 0) return -1; + } + i += 1; + } + return -1; +} + +/** + * If an ICU `'`-escape starts at `i`, the index just past it; `-1` when `i` + * isn't an escape at all. Unterminated runs report the end of the string, so a + * caller consumes the rest as literal rather than looping. + * + * Every brace-aware pass in this file (`findMatchingBrace` inline, the escaper, + * the scanners) has to agree on these rules — `''` is a literal apostrophe, + * `'{` / `'}` / `'#` open a literal run that ends at the next `'`, and a lone + * `'` is just an apostrophe — so they share one implementation of them. + */ +function skipEscape(input: string, i: number): number { + if (input[i] !== "'") return -1; + const next = input[i + 1]; + if (next === "'") return i + 2; + if (next === "{" || next === "}" || next === "#") { + const close = input.indexOf("'", i + 2); + return close < 0 ? input.length : close + 1; + } + return i + 1; +} + +/** + * Does `input` contain a real ICU argument (`{name}`, `{count, plural, …}`)? + * + * A brace pair is NOT enough: Tolgee stores a plain translation with literal + * braces as ICU-escaped text (`Use '{'braces'}'`), and treating that as an + * argument makes callers read the string as "advanced" — which, in + * `textOfNode`, means the stored translation outranks the live canvas + * `characters` and a designer's edit is silently dropped from the push. + */ +export function hasIcuArgument(input: string): boolean { + let i = 0; + while (i < input.length) { + const skipped = skipEscape(input, i); + if (skipped >= 0) { + i = skipped; + continue; + } + if (input[i] === "{") { + const close = findMatchingBrace(input, i); + if (close > i) { + if (ICU_ARGUMENT_RE.test(input.slice(i + 1, close))) return true; + i = close + 1; + continue; + } + } + i += 1; + } + return false; +} + +/** Header of a plural argument: `name, plural,` — the `{` and the variants + * are matched separately by the walker below. */ +const PLURAL_HEADER_RE = /^\s*([\w.-]+)\s*,\s*plural\s*,/; + +/** The argument NAME at the head of `{name}` / `{name, type, …}`. */ +const ARGUMENT_NAME_RE = /^\s*([\w.-]+)/; + +/** An argument that is followed by a VARIANT LIST rather than by a value — + * `plural`, `select`, `selectordinal`. */ +const VARIANT_HEADER_RE = /^\s*[\w.-]+\s*,\s*(?:plural|select|selectordinal)\s*,/i; + +/** + * Visit the body of every ARGUMENT `{…}` run in `input`, at any depth. + * + * Walks by the same `'`-escape rules `findMatchingBrace` uses, so an escaped + * brace never opens a phantom run. + * + * The `inVariantList` flag is what keeps this honest. Inside a plural or + * select, the immediate `{…}` runs are variant BODIES, not arguments — in + * `{gender, select, other {x}}`, `{x}` is the body of `other`, and reporting + * it as an argument named `x` would invent a parameter nobody declared. Their + * CONTENTS are ordinary again, so `one {Hello {name}}` still yields `name`. + */ +function walkIcuArguments( + input: string, + visit: (inner: string) => void, + inVariantList = false, +): void { + let i = 0; + while (i < input.length) { + const skipped = skipEscape(input, i); + if (skipped >= 0) { + i = skipped; + continue; + } + if (input[i] === "{") { + const close = findMatchingBrace(input, i); + if (close > i) { + const inner = input.slice(i + 1, close); + if (inVariantList) { + // A variant body: not an argument, but its contents can hold them. + walkIcuArguments(inner, visit, false); + } else { + visit(inner); + // `inner` is strictly shorter, so this terminates. + walkIcuArguments(inner, visit, VARIANT_HEADER_RE.test(inner)); + } + i = close + 1; + continue; + } + } + i += 1; + } +} + +/** + * Every plural argument NAME in `input`, at any nesting depth. + * + * Deliberately NOT `getTolgeeFormat(...).parameter`: that one answers "is this + * whole string one plural form?" — the right question for push diffing, and it + * rejects `"{count, plural, …} left"` because the string doesn't END at the + * plural's closing brace. Callers that only need to know "does a plural live + * somewhere in here, and what is it called?" (seeding a sample count before a + * render) must not reuse that stricter answer: a plural with any literal text + * around it would go unseeded and `IntlMessageFormat` would throw + * `MISSING_VALUE` on it. + */ +export function findPluralParameters(input: string): string[] { + const out: string[] = []; + walkIcuArguments(input, (inner) => { + const name = inner.match(PLURAL_HEADER_RE)?.[1]; + if (name && !out.includes(name)) out.push(name); + }); + return out; +} + +/** + * Every ICU argument NAME in `input`, at any nesting depth — `{name}`, + * `{count, plural, …}`, `{gender, select, …}`. + * + * A regex can't do this: an argument whose own body contains braces (any + * `select`, or a plural wrapping one) has no bounded `[^{}]*` tail to match, + * so it goes unseen. That's not academic — an unseeded argument makes + * `IntlMessageFormat` throw `MISSING_VALUE`, which drops the node in + * `copyApply` and leaves stale text on the canvas after Pull. + */ +export function findArgumentNames(input: string): string[] { + const out: string[] = []; + walkIcuArguments(input, (inner) => { + if (!ICU_ARGUMENT_RE.test(inner)) return; + const name = inner.match(ARGUMENT_NAME_RE)?.[1]; + if (name && !out.includes(name)) out.push(name); + }); + return out; +} + +// ============================================================ +// ICU generator +// ============================================================ + +/** + * Does the text between a `{` and its matching `}` read as a real ICU + * argument (`{name}`, `{count, plural, …}`) rather than prose that happens to + * sit between braces (`{not an argument}`)? An ICU argument name is one + * unbroken token, optionally followed by `,` and the rest of the argument. + */ +const ICU_ARGUMENT_RE = /^\s*[\w.-]+\s*(?:,[\s\S]*)?$/; + +/** + * Escape *literal* ICU syntax characters (`{` and `}`) inside a plural variant + * body so the body can never be misread as starting/ending a nested ICU + * argument. ICU's own escaping wraps a literal brace in single quotes: `{` → + * `'{'`, `}` → `'}'`. + * + * Three kinds of brace have to be told apart, which a blind replace cannot do: + * + * - a real nested argument (`One for {name}`) — must survive verbatim, or it + * stops interpolating and gets uploaded in that corrupted form; + * - an already-escaped literal (`a '{' b`) — escaping it again yields + * `a ''{'' b`, a literal apostrophe followed by an *unclosed* argument that + * `intl-messageformat` throws on; + * - a genuinely stray brace (`co{unt`) — the only one that needs escaping. + * + * So walk the string once by the same `'`-escape rules `findMatchingBrace` + * uses, skip over both of the first two kinds, and wrap only what is left. + */ +function escapeIcuBraces(text: string): string { + let out = ""; + let i = 0; + + while (i < text.length) { + // An escaped run (or a bare apostrophe) is copied through untouched — + // escaping it again would turn `a '{' b` into `a ''{'' b`, an unclosed + // argument. An unterminated run reports end-of-string, so the rest is + // copied verbatim rather than half-escaped. + const skipped = skipEscape(text, i); + if (skipped >= 0) { + out += text.slice(i, skipped); + i = skipped; + continue; + } + + const ch = text[i]; + + if (ch === "{") { + const close = findMatchingBrace(text, i); + if (close > i && ICU_ARGUMENT_RE.test(text.slice(i + 1, close))) { + out += text.slice(i, close + 1); + i = close + 1; + continue; + } + out += "'{'"; + i += 1; + continue; + } + + // Every `}` closing a real argument was consumed by the branch above, so + // whatever reaches here is stray. + if (ch === "}") { + out += "'}'"; + i += 1; + continue; + } + + out += ch; + i += 1; + } + + return out; +} + +/** + * Reconstruct an ICU plural string from a `TolgeeFormat` object. + * Mirrors `tolgeeFormatGenerateIcu` from `@tginternal/editor`. + * + * Two invariants the raw structural rebuild above didn't enforce, both + * fixed here to match upstream and keep the result parseable by + * `intl-messageformat`: + * - a missing `other` variant is force-appended as `other {}` — ICU + * requires every plural to have an `other` fallback; + * - literal `{`/`}` inside a variant body are escaped (see + * `escapeIcuBraces`) so they can't be mistaken for the start/end of a + * nested argument. + * If every variant body ends up empty, the plural wrapper is pointless + * (and, per upstream, actively dropped) — return `""` instead. + */ +export function tolgeeFormatGenerateIcu(format: TolgeeFormat, _raw: boolean): string { + const { parameter, variants } = format; + if (!parameter) return variants.other ?? ""; + + // Force-append `other {}` when absent, preserving key order otherwise — + // matches upstream's `s.other || (s.other = "")`. + const withOther: Record = + variants.other !== undefined ? variants : { ...variants, other: "" }; + + // All variant bodies empty (or undefined) → no meaningful plural to emit. + const allEmpty = Object.values(withOther).every((v) => !v); + if (allEmpty) return ""; + + const parts = Object.entries(withOther) + .filter((entry): entry is [string, string] => entry[1] !== undefined) + .map(([k, v]) => `${k} {${escapeIcuBraces(v)}}`) + .join(" "); + return `{${parameter}, plural, ${parts}}`; +} + +// ============================================================ +// Plural variant helpers +// ============================================================ + +const CLDR_ORDER = ["zero", "one", "two", "few", "many", "other"] as const; + +// `new Intl.PluralRules(locale)` allocates locale data and is the costly part of +// the helpers below. They run inside the plural editor's per-keystroke render +// loop (once per variant), so cache the instance per locale — locales are few +// and the rules are immutable. +const pluralRulesCache = new Map(); +function pluralRules(locale: string): Intl.PluralRules { + let rules = pluralRulesCache.get(locale); + if (!rules) { + rules = new Intl.PluralRules(locale); + pluralRulesCache.set(locale, rules); + } + return rules; +} + +/** + * Return the CLDR plural categories active for `locale`, in canonical order. + * Uses `Intl.PluralRules` probing for compatibility with all environments. + * Mirrors `getPluralVariants` from `@tginternal/editor`. + */ +export function getPluralVariants(locale: string): string[] { + try { + const rules = pluralRules(locale); + const found = new Set(["other"]); + const probes: Array<[Intl.LDMLPluralRule, number[]]> = [ + ["zero", [0]], + ["one", [1]], + ["two", [2]], + ["few", [3, 4, 5]], + ["many", [11, 20, 100]], + ]; + for (const [cat, values] of probes) { + for (const n of values) { + if (rules.select(n) === cat) { + found.add(cat); + break; + } + } + } + return CLDR_ORDER.filter((c) => found.has(c)); + } catch { + return ["one", "other"]; + } +} + +/** + * Return a representative example number for `variant` in `locale`. + * Numeric selectors like `=0` return their own value. + * Mirrors `getVariantExample` from `@tginternal/editor`. + */ +export function getVariantExample(locale: string, variant: string): number | undefined { + if (variant.startsWith("=")) { + const n = Number(variant.slice(1)); + return Number.isNaN(n) ? undefined : n; + } + try { + const rules = pluralRules(locale); + // Probe "nice" representative numbers in priority order — same examples + // Tolgee's editor shows (en: one→1, other→10). `0` is intentionally last so + // the "other" form shows a real plural count, not the zero edge case; the + // fractional probes resolve categories like Czech "many". + for (const n of [1, 10, 100, 2, 3, 4, 5, 0, 1.5, 0.5]) { + if (rules.select(n) === variant) return n; + } + return undefined; + } catch { + return undefined; + } +} + +/** + * Which CLDR plural category a concrete number falls into for `locale` + * (e.g. en: 1 → "one", 6 → "other"; cs: 2 → "few", 5 → "other"). Used to drop a + * literal count from a string into the matching plural form. Falls back to + * "other" for unknown locales. + */ +export function getPluralCategory(locale: string, n: number): string { + try { + return pluralRules(locale).select(n); + } catch { + return "other"; + } +} + +// ============================================================ +// Tokenizer for IcuTextarea syntax highlighting +// ============================================================ + +/** Distinct highlight roles for the `IcuTextarea` overlay. */ +export type TokenKind = "text" | "variable" | "keyword" | "hash" | "tag"; +export type Token = { kind: TokenKind; text: string }; + +const INLINE_TAG_RE = /^<\/?(?:b|strong|i|em|u|br)\s*\/?>/i; + +/** + * Tokenize a Tolgee/ICU string for the `IcuTextarea` highlight overlay. + * + * `nested` — set `true` when the input is a plural variant body so bare `#` + * is highlighted as a `hash` token instead of plain text. + */ +export function tokenize(input: string, nested = false): Token[] { + const out: Token[] = []; + _scanText(input, 0, input.length, nested, out); + return out; +} + +function _scanText(input: string, from: number, to: number, inPlural: boolean, out: Token[]): void { + let textStart = from; + let i = from; + + const flush = (end: number) => { + if (end > textStart) out.push({ kind: "text", text: input.slice(textStart, end) }); + textStart = end; + }; + + while (i < to) { + const ch = input[i]; + + if (ch === "#" && inPlural) { + flush(i); + out.push({ kind: "hash", text: "#" }); + i++; + textStart = i; + continue; + } + + if (ch === "<") { + const slice = input.slice(i, Math.min(i + 25, to)); + const m = slice.match(INLINE_TAG_RE); + if (m) { + flush(i); + out.push({ kind: "tag", text: m[0] }); + i += m[0].length; + textStart = i; + continue; + } + } + + if (ch === "{") { + const close = findMatchingBrace(input, i); + if (close > i && close < to) { + flush(i); + _scanBlock(input, i, close, out); + i = close + 1; + textStart = i; + continue; + } + } + + i++; + } + + flush(to); +} + +function _scanBlock(input: string, open: number, close: number, out: Token[]): void { + const inner = input.slice(open + 1, close); + + // Plural / select structure: `paramName , keyword , variants` + const m = inner.match(/^(\w+)(\s*,\s*)(plural|select|selectordinal)(\s*,\s*)/i); + if (m) { + const variantsFrom = open + 1 + m[0].length; + out.push({ kind: "text", text: "{" }); + out.push({ kind: "variable", text: m[1] ?? "" }); + out.push({ kind: "text", text: m[2] ?? "" }); + out.push({ kind: "keyword", text: m[3] ?? "" }); + out.push({ kind: "text", text: m[4] ?? "" }); + _scanVariants(input, variantsFrom, close, out); + out.push({ kind: "text", text: "}" }); + return; + } + + // Simple `{name}` — emit braces + name as a single variable token so the + // pill background in the overlay covers the whole placeholder. + if (/^\s*\w+\s*$/.test(inner)) { + out.push({ kind: "variable", text: input.slice(open, close + 1) }); + return; + } + + // Unknown format — emit the whole block in variable colour + out.push({ kind: "variable", text: input.slice(open, close + 1) }); +} + +function _scanVariants(input: string, from: number, to: number, out: Token[]): void { + let i = from; + let textStart = from; + + const flush = (end: number) => { + if (end > textStart) out.push({ kind: "text", text: input.slice(textStart, end) }); + textStart = end; + }; + + while (i < to) { + // Leading whitespace + const wsM = input.slice(i, to).match(/^\s+/); + if (wsM) { + flush(i); + out.push({ kind: "text", text: wsM[0] }); + i += wsM[0].length; + textStart = i; + continue; + } + + // Variant name: `one`, `other`, `few`, `=0`, … + const nameM = input.slice(i, to).match(/^(=\d+|[a-z]+)/i); + if (nameM) { + flush(i); + out.push({ kind: "keyword", text: nameM[0] }); + i += nameM[0].length; + textStart = i; + + // Optional whitespace before `{` + const ws2 = input.slice(i, to).match(/^\s*/); + if (ws2?.[0]) { + out.push({ kind: "text", text: ws2[0] }); + i += ws2[0].length; + textStart = i; + } + + // Variant body `{…}` + if (input[i] === "{") { + const bodyClose = findMatchingBrace(input, i); + if (bodyClose > i && bodyClose < to) { + out.push({ kind: "text", text: "{" }); + _scanText(input, i + 1, bodyClose, true, out); + out.push({ kind: "text", text: "}" }); + i = bodyClose + 1; + textStart = i; + continue; + } + } + continue; + } + + i++; + } + + flush(to); +} diff --git a/src/shared/types.ts b/src/shared/types.ts new file mode 100644 index 00000000..b0f4a1b1 --- /dev/null +++ b/src/shared/types.ts @@ -0,0 +1,182 @@ +export type SizeInfo = { + width: number; + height: number; +}; + +export type FrameInfo = SizeInfo & { + id: string; + name: string; +}; + +export type PositionInfo = { + x: number; + y: number; +}; + +export type FrameScreenshot = { + image: Uint8Array; + info: FrameInfo; + keys: (NodeInfo & SizeInfo & PositionInfo)[]; +}; + +export interface NodeInfo { + /** Sample COUNT for the plural variable (a number, e.g. "1"/"10"), as the + * published plugin persisted it — the plural variable's NAME is read from the + * ICU, not stored here. Rendering also accepts the count from + * `paramsValues[name]` (how this UI edits it), see `interpolate.renderParams`. */ + pluralParamValue?: string; + /** Sample values for ICU params, keyed by param NAME (incl. the plural one). */ + paramsValues?: Record; + name: string; + characters: string; + translation: string; + id: string; + isPlural: boolean; + key: string; + ns: string | undefined; + connected: boolean; + /** + * The exact key value the auto-prefill last generated for this node + * (persisted). Provenance marker so turning "Prefill key format" off can wipe + * ONLY untouched auto keys: `clearPrefilledKeys` clears the key only while it + * still equals this. Any manual edit changes `key` but not this marker, so + * hand-typed/edited keys are preserved; a format change re-generates both, so + * they stay in sync. Absent on manually-keyed nodes and on data from the + * published plugin (which ignores this extra field — rollback-safe). */ + prefilledKey?: string; + visible?: boolean; + /** + * Names of the node's relevant ancestors, resolved from the live Figma tree + * (like `name`/`characters`, NOT persisted). They feed the parent key-format + * placeholders — `{component}` / `{instance}` / `{frame}` / `{artboard}` / + * `{section}` / `{group}`. Computed only when the configured `keyFormat` + * actually uses one (see `getSelectionInfo`); otherwise left undefined. + */ + component?: string; + /** Layer name of the nearest component INSTANCE, feeding `{instance}` — + * distinct from `component` (the main component it was created from). */ + instance?: string; + frame?: string; + artboard?: string; + section?: string; + group?: string; +} + +export type PartialNodeInfo = Partial & { + id: string; +}; + +export type GlobalSettings = { + apiUrl: string; + apiKey: string; + ignorePrefix: string; + /** Ignore pure-integer strings ("100", "42") — matches the original plugin. */ + ignoreNumbers: boolean; + /** Opt-in extension of `ignoreNumbers`: also ignore formatted numbers — + * decimals, thousands separators and signs ("1,234.00", "3.14", "+420"). + * Off by default so the base behaviour stays pure-integer-only. */ + ignoreFormattedNumbers?: boolean; + updateScreenshots?: boolean; + addTags?: boolean; + tags?: string[]; + /** Whether to prefill the key name with the key format */ + prefillKeyFormat?: boolean; + /** + * A string that can contain some of the following placeholders and custom separators + * in order to generate a key name for the node. + * + * `{artboard}` + * + * `{frame}` + * + * `{elementName}` or `{elementText}` + * + * `{component}` + * + * `{section}` or `{group}` + */ + keyFormat?: string; + ignoreHiddenLayers?: boolean; + ignoreHiddenLayersIncludingChildren?: boolean; + ignoreTextLayers?: boolean; + variableCasing?: + /** "keep original format" — no transformation. Stored as "" (matches the + * original plugin), and the default when unset. */ + | "" + | "snake_case" + | "snake_case_capitalized" + | "camelCase" + | "PascalCase" + | "noSpaces"; +}; + +export type CurrentDocumentSettings = GlobalSettings & { + namespace: string; + branch?: string; + documentInfo: true; + /** + * Project id resolved from the API key during connection validation. + * Persisted at the document scope so the inspect (Dev Mode) UI — which + * cannot perform its own API validation — can construct project-aware + * deep links into the Tolgee web app. + */ + projectId?: number; +}; + +export type CurrentPageSettings = { + language: string; + pageInfo: boolean; + pageCopy: boolean; + pageStringDetails: boolean; + pageSettings: boolean; + nodeInfo?: NodeInfo; + /** + * The Figma page id this copy was cloned from — set only on pages created + * by `createCopy` (both "keys" and "languages" modes). Lets `CopyView` + * detect when the source page gained new connected keys since the copy + * was made (Download only refreshes text on keys the copy already has, it + * can't discover new ones) and offer to recreate the copy from scratch. + * Absent on copies from an older plugin version or from production — + * the staleness check just skips itself when it's missing. + */ + sourcePageId?: string; + /** + * The language a "languages" copy was created in — an IMMUTABLE property of + * the copy, written once by `createCopy`/`markPageAsCopy`. Distinct from + * `language` (the selectable Push/Pull language, which shares the page scope + * and can be repointed): `CopyView` must Download/Recreate in the language the + * copy actually holds, not whatever `language` currently reads, so it reads + * `copyLanguage` first. Absent on "keys" copies and pre-existing copies. + */ + copyLanguage?: string; +}; + +export type TolgeeConfig = CurrentDocumentSettings & CurrentPageSettings; + +export type FormattedNode = { + characters: string; + id: string; + name: string; +}; + +export type WindowSize = { + width: number; + height: number; +}; + +export type InitialState = { + config: Partial | null; + selectedNodes: Array; + allNodes: Array; +}; + +export type Route = + | { name: "index" } + | { name: "pageSetup" } + | { name: "copyView" } + | { name: "settings"; tab?: "project" | "strings" | "upload" } + | { name: "push" } + | { name: "pull"; lang: string } + | { name: "connect"; node: NodeInfo; bulkNodes?: NodeInfo[] } + | { name: "stringDetails"; node: NodeInfo } + | { name: "createCopy" }; diff --git a/src/shared/version.ts b/src/shared/version.ts new file mode 100644 index 00000000..487ab857 --- /dev/null +++ b/src/shared/version.ts @@ -0,0 +1,20 @@ +/** + * Release info shown as the quiet version tag in the Settings action bar + * (bottom-left, next to Cancel/Save). + * + * Policy: bump when we ship NEW functionality — not for pure bug-fix builds. + * Kept as plain constants (not the build date) so a rebuild without a version + * change stays byte-identical, which the delivery sync relies on. Keep + * `package.json`'s `version` in step for tooling. + */ +export const APP_VERSION = "2.0.0"; + +/** ISO date of this release. */ +export const APP_RELEASED = "2026-07-24"; + +/** + * The plugin's Figma Community page. Derived from the manifest plugin id + * (`scripts/build-manifest.mjs`). If the published slug differs, set the full + * URL here. + */ +export const FIGMA_PLUGIN_URL = "https://www.figma.com/community/plugin/1212381421658754793"; diff --git a/src/tools/compareNs.ts b/src/tools/compareNs.ts deleted file mode 100644 index 05c8a476..00000000 --- a/src/tools/compareNs.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const compareNs = (ns1: string | undefined, ns2: string | undefined) => { - return (ns1 || "") === (ns2 || ""); -}; diff --git a/src/tools/createProvider.tsx b/src/tools/createProvider.tsx deleted file mode 100644 index 1c4b8c9b..00000000 --- a/src/tools/createProvider.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { h, FunctionComponent, createContext } from "preact"; -import { useMemo, useRef, useContext } from "preact/hooks"; -import { - createContext as createContextSelectable, - useContextSelector, -} from "use-context-selector"; - -type SelectorType = (state: StateType) => ReturnType; - -export const createProvider = ( - controller: (props: ProviderProps) => [state: StateType, actions: Actions], -) => { - const StateContext = createContextSelectable(null as any); - const DispatchContext = createContext(null as any); - - const Provider: FunctionComponent = ({ - children, - ...props - }) => { - const [state, _actions] = controller(props as any); - const actionsRef = useRef(_actions); - - actionsRef.current = _actions; - - // stable actions - const actions = useMemo(() => { - const result = {} as any; - Object.keys(actionsRef.current as any).map((key) => { - result[key] = (...args: any[]) => - ((actionsRef.current as any)[key] as CallableFunction)?.(...args); - }); - return result as Actions; - }, [actionsRef]); - - return ( - - - {children} - - - ); - }; - - const useActions = () => useContext(DispatchContext); - const useStateContext = function ( - selector: SelectorType, - ) { - return useContextSelector(StateContext, selector); - }; - - return [Provider, useActions, useStateContext] as const; -}; diff --git a/src/tools/getConflictingNodes.ts b/src/tools/getConflictingNodes.ts deleted file mode 100644 index d08bf494..00000000 --- a/src/tools/getConflictingNodes.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NodeInfo } from "@/types"; - -export type Conflicts = Record; - -export const getConflictingNodes = (nodes: NodeInfo[]) => { - const groupMap = new Map(); - for (const node of nodes) { - if (!node.key) continue; - const groupKey = `${node.key}\0${node.ns ?? ""}`; - const group = groupMap.get(groupKey); - if (group) { - group.push(node); - } else { - groupMap.set(groupKey, [node]); - } - } - - const conflictingNodes: NodeInfo[] = []; - for (const group of groupMap.values()) { - if (group.length < 2) continue; - const hasConflict = group.some( - (n, i) => i > 0 && n.characters !== group[0].characters, - ); - if (hasConflict) { - conflictingNodes.push(...group); - } - } - return conflictingNodes; -}; diff --git a/src/tools/getConnectedNodes.ts b/src/tools/getConnectedNodes.ts deleted file mode 100644 index f8a06a65..00000000 --- a/src/tools/getConnectedNodes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NodeInfo } from "@/types"; - -export const getConnectedNodes = (nodes: NodeInfo[]) => { - return nodes.filter((n) => n.connected); -}; - -export const getNodesWithKey = (nodes: NodeInfo[]) => { - return nodes.filter((n) => n.key); -}; diff --git a/src/tools/getPullChanges.ts b/src/tools/getPullChanges.ts deleted file mode 100644 index 22040357..00000000 --- a/src/tools/getPullChanges.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { NodeInfo, PartialNodeInfo } from "@/types"; -import { TranslationData } from "../ui/client/types"; -import { createFormatIcu } from "../createFormatIcu"; -import { getTolgeeFormat } from "@tginternal/editor"; - -export const getPullChanges = ( - nodes: NodeInfo[], - lang: string, - keys: TranslationData, - hasNamespacesEnabled = true, -) => { - const changedNodes: NodeInfo[] = []; - const missingKeys: PartialNodeInfo[] = []; - const formatter = createFormatIcu(); - - nodes.forEach((node) => { - const nsLookup = hasNamespacesEnabled ? (node.ns ?? "") : ""; - const value = keys?.[nsLookup]?.[node.key]; - - if (value?.translation) { - if ( - value.translation !== node.translation || - value.keyIsPlural !== node.isPlural - ) { - const tolgeeValue = getTolgeeFormat( - value.translation, - value.keyIsPlural, - false, - ); - try { - const formatted = formatter.format({ - language: lang, - translation: value.translation, - params: { - ...node.paramsValues, - [tolgeeValue.parameter ?? ""]: node.pluralParamValue ?? "1", - }, - }); - changedNodes.push({ - ...node, - isPlural: value.keyIsPlural, - characters: formatted, - translation: value.translation, - }); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - changedNodes.push({ - ...node, - isPlural: value.keyIsPlural, - characters: value.translation, - translation: value.translation, - }); - } - } else if ( - node.characters !== value.translation && - !node.isPlural && - (!node.paramsValues || Object.keys(node.paramsValues).length === 0) - ) { - changedNodes.push({ - ...node, - isPlural: value.keyIsPlural, - characters: value.translation, - translation: value.translation, - }); - } - } else { - missingKeys.push({ id: node.id, key: node.key, ns: node.ns }); - } - }); - - return { changedNodes, missingKeys }; -}; diff --git a/src/tools/getPushChanges.ts b/src/tools/getPushChanges.ts deleted file mode 100644 index b387c904..00000000 --- a/src/tools/getPushChanges.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { FrameScreenshot, NodeInfo, TolgeeConfig } from "@/types"; -import { TranslationData } from "../ui/client/types"; -import { getTolgeeFormat } from "@tginternal/editor"; -import { stringFormatter } from "../main/utils/textFormattingTools"; - -export type KeyChangeValue = { - key: string; - ns: string | undefined; - oldValue?: string; - newValue: string; - isPlural: boolean; - oldIsPlural?: boolean; - screenshots: FrameScreenshot[]; -}; - -export type KeyChanges = { - newKeys: KeyChangeValue[]; - changedKeys: KeyChangeValue[]; - unchangedKeys: KeyChangeValue[]; - screenshots: FrameScreenshot[]; -}; - -export const getPushChanges = ( - nodes: NodeInfo[], - translations: TranslationData, - hasNamespacesEnabled: boolean, - screenshots: FrameScreenshot[], - tolgeeConfig: Partial | null, -): KeyChanges => { - const newKeys: KeyChangeValue[] = []; - const changedKeys: KeyChangeValue[] = []; - const unchangedKeys: KeyChangeValue[] = []; - - const screenshotsByKey = new Map(); - screenshots.forEach((screenshot) => { - // A frame screenshot can contain several nodes that share the same - // (key, ns). We must list the screenshot at most once per key, otherwise - // the push payload ends up with duplicate KeyScreenshotDto entries - // (same uploadedImageId, same positions) for that key. - const seenKeys = new Set(); - screenshot.keys.forEach((node) => { - const mapKey = `${node.key}\0${hasNamespacesEnabled ? node.ns || "" : ""}`; - if (seenKeys.has(mapKey)) return; - seenKeys.add(mapKey); - let list = screenshotsByKey.get(mapKey); - if (!list) { - list = []; - screenshotsByKey.set(mapKey, list); - } - list.push(screenshot); - }); - }); - - const getKeyScreenshots = (value: NodeInfo): FrameScreenshot[] => { - const mapKey = `${value.key}\0${hasNamespacesEnabled ? value.ns || "" : ""}`; - return screenshotsByKey.get(mapKey) ?? []; - }; - const newTags = tolgeeConfig?.tags; - - nodes.forEach((node) => { - const nsLookup = hasNamespacesEnabled ? (node.ns ?? "") : ""; - const oldValue = translations?.[nsLookup]?.[node.key]; - - const oldTags = oldValue?.keyTags.map((tag) => tag.name) ?? []; - - const hasChangesOutsideFromTolgee = - stringFormatter(node.translation ?? "") != - stringFormatter(node.characters ?? "") && - Object.keys(node.paramsValues ?? {}).length == 0 && - !node.isPlural; - - const oldTolgeeValue = oldValue - ? getTolgeeFormat(oldValue.translation, oldValue.keyIsPlural, false) - : null; - - const newTranslation = hasChangesOutsideFromTolgee - ? (node.characters ?? node.translation) - : node.translation || node.characters; - - const newTolgeeValue = getTolgeeFormat( - newTranslation, - node.isPlural, - false, - ); - - const hasChanges = - oldTolgeeValue == null || - (oldTolgeeValue && - (JSON.stringify(oldTolgeeValue) !== JSON.stringify(newTolgeeValue) || - oldValue.keyIsPlural !== node.isPlural)) || - newTags?.some((tag) => !oldTags.includes(tag)); - - const change: KeyChangeValue = { - key: node.key, - ns: hasNamespacesEnabled ? node.ns : undefined, - oldValue: oldValue ? oldValue.translation : undefined, - oldIsPlural: oldValue ? oldValue.keyIsPlural : undefined, - isPlural: node.isPlural, - newValue: newTranslation, - screenshots: getKeyScreenshots(node), - }; - - if (!oldValue) { - newKeys.push(change); - } else if (hasChanges) { - changedKeys.push(change); - } else { - unchangedKeys.push(change); - } - }); - - return { newKeys, changedKeys, unchangedKeys, screenshots }; -}; diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index ce41383f..00000000 --- a/src/types.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { EventHandler } from "@create-figma-plugin/utilities"; - -export interface SetupHandle extends EventHandler { - name: "SETUP"; - handler: (config: Partial) => void; -} - -export interface ResetHandler extends EventHandler { - name: "RESET"; - handler: () => void; -} - -export interface SetLanguageHandler extends EventHandler { - name: "SET_LANGUAGE"; - handler: (language: string) => void; -} - -export interface SetBranchHandler extends EventHandler { - name: "SET_BRANCH"; - handler: (branch: string) => void; -} - -export interface ConfigChangeHandler extends EventHandler { - name: "CONFIG_CHANGE"; - handler: (config: Partial) => void; -} - -export interface CloseHandler extends EventHandler { - name: "CLOSE"; - handler: () => void; -} - -export interface SyncCompleteHandler extends EventHandler { - name: "SYNC_COMPLETE"; - handler: () => void; -} - -export interface ResizeHandler extends EventHandler { - name: "RESIZE"; - handler: (size: WindowSize) => void; -} - -export interface SelectionChangeHandler extends EventHandler { - name: "SELECTION_CHANGE"; - handler: () => void; -} - -export interface DocumentChangeHandler extends EventHandler { - name: "DOCUMENT_CHANGE"; - handler: () => void; -} - -export interface PageChangeHandler extends EventHandler { - name: "CURRENT_PAGE_CHANGE"; - handler: (config: Partial) => void; -} - -export type SizeInfo = { - width: number; - height: number; -}; - -export type FrameInfo = SizeInfo & { - id: string; - name: string; -}; - -export type PositionInfo = { - x: number; - y: number; -}; - -export type FrameScreenshot = { - image: Uint8Array; - info: FrameInfo; - keys: (NodeInfo & SizeInfo & PositionInfo)[]; -}; - -export interface NodeInfo { - pluralParamValue?: string; - paramsValues?: Record; - name: string; - characters: string; - translation: string; - id: string; - isPlural: boolean; - key: string; - ns: string | undefined; - connected: boolean; - visible?: boolean; -} - -export type PartialNodeInfo = Partial & { - id: string; -}; - -export type GlobalSettings = { - apiUrl: string; - apiKey: string; - ignorePrefix: string; - ignoreNumbers: boolean; - updateScreenshots?: boolean; - addTags?: boolean; - tags?: string[]; - /** Whether to prefill the key name with the key format */ - prefillKeyFormat?: boolean; - /** - * A string that can contain some of the following placeholders and custom separators - * in order to generate a key name for the node. - * - * `{artboard}` - * - * `{frame}` - * - * `{elementName}` or `{elementText}` - * - * `{component}` - * - * `{section}` or `{group}` - */ - keyFormat?: string; - ignoreHiddenLayers?: boolean; - ignoreHiddenLayersIncludingChildren?: boolean; - ignoreTextLayers?: boolean; - variableCasing?: - | "snake_case" - | "snake_case_capitalized" - | "camelCase" - | "PascalCase" - | "noSpaces"; -}; - -export type CurrentDocumentSettings = GlobalSettings & { - namespace: string; - branch?: string; - documentInfo: true; -}; - -export type CurrentPageSettings = { - language: string; - pageInfo: boolean; - pageCopy: boolean; - pageStringDetails: boolean; - nodeInfo?: NodeInfo; -}; - -export type TolgeeConfig = CurrentDocumentSettings & CurrentPageSettings; - -export type FormattedNode = { - characters: string; - id: string; - name: string; -}; - -export type WindowSize = { - width: number; - height: number; -}; - -export type InitialState = { - config: Partial | null; - selectedNodes: Array; - allNodes: Array; -}; diff --git a/src/ui/App.svelte b/src/ui/App.svelte new file mode 100644 index 00000000..0435ecca --- /dev/null +++ b/src/ui/App.svelte @@ -0,0 +1,241 @@ + + + + + +
+ {#if appState.value.errorBanner} + + {/if} +
+ {#if appState.value.route.name === "settings"} + + + {:else if appState.value.config?.pageCopy} + + + {:else if appState.value.config != null && !appState.value.config.documentInfo} + + + {:else if !appState.value.config?.pageInfo && appState.value.config?.documentInfo} + + + {:else if appState.value.route.name === "index"} + + {:else if appState.value.route.name === "pageSetup"} + + {:else if appState.value.route.name === "copyView"} + + {:else if appState.value.route.name === "push"} + + {:else if appState.value.route.name === "pull"} + + {:else if appState.value.route.name === "connect"} + + {:else if appState.value.route.name === "stringDetails"} + + {:else if appState.value.route.name === "createCopy"} + + {/if} +
+ {#if appState.value.editorType !== "dev"} + + + {/if} +
+
diff --git a/src/ui/Plugin.tsx b/src/ui/Plugin.tsx deleted file mode 100644 index be3ed8a1..00000000 --- a/src/ui/Plugin.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { h } from "preact"; - -import "!./styles.css"; -import { QueryClient, QueryClientProvider } from "react-query"; - -import { InitialState } from "../types"; -import { GlobalState } from "./state/GlobalState"; -import { Router } from "./views/Router"; - -type Props = InitialState; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - cacheTime: 5 * 60 * 1000, - staleTime: 30 * 1000, - }, - }, -}); - -export function Plugin({ config }: Props) { - return ( - - - - - - ); -} diff --git a/src/ui/client/apiSchema.custom.ts b/src/ui/client/apiSchema.custom.ts deleted file mode 100644 index 5d9f4527..00000000 --- a/src/ui/client/apiSchema.custom.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { components } from "./apiSchema.generated"; - -export interface BranchModel { - id: number; - name: string; - isDefault: boolean; - isProtected: boolean; -} - -/** - * This file contains endpoints that are not generated by openapi-typescript but are still needed for the app. - */ -export interface customPaths { - "/v2/projects/{projectId}": { - /** - * Get project info - * @description Returns info about a project - */ - get: operations["get"]; - }; - "/v2/projects/{projectId}/branches": { - /** - * Get branches - * @description Returns all branches for a project - */ - get: operations["getBranches"]; - }; -} - -export interface operations { - /** - * Get project info - * @description Returns info about a project - */ - get: { - parameters: { - path: { - projectId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ProjectModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get branches - * @description Returns all branches for a project - */ - getBranches: { - parameters: { - path: { - projectId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": { - _embedded?: { - branches?: BranchModel[]; - }; - }; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; -} diff --git a/src/ui/client/apiSchema.generated.ts b/src/ui/client/apiSchema.generated.ts deleted file mode 100644 index aab37a99..00000000 --- a/src/ui/client/apiSchema.generated.ts +++ /dev/null @@ -1,11341 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - - -export interface paths { - "/api/project/export/jsonZip": { - /** - * Export to ZIP of jsons - * @deprecated - * @description Exports data as ZIP of jsons - */ - get: operations["doExportJsonZip_1"]; - }; - "/v2/api-keys/current": { - /** - * Get current API key info - * @description Returns info the API key which user currently authenticated with. Otherwise responds with 400 status code. - */ - get: operations["getCurrent_1"]; - }; - "/v2/api-keys/current-permissions": { - /** - * Get current permission info - * @description Returns current PAK or PAT permissions for current user, api-key and project - */ - get: operations["getCurrentPermissions"]; - }; - "/v2/auth-provider": { - /** Get current third party authentication provider */ - get: operations["getCurrentAuthProvider"]; - /** Initiate provider change to remove current third party authentication provider */ - delete: operations["deleteCurrentAuthProvider"]; - }; - "/v2/auth-provider/change": { - /** Get info about authentication provider which can replace the current one */ - get: operations["getChangedAuthProvider"]; - /** Accept change of the third party authentication provider */ - post: operations["acceptChangeAuthProvider"]; - /** Reject change of the third party authentication provider */ - delete: operations["rejectChangeAuthProvider"]; - }; - "/v2/image-upload": { - /** Upload an image for later use */ - post: operations["upload"]; - }; - "/v2/image-upload/{ids}": { - /** Delete uploaded images */ - delete: operations["delete_15"]; - }; - "/v2/notification": { - /** Gets notifications of the currently logged in user, newest is first. */ - get: operations["getNotifications"]; - }; - "/v2/notification-settings": { - /** - * Get notification settings - * @description Returns notification settings of the currently logged in user - */ - get: operations["getNotificationsSettings"]; - /** - * Save notification setting - * @description Saves new value for given parameters - */ - put: operations["putNotificationSetting"]; - }; - "/v2/notifications-mark-seen": { - /** Marks notifications of the currently logged in user with given IDs as seen. */ - put: operations["markNotificationsAsSeen"]; - }; - "/v2/organizations": { - /** - * Get all permitted organizations - * @description Returns all organizations, which is current user allowed to view - */ - get: operations["getAll_10"]; - /** Create organization */ - post: operations["create_12"]; - }; - "/v2/organizations/{id}": { - /** Get one organization */ - get: operations["get_15"]; - }; - "/v2/organizations/{organizationId}/glossaries": { - /** Get all organization glossaries */ - get: operations["getAll_12"]; - /** Create glossary */ - post: operations["create_13"]; - }; - "/v2/organizations/{organizationId}/glossaries-with-stats": { - /** Get all organization glossaries with some additional statistics */ - get: operations["getAllWithStats"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}": { - /** Get glossary */ - get: operations["get_13"]; - /** Update glossary */ - put: operations["update_8"]; - /** Delete glossary */ - delete: operations["delete_7"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}/assigned-projects": { - /** Get all projects assigned to glossary */ - get: operations["getAssignedProjects"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}/export": { - /** Export glossary terms as CSV */ - get: operations["export"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}/import": { - /** Import glossary terms from CSV */ - post: operations["importCsv"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}/languages": { - /** Get all languages in use by the glossary */ - get: operations["getLanguages"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}/terms": { - /** Get all glossary terms */ - get: operations["getAll_13"]; - /** Create a new glossary term */ - post: operations["create_14"]; - /** Batch delete multiple terms */ - delete: operations["deleteMultiple"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}/terms/{termId}": { - /** Get glossary term */ - get: operations["get_14"]; - /** Update glossary term */ - put: operations["update_9"]; - /** Delete glossary term */ - delete: operations["delete_8"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}/terms/{termId}/translations": { - /** Set a new glossary term translation for language */ - post: operations["update_12"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}/terms/{termId}/translations/{languageTag}": { - /** Get glossary term translation for language */ - get: operations["get_23"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}/termsIds": { - /** Get all glossary terms ids */ - get: operations["getAllIds"]; - }; - "/v2/organizations/{organizationId}/glossaries/{glossaryId}/termsWithTranslations": { - /** Get all glossary terms with translations */ - get: operations["getAllWithTranslations"]; - }; - "/v2/organizations/{organizationId}/machine-translation-credit-balance": { - /** - * Get credit balance for organization - * @description Returns machine translation credit balance for organization - */ - get: operations["getOrganizationCredits"]; - }; - "/v2/organizations/{slug}": { - /** Get organization by slug */ - get: operations["get_22"]; - }; - "/v2/pats/current": { - /** - * Return current PAK - * @description Returns current Personal Access Token. If the request is not authenticated with a Personal Access Token, it will return 400 response status. - */ - get: operations["getCurrent"]; - }; - "/v2/projects": { - /** - * Get all permitted - * @description Returns all projects where current user has any permission - */ - get: operations["getAll"]; - /** - * Create project - * @description Creates a new project with languages and initial settings. - */ - post: operations["createProject"]; - }; - "/v2/projects/activity": { - /** Get project activity */ - get: operations["getActivity_1"]; - }; - "/v2/projects/activity/revisions/{revisionId}": { - /** Get one revision data */ - get: operations["getSingleRevision_1"]; - }; - "/v2/projects/batch-jobs": { - /** List batch operations */ - get: operations["list_4"]; - }; - "/v2/projects/batch-jobs/{id}": { - /** Get batch operation */ - get: operations["get_21"]; - }; - "/v2/projects/batch-jobs/{id}/cancel": { - /** - * Stop batch operation - * @description Stops batch operation if possible. - */ - put: operations["cancel_1"]; - }; - "/v2/projects/big-meta": { - /** - * Store Big Meta - * @description Stores a bigMeta for a project - */ - post: operations["store_3"]; - }; - "/v2/projects/current-batch-jobs": { - /** - * Get all running and pending batch operations - * @description Returns all running and pending batch operations. Completed batch operations are returned only if they are not older than 1 hour. If user doesn't have permission to view all batch operations, only their operations are returned. - */ - get: operations["currentJobs_1"]; - }; - "/v2/projects/export": { - /** - * Export data - * @description - * Exports project data in various formats (JSON, properties, YAML, etc.). - * - * ## HTTP Conditional Requests Support - * - * This endpoint supports HTTP conditional requests using both If-Modified-Since and If-None-Match headers: - * - * - **If-Modified-Since header provided**: The server checks if the project data has been modified since the specified date - * - **If-None-Match header provided**: The server checks if the project data has changed by comparing the eTag value - * - **Data not modified**: Returns HTTP 304 Not Modified with empty body - * - **Data modified or no header**: Returns HTTP 200 OK with the exported data, Last-Modified header, and ETag header - * - * The Last-Modified header in the response contains the timestamp of the last project modification, - * and the ETag header contains a unique identifier for the current project state. Both can be used - * for subsequent conditional requests to avoid unnecessary data transfer when the project hasn't changed. - * - * Cache-Control header is set to max-age=0 to ensure validation on each request. - */ - get: operations["exportData_1"]; - /** - * Export data (post) - * @description - * Exports project data in various formats (JSON, properties, YAML, etc.). - * Useful when exceeding allowed URL size with GET requests. - * - * ## HTTP Conditional Requests Support - * - * This endpoint supports HTTP conditional requests using both If-Modified-Since and If-None-Match headers: - * - * - **If-Modified-Since header provided**: The server checks if the project data has been modified since the specified date - * - **If-None-Match header provided**: The server checks if the project data has changed by comparing the eTag value - * - **Data not modified**: Returns HTTP 304 Not Modified with empty body - * - **Data modified or no header**: Returns HTTP 200 OK with the exported data, Last-Modified header, and ETag header - * - * Note: This endpoint uses a custom implementation that returns 304 Not Modified for all HTTP methods - * (including POST) when conditional headers indicate the data hasn't changed. This differs from Spring's - * default behavior which returns 412 for POST requests, but is appropriate here since POST is used only - * to accommodate large request parameters, not to modify data. - * - * The Last-Modified header in the response contains the timestamp of the last project modification, - * and the ETag header contains a unique identifier for the current project state. Both can be used - * for subsequent conditional requests to avoid unnecessary data transfer when the project hasn't changed. - * - * Cache-Control header is set to max-age=0 to ensure validation on each request. - */ - post: operations["exportPost_1"]; - }; - "/v2/projects/import": { - /** - * Add files - * @description Prepares provided files to import. - */ - post: operations["addFiles_1"]; - /** - * Delete - * @description Deletes prepared import data. - */ - delete: operations["cancelImport_1"]; - }; - "/v2/projects/import-settings": { - /** - * Get Import Settings - * @description Returns import settings for the authenticated user and the project. - */ - get: operations["get_1"]; - /** - * Set Import Settings - * @description Stores import settings for the authenticated user and the project. - */ - put: operations["store_1"]; - }; - "/v2/projects/import/all-namespaces": { - /** - * Get namespaces - * @description Returns all existing and imported namespaces - */ - get: operations["getAllNamespaces_1"]; - }; -"/v2/projects/{projectId}": { - /** - * Get project info - * @description Returns info about a project - */ - get: operations["getProjectInfo"]; -}; - "/v2/projects/import/apply": { - /** - * Apply import - * @description Imports the data prepared in previous step - */ - put: operations["applyImport_1"]; - }; - "/v2/projects/import/apply-streaming": { - /** - * Apply import (streaming) - * @description Imports the data prepared in previous step. Streams current status. - */ - put: operations["applyImportStreaming_1"]; - }; - "/v2/projects/import/result": { - /** - * Get result - * @description Returns the result of preparation. - */ - get: operations["getImportResult_1"]; - }; - "/v2/projects/import/result/files/{fileId}/select-namespace": { - /** - * Select namespace - * @description Sets namespace for file to import. - */ - put: operations["selectNamespace_1"]; - }; - "/v2/projects/import/result/files/{importFileId}/issues": { - /** - * Get file issues - * @description Returns issues for uploaded file. - */ - get: operations["getImportFileIssues_1"]; - }; - "/v2/projects/import/result/languages/{importLanguageId}/reset-existing": { - /** - * Reset existing language pairing - * @description Resets existing language paired with language to import. - */ - put: operations["resetExistingLanguage_1"]; - }; - "/v2/projects/import/result/languages/{importLanguageId}/select-existing/{existingLanguageId}": { - /** - * Pair existing language - * @description Sets existing language to pair with language to import. Data will be imported to selected existing language when applied. - */ - put: operations["selectExistingLanguage_1"]; - }; - "/v2/projects/import/result/languages/{languageId}": { - /** - * Get import language - * @description Returns language prepared to import. - */ - get: operations["getImportLanguage_1"]; - /** - * Delete language - * @description Deletes language prepared to import. - */ - delete: operations["deleteLanguage_3"]; - }; - "/v2/projects/import/result/languages/{languageId}/resolve-all/set-keep-existing": { - /** - * Resolve all translation conflicts (keep existing) - * @description Resolves all translation conflicts for provided language. The old translations will be kept. - */ - put: operations["resolveTranslationSetKeepExisting_3"]; - }; - "/v2/projects/import/result/languages/{languageId}/resolve-all/set-override": { - /** - * Resolve all translation conflicts (override) - * @description Resolves all translation conflicts for provided language. The old translations will be overridden. - */ - put: operations["resolveTranslationSetOverride_3"]; - }; - "/v2/projects/import/result/languages/{languageId}/translations": { - /** - * Get translations - * @description Returns translations prepared to import. - */ - get: operations["getImportTranslations_1"]; - }; - "/v2/projects/import/result/languages/{languageId}/translations/{translationId}/resolve/set-keep-existing": { - /** - * Resolve conflict (keep existing) - * @description Resolves translation conflict. The old translation will be kept. - */ - put: operations["resolveTranslationSetKeepExisting_1"]; - }; - "/v2/projects/import/result/languages/{languageId}/translations/{translationId}/resolve/set-override": { - /** - * Resolve conflict (override) - * @description Resolves translation conflict. The old translation will be overridden. - */ - put: operations["resolveTranslationSetOverride_1"]; - }; - "/v2/projects/keys": { - /** Get all keys */ - get: operations["getAll_8"]; - /** Create new key */ - post: operations["create_8"]; - /** - * Delete one or multiple keys (post) - * @description Delete one or multiple keys by their IDs in request body. Useful for larger requests esxceeding allowed URL length. - */ - delete: operations["delete_12"]; - }; - "/v2/projects/keys/create": { - /** Create new key */ - post: operations["create_7"]; - }; - "/v2/projects/keys/import": { - /** - * Import keys - * @description Imports new keys with translations. If key already exists, its translations and tags are not updated. - */ - post: operations["importKeys_3"]; - }; - "/v2/projects/keys/import-resolvable": { - /** - * Import keys (resolvable) - * @deprecated - * @description - * Import's new keys with translations. Translations can be updated, when specified.\n\n - * DEPRECATED: Use /v2/projects/{projectId}/single-step-import-resolvable instead. - */ - post: operations["importKeys_1"]; - }; - "/v2/projects/keys/info": { - /** - * Get key info - * @description Returns information about keys. (KeyData, Screenshots, Translation in specified language)If key is not found, it's not included in the response. - */ - post: operations["getInfo_2"]; - }; - "/v2/projects/keys/search": { - /** - * Search for keys - * @description This endpoint helps you to find desired key by keyName, base translation or translation in specified language. - * - * Sort is ignored for this request. - */ - get: operations["searchForKey_1"]; - }; - "/v2/projects/keys/select": { - /** - * Select keys - * @description Returns all key IDs for specified filter values. This way, you can apply the same filter as in the translation view and get the resulting key IDs for future use. - */ - get: operations["selectKeys_3"]; - }; - "/v2/projects/keys/{ids}": { - /** Delete one or multiple keys */ - delete: operations["delete_14"]; - }; - "/v2/projects/keys/{id}": { - /** Get one key */ - get: operations["get_9"]; - /** Edit key name */ - put: operations["edit_1"]; - }; - "/v2/projects/keys/{id}/big-meta": { - /** Get Big Meta for key */ - get: operations["getBigMeta_1"]; - }; - "/v2/projects/keys/{id}/complex-update": { - /** - * Edit key and related data - * @description Edits key name, translations, tags, screenshots, and other data - */ - put: operations["complexEdit_1"]; - }; - "/v2/projects/keys/{id}/disabled-languages": { - /** - * Get disabled languages - * @description Returns languages, in which key is disabled - */ - get: operations["getDisabledLanguages_1"]; - /** - * Set disabled languages - * @description Sets languages, in which key is disabled - */ - put: operations["setDisabledLanguages_1"]; - }; - "/v2/projects/keys/{keyId}/auto-translate": { - /** - * Auto translates keys - * @description Uses enabled auto-translation methods. - * You need to set at least one of useMachineTranslation or useTranslationMemory to true. - * - * This will replace the the existing translation with the result obtained from specified source! - */ - put: operations["autoTranslate_1"]; - }; - "/v2/projects/keys/{keyId}/screenshots": { - /** Get screenshots */ - get: operations["getKeyScreenshots"]; - /** Upload screenshot */ - post: operations["uploadScreenshot"]; - }; - "/v2/projects/keys/{keyId}/screenshots/{ids}": { - /** Delete screenshots */ - delete: operations["deleteScreenshots"]; - }; - "/v2/projects/keys/{keyId}/tags": { - /** - * Tag key - * @description Tags a key with tag. If tag with provided name doesn't exist, it is created - */ - put: operations["tagKey_1"]; - }; - "/v2/projects/keys/{keyId}/tags/{tagId}": { - /** - * Remove tag - * @description Removes tag with provided id from key with provided id - */ - delete: operations["removeTag_1"]; - }; - "/v2/projects/labels": { - /** Get available project labels */ - get: operations["getAll_2"]; - /** Create label */ - post: operations["createLabel_1"]; - }; - "/v2/projects/labels/ids": { - /** Get labels by ids */ - get: operations["getLabelsByIds_1"]; - }; - "/v2/projects/labels/{labelId}": { - /** Update label */ - put: operations["updateLabel_1"]; - /** Delete label */ - delete: operations["deleteLabel_1"]; - }; - "/v2/projects/languages": { - /** Get all languages */ - get: operations["getAll_6"]; - /** Create language */ - post: operations["createLanguage_1"]; - }; - "/v2/projects/languages/{languageId}": { - /** Get one language */ - get: operations["get_7"]; - /** Update language */ - put: operations["editLanguage_1"]; - /** Delete specific language */ - delete: operations["deleteLanguage_1"]; - }; - "/v2/projects/languages/{languageId}/key/{keyId}/suggestion": { - /** Get suggestions */ - get: operations["getSuggestions_1"]; - /** Create translation suggestion */ - post: operations["createSuggestion_1"]; - }; - "/v2/projects/languages/{languageId}/key/{keyId}/suggestion/{suggestionId}": { - /** - * Delete suggestion - * @description User can only delete suggestion created by them - */ - delete: operations["deleteSuggestion_1"]; - }; - "/v2/projects/languages/{languageId}/key/{keyId}/suggestion/{suggestionId}/accept": { - /** Accept suggestion */ - put: operations["acceptSuggestion_1"]; - }; - "/v2/projects/languages/{languageId}/key/{keyId}/suggestion/{suggestionId}/decline": { - /** Decline suggestion */ - put: operations["declineSuggestion_1"]; - }; - "/v2/projects/languages/{languageId}/key/{keyId}/suggestion/{suggestionId}/set-active": { - /** Set suggestion active */ - put: operations["suggestionSetActive_1"]; - }; - "/v2/projects/my-batch-jobs": { - /** - * List user batch operations - * @description List all batch operations started by current user - */ - get: operations["myList_1"]; - }; - "/v2/projects/namespace-by-name/{name}": { - /** - * Get namespace by name - * @description Returns information about a namespace by its name - */ - get: operations["getByName_1"]; - }; - "/v2/projects/namespaces": { - /** Get namespaces */ - get: operations["getAllNamespaces_3"]; - }; - "/v2/projects/namespaces/{id}": { - /** Update namespace */ - put: operations["update_4"]; - }; - "/v2/projects/single-step-import": { - /** - * Single step import - * @description Unlike the /v2/projects/{projectId}/import endpoint, imports the data in single request by provided files and parameters. This is useful for automated importing via API or CLI. - */ - post: operations["singleStepFromFiles_1"]; - }; - "/v2/projects/single-step-import-resolvable": { - /** Single step import from body */ - post: operations["singleStepResolvableImport_1"]; - }; - "/v2/projects/start-batch-job/ai-playground-translate": { - /** Translates via llm and stores result in AiPlaygroundResult */ - post: operations["aiPlaygroundTranslate_1"]; - }; - "/v2/projects/start-batch-job/assign-translation-label": { - /** Assign labels to translations */ - post: operations["assignTranslationLabel_1"]; - }; - "/v2/projects/start-batch-job/clear-translations": { - /** - * Clear translation values - * @description Clear translation values for provided keys in selected languages. - */ - post: operations["clearTranslations_1"]; - }; - "/v2/projects/start-batch-job/copy-translations": { - /** - * Copy translation values - * @description Copy translation values from one language to other languages. - */ - post: operations["copyTranslations_1"]; - }; - "/v2/projects/start-batch-job/delete-keys": { - /** Delete keys */ - post: operations["deleteKeys_1"]; - }; - "/v2/projects/start-batch-job/machine-translate": { - /** - * Machine Translation - * @description Translate provided keys to provided languages through primary MT provider. - */ - post: operations["machineTranslation_1"]; - }; - "/v2/projects/start-batch-job/pre-translate-by-tm": { - /** - * Pre-translate by TM - * @description Pre-translate provided keys to provided languages by TM. - */ - post: operations["translate_1"]; - }; - "/v2/projects/start-batch-job/set-keys-namespace": { - /** Set keys namespace */ - post: operations["setKeysNamespace_1"]; - }; - "/v2/projects/start-batch-job/set-translation-state": { - /** Set translation state */ - post: operations["setTranslationState_3"]; - }; - "/v2/projects/start-batch-job/tag-keys": { - /** Add tags */ - post: operations["tagKeys_1"]; - }; - "/v2/projects/start-batch-job/unassign-translation-label": { - /** Unassign labels from translations */ - post: operations["unassignTranslationLabel_1"]; - }; - "/v2/projects/start-batch-job/untag-keys": { - /** Remove tags */ - post: operations["untagKeys_1"]; - }; - "/v2/projects/stats": { - /** Get project stats */ - get: operations["getProjectStats_1"]; - }; - "/v2/projects/stats/daily-activity": { - /** Get project daily amount of events */ - get: operations["getProjectDailyActivity_1"]; - }; - "/v2/projects/suggest/machine-translations": { - /** - * Get machine translation suggestions - * @description Suggests machine translations from enabled services - */ - post: operations["suggestMachineTranslations_1"]; - }; - "/v2/projects/suggest/machine-translations-streaming": { - /** - * Get machine translation suggestions (streaming) - * @description Suggests machine translations from enabled services. The results are streamed to the output in ndjson format. If an error occurs when for any service provider used, the error information is returned as a part of the result item, while the response has 200 status code. - */ - post: operations["suggestMachineTranslationsStreaming_1"]; - }; - "/v2/projects/suggest/translation-memory": { - /** - * Get suggestions from translation memory - * @description Suggests machine translations from translation memory. The result is always sorted by similarity, so sorting is not supported. - */ - post: operations["suggestTranslationMemory_1"]; - }; - "/v2/projects/tag-complex": { - /** Execute complex tag operation */ - put: operations["executeComplexTagOperation_1"]; - }; - "/v2/projects/tags": { - /** Get tags */ - get: operations["getAll_15"]; - }; - "/v2/projects/tasks": { - /** Get tasks */ - get: operations["getTasks_1"]; - /** Create task */ - post: operations["createTask_1"]; - }; - "/v2/projects/tasks/calculate-scope": { - /** Calculate scope */ - post: operations["calculateScope_1"]; - }; - "/v2/projects/tasks/create-multiple-tasks": { - /** Create multiple tasks */ - post: operations["createTasks_1"]; - }; - "/v2/projects/tasks/possible-assignees": { - /** Get possible assignees */ - get: operations["getPossibleAssignees_1"]; - }; - "/v2/projects/tasks/{taskNumber}": { - /** Get task */ - get: operations["getTask_1"]; - /** Update task */ - put: operations["updateTask_1"]; - }; - "/v2/projects/tasks/{taskNumber}/blocking-tasks": { - /** - * Get blocking task numbers - * @description If the tasks is blocked by other tasks, it returns numbers of these tasks. - */ - get: operations["getBlockingTasks_1"]; - }; - "/v2/projects/tasks/{taskNumber}/cancel": { - /** Close task */ - put: operations["cancelTask_1"]; - }; - "/v2/projects/tasks/{taskNumber}/close": { - /** - * Close task - * @deprecated - */ - put: operations["closeTask_1"]; - }; - "/v2/projects/tasks/{taskNumber}/finish": { - /** Finish task */ - put: operations["finishTask_1"]; - }; - "/v2/projects/tasks/{taskNumber}/keys": { - /** Get task keys */ - get: operations["getTaskKeys_1"]; - /** Add or remove task keys */ - put: operations["updateTaskKeys_1"]; - }; - "/v2/projects/tasks/{taskNumber}/keys/{keyId}": { - /** - * Update task key - * @description Mark key as done, which updates task progress. - */ - put: operations["updateTaskKey_1"]; - }; - "/v2/projects/tasks/{taskNumber}/per-user-report": { - /** - * Get report - * @description Detailed statistics for every assignee - */ - get: operations["getPerUserReport_1"]; - }; - "/v2/projects/tasks/{taskNumber}/reopen": { - /** Reopen task */ - put: operations["reopenTask_1"]; - }; - "/v2/projects/tasks/{taskNumber}/xlsx-report": { - /** - * Get report in XLSX - * @description Detailed statistics about the task results - */ - get: operations["getXlsxReport_1"]; - }; - "/v2/projects/translations": { - /** Get translations in project */ - get: operations["getTranslations_1"]; - /** - * Update translations for existing key - * @description Sets translations for existing key - */ - put: operations["setTranslations_1"]; - /** - * Create key or update translations - * @description Sets translations for existing key or creates new key and sets the translations to it. - */ - post: operations["createOrUpdateTranslations_1"]; - }; - "/v2/projects/translations/create-comment": { - /** - * Create translation comment - * @description Creates a translation comment. Empty translation is stored, when not exists. - */ - post: operations["create_4"]; - }; - "/v2/projects/translations/label": { - /** Add label to translation by key and language id */ - put: operations["assignLabel_3"]; - }; - "/v2/projects/translations/select-all": { - /** - * Select keys - * @description Returns all key IDs for specified filter values. This way, you can apply the same filter as in the translation view and get the resulting key IDs for future use. - */ - get: operations["selectKeys_1"]; - }; - "/v2/projects/translations/{languages}": { - /** - * Get all translations - * @description Returns all translations for specified languages - */ - get: operations["getAllTranslations_1"]; - }; - "/v2/projects/translations/{translationId}/comments": { - /** - * Get translation comments - * @description Returns translation comments of translation - */ - get: operations["getAll_4"]; - /** Create translation comment */ - post: operations["create_2"]; - }; - "/v2/projects/translations/{translationId}/comments/{commentId}": { - /** Get one translation comment */ - get: operations["get_5"]; - /** Update translation comment */ - put: operations["update_2"]; - /** Delete translation comment */ - delete: operations["delete_3"]; - }; - "/v2/projects/translations/{translationId}/comments/{commentId}/set-state/{state}": { - /** Set state of translation comment */ - put: operations["setState_1"]; - }; - "/v2/projects/translations/{translationId}/dismiss-auto-translated-state": { - /** - * Dismiss auto-translated - * @description Removes "auto translated" indication - */ - put: operations["dismissAutoTranslatedState_1"]; - }; - "/v2/projects/translations/{translationId}/history": { - /** - * Get translation history - * @description Sorting is not supported for supported. It is automatically sorted from newest to oldest. - */ - get: operations["getTranslationHistory_1"]; - }; - "/v2/projects/translations/{translationId}/label/{labelId}": { - /** Add label to translation */ - put: operations["assignLabel_1"]; - /** Remove label from translation */ - delete: operations["unassignLabel_1"]; - }; - "/v2/projects/translations/{translationId}/set-outdated-flag/{state}": { - /** - * Set outdated value - * @description Set's "outdated" flag indicating the base translation was changed without updating current translation. - */ - put: operations["setOutdated_1"]; - }; - "/v2/projects/translations/{translationId}/set-state/{state}": { - /** Set translation state */ - put: operations["setTranslationState_1"]; - }; - "/v2/projects/used-namespaces": { - /** - * Get used namespaces - * @description Returns all used project namespaces. Response contains default (null) namespace if used. - */ - get: operations["getUsedNamespaces_1"]; - }; - "/v2/user": { - /** - * Get user info - * @description Returns information about currently authenticated user. - */ - get: operations["getInfo"]; - }; - "/v2/user-tasks": { - /** Get user tasks */ - get: operations["getTasks_2"]; - }; - "/v2/user/sso": { - /** - * Get information about SSO configuration - * @description Returns information about sso configuration affecting the user. - */ - get: operations["getSso"]; - }; -} - -export type webhooks = Record; - -export interface components { - schemas: { - AcceptAuthProviderChangeRequest: { - id: string; - }; - ApiKeyPermissionsModel: { - project: components["schemas"]["SimpleProjectModel"]; - /** - * Format: int64 - * @description The API key's project id or the one provided as query param - */ - projectId: number; - /** - * @description Granted scopes to the user. When user has type permissions, this field contains permission scopes of the type. - * @example [ - * "KEYS_EDIT", - * "TRANSLATIONS_VIEW" - * ] - */ - scopes: ("translations.view" | "translations.edit" | "translations.suggest" | "keys.edit" | "screenshots.upload" | "screenshots.delete" | "screenshots.view" | "activity.view" | "languages.edit" | "admin" | "project.edit" | "members.view" | "members.edit" | "translation-comments.add" | "translation-comments.edit" | "translation-comments.set-state" | "translations.state-edit" | "keys.view" | "keys.delete" | "keys.create" | "batch-jobs.view" | "batch-jobs.cancel" | "translations.batch-by-tm" | "translations.batch-machine" | "content-delivery.manage" | "content-delivery.publish" | "webhooks.manage" | "tasks.view" | "tasks.edit" | "prompts.view" | "prompts.edit" | "translation-labels.manage" | "translation-labels.assign" | "all.view")[]; - /** - * @description List of languages user can change state to. If null, changing state of all language values is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - stateChangeLanguageIds?: number[]; - /** - * @description List of languages user can suggest to. If null, suggesting to all languages is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - suggestLanguageIds?: number[]; - /** - * @description List of languages user can translate to. If null, all languages editing is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - translateLanguageIds?: number[]; - /** - * @description The user's permission type. This field is null if user has assigned granular permissions or if returning API key's permissions - * @enum {string} - */ - type?: "NONE" | "VIEW" | "TRANSLATE" | "REVIEW" | "EDIT" | "MANAGE"; - /** - * @description List of languages user can view. If null, all languages view is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - viewLanguageIds?: number[]; - }; - ApiKeyWithLanguagesModel: { - description: string; - /** Format: int64 */ - expiresAt?: number; - /** Format: int64 */ - id: number; - /** Format: int64 */ - lastUsedAt?: number; - /** - * @deprecated - * @description Languages for which user has translate permission. - */ - permittedLanguageIds?: number[]; - /** Format: int64 */ - projectId: number; - projectName: string; - scopes: string[]; - userFullName?: string; - username?: string; - }; - AuthProviderDto: { - /** @enum {string} */ - authType?: "GOOGLE" | "GITHUB" | "OAUTH2" | "SSO" | "SSO_GLOBAL"; - id?: string; - ssoDomain?: string; - }; - Avatar: { - large: string; - thumbnail: string; - }; - BatchJobModel: { - /** - * Format: int64 - * @description The activity revision id, that stores the activity details of the job - */ - activityRevisionId?: number; - /** @description The user who started the job */ - author?: components["schemas"]["SimpleUserAccountModel"]; - /** - * Format: int64 - * @description The time when the job created - */ - createdAt: number; - /** @description If the job failed, this is the error message */ - errorMessage?: string; - /** - * Format: int64 - * @description Batch job id - */ - id: number; - /** - * Format: int32 - * @description Total items, that have been processed so far - */ - progress: number; - /** - * @description Status of the batch job - * @enum {string} - */ - status: "PENDING" | "RUNNING" | "SUCCESS" | "FAILED" | "CANCELLED" | "DEBOUNCED"; - /** - * Format: int32 - * @description Total items - */ - totalItems: number; - /** - * @description Type of the batch job - * @enum {string} - */ - type: "AI_PLAYGROUND_TRANSLATE" | "PRE_TRANSLATE_BT_TM" | "MACHINE_TRANSLATE" | "AUTO_TRANSLATE" | "DELETE_KEYS" | "SET_TRANSLATIONS_STATE" | "CLEAR_TRANSLATIONS" | "COPY_TRANSLATIONS" | "TAG_KEYS" | "UNTAG_KEYS" | "SET_KEYS_NAMESPACE" | "AUTOMATION" | "BILLING_TRIAL_EXPIRATION_NOTICE" | "ASSIGN_TRANSLATION_LABEL" | "UNASSIGN_TRANSLATION_LABEL"; - /** - * Format: int64 - * @description The time when the job was last updated (status change) - */ - updatedAt: number; - }; - BigMetaDto: { - /** @description Keys in the document used as a context for machine translation. Keys in the same order as they appear in the document. The order is important! We are using it for graph distance calculation. */ - relatedKeysInOrder?: components["schemas"]["RelatedKeyDto"][]; - }; - CalculateScopeRequest: { - keys: number[]; - /** Format: int64 */ - languageId: number; - /** @enum {string} */ - type: "TRANSLATE" | "REVIEW"; - }; - ClearTranslationsRequest: { - keyIds: number[]; - languageIds: number[]; - }; - CollectionModelBatchJobModel: { - _embedded?: { - batchJobs?: components["schemas"]["BatchJobModel"][]; - }; - }; - CollectionModelGlossaryLanguageDto: { - _embedded?: { - glossaryLanguageDtoList?: components["schemas"]["GlossaryLanguageDto"][]; - }; - }; - CollectionModelImportNamespaceModel: { - _embedded?: { - namespaces?: components["schemas"]["ImportNamespaceModel"][]; - }; - }; - CollectionModelKeyWithBaseTranslationModel: { - _embedded?: { - keys?: components["schemas"]["KeyWithBaseTranslationModel"][]; - }; - }; - CollectionModelKeyWithDataModel: { - _embedded?: { - keys?: components["schemas"]["KeyWithDataModel"][]; - }; - }; - CollectionModelLanguageModel: { - _embedded?: { - languages?: components["schemas"]["LanguageModel"][]; - }; - }; - CollectionModelLong: { - _embedded?: { - longList?: number[]; - }; - }; - CollectionModelScreenshotModel: { - _embedded?: { - screenshots?: components["schemas"]["ScreenshotModel"][]; - }; - }; - CollectionModelSimpleProjectModel: { - _embedded?: { - projects?: components["schemas"]["SimpleProjectModel"][]; - }; - }; - CollectionModelUsedNamespaceModel: { - _embedded?: { - namespaces?: components["schemas"]["UsedNamespaceModel"][]; - }; - }; - ComplexEditKeyDto: { - /** @description Custom values of the key. If not provided, custom values won't be modified */ - custom?: { - [key: string]: unknown; - }; - /** @description Description of the key. It's also used as a context for Tolgee AI translator */ - description?: string; - /** @description If key is pluralized. If it will be reflected in the editor. If null, value won't be modified. */ - isPlural?: boolean; - /** @description Name of the key */ - name: string; - namespace?: string; - /** @description The argument name for the plural. If null, value won't be modified. If isPlural is false, this value will be ignored. */ - pluralArgName?: string; - /** @description Keys in the document used as a context for machine translation. Keys in the same order as they appear in the document. The order is important! We are using it for graph distance calculation. */ - relatedKeysInOrder?: components["schemas"]["RelatedKeyDto"][]; - /** @description IDs of screenshots to delete */ - screenshotIdsToDelete?: number[]; - /** - * @deprecated - * @description Ids of screenshots uploaded with /v2/image-upload endpoint - */ - screenshotUploadedImageIds?: number[]; - screenshotsToAdd?: components["schemas"]["KeyScreenshotDto"][]; - /** @description Translation states to update, if not provided states won't be modified */ - states?: { - [key: string]: "TRANSLATED" | "REVIEWED"; - }; - /** @description Tags of the key. If not provided tags won't be modified */ - tags?: string[]; - /** @description Translations to update */ - translations?: { - [key: string]: string; - }; - /** @description If true, it will fail with 400 (with code plural_forms_data_loss) if plural is disabled and there are plural forms, which would be lost by the action. You can get rid of this warning by setting this value to false. */ - warnOnDataLoss?: boolean; - }; - ComplexTagKeysRequest: { - /** @description Include keys filtered by the provided key information */ - filterKeys?: components["schemas"]["KeyId"][]; - /** @description Exclude keys filtered by the provided key information */ - filterKeysNot?: components["schemas"]["KeyId"][]; - /** @description Include keys filtered by the provided tag information. This filter supports wildcards. For example, `draft-*` will match all tags starting with `draft-`. */ - filterTag?: string[]; - /** @description Exclude keys filtered by the provided tag information. This filter supports wildcards. For example, `draft-*` will match all tags starting with `draft-`. */ - filterTagNot?: string[]; - /** @description Specified tags will be added to filtered keys */ - tagFiltered?: string[]; - /** @description Specified tags will be added to keys not filtered by any of the specified filters. */ - tagOther?: string[]; - /** @description Specified tags will be removed from filtered keys. It supports wildcards. For example, `draft-*` will remove all tags starting with `draft-`. */ - untagFiltered?: string[]; - /** @description Specified tags will be removed from keys not filtered by any of the specified filters. It supports wildcards. For example, `draft-*` will remove all tags starting with `draft-`. */ - untagOther?: string[]; - }; - ComputedPermissionModel: { - /** @enum {string} */ - origin: "ORGANIZATION_BASE" | "DIRECT" | "ORGANIZATION_OWNER" | "NONE" | "SERVER_ADMIN" | "SERVER_SUPPORTER"; - permissionModel?: components["schemas"]["PermissionModel"]; - /** - * @deprecated - * @description Deprecated (use translateLanguageIds). - * - * List of languages current user has TRANSLATE permission to. If null, all languages edition is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - permittedLanguageIds?: number[]; - /** - * @description Granted scopes to the user. When user has type permissions, this field contains permission scopes of the type. - * @example [ - * "KEYS_EDIT", - * "TRANSLATIONS_VIEW" - * ] - */ - scopes: ("translations.view" | "translations.edit" | "translations.suggest" | "keys.edit" | "screenshots.upload" | "screenshots.delete" | "screenshots.view" | "activity.view" | "languages.edit" | "admin" | "project.edit" | "members.view" | "members.edit" | "translation-comments.add" | "translation-comments.edit" | "translation-comments.set-state" | "translations.state-edit" | "keys.view" | "keys.delete" | "keys.create" | "batch-jobs.view" | "batch-jobs.cancel" | "translations.batch-by-tm" | "translations.batch-machine" | "content-delivery.manage" | "content-delivery.publish" | "webhooks.manage" | "tasks.view" | "tasks.edit" | "prompts.view" | "prompts.edit" | "translation-labels.manage" | "translation-labels.assign" | "all.view")[]; - /** - * @description List of languages user can change state to. If null, changing state of all language values is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - stateChangeLanguageIds?: number[]; - /** - * @description List of languages user can suggest to. If null, suggesting to all languages is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - suggestLanguageIds?: number[]; - /** - * @description List of languages user can translate to. If null, all languages editing is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - translateLanguageIds?: number[]; - /** - * @description The user's permission type. This field is null if uses granular permissions - * @enum {string} - */ - type?: "NONE" | "VIEW" | "TRANSLATE" | "REVIEW" | "EDIT" | "MANAGE"; - /** - * @description List of languages user can view. If null, all languages view is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - viewLanguageIds?: number[]; - }; - CopyTranslationRequest: { - keyIds: number[]; - /** Format: int64 */ - sourceLanguageId: number; - targetLanguageIds: number[]; - }; - CreateGlossaryRequest: { - /** @description IDs of projects to be assigned to glossary */ - assignedProjectIds: number[]; - /** - * @description Language tag according to BCP 47 definition - * @example cs-CZ - */ - baseLanguageTag: string; - /** - * @description Glossary name - * @example My glossary - */ - name: string; - }; - CreateGlossaryTermWithTranslationRequest: { - /** - * @description A detailed explanation or definition of the glossary term - * @example It's trademark - */ - description: string; - /** @description Specifies whether the term represents a shortened form of a word or phrase */ - flagAbbreviation: boolean; - /** @description When true, the term matching considers uppercase and lowercase characters as distinct */ - flagCaseSensitive: boolean; - /** @description When true, marks this term as prohibited or not recommended for use in translations */ - flagForbiddenTerm: boolean; - /** @description When true, this term will have the same translation across all target languages */ - flagNonTranslatable: boolean; - text: string; - }; - CreateKeyDto: { - /** - * @description Description of the key - * @example This key is used on homepage. It's a label of sign up button. - */ - description?: string; - /** @description If key is pluralized. If it will be reflected in the editor */ - isPlural: boolean; - /** @description Name of the key */ - name: string; - namespace?: string; - /** @description The argument name for the plural. If null, value will be guessed from the values provided in translations. */ - pluralArgName?: string; - /** @description Keys in the document used as a context for machine translation. Keys in the same order as they appear in the document. The order is important! We are using it for graph distance calculation. */ - relatedKeysInOrder?: components["schemas"]["RelatedKeyDto"][]; - /** - * @deprecated - * @description Ids of screenshots uploaded with /v2/image-upload endpoint - */ - screenshotUploadedImageIds?: number[]; - screenshots?: components["schemas"]["KeyScreenshotDto"][]; - /** @description Translation states to update, if not provided states won't be modified */ - states?: { - [key: string]: "TRANSLATED" | "REVIEWED"; - }; - tags?: string[]; - translations?: { - [key: string]: string; - }; - }; - CreateMultipleTasksRequest: { - tasks: components["schemas"]["CreateTaskRequest"][]; - }; - CreateProjectRequest: { - /** @description Tag of one of created languages, to select it as base language. If not provided, first language will be selected as base. */ - baseLanguageTag?: string; - /** @description Whether to use ICU placeholder visualization in the editor and it's support. */ - icuPlaceholders: boolean; - languages: components["schemas"]["LanguageRequest"][]; - name: string; - /** - * Format: int64 - * @description Organization to create the project in - */ - organizationId: number; - /** @description Slug of your project used in url e.g. "/v2/projects/what-a-project". If not provided, it will be generated */ - slug?: string; - }; - CreateTaskRequest: { - assignees: number[]; - description: string; - /** - * Format: int64 - * @description Due to date in epoch format (milliseconds). - * @example 1661172869000 - */ - dueDate?: number; - keys: number[]; - /** - * Format: int64 - * @description Id of language, this task is attached to. - * @example 1 - */ - languageId: number; - name?: string; - /** @enum {string} */ - type: "TRANSLATE" | "REVIEW"; - }; - CreateTranslationSuggestionRequest: { - translation: string; - }; - CreateUpdateGlossaryTermResponse: { - term: components["schemas"]["SimpleGlossaryTermModel"]; - translation?: components["schemas"]["GlossaryTermTranslationModel"]; - }; - CreditBalanceModel: { - /** Format: int64 */ - bucketSize: number; - /** Format: int64 */ - creditBalance: number; - /** - * Format: int64 - * @deprecated - * @description Customers were able to buy extra credits separately in the past. - * - * This option is not available anymore and this field is kept only for backward compatibility purposes and is always 0. - */ - extraCreditBalance: number; - }; - DeleteKeysDto: { - /** @description IDs of keys to delete */ - ids: number[]; - }; - DeleteKeysRequest: { - keyIds: number[]; - }; - DeleteMultipleGlossaryTermsRequest: { - termIds: number[]; - }; - EditKeyDto: { - /** - * @description Description of the key - * @example This key is used on homepage. It's a label of sign up button. - */ - description?: string; - name: string; - namespace?: string; - }; - EntityDescriptionWithRelations: { - data: { - [key: string]: unknown; - }; - entityClass: string; - /** Format: int64 */ - entityId: number; - }; - ErrorResponseBody: { - code: string; - params?: unknown[]; - }; - ErrorResponseTyped: { - /** @enum {string} */ - code: "unauthenticated" | "api_access_forbidden" | "api_key_not_found" | "invalid_api_key" | "invalid_project_api_key" | "project_api_key_expired" | "bad_credentials" | "mfa_enabled" | "invalid_otp_code" | "mfa_not_enabled" | "can_not_revoke_own_permissions" | "data_corrupted" | "invitation_code_does_not_exist_or_expired" | "language_tag_exists" | "language_name_exists" | "language_not_found" | "operation_not_permitted" | "registrations_not_allowed" | "project_not_found" | "resource_not_found" | "scope_not_found" | "key_exists" | "third_party_auth_error_message" | "third_party_auth_no_email" | "third_party_auth_non_matching_email" | "third_party_auth_no_sub" | "third_party_auth_unknown_error" | "email_already_verified" | "third_party_unauthorized" | "third_party_google_workspace_mismatch" | "third_party_switch_initiated" | "third_party_switch_conflict" | "username_already_exists" | "username_or_password_invalid" | "user_already_has_permissions" | "user_already_has_role" | "user_not_found" | "file_not_image" | "file_too_big" | "invalid_timestamp" | "email_not_verified" | "missing_callback_url" | "invalid_jwt_token" | "expired_jwt_token" | "general_jwt_error" | "cannot_find_suitable_address_part" | "slug_not_unique" | "user_is_not_member_of_organization" | "organization_has_no_other_owner" | "user_has_no_project_access" | "user_is_organization_owner" | "cannot_set_your_own_permissions" | "user_is_organization_member" | "property_not_mutable" | "import_language_not_from_project" | "existing_language_not_selected" | "conflict_is_not_resolved" | "language_already_selected" | "cannot_parse_file" | "could_not_resolve_property" | "cannot_add_more_then_100_languages" | "no_languages_provided" | "language_with_base_language_tag_not_found" | "language_not_from_project" | "namespace_not_from_project" | "cannot_delete_base_language" | "key_not_from_project" | "max_screenshots_exceeded" | "translation_not_from_project" | "can_edit_only_own_comment" | "request_parse_error" | "filter_by_value_state_not_valid" | "import_has_expired" | "tag_not_from_project" | "translation_text_too_long" | "invalid_recaptcha_token" | "cannot_leave_owning_project" | "cannot_leave_project_with_organization_role" | "dont_have_direct_permissions" | "tag_too_log" | "too_many_uploaded_images" | "one_or_more_images_not_found" | "screenshot_not_of_key" | "service_not_found" | "too_many_requests" | "translation_not_found" | "out_of_credits" | "key_not_found" | "organization_not_found" | "cannot_find_base_language" | "base_language_not_found" | "no_exported_result" | "cannot_set_your_own_role" | "only_translate_review_or_view_permission_accepts_view_languages" | "oauth2_token_url_not_set" | "oauth2_user_url_not_set" | "email_already_invited_or_member" | "price_not_found" | "invoice_not_from_organization" | "invoice_not_found" | "plan_not_found" | "plan_not_available_any_more" | "no_auto_translation_method" | "cannot_translate_base_language" | "pat_not_found" | "invalid_pat" | "pat_expired" | "operation_unavailable_for_account_type" | "validation_email_is_not_valid" | "current_password_required" | "cannot_create_organization" | "wrong_current_password" | "wrong_param_type" | "user_missing_password" | "expired_super_jwt_token" | "cannot_delete_your_own_account" | "cannot_sort_by_this_column" | "namespace_not_found" | "namespace_exists" | "invalid_authentication_method" | "unknown_sort_property" | "only_review_permission_accepts_state_change_languages" | "only_translate_or_review_permission_accepts_translate_languages" | "cannot_set_language_permissions_for_admin_scope" | "cannot_set_view_languages_without_translations_view_scope" | "cannot_set_translate_languages_without_translations_edit_scope" | "cannot_set_state_change_languages_without_translations_state_edit_scope" | "language_not_permitted" | "scopes_has_to_be_set" | "set_exactly_one_of_scopes_or_type" | "translation_exists" | "import_keys_error" | "provide_only_one_of_screenshots_and_screenshot_uploaded_image_ids" | "multiple_projects_not_supported" | "plan_translation_limit_exceeded" | "feature_not_enabled" | "license_key_not_found" | "cannot_set_view_languages_without_for_level_based_permissions" | "cannot_set_different_translate_and_state_change_languages_for_level_based_permissions" | "cannot_disable_your_own_account" | "subscription_not_found" | "invoice_does_not_have_usage" | "customer_not_found" | "subscription_not_active" | "organization_already_subscribed" | "organization_not_subscribed" | "license_key_used_by_another_instance" | "translation_spending_limit_exceeded" | "credit_spending_limit_exceeded" | "seats_spending_limit_exceeded" | "this_instance_is_already_licensed" | "big_meta_not_from_project" | "mt_service_not_enabled" | "project_not_selected" | "organization_not_selected" | "plan_has_subscribers" | "translation_failed" | "batch_job_not_found" | "key_exists_in_namespace" | "tag_is_blank" | "execution_failed_on_management_error" | "translation_api_rate_limit" | "cannot_finalize_activity" | "formality_not_supported_by_service" | "language_not_supported_by_service" | "rate_limited" | "pat_access_not_allowed" | "pak_access_not_allowed" | "cannot_modify_disabled_translation" | "azure_config_required" | "s3_config_required" | "content_storage_config_required" | "content_storage_test_failed" | "content_storage_config_invalid" | "invalid_connection_string" | "cannot_create_azure_storage_client" | "s3_access_key_required" | "azure_connection_string_required" | "s3_secret_key_required" | "cannot_store_file_to_content_storage" | "unexpected_error_while_publishing_to_content_storage" | "webhook_responded_with_non_200_status" | "unexpected_error_while_executing_webhook" | "content_storage_is_in_use" | "cannot_set_state_for_missing_translation" | "no_project_id_provided" | "license_key_not_provided" | "subscription_already_canceled" | "user_is_subscribed_to_paid_plan" | "cannot_create_free_plan_without_fixed_type" | "cannot_modify_plan_free_status" | "key_id_not_provided" | "free_self_hosted_seat_limit_exceeded" | "advanced_params_not_supported" | "plural_forms_not_found_for_language" | "nested_plurals_not_supported" | "message_is_not_plural" | "content_outside_plural_forms" | "invalid_plural_form" | "multiple_plurals_not_supported" | "custom_values_json_too_long" | "unsupported_po_message_format" | "plural_forms_data_loss" | "current_user_does_not_own_image" | "user_cannot_view_this_organization" | "user_is_not_owner_of_organization" | "user_is_not_owner_or_maintainer_of_organization" | "pak_created_for_different_project" | "custom_slug_is_only_applicable_for_custom_storage" | "invalid_slug_format" | "batch_job_cancellation_timeout" | "import_failed" | "cannot_add_more_then_1000_languages" | "no_data_to_import" | "multiple_namespaces_mapped_to_single_file" | "multiple_mappings_for_same_file_language_name" | "multiple_mappings_for_null_file_language_name" | "too_many_mappings_for_file" | "missing_placeholder_in_template" | "tag_not_found" | "cannot_parse_encrypted_slack_login_data" | "slack_workspace_not_found" | "cannot_fetch_user_details_from_slack" | "slack_missing_scope" | "slack_not_connected_to_your_account" | "slack_invalid_command" | "slack_not_subscribed_yet" | "slack_connection_failed" | "tolgee_account_already_connected" | "slack_not_configured" | "slack_workspace_already_connected" | "slack_connection_error" | "email_verification_code_not_valid" | "cannot_subscribe_to_free_plan" | "plan_auto_assignment_only_for_free_plans" | "plan_auto_assignment_only_for_private_plans" | "task_not_found" | "task_not_finished" | "task_not_open" | "translation_agency_not_found" | "this_feature_is_not_implemented_in_oss" | "sso_token_exchange_failed" | "sso_user_info_retrieval_failed" | "sso_id_token_expired" | "sso_user_cannot_create_organization" | "sso_cant_verify_user" | "sso_auth_missing_domain" | "sso_domain_not_found_or_disabled" | "authentication_method_disabled" | "native_authentication_disabled" | "invitation_organization_mismatch" | "user_is_managed_by_organization" | "cannot_set_sso_provider_missing_fields" | "namespaces_cannot_be_disabled_when_namespace_exists" | "namespace_cannot_be_used_when_feature_is_disabled" | "sso_domain_not_allowed" | "sso_login_forced_for_this_account" | "use_sso_for_authentication_instead" | "date_has_to_be_in_the_future" | "custom_plan_and_plan_id_cannot_be_set_together" | "specify_plan_id_or_custom_plan" | "custom_plans_has_to_be_private" | "cannot_create_free_plan_with_prices" | "subscription_not_scheduled_for_cancellation" | "cannot_cancel_trial" | "cannot_update_without_modification" | "current_subscription_is_not_trialing" | "sorting_and_paging_is_not_supported_when_using_cursor" | "strings_metric_are_not_supported" | "plan_key_limit_exceeded" | "keys_spending_limit_exceeded" | "plan_seat_limit_exceeded" | "instance_not_using_license_key" | "invalid_path" | "llm_provider_not_found" | "llm_provider_error" | "prompt_not_found" | "llm_provider_not_returned_json" | "llm_template_parsing_error" | "llm_rate_limited" | "llm_provider_timeout" | "no_llm_provider_configured" | "glossary_not_found" | "glossary_term_not_found" | "glossary_term_translation_not_found" | "glossary_non_translatable_term_cannot_be_translated" | "llm_content_filter" | "llm_provider_empty_response" | "label_not_found" | "label_not_from_project" | "label_already_exists" | "filter_by_value_label_not_valid" | "suggestion_not_found" | "user_can_only_delete_his_suggestions" | "cannot_modify_reviewed_translation" | "cannot_modify_keys" | "expect_no_conflict_failed" | "suggestion_cant_be_plural" | "suggestion_must_be_plural" | "duplicate_suggestion" | "unsupported_media_type" | "impersonation_of_admin_by_supporter_not_allowed" | "already_impersonating_user" | "operation_not_permitted_in_read_only_mode" | "file_processing_failed"; - params?: unknown[]; - }; - ExistenceEntityDescription: { - data: { - [key: string]: unknown; - }; - entityClass: string; - /** Format: int64 */ - entityId: number; - exists?: boolean; - relations: { - [key: string]: components["schemas"]["EntityDescriptionWithRelations"]; - }; - }; - ExportParams: { - /** - * @description If true, HTML tags are escaped in the exported file. (Supported in the XLIFF format only). - * - * e.g. Key hello will be exported as <b>hello</b> - */ - escapeHtml?: boolean; - /** - * @description This is a template that defines the structure of the resulting .zip file content. - * - * The template is a string that can contain the following placeholders: {namespace}, {languageTag}, - * {androidLanguageTag}, {snakeLanguageTag}, {extension}. - * - * For example, when exporting to JSON with the template `{namespace}/{languageTag}.{extension}`, - * the English translations of the `home` namespace will be stored in `home/en.json`. - * - * The `{snakeLanguageTag}` placeholder is the same as `{languageTag}` but in snake case. (e.g., en_US). - * - * The Android specific `{androidLanguageTag}` placeholder is the same as `{languageTag}` - * but in Android format. (e.g., en-rUS) - */ - fileStructureTemplate?: string; - /** @description Filter key IDs to be contained in export */ - filterKeyId?: number[]; - /** @description Filter key IDs not to be contained in export */ - filterKeyIdNot?: number[]; - /** @description Filter keys with prefix */ - filterKeyPrefix?: string; - /** @description Filter translations with namespace. By default, all namespaces everything are exported. To export default namespace, use empty string. */ - filterNamespace?: string[]; - /** @description Filter translations with state. By default, all states except untranslated is exported. */ - filterState?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED")[]; - /** - * @description Filter keys tagged by. - * - * This filter works the same as `filterTagIn` but in this cases it accepts single tag only. - */ - filterTag?: string; - /** @description Filter keys tagged by one of provided tags */ - filterTagIn?: string[]; - /** @description Filter keys not tagged by one of provided tags */ - filterTagNotIn?: string[]; - /** - * @description Format to export to - * @enum {string} - */ - format: "JSON" | "JSON_TOLGEE" | "XLIFF" | "PO" | "APPLE_STRINGS_STRINGSDICT" | "APPLE_XLIFF" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" | "PROPERTIES" | "YAML_RUBY" | "YAML" | "JSON_I18NEXT" | "CSV" | "RESX_ICU" | "XLSX" | "APPLE_XCSTRINGS" | "ANDROID_SDK" | "APPLE_SDK"; - /** - * @description Languages to be contained in export. - * - * If null, all languages are exported - * @example en - */ - languages?: string[]; - /** - * @description Message format to be used for export. - * - * e.g. PHP_PO: Hello %s, ICU: Hello {name}. - * - * This property is honored only for generic formats like JSON or YAML. - * For specific formats like `YAML_RUBY` it's ignored. - * @enum {string} - */ - messageFormat?: "C_SPRINTF" | "PHP_SPRINTF" | "JAVA_STRING_FORMAT" | "APPLE_SPRINTF" | "RUBY_SPRINTF" | "I18NEXT" | "ICU" | "PYTHON_PERCENT"; - /** - * @description Delimiter to structure file content. - * - * e.g. For key "home.header.title" would result in {"home": {"header": "title": {"Hello"}}} structure. - * - * When null, resulting file won't be structured. Works only for generic structured formats (e.g. JSON, YAML), - * specific formats like `YAML_RUBY` don't honor this parameter. - */ - structureDelimiter?: string; - /** - * @description If true, for structured formats (like JSON) arrays are supported. - * - * e.g. Key hello[0] will be exported as {"hello": ["..."]} - */ - supportArrays: boolean; - zip: boolean; - }; - GetKeysRequestDto: { - keys: components["schemas"]["KeyDefinitionDto"][]; - /** @description Tags to return language translations in */ - languageTags: string[]; - }; - GlossaryImportResult: { - /** - * Format: int32 - * @description Number of imported terms - * @example 42 - */ - imported: number; - }; - GlossaryLanguageDto: { - /** - * @description Indicates if this is the base (main) language of the glossary - * @example true - */ - base: boolean; - /** - * @description The language code (e.g., 'en' for English) - * @example en - */ - tag: string; - }; - GlossaryModel: { - /** - * @description Language tag for default translations for terms - * @example en - */ - baseLanguageTag: string; - /** Format: int64 */ - id: number; - name: string; - organizationOwner: components["schemas"]["SimpleOrganizationModel"]; - }; - GlossaryTermModel: { - description: string; - /** @description Specifies whether the term represents a shortened form of a word or phrase */ - flagAbbreviation: boolean; - /** @description When true, the term matching considers uppercase and lowercase characters as distinct */ - flagCaseSensitive: boolean; - /** @description When true, marks this term as prohibited or not recommended for use in translations */ - flagForbiddenTerm: boolean; - /** @description When true, this term has the same translation across all target languages */ - flagNonTranslatable: boolean; - glossary: components["schemas"]["GlossaryModel"]; - /** Format: int64 */ - id: number; - translations: components["schemas"]["GlossaryTermTranslationModel"][]; - }; - GlossaryTermTranslationModel: { - languageTag: string; - text: string; - }; - ImageUploadInfoDto: { - location?: string; - }; - ImportAddFilesResultModel: { - errors: components["schemas"]["ErrorResponseBody"][]; - result?: components["schemas"]["PagedModelImportLanguageModel"]; - warnings: components["schemas"]["ErrorResponseBody"][]; - }; - ImportFileIssueModel: { - /** Format: int64 */ - id: number; - params: components["schemas"]["ImportFileIssueParamModel"][]; - /** @enum {string} */ - type: "KEY_IS_NOT_STRING" | "MULTIPLE_VALUES_FOR_KEY_AND_LANGUAGE" | "VALUE_IS_NOT_STRING" | "KEY_IS_EMPTY" | "VALUE_IS_EMPTY" | "PO_MSGCTXT_NOT_SUPPORTED" | "ID_ATTRIBUTE_NOT_PROVIDED" | "TARGET_NOT_PROVIDED" | "TRANSLATION_TOO_LONG" | "KEY_IS_BLANK" | "TRANSLATION_DEFINED_IN_ANOTHER_FILE" | "INVALID_CUSTOM_VALUES" | "DESCRIPTION_TOO_LONG"; - }; - ImportFileIssueParamModel: { - /** @enum {string} */ - type: "KEY_NAME" | "KEY_ID" | "LANGUAGE_ID" | "KEY_INDEX" | "VALUE" | "LINE" | "FILE_NODE_ORIGINAL" | "LANGUAGE_NAME"; - value?: string; - }; - ImportFileMapping: { - /** @description Name of the file to import. This is the name of the file provided in `files` request part or in uploaded archive. */ - fileName: string; - /** - * @description Format of the file. If not provided, Tolgee will try to guess the format from the file name or file contents. - * - * It is recommended to provide these values to prevent any issues with format detection. - * @enum {string} - */ - format?: "CSV_ICU" | "CSV_JAVA" | "CSV_PHP" | "CSV_RUBY" | "JSON_I18NEXT" | "JSON_ICU" | "JSON_JAVA" | "JSON_PHP" | "JSON_RUBY" | "JSON_C" | "PO_PHP" | "PO_C" | "PO_JAVA" | "PO_ICU" | "PO_RUBY" | "PO_PYTHON" | "STRINGS" | "STRINGSDICT" | "APPLE_XLIFF" | "APPLE_XCSTRINGS" | "PROPERTIES_ICU" | "PROPERTIES_JAVA" | "PROPERTIES_UNKNOWN" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" | "YAML_RUBY" | "YAML_JAVA" | "YAML_ICU" | "YAML_PHP" | "YAML_UNKNOWN" | "XLIFF_ICU" | "XLIFF_JAVA" | "XLIFF_PHP" | "XLIFF_RUBY" | "RESX_ICU" | "XLSX_ICU" | "XLSX_JAVA" | "XLSX_PHP" | "XLSX_RUBY"; - /** - * @description The existing language tag in the Tolgee platform to which the imported language should be mapped. - * - * When null, Tolgee will try to guess the language from the file contents or file name. - */ - languageTag?: string; - /** - * @description Tags of languages to be imported. When null, all languages from will be imported. - * - * This field is useful when the file contains multiple languages and you want to import only some of them. For example when importing Apple String Catalog (APPLE_XCSTRINGS), you might want only to import the base language. - */ - languageTagsToImport?: string[]; - /** @description Namespace to import the file to. If not provided, the key will be imported without namespace. */ - namespace?: string; - }; - ImportKeysDto: { - keys: components["schemas"]["ImportKeysItemDto"][]; - }; - ImportKeysItemDto: { - /** - * @description Description of key - * @example This key is used on homepage. It's a label of sign up button. - */ - description?: string; - /** - * @description Key name to set translations for - * @example what_a_key_to_translate - */ - name: string; - /** @description The namespace of the key. (When empty or null default namespace will be used) */ - namespace?: string; - /** - * @description Tags of the key - * @example [ - * "homepage", - * "user-profile" - * ] - */ - tags?: string[]; - /** - * @description Object mapping language tag to translation - * @example { - * "en": "What a translated value!", - * "cs": "Jaká to přeložená hodnota!" - * } - */ - translations: { - [key: string]: string; - }; - }; - ImportKeysResolvableDto: { - keys: components["schemas"]["ImportKeysResolvableItemDto"][]; - }; - ImportKeysResolvableItemDto: { - /** - * @description Key name to set translations for - * @example what_a_key_to_translate - */ - name: string; - branch?: string; - /** @description The namespace of the key. (When empty or null default namespace will be used) */ - namespace?: string; - screenshots?: components["schemas"]["KeyScreenshotDto"][]; - /** @description Object mapping language tag to translation */ - translations: { - [key: string]: components["schemas"]["ImportTranslationResolvableDto"]; - }; - }; - ImportLanguageModel: { - /** Format: int32 */ - conflictCount: number; - /** - * @deprecated - * @description Use existingLanguageTag - */ - existingLanguageAbbreviation?: string; - /** Format: int64 */ - existingLanguageId?: number; - existingLanguageName?: string; - existingLanguageTag?: string; - /** Format: int64 */ - id: number; - /** Format: int64 */ - importFileId: number; - /** Format: int32 */ - importFileIssueCount: number; - importFileName: string; - name: string; - namespace?: string; - /** Format: int32 */ - resolvedCount: number; - /** Format: int32 */ - totalCount: number; - }; - ImportNamespaceModel: { - /** - * Format: int64 - * @description The id of namespace. When null, namespace doesn't exist and will be created by import. - * @example 10000048 - */ - id?: number; - /** @example homepage */ - name: string; - }; - ImportResult: { - unresolvedConflicts?: components["schemas"]["SimpleImportConflictResult"][]; - }; - ImportSettingsModel: { - /** @description If true, placeholders from other formats will be converted to ICU when possible */ - convertPlaceholdersToIcu: boolean; - /** @description If false, only updates keys, skipping the creation of new keys */ - createNewKeys: boolean; - /** @description If true, key descriptions will be overridden by the import */ - overrideKeyDescriptions: boolean; - }; - ImportSettingsRequest: { - /** @description If true, placeholders from other formats will be converted to ICU when possible */ - convertPlaceholdersToIcu: boolean; - /** @description If false, only updates keys, skipping the creation of new keys */ - createNewKeys: boolean; - /** @description If true, key descriptions will be overridden by the import */ - overrideKeyDescriptions: boolean; - }; - ImportTranslationModel: { - /** Format: int64 */ - conflictId?: number; - conflictText?: string; - /** @enum {string} */ - conflictType?: "SHOULD_NOT_EDIT_REVIEWED" | "CANNOT_EDIT_REVIEWED" | "CANNOT_EDIT_DISABLED"; - existingKeyIsPlural: boolean; - /** Format: int64 */ - id: number; - isOverridable: boolean; - isPlural: boolean; - keyDescription?: string; - /** Format: int64 */ - keyId: number; - keyName: string; - override: boolean; - resolved: boolean; - text?: string; - }; - ImportTranslationResolvableDto: { - /** - * @description Determines, how conflict is resolved. - * - KEEP: Translation is not changed - * - OVERRIDE: Translation is overridden - * - NEW: New translation is created - * - FORCE_OVERRIDE: Translation is updated, created or kept. - * - * @example OVERRIDE - * @enum {string} - */ - resolution: "KEEP" | "OVERRIDE" | "NEW" | "FORCE_OVERRIDE"; - /** - * @description Translation text - * @example Hello! I am a translation! - */ - text: string; - }; - JwtAuthenticationResponse: { - accessToken?: string; - tokenType?: string; - }; - KeyDefinitionDto: { - name: string; - namespace?: string; - }; - KeyId: { - /** - * Format: int64 - * @description If key id is provided, name and namespace are ignored. - */ - id?: number; - name?: string; - namespace?: string; - }; - KeyImportResolvableResultModel: { - /** @description List of keys */ - keys: components["schemas"]["KeyModel"][]; - /** @description Map uploadedImageId to screenshot */ - screenshots: { - [key: string]: components["schemas"]["ScreenshotModel"]; - }; - }; - KeyInScreenshotModel: { - /** Format: int64 */ - keyId: number; - keyName: string; - keyNamespace?: string; - originalText?: string; - position?: components["schemas"]["KeyInScreenshotPosition"]; - }; - KeyInScreenshotPosition: { - /** Format: int32 */ - height: number; - /** Format: int32 */ - width: number; - /** Format: int32 */ - x: number; - /** Format: int32 */ - y: number; - }; - KeyInScreenshotPositionDto: { - /** Format: int32 */ - height: number; - /** Format: int32 */ - width: number; - /** Format: int32 */ - x: number; - /** Format: int32 */ - y: number; - }; - KeyModel: { - /** @description Custom values of the key */ - custom?: { - [key: string]: unknown; - }; - /** - * @description Description of key - * @example This key is used on homepage. It's a label of sign up button. - */ - description?: string; - /** - * Format: int64 - * @description Id of key record - */ - id: number; - /** - * @description Name of key - * @example this_is_super_key - */ - name: string; - /** - * @description Namespace of key - * @example homepage - */ - namespace?: string; - }; - KeyScreenshotDto: { - positions?: components["schemas"]["KeyInScreenshotPositionDto"][]; - text?: string; - /** - * Format: int64 - * @description Ids of screenshot uploaded with /v2/image-upload endpoint - */ - uploadedImageId: number; - }; - KeySearchResultView: { - baseTranslation?: string; - description?: string; - /** Format: int64 */ - id: number; - name: string; - namespace?: string; - translation?: string; - }; - KeySearchSearchResultModel: { - baseTranslation?: string; - description?: string; - /** Format: int64 */ - id: number; - name: string; - namespace?: string; - translation?: string; - view?: components["schemas"]["KeySearchResultView"]; - }; - KeyTaskViewModel: { - done: boolean; - /** Format: int64 */ - languageId: number; - languageTag: string; - /** Format: int64 */ - number: number; - /** @enum {string} */ - type: "TRANSLATE" | "REVIEW"; - userAssigned: boolean; - }; - KeyWithBaseTranslationModel: { - /** - * @description Base translation - * @example This is translation - */ - baseTranslation?: string; - /** - * Format: int64 - * @description Id of key record - */ - id: number; - /** - * @description Name of key - * @example this_is_super_key - */ - name: string; - /** - * @description Namespace of key - * @example homepage - */ - namespace?: string; - }; - KeyWithDataModel: { - /** @description Custom values of the key */ - custom: { - [key: string]: unknown; - }; - /** - * @description Description of key - * @example This key is used on homepage. It's a label of sign up button. - */ - description?: string; - /** - * Format: int64 - * @description Id of key record - */ - id: number; - /** @description If key is pluralized. If it will be reflected in the editor */ - isPlural: boolean; - /** - * @description Name of key - * @example this_is_super_key - */ - name: string; - /** - * @description Namespace of key - * @example homepage - */ - namespace?: string; - /** @description The argument name for the plural */ - pluralArgName?: string; - /** @description Screenshots of the key */ - screenshots: components["schemas"]["ScreenshotModel"][]; - /** @description Tags of key */ - tags: components["schemas"]["TagModel"][]; - /** - * @description Translations object containing values updated in this request - * @example { - * "en": { - * "id": 100000003, - * "text": "This is super translation!" - * } - * } - */ - translations: { - [key: string]: components["schemas"]["TranslationModel"]; - }; - }; - KeyWithTranslationsModel: { - /** @description There is a context available for this key */ - contextPresent: boolean; - /** - * Format: int64 - * @description The time when the key was created - */ - createdAt: number; - /** - * @description The namespace of the key - * @example homepage - */ - keyDescription?: string; - /** - * Format: int64 - * @description Id of key record - */ - keyId: number; - /** - * @description Is this key a plural? - * @example true - */ - keyIsPlural: boolean; - /** - * @description Name of key - * @example this_is_super_key - */ - keyName: string; - /** - * @description The namespace of the key - * @example homepage - */ - keyNamespace?: string; - /** - * Format: int64 - * @description The namespace id of the key - * @example 100000282 - */ - keyNamespaceId?: number; - /** - * @description The placeholder name for plural parameter - * @example value - */ - keyPluralArgName?: string; - /** @description Tags of key */ - keyTags: components["schemas"]["TagModel"][]; - /** - * Format: int64 - * @description Count of screenshots provided for the key - * @example 1 - */ - screenshotCount: number; - /** @description Key screenshots. Not provided when API key hasn't screenshots.view scope permission. */ - screenshots?: components["schemas"]["ScreenshotModel"][]; - /** @description Tasks related to this key */ - tasks?: components["schemas"]["KeyTaskViewModel"][]; - /** - * @description Translations object - * @example - * { - * "en": { - * "id": 100000003, - * "text": "This is super translation!" - * "state": "TRANSLATED", - * "commentCount": 1 - * } - * } - */ - translations: { - [key: string]: components["schemas"]["TranslationViewModel"]; - }; - }; - KeysScopeView: { - /** Format: int64 */ - characterCount: number; - /** Format: int64 */ - keyCount: number; - /** Format: int64 */ - keyCountIncludingConflicts: number; - /** Format: int64 */ - wordCount: number; - }; - KeysWithTranslationsPageModel: { - _embedded?: { - keys?: components["schemas"]["KeyWithTranslationsModel"][]; - }; - /** - * @description Cursor to get next data - * @example eyJrZXlJZCI6eyJkaXJlY3Rpb24iOiJBU0MiLCJ2YWx1ZSI6IjEwMDAwMDAxMjAifX0= - */ - nextCursor?: string; - page?: components["schemas"]["PageMetadata"]; - pagedModel?: components["schemas"]["PagedModelKeyWithTranslationsModel"]; - /** @description Provided languages data */ - selectedLanguages: components["schemas"]["LanguageModel"][]; - }; - LabelModel: { - color: string; - description?: string; - /** Format: int64 */ - id: number; - name: string; - }; - LabelRequest: { - /** - * @description Hex color in format #RRGGBB. - * @example #FF5733 - */ - color: string; - description?: string; - name: string; - }; - LabelTranslationsRequest: { - keyIds: number[]; - labelIds: number[]; - languageIds: number[]; - }; - LanguageMapping: { - /** - * @description The language from the imported file. - * - * For xliff files, this is the `source-language` or the `target-language` attribute value of `file` element. - * @example en-US - */ - importLanguage: string; - /** - * @description The tag of language existing in the Tolgee platform to which the imported language should be mapped. - * @example en-US - */ - platformLanguageTag: string; - }; - LanguageModel: { - /** - * @description Whether is base language of project - * @example false - */ - base: boolean; - /** - * @description Language flag emoji as UTF-8 emoji - * @example 🇨🇿 - */ - flagEmoji?: string; - /** Format: int64 */ - id: number; - /** - * @description Language name in english - * @example Czech - */ - name: string; - /** - * @description Language name in this language - * @example čeština - */ - originalName?: string; - /** - * @description Language tag according to BCP 47 definition - * @example cs-CZ - */ - tag: string; - }; - LanguageRequest: { - /** - * @description Language flag emoji as UTF-8 emoji - * @example 🇨🇿 - */ - flagEmoji?: string; - /** - * @description Language name in english - * @example Czech - */ - name: string; - /** - * @description Language name in this language - * @example čeština - */ - originalName: string; - /** - * @description Language tag according to BCP 47 definition - * @example cs-CZ - */ - tag: string; - }; - LanguageStatsModel: { - languageFlagEmoji?: string; - /** Format: int64 */ - languageId?: number; - languageName?: string; - languageOriginalName?: string; - languageTag?: string; - /** Format: int64 */ - reviewedKeyCount: number; - /** Format: double */ - reviewedPercentage: number; - /** Format: int64 */ - reviewedWordCount: number; - /** Format: int64 */ - translatedKeyCount: number; - /** Format: double */ - translatedPercentage: number; - /** Format: int64 */ - translatedWordCount: number; - /** Format: date-time */ - translationsUpdatedAt?: string; - /** Format: int64 */ - untranslatedKeyCount: number; - /** Format: double */ - untranslatedPercentage: number; - /** Format: int64 */ - untranslatedWordCount: number; - }; - MachineTranslationRequest: { - keyIds: number[]; - llmPrompt?: components["schemas"]["PromptDto"]; - targetLanguageIds: number[]; - }; - ModifiedEntityModel: { - description?: { - [key: string]: unknown; - }; - entityClass: string; - /** Format: int64 */ - entityId: number; - exists?: boolean; - modifications?: { - [key: string]: components["schemas"]["PropertyModification"]; - }; - relations?: { - [key: string]: components["schemas"]["ExistenceEntityDescription"]; - }; - }; - NamespaceModel: { - /** - * Format: int64 - * @description The id of namespace - * @example 10000048 - */ - id: number; - /** @example homepage */ - name: string; - }; - NotificationModel: { - /** Format: date-time */ - createdAt?: string; - /** Format: int64 */ - id: number; - linkedTask?: components["schemas"]["TaskModel"]; - originatingUser?: components["schemas"]["SimpleUserAccountModel"]; - project?: components["schemas"]["SimpleProjectModel"]; - /** @enum {string} */ - type: "TASK_ASSIGNED" | "TASK_FINISHED" | "TASK_CANCELED" | "MFA_ENABLED" | "MFA_DISABLED" | "PASSWORD_CHANGED"; - }; - NotificationSettingGroupModel: { - email: boolean; - inApp: boolean; - }; - NotificationSettingModel: { - accountSecurity: components["schemas"]["NotificationSettingGroupModel"]; - tasks: components["schemas"]["NotificationSettingGroupModel"]; - }; - NotificationSettingsRequest: { - /** - * @example IN_APP - * @enum {string} - */ - channel: "IN_APP" | "EMAIL"; - /** - * @description True if the setting should be enabled, false for disabled - * @example false - */ - enabled: boolean; - /** - * @example TASKS - * @enum {string} - */ - group: "ACCOUNT_SECURITY" | "TASKS"; - }; - NotificationsMarkSeenRequest: { - /** - * @description Notification IDs to be marked as seen - * @example [ - * 1, - * 2, - * 3 - * ] - */ - notificationIds: number[]; - }; - OrganizationDto: { - /** @example This is a beautiful organization full of beautiful and clever people */ - description?: string; - /** @example Beautiful organization */ - name: string; - /** @example btforg */ - slug?: string; - }; - OrganizationModel: { - /** @example Links to avatar images */ - avatar?: components["schemas"]["Avatar"]; - basePermissions: components["schemas"]["PermissionModel"]; - /** - * @description The role of currently authorized user. - * - * Can be null when user has direct access to one of the projects owned by the organization. - * @enum {string} - */ - currentUserRole?: "MEMBER" | "OWNER" | "MAINTAINER"; - /** @example This is a beautiful organization full of beautiful and clever people */ - description?: string; - /** Format: int64 */ - id: number; - /** @example Beautiful organization */ - name: string; - /** @example btforg */ - slug: string; - }; - PageMetadata: { - /** Format: int64 */ - number?: number; - /** Format: int64 */ - size?: number; - /** Format: int64 */ - totalElements?: number; - /** Format: int64 */ - totalPages?: number; - }; - PagedModelBatchJobModel: { - _embedded?: { - batchJobs?: components["schemas"]["BatchJobModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelImportFileIssueModel: { - _embedded?: { - importFileIssues?: components["schemas"]["ImportFileIssueModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelImportLanguageModel: { - _embedded?: { - languages?: components["schemas"]["ImportLanguageModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelImportTranslationModel: { - _embedded?: { - translations?: components["schemas"]["ImportTranslationModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelKeyModel: { - _embedded?: { - keys?: components["schemas"]["KeyModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelKeySearchSearchResultModel: { - _embedded?: { - keys?: components["schemas"]["KeySearchSearchResultModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelKeyWithTranslationsModel: { - _embedded?: { - keys?: components["schemas"]["KeyWithTranslationsModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelLabelModel: { - _embedded?: { - labels?: components["schemas"]["LabelModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelLanguageModel: { - _embedded?: { - languages?: components["schemas"]["LanguageModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelNamespaceModel: { - _embedded?: { - namespaces?: components["schemas"]["NamespaceModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelNotificationModel: { - _embedded?: { - notificationModelList?: components["schemas"]["NotificationModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelOrganizationModel: { - _embedded?: { - organizations?: components["schemas"]["OrganizationModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelProjectActivityModel: { - _embedded?: { - activities?: components["schemas"]["ProjectActivityModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelProjectModel: { - _embedded?: { - projects?: components["schemas"]["ProjectModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelSimpleGlossaryModel: { - _embedded?: { - glossaries?: components["schemas"]["SimpleGlossaryModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelSimpleGlossaryTermModel: { - _embedded?: { - glossaryTerms?: components["schemas"]["SimpleGlossaryTermModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelSimpleGlossaryTermWithTranslationsModel: { - _embedded?: { - glossaryTerms?: components["schemas"]["SimpleGlossaryTermWithTranslationsModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelSimpleGlossaryWithStatsModel: { - _embedded?: { - glossaries?: components["schemas"]["SimpleGlossaryWithStatsModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelSimpleUserAccountModel: { - _embedded?: { - users?: components["schemas"]["SimpleUserAccountModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelTagModel: { - _embedded?: { - tags?: components["schemas"]["TagModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelTaskModel: { - _embedded?: { - tasks?: components["schemas"]["TaskModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelTaskWithProjectModel: { - _embedded?: { - tasks?: components["schemas"]["TaskWithProjectModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelTranslationCommentModel: { - _embedded?: { - translationComments?: components["schemas"]["TranslationCommentModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelTranslationHistoryModel: { - _embedded?: { - revisions?: components["schemas"]["TranslationHistoryModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelTranslationMemoryItemModel: { - _embedded?: { - translationMemoryItems?: components["schemas"]["TranslationMemoryItemModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelTranslationSuggestionModel: { - _embedded?: { - suggestions?: components["schemas"]["TranslationSuggestionModel"][]; - }; - page?: components["schemas"]["PageMetadata"]; - }; - PagedModelWithNextCursorNotificationModel: { - _embedded?: { - notificationModelList?: components["schemas"]["NotificationModel"][]; - }; - /** - * @description Cursor to get next data - * @example eyJrZXlJZCI6eyJkaXJlY3Rpb24iOiJBU0MiLCJ2YWx1ZSI6IjEwMDAwMDAxMjAifX0= - */ - nextCursor?: string; - page?: components["schemas"]["PageMetadata"]; - pagedModel?: components["schemas"]["PagedModelNotificationModel"]; - }; - PatWithUserModel: { - /** Format: int64 */ - createdAt: number; - description: string; - /** Format: int64 */ - expiresAt?: number; - /** Format: int64 */ - id: number; - /** Format: int64 */ - lastUsedAt?: number; - /** Format: int64 */ - updatedAt: number; - user: components["schemas"]["SimpleUserAccountModel"]; - }; - PermissionModel: { - /** - * @deprecated - * @description Deprecated (use translateLanguageIds). - * - * List of languages current user has TRANSLATE permission to. If null, all languages edition is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - permittedLanguageIds?: number[]; - /** - * @description Granted scopes to the user. When user has type permissions, this field contains permission scopes of the type. - * @example [ - * "KEYS_EDIT", - * "TRANSLATIONS_VIEW" - * ] - */ - scopes: ("translations.view" | "translations.edit" | "translations.suggest" | "keys.edit" | "screenshots.upload" | "screenshots.delete" | "screenshots.view" | "activity.view" | "languages.edit" | "admin" | "project.edit" | "members.view" | "members.edit" | "translation-comments.add" | "translation-comments.edit" | "translation-comments.set-state" | "translations.state-edit" | "keys.view" | "keys.delete" | "keys.create" | "batch-jobs.view" | "batch-jobs.cancel" | "translations.batch-by-tm" | "translations.batch-machine" | "content-delivery.manage" | "content-delivery.publish" | "webhooks.manage" | "tasks.view" | "tasks.edit" | "prompts.view" | "prompts.edit" | "translation-labels.manage" | "translation-labels.assign" | "all.view")[]; - /** - * @description List of languages user can change state to. If null, changing state of all language values is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - stateChangeLanguageIds?: number[]; - /** - * @description List of languages user can suggest to. If null, suggesting to all languages is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - suggestLanguageIds?: number[]; - /** - * @description List of languages user can translate to. If null, all languages editing is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - translateLanguageIds?: number[]; - /** - * @description The user's permission type. This field is null if uses granular permissions - * @enum {string} - */ - type?: "NONE" | "VIEW" | "TRANSLATE" | "REVIEW" | "EDIT" | "MANAGE"; - /** - * @description List of languages user can view. If null, all languages view is permitted. - * @example [ - * 200001, - * 200004 - * ] - */ - viewLanguageIds?: number[]; - }; - PreTranslationByTmRequest: { - keyIds: number[]; - targetLanguageIds: number[]; - }; - PrivateUserAccountModel: { - /** @enum {string} */ - accountType: "LOCAL" | "MANAGED" | "THIRD_PARTY"; - avatar?: components["schemas"]["Avatar"]; - deletable: boolean; - domain?: string; - emailAwaitingVerification?: string; - /** @enum {string} */ - globalServerRole: "USER" | "ADMIN" | "SUPPORTER"; - /** Format: int64 */ - id: number; - mfaEnabled: boolean; - name?: string; - needsSuperJwtToken: boolean; - /** @enum {string} */ - thirdPartyAuthType?: "GOOGLE" | "GITHUB" | "OAUTH2" | "SSO" | "SSO_GLOBAL"; - username: string; - }; - ProjectActivityAuthorModel: { - avatar?: components["schemas"]["Avatar"]; - deleted: boolean; - /** Format: int64 */ - id: number; - name?: string; - username?: string; - }; - ProjectActivityModel: { - author?: components["schemas"]["ProjectActivityAuthorModel"]; - counts?: { - [key: string]: number; - }; - meta?: { - [key: string]: unknown; - }; - modifiedEntities?: { - [key: string]: components["schemas"]["ModifiedEntityModel"][]; - }; - params?: unknown; - /** Format: int64 */ - revisionId: number; - /** Format: int64 */ - timestamp: number; - /** @enum {string} */ - type: "UNKNOWN" | "SET_TRANSLATION_STATE" | "SET_TRANSLATIONS" | "DISMISS_AUTO_TRANSLATED_STATE" | "SET_OUTDATED_FLAG" | "TRANSLATION_COMMENT_ADD" | "TRANSLATION_COMMENT_DELETE" | "TRANSLATION_COMMENT_EDIT" | "TRANSLATION_COMMENT_SET_STATE" | "SCREENSHOT_DELETE" | "SCREENSHOT_ADD" | "KEY_TAGS_EDIT" | "KEY_NAME_EDIT" | "KEY_DELETE" | "CREATE_KEY" | "COMPLEX_EDIT" | "IMPORT" | "CREATE_LANGUAGE" | "EDIT_LANGUAGE" | "DELETE_LANGUAGE" | "HARD_DELETE_LANGUAGE" | "CREATE_PROJECT" | "EDIT_PROJECT" | "NAMESPACE_EDIT" | "BATCH_PRE_TRANSLATE_BY_TM" | "BATCH_MACHINE_TRANSLATE" | "AUTO_TRANSLATE" | "BATCH_CLEAR_TRANSLATIONS" | "BATCH_COPY_TRANSLATIONS" | "BATCH_SET_TRANSLATION_STATE" | "BATCH_TAG_KEYS" | "BATCH_UNTAG_KEYS" | "BATCH_SET_KEYS_NAMESPACE" | "BATCH_ASSIGN_TRANSLATION_LABEL" | "BATCH_UNASSIGN_TRANSLATION_LABEL" | "AUTOMATION" | "CONTENT_DELIVERY_CONFIG_CREATE" | "CONTENT_DELIVERY_CONFIG_UPDATE" | "CONTENT_DELIVERY_CONFIG_DELETE" | "CONTENT_STORAGE_CREATE" | "CONTENT_STORAGE_UPDATE" | "CONTENT_STORAGE_DELETE" | "WEBHOOK_CONFIG_CREATE" | "WEBHOOK_CONFIG_UPDATE" | "WEBHOOK_CONFIG_DELETE" | "COMPLEX_TAG_OPERATION" | "TASKS_CREATE" | "TASK_CREATE" | "TASK_UPDATE" | "TASK_KEYS_UPDATE" | "TASK_FINISH" | "TASK_CLOSE" | "TASK_REOPEN" | "TASK_KEY_UPDATE" | "ORDER_TRANSLATION" | "GLOSSARY_CREATE" | "GLOSSARY_UPDATE" | "GLOSSARY_DELETE" | "GLOSSARY_IMPORT" | "GLOSSARY_TERM_CREATE" | "GLOSSARY_TERM_UPDATE" | "GLOSSARY_TERM_DELETE" | "GLOSSARY_TERM_TRANSLATION_UPDATE" | "TRANSLATION_LABELS_EDIT" | "TRANSLATION_LABEL_ASSIGN" | "TRANSLATION_LABEL_CREATE" | "TRANSLATION_LABEL_UPDATE" | "TRANSLATION_LABEL_DELETE" | "CREATE_SUGGESTION" | "DECLINE_SUGGESTION" | "ACCEPT_SUGGESTION" | "REVERSE_SUGGESTION" | "DELETE_SUGGESTION" | "SUGGESTION_SET_ACTIVE" | "AI_PROMPT_CREATE" | "AI_PROMPT_UPDATE" | "AI_PROMPT_DELETE"; - }; - ProjectModel: { - avatar?: components["schemas"]["Avatar"]; - baseLanguage?: components["schemas"]["LanguageModel"]; - computedPermission: components["schemas"]["ComputedPermissionModel"]; - defaultNamespace?: components["schemas"]["NamespaceModel"]; - description?: string; - /** - * @description Current user's direct permission - * @example MANAGE - */ - directPermission?: components["schemas"]["PermissionModel"]; - /** @description Whether to disable ICU placeholder visualization in the editor and it's support. */ - icuPlaceholders: boolean; - /** Format: int64 */ - id: number; - name: string; - organizationOwner?: components["schemas"]["SimpleOrganizationModel"]; - /** @enum {string} */ - organizationRole?: "MEMBER" | "OWNER" | "MAINTAINER"; - slug?: string; - /** - * @description Suggestions for translations - * @enum {string} - */ - suggestionsMode: "DISABLED" | "ENABLED"; - /** - * @description Level of protection of translations - * @enum {string} - */ - translationProtection: "NONE" | "PROTECT_REVIEWED"; - useBranching: boolean; - useNamespaces: boolean; - }; - ProjectStatsModel: { - /** Format: int64 */ - baseWordsCount: number; - /** Format: int64 */ - keyCount: number; - /** Format: int32 */ - languageCount: number; - languageStats: components["schemas"]["LanguageStatsModel"][]; - /** Format: int64 */ - membersCount: number; - /** Format: int64 */ - projectId: number; - /** Format: double */ - reviewedPercentage: number; - /** Format: int64 */ - tagCount: number; - /** Format: int64 */ - taskCount: number; - /** Format: double */ - translatedPercentage: number; - }; - PromptDto: { - basicPromptOptions?: ("KEY_NAME" | "KEY_DESCRIPTION" | "KEY_CONTEXT" | "PROJECT_DESCRIPTION" | "LANGUAGE_NOTES" | "TM_SUGGESTIONS" | "SCREENSHOT" | "GLOSSARY")[]; - name: string; - providerName: string; - template?: string; - }; - PropertyModification: { - new?: unknown; - old?: unknown; - }; - PublicSsoTenantModel: { - domain: string; - force: boolean; - global: boolean; - }; - RelatedKeyDto: { - keyName: string; - namespace?: string; - }; - ScreenshotInfoDto: { - location?: string; - positions?: components["schemas"]["KeyInScreenshotPositionDto"][]; - text?: string; - }; - ScreenshotModel: { - /** Format: date-time */ - createdAt?: string; - fileUrl: string; - /** - * @description File name, which may be downloaded from the screenshot path. - * - * When images are secured. Encrypted timestamp is appended to the filename. - */ - filename: string; - /** Format: int32 */ - height?: number; - /** Format: int64 */ - id: number; - keyReferences: components["schemas"]["KeyInScreenshotModel"][]; - location?: string; - middleSized?: string; - middleSizedUrl?: string; - /** - * @description Thumbnail file name, which may be downloaded from the screenshot path. - * - * When images are secured. Encrypted timestamp is appended to the filename. - */ - thumbnail: string; - thumbnailUrl: string; - /** Format: int32 */ - width?: number; - }; - SelectAllResponse: { - ids: number[]; - }; - SetDisabledLanguagesRequest: { - languageIds: number[]; - }; - SetFileNamespaceRequest: { - namespace?: string; - }; - SetKeysNamespaceRequest: { - keyIds: number[]; - namespace?: string; - }; - SetTranslationsResponseModel: { - /** - * Format: int64 - * @description Id of key record - */ - keyId: number; - keyIsPlural: boolean; - /** - * @description Name of key - * @example this_is_super_key - */ - keyName: string; - /** - * @description The namespace of the key - * @example homepage - */ - keyNamespace?: string; - /** - * @description Translations object containing values updated in this request - * @example { - * "en": { - * "id": 100000003, - * "text": "This is super translation!" - * } - * } - */ - translations: { - [key: string]: components["schemas"]["TranslationModel"]; - }; - }; - SetTranslationsStateStateRequest: { - keyIds: number[]; - languageIds: number[]; - /** @enum {string} */ - state: "UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED"; - }; - SetTranslationsWithKeyDto: { - /** - * @description Key name to set translations for - * @example what_a_key_to_translate - */ - key: string; - /** - * @description List of languages to return translations for. - * - * If not provided, only modified translation will be provided. - * - * @example [ - * "en", - * "de", - * "fr" - * ] - */ - languagesToReturn?: string[]; - /** @description The namespace of the key. (When empty or null default namespace will be used) */ - namespace?: string; - /** - * @description Object mapping language tag to translation - * @example { - * "en": "What a translated value!", - * "cs": "Jaká to přeložená hodnota!" - * } - */ - translations: { - [key: string]: string; - }; - }; - SimpleGlossaryModel: { - /** - * @description Language tag for default translations for terms - * @example en - */ - baseLanguageTag: string; - /** Format: int64 */ - id: number; - name: string; - }; - SimpleGlossaryTermModel: { - description: string; - /** @description Specifies whether the term represents a shortened form of a word or phrase */ - flagAbbreviation: boolean; - /** @description When true, the term matching considers uppercase and lowercase characters as distinct */ - flagCaseSensitive: boolean; - /** @description When true, marks this term as prohibited or not recommended for use in translations */ - flagForbiddenTerm: boolean; - /** @description When true, this term has the same translation across all target languages */ - flagNonTranslatable: boolean; - /** Format: int64 */ - id: number; - }; - SimpleGlossaryTermWithTranslationsModel: { - /** - * @description A detailed explanation or definition of the glossary term - * @example It's trademark - */ - description: string; - /** @description Specifies whether the term represents a shortened form of a word or phrase */ - flagAbbreviation: boolean; - /** @description When true, the term matching considers uppercase and lowercase characters as distinct */ - flagCaseSensitive: boolean; - /** @description When true, marks this term as prohibited or not recommended for use in translations */ - flagForbiddenTerm: boolean; - /** @description When true, this term has the same translation across all target languages */ - flagNonTranslatable: boolean; - /** Format: int64 */ - id: number; - translations: components["schemas"]["GlossaryTermTranslationModel"][]; - }; - SimpleGlossaryWithStatsModel: { - /** - * Format: int64 - * @description Total number of projects currently using this glossary - * @example 69 - */ - assignedProjectsCount: number; - /** - * @description The primary language code used for terms (e.g., 'en' for English) - * @example en - */ - baseLanguageTag: string; - /** - * @description The name of one project using this glossary (can be shown as a preview) - * @example My Project - */ - firstAssignedProjectName?: string; - /** Format: int64 */ - id: number; - name: string; - }; - SimpleImportConflictResult: { - isOverridable: boolean; - keyName: string; - keyNamespace?: string; - language: string; - }; - SimpleOrganizationModel: { - /** @example Links to avatar images */ - avatar?: components["schemas"]["Avatar"]; - basePermissions: components["schemas"]["PermissionModel"]; - /** @example This is a beautiful organization full of beautiful and clever people */ - description?: string; - /** Format: int64 */ - id: number; - /** @example Beautiful organization */ - name: string; - /** @example btforg */ - slug: string; - }; - SimpleProjectModel: { - avatar?: components["schemas"]["Avatar"]; - baseLanguage?: components["schemas"]["LanguageModel"]; - description?: string; - icuPlaceholders: boolean; - /** Format: int64 */ - id: number; - name: string; - slug?: string; - }; - SimpleUserAccountModel: { - avatar?: components["schemas"]["Avatar"]; - deleted: boolean; - /** Format: int64 */ - id: number; - name?: string; - username: string; - }; - SingleStepImportRequest: { - /** @description If true, placeholders from other formats will be converted to ICU when possible */ - convertPlaceholdersToIcu: boolean; - /** @description If false, only updates keys, skipping the creation of new keys */ - createNewKeys: boolean; - /** - * @description If `false`, import will apply all `non-failed` overrides and reports `unresolvedConflict` - * .If `true`, import will fail completely on unresolved conflict and won't apply any changes. Unresolved conflicts are reported in the `params` of the error response - */ - errorOnUnresolvedConflict?: boolean; - /** @description Definition of mapping for each file to import. */ - fileMappings: components["schemas"]["ImportFileMapping"][]; - /** - * @description Whether to override existing translation data. - * - * When set to `KEEP`, existing translations will be kept. - * When set to `NO_FORCE`, error will be thrown on conflict. - * When set to `OVERRIDE`, existing translations will be overwritten - * @enum {string} - */ - forceMode: "OVERRIDE" | "KEEP" | "NO_FORCE"; - /** - * @description Maps the languages from imported files to languages existing in the Tolgee platform. - * - * Use this field only when your files contain multiple languages (e.g., XLIFF files). - * - * Otherwise, use the `languageTag` property of `fileMappings`. - * - * Example: In xliff files, there are `source-language` and `target-language` attributes defined on `file` element. Using this field you can map source and target values to languages stored in the Tolgee Platform. - */ - languageMappings?: components["schemas"]["LanguageMapping"][]; - /** @description If true, key descriptions will be overridden by the import */ - overrideKeyDescriptions: boolean; - /** - * @description Some translations are forbidden or protected: - * - * When set to `RECOMMENDED` it will fail for DISABLED translations and protected REVIEWED translations. - * When set to `ALL` it will fail for DISABLED translations, but will try to update protected REVIEWED translations (fails only if user has no permission) - * - * @enum {string} - */ - overrideMode?: "RECOMMENDED" | "ALL"; - /** @description If yes, keys from project that were not included in import will be deleted (only within namespaces which are included in the import). */ - removeOtherKeys?: boolean; - /** - * @description When importing files in structured formats (e.g., JSON, YAML), this field defines the delimiter which will be used in names of imported keys. - * @example . - */ - structureDelimiter?: string; - /** @description Keys created by this import will be tagged with these tags. It add tags only to new keys. The keys that already exist will not be tagged. */ - tagNewKeys: string[]; - }; - SingleStepImportResolvableItemRequest: { - /** - * @description Key name to set translations for - * @example what_a_key_to_translate - */ - name: string; - /** @description The namespace of the key. (When empty or null default namespace will be used) */ - namespace?: string; - screenshots?: components["schemas"]["KeyScreenshotDto"][]; - /** @description Object mapping language tag to translation */ - translations: { - [key: string]: components["schemas"]["SingleStepImportResolvableTranslationRequest"]; - }; - }; - SingleStepImportResolvableRequest: { - /** - * @description If `false`, import will apply all `non-failed` overrides and reports `unresolvedConflict` - * .If `true`, import will fail completely on unresolved conflict and won't apply any changes. Unresolved conflicts are reported in the `params` of the error response - */ - errorOnUnresolvedConflict?: boolean; - branch?: string; - /** @description List of keys to import */ - keys: components["schemas"]["SingleStepImportResolvableItemRequest"][]; - /** - * @description Some translations are forbidden or protected: - * - * When set to `RECOMMENDED` it will fail for DISABLED translations and protected REVIEWED translations. - * When set to `ALL` it will fail for DISABLED translations, but will try to update protected REVIEWED translations (fails only if user has no permission) - * - * @enum {string} - */ - overrideMode?: "RECOMMENDED" | "ALL"; - }; - SingleStepImportResolvableTranslationRequest: { - /** - * @description - * To ensure the import doesn't override something that should not be (in case data have changed unexpectedly), - * you can specify what do you "expect": - * - EXPECT_NO_CONFLICT: There should be no conflict, if there is, import fails - * - OVERRIDE: New translation is applied over the existing in every case (Default) - * - * @example OVERRIDE - * @enum {string} - */ - resolution?: "EXPECT_NO_CONFLICT" | "OVERRIDE"; - /** - * @description Translation text - * @example Hello! I am a translation! - */ - text: string; - }; - StreamingResponseBody: unknown; - SuggestRequestDto: { - /** @description Text value of base translation. Useful, when base translation is not stored yet. */ - baseText?: string; - /** @description Whether base text is plural. This value is ignored if baseText is null. */ - isPlural?: boolean; - /** - * Format: int64 - * @description Key Id to get results for. Use when key is stored already. - */ - keyId?: number; - plural?: boolean; - /** @description List of services to use. If null, then all enabled services are used. */ - services?: ("GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "PROMPT")[]; - /** Format: int64 */ - targetLanguageId: number; - }; - SuggestResultModel: { - /** @description If true, the base translation was empty and no translation was provided. */ - baseBlank: boolean; - /** - * @deprecated - * @description String translations provided by enabled services. (deprecated, use `result` instead) - * @example - * { - * "GOOGLE": "This was translated by Google", - * "TOLGEE": "This was translated by Tolgee Translator", - * } - */ - machineTranslations?: { - [key: string]: string; - }; - /** - * @description Results provided by enabled services. - * @example { - * "GOOGLE": { - * "output": "This was translated by Google", - * "contextDescription": null - * }, - * "TOLGEE": { - * "output": "This was translated by Tolgee Translator", - * "contextDescription": "This is an example in swagger" - * } - * } - */ - result?: { - [key: string]: components["schemas"]["TranslationItemModel"]; - }; - }; - TagKeyDto: { - name: string; - }; - TagKeysRequest: { - keyIds: number[]; - tags: string[]; - }; - TagModel: { - /** Format: int64 */ - id: number; - name: string; - }; - TaskKeysResponse: { - keys: number[]; - }; - TaskModel: { - agency?: components["schemas"]["TranslationAgencySimpleModel"]; - assignees: components["schemas"]["SimpleUserAccountModel"][]; - author?: components["schemas"]["SimpleUserAccountModel"]; - /** Format: int64 */ - baseCharacterCount: number; - /** Format: int64 */ - baseWordCount: number; - /** Format: int64 */ - closedAt?: number; - /** Format: int64 */ - createdAt?: number; - description: string; - /** Format: int64 */ - doneItems: number; - /** Format: int64 */ - dueDate?: number; - language: components["schemas"]["LanguageModel"]; - name?: string; - /** Format: int64 */ - number: number; - /** @enum {string} */ - state: "NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED"; - /** Format: int64 */ - totalItems: number; - /** @enum {string} */ - type: "TRANSLATE" | "REVIEW"; - }; - TaskPerUserReportModel: { - /** Format: int64 */ - baseCharacterCount: number; - /** Format: int64 */ - baseWordCount: number; - /** Format: int64 */ - doneItems: number; - user: components["schemas"]["SimpleUserAccountModel"]; - }; - TaskWithProjectModel: { - agency?: components["schemas"]["TranslationAgencySimpleModel"]; - assignees: components["schemas"]["SimpleUserAccountModel"][]; - author?: components["schemas"]["SimpleUserAccountModel"]; - /** Format: int64 */ - baseCharacterCount: number; - /** Format: int64 */ - baseWordCount: number; - /** Format: int64 */ - closedAt?: number; - /** Format: int64 */ - createdAt?: number; - description: string; - /** Format: int64 */ - doneItems: number; - /** Format: int64 */ - dueDate?: number; - language: components["schemas"]["LanguageModel"]; - name?: string; - /** Format: int64 */ - number: number; - project: components["schemas"]["SimpleProjectModel"]; - /** @enum {string} */ - state: "NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED"; - /** Format: int64 */ - totalItems: number; - /** @enum {string} */ - type: "TRANSLATE" | "REVIEW"; - }; - TranslationAgencySimpleModel: { - avatar?: components["schemas"]["Avatar"]; - /** Format: int64 */ - id: number; - name: string; - url?: string; - }; - TranslationCommentDto: { - /** @enum {string} */ - state: "RESOLUTION_NOT_NEEDED" | "NEEDS_RESOLUTION" | "RESOLVED"; - text: string; - }; - TranslationCommentModel: { - /** @description User who created the comment */ - author: components["schemas"]["SimpleUserAccountModel"]; - /** - * Format: date-time - * @description Date when it was created - */ - createdAt: string; - /** - * Format: int64 - * @description Id of translation comment record - */ - id: number; - /** - * @description State of translation - * @enum {string} - */ - state: "RESOLUTION_NOT_NEEDED" | "NEEDS_RESOLUTION" | "RESOLVED"; - /** @description Text of comment */ - text: string; - /** - * Format: date-time - * @description Date when it was updated - */ - updatedAt: string; - }; - TranslationCommentWithLangKeyDto: { - /** Format: int64 */ - keyId: number; - /** Format: int64 */ - languageId: number; - /** @enum {string} */ - state: "RESOLUTION_NOT_NEEDED" | "NEEDS_RESOLUTION" | "RESOLVED"; - text: string; - }; - TranslationHistoryModel: { - /** @description Author of the change */ - author?: components["schemas"]["SimpleUserAccountModel"]; - /** @description Modified fields */ - modifications?: { - [key: string]: components["schemas"]["PropertyModification"]; - }; - /** @enum {string} */ - revisionType: "ADD" | "MOD" | "DEL"; - /** - * Format: int64 - * @description Unix timestamp of the revision - */ - timestamp: number; - }; - TranslationItemModel: { - contextDescription?: string; - output: string; - }; - TranslationLabelRequest: { - /** Format: int64 */ - keyId: number; - /** Format: int64 */ - labelId: number; - /** Format: int64 */ - languageId: number; - }; - TranslationMemoryItemModel: { - baseText: string; - keyName: string; - /** Format: float */ - similarity: number; - targetText: string; - }; - TranslationModel: { - /** @description Was translated using Translation Memory or Machine translation service? */ - auto: boolean; - /** - * Format: int64 - * @description Id of translation record - */ - id: number; - /** - * @description Which machine translation service was used to auto translate this - * @enum {string} - */ - mtProvider?: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "PROMPT"; - /** @description Whether base language translation was changed after this translation was updated */ - outdated: boolean; - /** - * @description State of translation - * @enum {string} - */ - state: "UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED"; - /** @description Translation text */ - text?: string; - }; - TranslationSuggestionAcceptResponse: { - accepted: components["schemas"]["TranslationSuggestionModel"]; - declined: number[]; - }; - TranslationSuggestionModel: { - author: components["schemas"]["SimpleUserAccountModel"]; - /** Format: date-time */ - createdAt: string; - /** Format: int64 */ - id: number; - isPlural: boolean; - /** Format: int64 */ - keyId: number; - /** Format: int64 */ - languageId: number; - /** @enum {string} */ - state: "ACTIVE" | "ACCEPTED" | "DECLINED"; - translation?: string; - /** Format: date-time */ - updatedAt: string; - }; - TranslationSuggestionSimpleModel: { - author: components["schemas"]["SimpleUserAccountModel"]; - /** Format: int64 */ - id: number; - isPlural: boolean; - /** @enum {string} */ - state: "ACTIVE" | "ACCEPTED" | "DECLINED"; - translation?: string; - }; - TranslationViewModel: { - /** - * Format: int64 - * @description Number of active suggestions - */ - activeSuggestionCount: number; - /** @description Was translated using Translation Memory or Machine translation service? */ - auto: boolean; - /** - * Format: int64 - * @description Count of translation comments - */ - commentCount: number; - /** @description Was translation memory used to translate this? */ - fromTranslationMemory: boolean; - /** - * Format: int64 - * @description Id of translation record - */ - id?: number; - /** @description Labels assigned to this translation */ - labels?: components["schemas"]["LabelModel"][]; - /** - * @description Which machine translation service was used to auto translate this - * @enum {string} - */ - mtProvider?: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "PROMPT"; - /** @description Whether base language translation was changed after this translation was updated */ - outdated: boolean; - /** - * @description State of translation - * @enum {string} - */ - state: "UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED"; - /** @description First suggestion */ - suggestions?: components["schemas"]["TranslationSuggestionSimpleModel"][]; - /** @description Translation text */ - text?: string; - /** - * Format: int64 - * @description Number of all suggestions - */ - totalSuggestionCount: number; - /** - * Format: int64 - * @description Count of unresolved translation comments - */ - unresolvedCommentCount: number; - }; - TranslationWithCommentModel: { - comment: components["schemas"]["TranslationCommentModel"]; - translation: components["schemas"]["TranslationModel"]; - }; - UntagKeysRequest: { - keyIds: number[]; - tags: string[]; - }; - UpdateGlossaryRequest: { - /** @description Projects assigned to glossary; when null, assigned projects will be kept unchanged. */ - assignedProjectIds?: number[]; - /** - * @description Language tag according to BCP 47 definition - * @example cs-CZ - */ - baseLanguageTag: string; - /** - * @description Glossary name - * @example My glossary - */ - name: string; - }; - UpdateGlossaryTermTranslationRequest: { - /** - * @description Language tag according to BCP 47 definition - * @example cs-CZ - */ - languageTag: string; - /** - * @description Translation text - * @example Translated text to language of languageTag - */ - text: string; - }; - UpdateGlossaryTermWithTranslationRequest: { - description?: string; - /** @description Specifies whether the term represents a shortened form of a word or phrase */ - flagAbbreviation?: boolean; - /** @description When true, the term matching considers uppercase and lowercase characters as distinct */ - flagCaseSensitive?: boolean; - /** @description When true, marks this term as prohibited or not recommended for use in translations */ - flagForbiddenTerm?: boolean; - /** @description When true, this term will have the same translation across all target languages */ - flagNonTranslatable?: boolean; - text?: string; - }; - UpdateNamespaceDto: { - name: string; - }; - UpdateTaskKeyRequest: { - done: boolean; - }; - UpdateTaskKeyResponse: { - /** @description Task key is marked as done */ - done: boolean; - /** @description Task progress is 100% */ - taskFinished: boolean; - }; - UpdateTaskKeysRequest: { - /** @description Keys to add to task */ - addKeys?: number[]; - /** @description Keys to remove from task */ - removeKeys?: number[]; - }; - UpdateTaskRequest: { - assignees: number[]; - description: string; - /** - * Format: int64 - * @description Due to date in epoch format (milliseconds). - * @example 1661172869000 - */ - dueDate?: number; - name?: string; - }; - UploadedImageModel: { - /** Format: date-time */ - createdAt: string; - fileUrl: string; - filename: string; - /** Format: int64 */ - id: number; - location?: string; - requestFilename: string; - }; - UsedNamespaceModel: { - /** - * Format: int64 - * @description The id of namespace. Null for default namespace. - * @example 10000048 - */ - id?: number; - /** - * @description Name of namespace. Null if default. - * @example homepage - */ - name?: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} - -export type $defs = Record; - -export type external = Record; - -export interface operations { - - /** - * Export to ZIP of jsons - * @deprecated - * @description Exports data as ZIP of jsons - */ - doExportJsonZip_1: { - responses: { - /** @description OK */ - 200: { - content: { - "application/zip": components["schemas"]["StreamingResponseBody"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get current API key info - * @description Returns info the API key which user currently authenticated with. Otherwise responds with 400 status code. - */ - getCurrent_1: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ApiKeyWithLanguagesModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get current permission info - * @description Returns current PAK or PAT permissions for current user, api-key and project - */ - getCurrentPermissions: { - parameters: { - query?: { - /** @description Required when using with PAT */ - projectId?: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ApiKeyPermissionsModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get current third party authentication provider */ - getCurrentAuthProvider: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["AuthProviderDto"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Initiate provider change to remove current third party authentication provider */ - deleteCurrentAuthProvider: { - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get info about authentication provider which can replace the current one */ - getChangedAuthProvider: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["AuthProviderDto"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Accept change of the third party authentication provider */ - acceptChangeAuthProvider: { - requestBody: { - content: { - "application/json": components["schemas"]["AcceptAuthProviderChangeRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["JwtAuthenticationResponse"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Reject change of the third party authentication provider */ - rejectChangeAuthProvider: { - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Upload an image for later use */ - upload: { - requestBody?: { - content: { - "multipart/form-data": { - /** Format: binary */ - image: string; - info?: components["schemas"]["ImageUploadInfoDto"]; - }; - }; - }; - responses: { - /** @description Created */ - 201: { - content: { - "*/*": components["schemas"]["UploadedImageModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Delete uploaded images */ - delete_15: { - parameters: { - path: { - ids: number[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Gets notifications of the currently logged in user, newest is first. */ - getNotifications: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - /** - * @description Filter by the `seen` parameter. - * - * no value = request everything - * - * true = only seen - * - * false = only unseen - */ - filterSeen?: boolean; - cursor?: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelWithNextCursorNotificationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get notification settings - * @description Returns notification settings of the currently logged in user - */ - getNotificationsSettings: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["NotificationSettingModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Save notification setting - * @description Saves new value for given parameters - */ - putNotificationSetting: { - requestBody: { - content: { - "application/json": components["schemas"]["NotificationSettingsRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Marks notifications of the currently logged in user with given IDs as seen. */ - markNotificationsAsSeen: { - requestBody: { - content: { - "application/json": components["schemas"]["NotificationsMarkSeenRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get all permitted organizations - * @description Returns all organizations, which is current user allowed to view - */ - getAll_10: { - parameters: { - query: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - filterCurrentUserOwner: boolean; - search?: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelOrganizationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create organization */ - create_12: { - requestBody: { - content: { - "application/json": components["schemas"]["OrganizationDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["OrganizationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get one organization */ - get_15: { - parameters: { - path: { - id: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["OrganizationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get all organization glossaries */ - getAll_12: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - search?: string; - }; - path: { - organizationId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelSimpleGlossaryModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create glossary */ - create_13: { - parameters: { - path: { - organizationId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateGlossaryRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["GlossaryModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get all organization glossaries with some additional statistics */ - getAllWithStats: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - search?: string; - }; - path: { - organizationId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelSimpleGlossaryWithStatsModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get glossary */ - get_13: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["GlossaryModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Update glossary */ - update_8: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateGlossaryRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["GlossaryModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Delete glossary */ - delete_7: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get all projects assigned to glossary */ - getAssignedProjects: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelSimpleProjectModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Export glossary terms as CSV */ - export: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["StreamingResponseBody"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Import glossary terms from CSV */ - importCsv: { - parameters: { - query?: { - removeExistingTerms?: boolean; - }; - path: { - organizationId: number; - glossaryId: number; - }; - }; - requestBody?: { - content: { - "multipart/form-data": { - /** Format: binary */ - file: string; - }; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["GlossaryImportResult"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get all languages in use by the glossary */ - getLanguages: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelGlossaryLanguageDto"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get all glossary terms */ - getAll_13: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - search?: string; - languageTags?: string[]; - }; - path: { - organizationId: number; - glossaryId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelSimpleGlossaryTermModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create a new glossary term */ - create_14: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateGlossaryTermWithTranslationRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CreateUpdateGlossaryTermResponse"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Batch delete multiple terms */ - deleteMultiple: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["DeleteMultipleGlossaryTermsRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get glossary term */ - get_14: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - termId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["GlossaryTermModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Update glossary term */ - update_9: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - termId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateGlossaryTermWithTranslationRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CreateUpdateGlossaryTermResponse"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Delete glossary term */ - delete_8: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - termId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Set a new glossary term translation for language */ - update_12: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - termId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateGlossaryTermTranslationRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["GlossaryTermTranslationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get glossary term translation for language */ - get_23: { - parameters: { - path: { - organizationId: number; - glossaryId: number; - termId: number; - languageTag: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["GlossaryTermTranslationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get all glossary terms ids */ - getAllIds: { - parameters: { - query?: { - search?: string; - languageTags?: string[]; - }; - path: { - organizationId: number; - glossaryId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelLong"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get all glossary terms with translations */ - getAllWithTranslations: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - search?: string; - languageTags?: string[]; - }; - path: { - organizationId: number; - glossaryId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelSimpleGlossaryTermWithTranslationsModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get credit balance for organization - * @description Returns machine translation credit balance for organization - */ - getOrganizationCredits: { - parameters: { - path: { - organizationId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CreditBalanceModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get organization by slug */ - get_22: { - parameters: { - path: { - slug: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["OrganizationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Return current PAK - * @description Returns current Personal Access Token. If the request is not authenticated with a Personal Access Token, it will return 400 response status. - */ - getCurrent: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PatWithUserModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get all permitted - * @description Returns all projects where current user has any permission - */ - getAll: { - parameters: { - query?: { - /** @description Filter projects by id */ - filterId?: number[]; - /** @description Filter projects without id */ - filterNotId?: number[]; - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - search?: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelProjectModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Create project - * @description Creates a new project with languages and initial settings. - */ - createProject: { - requestBody: { - content: { - "application/json": components["schemas"]["CreateProjectRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ProjectModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get project activity */ - getActivity_1: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelProjectActivityModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get one revision data */ - getSingleRevision_1: { - parameters: { - path: { - revisionId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ProjectActivityModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** List batch operations */ - list_4: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelBatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get batch operation */ - get_21: { - parameters: { - path: { - id: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Stop batch operation - * @description Stops batch operation if possible. - */ - cancel_1: { - parameters: { - path: { - id: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Store Big Meta - * @description Stores a bigMeta for a project - */ - store_3: { - parameters?: { - query?: { - /** @description Branch name to store big meta on */ - branch?: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["BigMetaDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get all running and pending batch operations - * @description Returns all running and pending batch operations. Completed batch operations are returned only if they are not older than 1 hour. If user doesn't have permission to view all batch operations, only their operations are returned. - */ - currentJobs_1: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelBatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Export data - * @description - * Exports project data in various formats (JSON, properties, YAML, etc.). - * - * ## HTTP Conditional Requests Support - * - * This endpoint supports HTTP conditional requests using both If-Modified-Since and If-None-Match headers: - * - * - **If-Modified-Since header provided**: The server checks if the project data has been modified since the specified date - * - **If-None-Match header provided**: The server checks if the project data has changed by comparing the eTag value - * - **Data not modified**: Returns HTTP 304 Not Modified with empty body - * - **Data modified or no header**: Returns HTTP 200 OK with the exported data, Last-Modified header, and ETag header - * - * The Last-Modified header in the response contains the timestamp of the last project modification, - * and the ETag header contains a unique identifier for the current project state. Both can be used - * for subsequent conditional requests to avoid unnecessary data transfer when the project hasn't changed. - * - * Cache-Control header is set to max-age=0 to ensure validation on each request. - */ - exportData_1: { - parameters: { - query: { - /** - * @description Languages to be contained in export. - * - * If null, all languages are exported - * @example en - */ - languages?: string[]; - /** @description Format to export to */ - format: "JSON" | "JSON_TOLGEE" | "XLIFF" | "PO" | "APPLE_STRINGS_STRINGSDICT" | "APPLE_XLIFF" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" | "PROPERTIES" | "YAML_RUBY" | "YAML" | "JSON_I18NEXT" | "CSV" | "RESX_ICU" | "XLSX" | "APPLE_XCSTRINGS" | "ANDROID_SDK" | "APPLE_SDK"; - /** - * @description Delimiter to structure file content. - * - * e.g. For key "home.header.title" would result in {"home": {"header": "title": {"Hello"}}} structure. - * - * When null, resulting file won't be structured. Works only for generic structured formats (e.g. JSON, YAML), - * specific formats like `YAML_RUBY` don't honor this parameter. - */ - structureDelimiter?: string; - /** @description Filter key IDs to be contained in export */ - filterKeyId?: number[]; - /** @description Filter key IDs not to be contained in export */ - filterKeyIdNot?: number[]; - /** - * @description Filter keys tagged by. - * - * This filter works the same as `filterTagIn` but in this cases it accepts single tag only. - */ - filterTag?: string; - /** @description Filter keys tagged by one of provided tags */ - filterTagIn?: string[]; - /** @description Filter keys not tagged by one of provided tags */ - filterTagNotIn?: string[]; - /** @description Filter keys with prefix */ - filterKeyPrefix?: string; - /** @description Filter translations with state. By default, all states except untranslated is exported. */ - filterState?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED")[]; - /** @description Filter translations with namespace. By default, all namespaces everything are exported. To export default namespace, use empty string. */ - filterNamespace?: string[]; - /** - * @description If false, it doesn't return zip of files, but it returns single file. - * - * This is possible only when single language is exported. Otherwise it returns "400 - Bad Request" response. - */ - zip: boolean; - /** - * @description Message format to be used for export. - * - * e.g. PHP_PO: Hello %s, ICU: Hello {name}. - * - * This property is honored only for generic formats like JSON or YAML. - * For specific formats like `YAML_RUBY` it's ignored. - */ - messageFormat?: "C_SPRINTF" | "PHP_SPRINTF" | "JAVA_STRING_FORMAT" | "APPLE_SPRINTF" | "RUBY_SPRINTF" | "I18NEXT" | "ICU" | "PYTHON_PERCENT"; - /** - * @description This is a template that defines the structure of the resulting .zip file content. - * - * The template is a string that can contain the following placeholders: {namespace}, {languageTag}, - * {androidLanguageTag}, {snakeLanguageTag}, {extension}. - * - * For example, when exporting to JSON with the template `{namespace}/{languageTag}.{extension}`, - * the English translations of the `home` namespace will be stored in `home/en.json`. - * - * The `{snakeLanguageTag}` placeholder is the same as `{languageTag}` but in snake case. (e.g., en_US). - * - * The Android specific `{androidLanguageTag}` placeholder is the same as `{languageTag}` - * but in Android format. (e.g., en-rUS) - */ - fileStructureTemplate?: string; - /** - * @description If true, for structured formats (like JSON) arrays are supported. - * - * e.g. Key hello[0] will be exported as {"hello": ["..."]} - */ - supportArrays: boolean; - /** - * @description If true, HTML tags are escaped in the exported file. (Supported in the XLIFF format only). - * - * e.g. Key hello will be exported as <b>hello</b> - */ - escapeHtml?: boolean; - }; - }; - responses: { - /** - * @description When multiple files are exported, they are zipped and returned as a single zip file. - * When a single file is exported, it is returned directly. - */ - 200: { - content: { - "application/*": unknown; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Export data (post) - * @description - * Exports project data in various formats (JSON, properties, YAML, etc.). - * Useful when exceeding allowed URL size with GET requests. - * - * ## HTTP Conditional Requests Support - * - * This endpoint supports HTTP conditional requests using both If-Modified-Since and If-None-Match headers: - * - * - **If-Modified-Since header provided**: The server checks if the project data has been modified since the specified date - * - **If-None-Match header provided**: The server checks if the project data has changed by comparing the eTag value - * - **Data not modified**: Returns HTTP 304 Not Modified with empty body - * - **Data modified or no header**: Returns HTTP 200 OK with the exported data, Last-Modified header, and ETag header - * - * Note: This endpoint uses a custom implementation that returns 304 Not Modified for all HTTP methods - * (including POST) when conditional headers indicate the data hasn't changed. This differs from Spring's - * default behavior which returns 412 for POST requests, but is appropriate here since POST is used only - * to accommodate large request parameters, not to modify data. - * - * The Last-Modified header in the response contains the timestamp of the last project modification, - * and the ETag header contains a unique identifier for the current project state. Both can be used - * for subsequent conditional requests to avoid unnecessary data transfer when the project hasn't changed. - * - * Cache-Control header is set to max-age=0 to ensure validation on each request. - */ - exportPost_1: { - requestBody: { - content: { - "application/json": components["schemas"]["ExportParams"]; - }; - }; - responses: { - /** - * @description When multiple files are exported, they are zipped and returned as a single zip file. - * When a single file is exported, it is returned directly. - */ - 200: { - content: { - "application/*": unknown; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Add files - * @description Prepares provided files to import. - */ - addFiles_1: { - parameters: { - query?: { - /** - * @description When importing files in structured formats (e.g., JSON, YAML), this field defines the delimiter which will be used in names of imported keys. - * @example . - */ - structureDelimiter?: string; - }; - }; - requestBody?: { - content: { - "multipart/form-data": { - files: string[]; - }; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ImportAddFilesResultModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Delete - * @description Deletes prepared import data. - */ - cancelImport_1: { - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get Import Settings - * @description Returns import settings for the authenticated user and the project. - */ - get_1: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ImportSettingsModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Set Import Settings - * @description Stores import settings for the authenticated user and the project. - */ - store_1: { - requestBody: { - content: { - "application/json": components["schemas"]["ImportSettingsRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ImportSettingsModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get namespaces - * @description Returns all existing and imported namespaces - */ - getAllNamespaces_1: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelImportNamespaceModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - getProjectInfo: { - parameters: { - path: { - projectId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ProjectModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": - | components["schemas"]["ErrorResponseTyped"] - | components["schemas"]["ErrorResponseBody"]; - }; - }; - }, - }, - /** - * Apply import - * @description Imports the data prepared in previous step - */ - applyImport_1: { - parameters: { - query?: { - /** @description Whether override or keep all translations with unresolved conflicts */ - forceMode?: "OVERRIDE" | "KEEP" | "NO_FORCE"; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Apply import (streaming) - * @description Imports the data prepared in previous step. Streams current status. - */ - applyImportStreaming_1: { - parameters: { - query?: { - /** @description Whether override or keep all translations with unresolved conflicts */ - forceMode?: "OVERRIDE" | "KEEP" | "NO_FORCE"; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/x-ndjson": components["schemas"]["StreamingResponseBody"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get result - * @description Returns the result of preparation. - */ - getImportResult_1: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelImportLanguageModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Select namespace - * @description Sets namespace for file to import. - */ - selectNamespace_1: { - parameters: { - path: { - fileId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SetFileNamespaceRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get file issues - * @description Returns issues for uploaded file. - */ - getImportFileIssues_1: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - path: { - importFileId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelImportFileIssueModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Reset existing language pairing - * @description Resets existing language paired with language to import. - */ - resetExistingLanguage_1: { - parameters: { - path: { - importLanguageId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Pair existing language - * @description Sets existing language to pair with language to import. Data will be imported to selected existing language when applied. - */ - selectExistingLanguage_1: { - parameters: { - path: { - importLanguageId: number; - existingLanguageId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get import language - * @description Returns language prepared to import. - */ - getImportLanguage_1: { - parameters: { - path: { - languageId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ImportLanguageModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Delete language - * @description Deletes language prepared to import. - */ - deleteLanguage_3: { - parameters: { - path: { - languageId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Resolve all translation conflicts (keep existing) - * @description Resolves all translation conflicts for provided language. The old translations will be kept. - */ - resolveTranslationSetKeepExisting_3: { - parameters: { - path: { - languageId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Resolve all translation conflicts (override) - * @description Resolves all translation conflicts for provided language. The old translations will be overridden. - */ - resolveTranslationSetOverride_3: { - parameters: { - path: { - languageId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get translations - * @description Returns translations prepared to import. - */ - getImportTranslations_1: { - parameters: { - query?: { - /** @description Whether only translations, which are in conflict with existing translations should be returned */ - onlyConflicts?: boolean; - /** @description Whether only translations with unresolved conflictswith existing translations should be returned */ - onlyUnresolved?: boolean; - /** @description String to search in translation text or key */ - search?: string; - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - path: { - languageId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelImportTranslationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Resolve conflict (keep existing) - * @description Resolves translation conflict. The old translation will be kept. - */ - resolveTranslationSetKeepExisting_1: { - parameters: { - path: { - languageId: number; - translationId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Resolve conflict (override) - * @description Resolves translation conflict. The old translation will be overridden. - */ - resolveTranslationSetOverride_1: { - parameters: { - path: { - languageId: number; - translationId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get all keys */ - getAll_8: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelKeyModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create new key */ - create_8: { - requestBody: { - content: { - "application/json": components["schemas"]["CreateKeyDto"]; - }; - }; - responses: { - /** @description Created */ - 201: { - content: { - "*/*": components["schemas"]["KeyWithDataModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Delete one or multiple keys (post) - * @description Delete one or multiple keys by their IDs in request body. Useful for larger requests esxceeding allowed URL length. - */ - delete_12: { - requestBody: { - content: { - "application/json": components["schemas"]["DeleteKeysDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create new key */ - create_7: { - requestBody: { - content: { - "application/json": components["schemas"]["CreateKeyDto"]; - }; - }; - responses: { - /** @description Created */ - 201: { - content: { - "*/*": components["schemas"]["KeyWithDataModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Import keys - * @description Imports new keys with translations. If key already exists, its translations and tags are not updated. - */ - importKeys_3: { - requestBody: { - content: { - "application/json": components["schemas"]["ImportKeysDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Import keys (resolvable) - * @deprecated - * @description - * Import's new keys with translations. Translations can be updated, when specified.\n\n - * DEPRECATED: Use /v2/projects/{projectId}/single-step-import-resolvable instead. - */ - importKeys_1: { - requestBody: { - content: { - "application/json": components["schemas"]["ImportKeysResolvableDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["KeyImportResolvableResultModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get key info - * @description Returns information about keys. (KeyData, Screenshots, Translation in specified language)If key is not found, it's not included in the response. - */ - getInfo_2: { - requestBody: { - content: { - "application/json": components["schemas"]["GetKeysRequestDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelKeyWithDataModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Search for keys - * @description This endpoint helps you to find desired key by keyName, base translation or translation in specified language. - * - * Sort is ignored for this request. - */ - searchForKey_1: { - parameters: { - query: { - /** @description Search query */ - search: string; - /** @description Language to search in */ - languageTag?: string; - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelKeySearchSearchResultModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Select keys - * @description Returns all key IDs for specified filter values. This way, you can apply the same filter as in the translation view and get the resulting key IDs for future use. - */ - selectKeys_3: { - parameters: { - query?: { - /** - * @description Translation state in the format: languageTag,state. You can use this parameter multiple times. - * - * When used with multiple states for same language it is applied with logical OR. - * - * When used with multiple languages, it is applied with logical AND. - */ - filterState?: string[]; - /** - * @description Languages to be contained in response. - * - * To add multiple languages, repeat this param (eg. ?languages=en&languages=de) - * @example en - */ - languages?: string[]; - /** @description String to search in key name or translation text */ - search?: string; - /** @description Selects key with provided names. Use this param multiple times to fetch more keys. */ - filterKeyName?: string[]; - /** @description Selects key with provided ID. Use this param multiple times to fetch more keys. */ - filterKeyId?: number[]; - /** @description Selects only keys for which the translation is missing in any returned language. It only filters for translations included in returned languages. */ - filterUntranslatedAny?: boolean; - /** @description Selects only keys, where translation is provided in any language */ - filterTranslatedAny?: boolean; - /** - * @description Selects only keys where the translation is missing for the specified language. The specified language must be included in the returned languages. Otherwise, this filter doesn't apply. - * @example en-US - */ - filterUntranslatedInLang?: string; - /** - * @description Selects only keys, where translation is provided in specified language - * @example en-US - */ - filterTranslatedInLang?: string; - /** - * @description Selects only keys, where translation was auto translated for specified languages. - * @example en-US - */ - filterAutoTranslatedInLang?: string[]; - /** @description Selects only keys with screenshots */ - filterHasScreenshot?: boolean; - /** @description Selects only keys without screenshots */ - filterHasNoScreenshot?: boolean; - /** - * @description Selects only keys with provided namespaces. - * - * To filter default namespace, set to empty string. - */ - filterNamespace?: string[]; - /** - * @description Selects only keys without provided namespaces. - * - * To filter default namespace, set to empty string. - */ - filterNoNamespace?: string[]; - /** @description Selects only keys with provided tag */ - filterTag?: string[]; - /** @description Selects only keys without provided tag */ - filterNoTag?: string[]; - /** - * @description Selects only keys, where translation in provided langs is in outdated state - * @example en-US - */ - filterOutdatedLanguage?: string[]; - /** - * @description Selects only keys, where translation in provided langs is not in outdated state - * @example en-US - */ - filterNotOutdatedLanguage?: string[]; - /** - * @description Selects only key affected by activity with specidfied revision ID - * @example 1234567 - */ - filterRevisionId?: number[]; - /** @description Select only keys which were not successfully translated by batch job with provided id */ - filterFailedKeysOfJob?: number; - /** @description Select only keys which are in specified task */ - filterTaskNumber?: number[]; - /** @description Filter task keys which are `not done` */ - filterTaskKeysNotDone?: boolean; - /** @description Filter task keys which are `done` */ - filterTaskKeysDone?: boolean; - /** @description Filter keys with unresolved comments in lang */ - filterHasUnresolvedCommentsInLang?: string[]; - /** @description Filter keys with any comments in lang */ - filterHasCommentsInLang?: string[]; - /** - * @description Filter key translations with labels - * @example labelId1,labelId2 - */ - filterLabel?: string[]; - /** @description Filter keys with any suggestions in lang */ - filterHasSuggestionsInLang?: string[]; - /** @description Filter keys with no suggestions in lang */ - filterHasNoSuggestionsInLang?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["SelectAllResponse"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Delete one or multiple keys */ - delete_14: { - parameters: { - path: { - ids: number[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get one key */ - get_9: { - parameters: { - path: { - id: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["KeyModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Edit key name */ - edit_1: { - parameters: { - path: { - id: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["EditKeyDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["KeyModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get Big Meta for key */ - getBigMeta_1: { - parameters: { - path: { - id: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelKeyWithBaseTranslationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Edit key and related data - * @description Edits key name, translations, tags, screenshots, and other data - */ - complexEdit_1: { - parameters: { - path: { - id: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ComplexEditKeyDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["KeyWithDataModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get disabled languages - * @description Returns languages, in which key is disabled - */ - getDisabledLanguages_1: { - parameters: { - path: { - id: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelLanguageModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Set disabled languages - * @description Sets languages, in which key is disabled - */ - setDisabledLanguages_1: { - parameters: { - path: { - id: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SetDisabledLanguagesRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelLanguageModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Auto translates keys - * @description Uses enabled auto-translation methods. - * You need to set at least one of useMachineTranslation or useTranslationMemory to true. - * - * This will replace the the existing translation with the result obtained from specified source! - */ - autoTranslate_1: { - parameters: { - query?: { - /** - * @description Tags of languages to auto-translate. - * When no languages provided, it translates only untranslated languages. - */ - languages?: string[]; - useMachineTranslation?: boolean; - useTranslationMemory?: boolean; - }; - path: { - keyId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get screenshots */ - getKeyScreenshots: { - parameters: { - path: { - keyId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelScreenshotModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Upload screenshot */ - uploadScreenshot: { - parameters: { - path: { - keyId: number; - }; - }; - requestBody?: { - content: { - "multipart/form-data": { - /** Format: binary */ - screenshot: string; - info?: components["schemas"]["ScreenshotInfoDto"]; - }; - }; - }; - responses: { - /** @description Created */ - 201: { - content: { - "*/*": components["schemas"]["ScreenshotModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Delete screenshots */ - deleteScreenshots: { - parameters: { - path: { - ids: number[]; - keyId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Tag key - * @description Tags a key with tag. If tag with provided name doesn't exist, it is created - */ - tagKey_1: { - parameters: { - path: { - keyId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TagKeyDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TagModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Remove tag - * @description Removes tag with provided id from key with provided id - */ - removeTag_1: { - parameters: { - path: { - keyId: number; - tagId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get available project labels */ - getAll_2: { - parameters: { - query?: { - search?: string; - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelLabelModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create label */ - createLabel_1: { - requestBody: { - content: { - "application/json": components["schemas"]["LabelRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["LabelModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get labels by ids */ - getLabelsByIds_1: { - parameters: { - query: { - id: number[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["LabelModel"][]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Update label */ - updateLabel_1: { - parameters: { - path: { - labelId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["LabelRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["LabelModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Delete label */ - deleteLabel_1: { - parameters: { - path: { - labelId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get all languages */ - getAll_6: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - /** @description Filter languages by id */ - filterId?: number[]; - /** @description Filter languages without id */ - filterNotId?: number[]; - /** @description Filter languages by name or tag */ - search?: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelLanguageModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create language */ - createLanguage_1: { - requestBody: { - content: { - "application/json": components["schemas"]["LanguageRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["LanguageModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get one language */ - get_7: { - parameters: { - path: { - languageId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["LanguageModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Update language */ - editLanguage_1: { - parameters: { - path: { - languageId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["LanguageRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["LanguageModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Delete specific language */ - deleteLanguage_1: { - parameters: { - path: { - languageId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get suggestions */ - getSuggestions_1: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - /** @description Filter by suggestion state */ - filterState?: ("ACTIVE" | "ACCEPTED" | "DECLINED")[]; - }; - path: { - languageId: number; - keyId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelTranslationSuggestionModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create translation suggestion */ - createSuggestion_1: { - parameters: { - path: { - languageId: number; - keyId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateTranslationSuggestionRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TranslationSuggestionModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Delete suggestion - * @description User can only delete suggestion created by them - */ - deleteSuggestion_1: { - parameters: { - path: { - languageId: number; - keyId: number; - suggestionId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Accept suggestion */ - acceptSuggestion_1: { - parameters: { - query?: { - declineOther?: boolean; - }; - path: { - languageId: number; - keyId: number; - suggestionId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TranslationSuggestionAcceptResponse"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Decline suggestion */ - declineSuggestion_1: { - parameters: { - path: { - languageId: number; - keyId: number; - suggestionId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TranslationSuggestionModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Set suggestion active */ - suggestionSetActive_1: { - parameters: { - path: { - languageId: number; - keyId: number; - suggestionId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TranslationSuggestionModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * List user batch operations - * @description List all batch operations started by current user - */ - myList_1: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelBatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get namespace by name - * @description Returns information about a namespace by its name - */ - getByName_1: { - parameters: { - path: { - name: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["NamespaceModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get namespaces */ - getAllNamespaces_3: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - search?: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelNamespaceModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Update namespace */ - update_4: { - parameters: { - path: { - id: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateNamespaceDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["NamespaceModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Single step import - * @description Unlike the /v2/projects/{projectId}/import endpoint, imports the data in single request by provided files and parameters. This is useful for automated importing via API or CLI. - */ - singleStepFromFiles_1: { - requestBody?: { - content: { - "multipart/form-data": { - files: string[]; - params: components["schemas"]["SingleStepImportRequest"]; - }; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ImportResult"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Single step import from body */ - singleStepResolvableImport_1: { - requestBody: { - content: { - "application/json": components["schemas"]["SingleStepImportResolvableRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ImportResult"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Translates via llm and stores result in AiPlaygroundResult */ - aiPlaygroundTranslate_1: { - requestBody: { - content: { - "application/json": components["schemas"]["MachineTranslationRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Assign labels to translations */ - assignTranslationLabel_1: { - requestBody: { - content: { - "application/json": components["schemas"]["LabelTranslationsRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Clear translation values - * @description Clear translation values for provided keys in selected languages. - */ - clearTranslations_1: { - requestBody: { - content: { - "application/json": components["schemas"]["ClearTranslationsRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Copy translation values - * @description Copy translation values from one language to other languages. - */ - copyTranslations_1: { - requestBody: { - content: { - "application/json": components["schemas"]["CopyTranslationRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Delete keys */ - deleteKeys_1: { - requestBody: { - content: { - "application/json": components["schemas"]["DeleteKeysRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Machine Translation - * @description Translate provided keys to provided languages through primary MT provider. - */ - machineTranslation_1: { - requestBody: { - content: { - "application/json": components["schemas"]["MachineTranslationRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Pre-translate by TM - * @description Pre-translate provided keys to provided languages by TM. - */ - translate_1: { - requestBody: { - content: { - "application/json": components["schemas"]["PreTranslationByTmRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Set keys namespace */ - setKeysNamespace_1: { - requestBody: { - content: { - "application/json": components["schemas"]["SetKeysNamespaceRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Set translation state */ - setTranslationState_3: { - requestBody: { - content: { - "application/json": components["schemas"]["SetTranslationsStateStateRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Add tags */ - tagKeys_1: { - requestBody: { - content: { - "application/json": components["schemas"]["TagKeysRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Unassign labels from translations */ - unassignTranslationLabel_1: { - requestBody: { - content: { - "application/json": components["schemas"]["LabelTranslationsRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Remove tags */ - untagKeys_1: { - requestBody: { - content: { - "application/json": components["schemas"]["UntagKeysRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["BatchJobModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get project stats */ - getProjectStats_1: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["ProjectStatsModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get project daily amount of events */ - getProjectDailyActivity_1: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": { - [key: string]: number; - }; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get machine translation suggestions - * @description Suggests machine translations from enabled services - */ - suggestMachineTranslations_1: { - requestBody: { - content: { - "application/json": components["schemas"]["SuggestRequestDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["SuggestResultModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get machine translation suggestions (streaming) - * @description Suggests machine translations from enabled services. The results are streamed to the output in ndjson format. If an error occurs when for any service provider used, the error information is returned as a part of the result item, while the response has 200 status code. - */ - suggestMachineTranslationsStreaming_1: { - requestBody: { - content: { - "application/json": components["schemas"]["SuggestRequestDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/x-ndjson": components["schemas"]["StreamingResponseBody"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get suggestions from translation memory - * @description Suggests machine translations from translation memory. The result is always sorted by similarity, so sorting is not supported. - */ - suggestTranslationMemory_1: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SuggestRequestDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelTranslationMemoryItemModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Execute complex tag operation */ - executeComplexTagOperation_1: { - parameters?: { - query?: { - /** @description Branch name to tag keys on */ - branch?: string; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ComplexTagKeysRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get tags */ - getAll_15: { - parameters: { - query?: { - search?: string; - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelTagModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get tasks */ - getTasks_1: { - parameters: { - query?: { - /** @description Filter tasks by state */ - filterState?: ("NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED")[]; - /** @description Filter tasks without state */ - filterNotState?: ("NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED")[]; - /** @description Filter tasks by assignee */ - filterAssignee?: number[]; - /** @description Filter tasks by type */ - filterType?: ("TRANSLATE" | "REVIEW")[]; - /** @description Filter tasks by id */ - filterId?: number[]; - /** @description Filter tasks without id */ - filterNotId?: number[]; - /** @description Filter tasks by project */ - filterProject?: number[]; - /** @description Filter tasks without project */ - filterNotProject?: number[]; - /** @description Filter tasks by language */ - filterLanguage?: number[]; - /** @description Filter tasks by key */ - filterKey?: number[]; - /** @description Filter tasks by agency */ - filterAgency?: number[]; - /** @description Exclude tasks which were closed before specified timestamp */ - filterNotClosedBefore?: number; - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - search?: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelTaskModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create task */ - createTask_1: { - parameters: { - query?: { - filterState?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED")[]; - filterOutdated?: boolean; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateTaskRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TaskModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Calculate scope */ - calculateScope_1: { - parameters: { - query?: { - filterState?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED")[]; - filterOutdated?: boolean; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CalculateScopeRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["KeysScopeView"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create multiple tasks */ - createTasks_1: { - parameters: { - query?: { - filterState?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED")[]; - filterOutdated?: boolean; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateMultipleTasksRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get possible assignees */ - getPossibleAssignees_1: { - parameters: { - query?: { - /** @description Filter users by id */ - filterId?: number[]; - /** @description Filter only users that have at least following scopes */ - filterMinimalScope?: string; - /** @description Filter only users that can view language */ - filterViewLanguageId?: number; - /** @description Filter only users that can edit language */ - filterEditLanguageId?: number; - /** @description Filter only users that can edit state of language */ - filterStateLanguageId?: number; - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - search?: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelSimpleUserAccountModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get task */ - getTask_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TaskModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Update task */ - updateTask_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateTaskRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TaskModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get blocking task numbers - * @description If the tasks is blocked by other tasks, it returns numbers of these tasks. - */ - getBlockingTasks_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": number[]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Close task */ - cancelTask_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TaskModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Close task - * @deprecated - */ - closeTask_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TaskModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Finish task */ - finishTask_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TaskModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get task keys */ - getTaskKeys_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TaskKeysResponse"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Add or remove task keys */ - updateTaskKeys_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateTaskKeysRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Update task key - * @description Mark key as done, which updates task progress. - */ - updateTaskKey_1: { - parameters: { - path: { - taskNumber: number; - keyId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateTaskKeyRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["UpdateTaskKeyResponse"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get report - * @description Detailed statistics for every assignee - */ - getPerUserReport_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TaskPerUserReportModel"][]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Reopen task */ - reopenTask_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TaskModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get report in XLSX - * @description Detailed statistics about the task results - */ - getXlsxReport_1: { - parameters: { - path: { - taskNumber: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": string; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get translations in project */ - getTranslations_1: { - parameters: { - query?: { - /** @description Branch name to fetch translations from */ - branch?: string; - /** @description Cursor to get next data */ - cursor?: string; - /** - * @description Translation state in the format: languageTag,state. You can use this parameter multiple times. - * - * When used with multiple states for same language it is applied with logical OR. - * - * When used with multiple languages, it is applied with logical AND. - */ - filterState?: string[]; - /** - * @description Languages to be contained in response. - * - * To add multiple languages, repeat this param (eg. ?languages=en&languages=de) - * @example en - */ - languages?: string[]; - /** @description String to search in key name or translation text */ - search?: string; - /** @description Selects key with provided names. Use this param multiple times to fetch more keys. */ - filterKeyName?: string[]; - /** @description Selects key with provided ID. Use this param multiple times to fetch more keys. */ - filterKeyId?: number[]; - /** @description Selects only keys for which the translation is missing in any returned language. It only filters for translations included in returned languages. */ - filterUntranslatedAny?: boolean; - /** @description Selects only keys, where translation is provided in any language */ - filterTranslatedAny?: boolean; - /** - * @description Selects only keys where the translation is missing for the specified language. The specified language must be included in the returned languages. Otherwise, this filter doesn't apply. - * @example en-US - */ - filterUntranslatedInLang?: string; - /** - * @description Selects only keys, where translation is provided in specified language - * @example en-US - */ - filterTranslatedInLang?: string; - /** - * @description Selects only keys, where translation was auto translated for specified languages. - * @example en-US - */ - filterAutoTranslatedInLang?: string[]; - /** @description Selects only keys with screenshots */ - filterHasScreenshot?: boolean; - /** @description Selects only keys without screenshots */ - filterHasNoScreenshot?: boolean; - /** - * @description Selects only keys with provided namespaces. - * - * To filter default namespace, set to empty string. - */ - filterNamespace?: string[]; - /** - * @description Selects only keys without provided namespaces. - * - * To filter default namespace, set to empty string. - */ - filterNoNamespace?: string[]; - /** @description Selects only keys with provided tag */ - filterTag?: string[]; - /** @description Selects only keys without provided tag */ - filterNoTag?: string[]; - /** - * @description Selects only keys, where translation in provided langs is in outdated state - * @example en-US - */ - filterOutdatedLanguage?: string[]; - /** - * @description Selects only keys, where translation in provided langs is not in outdated state - * @example en-US - */ - filterNotOutdatedLanguage?: string[]; - /** - * @description Selects only key affected by activity with specidfied revision ID - * @example 1234567 - */ - filterRevisionId?: number[]; - /** @description Select only keys which were not successfully translated by batch job with provided id */ - filterFailedKeysOfJob?: number; - /** @description Select only keys which are in specified task */ - filterTaskNumber?: number[]; - /** @description Filter task keys which are `not done` */ - filterTaskKeysNotDone?: boolean; - /** @description Filter task keys which are `done` */ - filterTaskKeysDone?: boolean; - /** @description Filter keys with unresolved comments in lang */ - filterHasUnresolvedCommentsInLang?: string[]; - /** @description Filter keys with any comments in lang */ - filterHasCommentsInLang?: string[]; - /** - * @description Filter key translations with labels - * @example labelId1,labelId2 - */ - filterLabel?: string[]; - /** @description Filter keys with any suggestions in lang */ - filterHasSuggestionsInLang?: string[]; - /** @description Filter keys with no suggestions in lang */ - filterHasNoSuggestionsInLang?: string[]; - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["KeysWithTranslationsPageModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Update translations for existing key - * @description Sets translations for existing key - */ - setTranslations_1: { - requestBody: { - content: { - "application/json": components["schemas"]["SetTranslationsWithKeyDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["SetTranslationsResponseModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Create key or update translations - * @description Sets translations for existing key or creates new key and sets the translations to it. - */ - createOrUpdateTranslations_1: { - requestBody: { - content: { - "application/json": components["schemas"]["SetTranslationsWithKeyDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["SetTranslationsResponseModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Create translation comment - * @description Creates a translation comment. Empty translation is stored, when not exists. - */ - create_4: { - requestBody: { - content: { - "application/json": components["schemas"]["TranslationCommentWithLangKeyDto"]; - }; - }; - responses: { - /** @description Created */ - 201: { - content: { - "*/*": components["schemas"]["TranslationWithCommentModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Add label to translation by key and language id */ - assignLabel_3: { - requestBody: { - content: { - "application/json": components["schemas"]["TranslationLabelRequest"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["LabelModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Select keys - * @description Returns all key IDs for specified filter values. This way, you can apply the same filter as in the translation view and get the resulting key IDs for future use. - */ - selectKeys_1: { - parameters: { - query?: { - /** - * @description Translation state in the format: languageTag,state. You can use this parameter multiple times. - * - * When used with multiple states for same language it is applied with logical OR. - * - * When used with multiple languages, it is applied with logical AND. - */ - filterState?: string[]; - /** - * @description Languages to be contained in response. - * - * To add multiple languages, repeat this param (eg. ?languages=en&languages=de) - * @example en - */ - languages?: string[]; - /** @description String to search in key name or translation text */ - search?: string; - /** @description Selects key with provided names. Use this param multiple times to fetch more keys. */ - filterKeyName?: string[]; - /** @description Selects key with provided ID. Use this param multiple times to fetch more keys. */ - filterKeyId?: number[]; - /** @description Selects only keys for which the translation is missing in any returned language. It only filters for translations included in returned languages. */ - filterUntranslatedAny?: boolean; - /** @description Selects only keys, where translation is provided in any language */ - filterTranslatedAny?: boolean; - /** - * @description Selects only keys where the translation is missing for the specified language. The specified language must be included in the returned languages. Otherwise, this filter doesn't apply. - * @example en-US - */ - filterUntranslatedInLang?: string; - /** - * @description Selects only keys, where translation is provided in specified language - * @example en-US - */ - filterTranslatedInLang?: string; - /** - * @description Selects only keys, where translation was auto translated for specified languages. - * @example en-US - */ - filterAutoTranslatedInLang?: string[]; - /** @description Selects only keys with screenshots */ - filterHasScreenshot?: boolean; - /** @description Selects only keys without screenshots */ - filterHasNoScreenshot?: boolean; - /** - * @description Selects only keys with provided namespaces. - * - * To filter default namespace, set to empty string. - */ - filterNamespace?: string[]; - /** - * @description Selects only keys without provided namespaces. - * - * To filter default namespace, set to empty string. - */ - filterNoNamespace?: string[]; - /** @description Selects only keys with provided tag */ - filterTag?: string[]; - /** @description Selects only keys without provided tag */ - filterNoTag?: string[]; - /** - * @description Selects only keys, where translation in provided langs is in outdated state - * @example en-US - */ - filterOutdatedLanguage?: string[]; - /** - * @description Selects only keys, where translation in provided langs is not in outdated state - * @example en-US - */ - filterNotOutdatedLanguage?: string[]; - /** - * @description Selects only key affected by activity with specidfied revision ID - * @example 1234567 - */ - filterRevisionId?: number[]; - /** @description Select only keys which were not successfully translated by batch job with provided id */ - filterFailedKeysOfJob?: number; - /** @description Select only keys which are in specified task */ - filterTaskNumber?: number[]; - /** @description Filter task keys which are `not done` */ - filterTaskKeysNotDone?: boolean; - /** @description Filter task keys which are `done` */ - filterTaskKeysDone?: boolean; - /** @description Filter keys with unresolved comments in lang */ - filterHasUnresolvedCommentsInLang?: string[]; - /** @description Filter keys with any comments in lang */ - filterHasCommentsInLang?: string[]; - /** - * @description Filter key translations with labels - * @example labelId1,labelId2 - */ - filterLabel?: string[]; - /** @description Filter keys with any suggestions in lang */ - filterHasSuggestionsInLang?: string[]; - /** @description Filter keys with no suggestions in lang */ - filterHasNoSuggestionsInLang?: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["SelectAllResponse"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get all translations - * @description Returns all translations for specified languages - */ - getAllTranslations_1: { - parameters: { - query?: { - /** @description Namespace to return */ - ns?: string; - /** - * @description Delimiter to structure response content. - * - * e.g. For key "home.header.title" would result in {"home": {"header": {"title": "Hello"}}} structure. - * - * When null, resulting file will be a flat key-value object. - */ - structureDelimiter?: string; - /** - * @description Enables filtering of returned keys by their tags. - * Only keys with at least one provided tag will be returned. - * Optional, filtering is not applied if not specified. - * @example [ - * "productionReady", - * "nextRelease" - * ] - */ - filterTag?: string[]; - }; - path: { - /** - * @description Comma-separated language tags to return translations in. Languages you are not permitted to see will be silently dropped and not returned. - * @example [ - * "en", - * "de", - * "fr" - * ] - */ - languages: string[]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": string; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get translation comments - * @description Returns translation comments of translation - */ - getAll_4: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - path: { - translationId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelTranslationCommentModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Create translation comment */ - create_2: { - parameters: { - path: { - translationId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TranslationCommentDto"]; - }; - }; - responses: { - /** @description Created */ - 201: { - content: { - "*/*": components["schemas"]["TranslationCommentModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get one translation comment */ - get_5: { - parameters: { - path: { - translationId: number; - commentId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TranslationCommentModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Update translation comment */ - update_2: { - parameters: { - path: { - commentId: number; - translationId: number; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["TranslationCommentDto"]; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TranslationCommentModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Delete translation comment */ - delete_3: { - parameters: { - path: { - translationId: number; - commentId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Set state of translation comment */ - setState_1: { - parameters: { - path: { - translationId: number; - commentId: number; - state: "RESOLUTION_NOT_NEEDED" | "NEEDS_RESOLUTION" | "RESOLVED"; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TranslationCommentModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Dismiss auto-translated - * @description Removes "auto translated" indication - */ - dismissAutoTranslatedState_1: { - parameters: { - path: { - translationId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TranslationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get translation history - * @description Sorting is not supported for supported. It is automatically sorted from newest to oldest. - */ - getTranslationHistory_1: { - parameters: { - query?: { - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - }; - path: { - translationId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelTranslationHistoryModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Add label to translation */ - assignLabel_1: { - parameters: { - path: { - translationId: number; - labelId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["LabelModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Remove label from translation */ - unassignLabel_1: { - parameters: { - path: { - translationId: number; - labelId: number; - }; - }; - responses: { - /** @description OK */ - 200: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Set outdated value - * @description Set's "outdated" flag indicating the base translation was changed without updating current translation. - */ - setOutdated_1: { - parameters: { - path: { - translationId: number; - state: boolean; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TranslationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Set translation state */ - setTranslationState_1: { - parameters: { - path: { - translationId: number; - state: "TRANSLATED" | "REVIEWED"; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["TranslationModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get used namespaces - * @description Returns all used project namespaces. Response contains default (null) namespace if used. - */ - getUsedNamespaces_1: { - parameters?: { - query?: { - /** @description Branch name to get namespaces from */ - branch?: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelUsedNamespaceModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get user info - * @description Returns information about currently authenticated user. - */ - getInfo: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PrivateUserAccountModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** Get user tasks */ - getTasks_2: { - parameters: { - query?: { - /** @description Filter tasks by state */ - filterState?: ("NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED")[]; - /** @description Filter tasks without state */ - filterNotState?: ("NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED")[]; - /** @description Filter tasks by assignee */ - filterAssignee?: number[]; - /** @description Filter tasks by type */ - filterType?: ("TRANSLATE" | "REVIEW")[]; - /** @description Filter tasks by id */ - filterId?: number[]; - /** @description Filter tasks without id */ - filterNotId?: number[]; - /** @description Filter tasks by project */ - filterProject?: number[]; - /** @description Filter tasks without project */ - filterNotProject?: number[]; - /** @description Filter tasks by language */ - filterLanguage?: number[]; - /** @description Filter tasks by key */ - filterKey?: number[]; - /** @description Filter tasks by agency */ - filterAgency?: number[]; - /** @description Exclude tasks which were closed before specified timestamp */ - filterNotClosedBefore?: number; - /** @description Zero-based page index (0..N) */ - page?: number; - /** @description The size of the page to be returned */ - size?: number; - /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ - sort?: string[]; - search?: string; - }; - }; - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PagedModelTaskWithProjectModel"]; - }; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; - /** - * Get information about SSO configuration - * @description Returns information about sso configuration affecting the user. - */ - getSso: { - responses: { - /** @description OK */ - 200: { - content: { - "application/json": components["schemas"]["PublicSsoTenantModel"]; - }; - }; - /** @description No SSO configuration available for this user */ - 204: { - content: never; - }; - /** @description Bad Request */ - 400: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Unauthorized */ - 401: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Forbidden */ - 403: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - /** @description Not Found */ - 404: { - content: { - "application/json": components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]; - }; - }; - }; - }; -} diff --git a/src/ui/client/client.ts b/src/ui/client/client.ts deleted file mode 100644 index e268d6db..00000000 --- a/src/ui/client/client.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { getProjectIdFromApiKey } from "./decodeApiKey"; -import { paths } from "./apiSchema.generated"; -import { RequestParamsType, ResponseContent } from "./types"; -import { globalState } from "../state/GlobalState"; -import { errorToText } from "./errorCodes"; -import { customPaths } from "./apiSchema.custom"; - -type GlobalOptions = { - apiUrl?: string; - apiKey?: string; - apiTimeout?: number; -}; - -export type ClientOptions = - | { - config: GlobalOptions; - } - | undefined; - -type Params = { - [k: string]: string | string[] | null | undefined | Params; -}; - -async function getResObject(r: Response) { - const textBody = await r.text(); - try { - return JSON.parse(textBody); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - return textBody; - } -} - -const flattenParams = ( - params: Params | null | undefined, -): Record => { - if (params) { - return Object.entries(params).reduce( - (acc, [key, value]) => - Array.isArray(value) || typeof value !== "object" - ? { ...acc, [key]: value } - : { ...acc, ...flattenParams(value) }, - {}, - ); - } - return {}; -}; - -function buildQuery(object: { [key: string]: any }): string { - return Object.keys(object) - .filter((k) => object[k] !== undefined && object[k] !== null) - .map((k) => { - if (Array.isArray(object[k])) { - return object[k] - .map((v: any) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) - .join("&"); - } - return `${encodeURIComponent(k)}=${encodeURIComponent(object[k])}`; - }) - .join("&"); -} - -async function customFetch( - input: RequestInfo, - options: GlobalOptions, - init?: RequestInit, -): Promise { - if (!options.apiUrl) { - throw "URL not specified"; - } - if (!options.apiKey) { - throw "API key not specified"; - } - - init = init || {}; - init.headers = init.headers || {}; - init.headers = { - "X-API-Key": options.apiKey, - "X-Tolgee-SDK-Type": "Figma", - ...init.headers, - }; - - if (options.apiTimeout) { - if (typeof AbortSignal.timeout === "function") { - init.signal = AbortSignal.timeout(options.apiTimeout); - } else { - const controller = new AbortController(); - init.signal = controller.signal; - setTimeout(() => controller.abort(), options.apiTimeout); - } - } - - return fetch(options.apiUrl + input, init) - .then(async (r) => { - if (!r.ok) { - const data = await getResObject(r); - const message = errorToText(data?.message || data?.error || data?.code); - - if (r.status === 403 && message === "Forbidden") { - globalState.actions.setGlobalError("Invalid API key"); - } - - throw new Error(message); - } - return await getResObject(r); - }) - .catch((e) => { - if (e instanceof Error && e.name === "TimeoutError") { - throw new Error("Connection timeout"); - } - if (!e.message || e.message === "Failed to fetch") { - const message = "Could not connect to server"; - globalState.actions.setGlobalError(message); - throw message; - } else { - throw e.message; - } - }); -} - -export const addProjectIdToUrl = (url: string) => { - return url.replace("/projects/", "/projects/{projectId}/"); -}; - -export async function client< - Url extends keyof Paths, - Method extends keyof Paths[Url], - Paths = paths & customPaths, ->( - url: Url, - method: Method, - request: RequestParamsType, - clientOptions: ClientOptions, - options: GlobalOptions, -) { - const fetchOptions = { ...options, ...clientOptions?.config }; - const pathParams = (request as any)?.path || {}; - let urlResult = url as string; - - const projectId = getProjectIdFromApiKey(fetchOptions.apiKey); - - if (projectId !== undefined) { - if (pathParams.projectId === undefined) { - urlResult = addProjectIdToUrl(urlResult); - } - pathParams.projectId = projectId; - } - - if (pathParams) { - Object.entries(pathParams).forEach(([key, value]) => { - urlResult = urlResult.replace(`{${key}}`, value as any); - }); - } - - const formData = request?.content?.["multipart/form-data"] as Record< - string, - unknown - >; - let body: FormData | undefined; - if (formData) { - body = new FormData(); - Object.entries(formData).forEach(([key, value]) => { - if (Array.isArray(value)) { - let fileName: undefined | string; - if (Object.prototype.toString.call(value) === "[object File]") { - fileName = (value as any as File).name; - } - - value.forEach((item) => body?.append(key, item as any, fileName)); - return; - } - body?.append(key, value as any); - }); - } - - const jsonBody = JSON.stringify(request?.content?.["application/json"]); - - const queryParams = request?.query as any; - let queryString = ""; - - const params = flattenParams(queryParams); - const query = buildQuery(params); - if (query) { - queryString = `?${query}`; - } - - const defaultHeaders = jsonBody - ? { - "Content-Type": "application/json", - } - : {}; - - return customFetch(urlResult + queryString, fetchOptions, { - method: method as string, - body: body || jsonBody, - headers: defaultHeaders as HeadersInit, - }) as Promise>; -} diff --git a/src/ui/client/decodeApiKey.ts b/src/ui/client/decodeApiKey.ts deleted file mode 100644 index fda1c73d..00000000 --- a/src/ui/client/decodeApiKey.ts +++ /dev/null @@ -1,87 +0,0 @@ -const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - -function readChar(char: string) { - const idx = alphabet.indexOf(char); - - if (idx === -1) { - throw new Error(`Invalid character found: ${char}`); - } - - return idx; -} - -function arrayBufferToString(buffer: any) { - const bufView = new Uint8Array(buffer); - const length = bufView.length; - let result = ""; - let addition = Math.pow(2, 16) - 1; - - for (let i = 0; i < length; i += addition) { - if (i + addition > length) { - addition = length - i; - } - result += String.fromCharCode.apply( - null, - // @ts-ignore - bufView.subarray(i, i + addition), - ); - } - - return result; -} - -function base32Decode(input: string) { - input = input.toUpperCase(); - - const length = input.length; - - let bits = 0; - let value = 0; - - let index = 0; - const output = new Uint8Array(((length * 5) / 8) | 0); - - for (let i = 0; i < length; i++) { - value = (value << 5) | readChar(input[i]); - bits += 5; - - if (bits >= 8) { - output[index++] = (value >>> (bits - 8)) & 255; - bits -= 8; - } - } - - return arrayBufferToString(output.buffer); -} - -export function getProjectIdFromApiKey( - key: string | undefined, -): number | undefined { - if (!key) { - return undefined; - } - try { - const [prefix, rest] = key.split("_"); - if (prefix === "tgpak") { - const [projectId] = base32Decode(rest).split("_"); - return Number(projectId); - } - } catch { - console.warn("Tolgee: API key can't be parsed"); - } - return undefined; -} - -export function getApiKeyType(key: string | undefined) { - if (!key) { - return undefined; - } - const [prefix] = key.split("_"); - if (prefix === "tgpak") { - return "tgpak"; - } else if (prefix === "tgpat") { - return "tgpat"; - } - - return "legacy"; -} diff --git a/src/ui/client/errorCodes.ts b/src/ui/client/errorCodes.ts deleted file mode 100644 index 6a54e8e5..00000000 --- a/src/ui/client/errorCodes.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function errorToText(code: string) { - switch (code) { - case "plan_translation_limit_exceeded": - return "Plan translation limit exceeded (check your organization billing in Tolgee platform)"; - case "operation_not_permitted": - return "Operation not permitted"; - default: - return code || "Error status code from server"; - } -} diff --git a/src/ui/client/types.ts b/src/ui/client/types.ts deleted file mode 100644 index 9dfa2c88..00000000 --- a/src/ui/client/types.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { components, paths } from "./apiSchema.generated"; - -export type RequestParamsType< - Url extends keyof Paths, - Method extends keyof Paths[Url], - Paths = paths, -> = OperationSchema["parameters"] & - OperationSchema["requestBody"]; - -export type ResponseContent< - Url extends keyof Paths, - Method extends keyof Paths[Url], - Paths, -> = OperationSchema< - Url, - Method, - Paths ->["responses"][200] extends NotNullAnyContent - ? OperationSchema["responses"][200]["content"]["*/*"] - : OperationSchema< - Url, - Method, - Paths - >["responses"][200] extends NotNullJsonHalContent - ? OperationSchema< - Url, - Method, - Paths - >["responses"][200]["content"]["application/hal+json"] - : OperationSchema< - Url, - Method, - Paths - >["responses"][200] extends NotNullJsonContent - ? OperationSchema< - Url, - Method, - Paths - >["responses"][200]["content"]["application/json"] - : OperationSchema< - Url, - Method, - Paths - >["responses"][201] extends NotNullAnyContent - ? OperationSchema< - Url, - Method, - Paths - >["responses"][201]["content"]["*/*"] - : void; - -type NotNullAnyContent = { - content: { - "*/*": any; - }; -}; - -type NotNullJsonHalContent = { - content: { - "application/hal+json": any; - }; -}; - -type NotNullJsonContent = { - content: { - "application/json": any; - }; -}; - -type ResponseType = { - 200?: - | { - content?: { - "*/*"?: any; - "application/json"?: any; - "application/hal+json"?: any; - }; - } - | unknown; - 201?: - | { - content?: { - "*/*"?: any; - }; - } - | unknown; -}; - -type OperationSchemaType = { - requestBody?: { - content?: { - "multipart/form-data"?: { [key: string]: any }; - "application/json"?: any; - }; - }; - parameters?: { - path?: { [key: string]: any }; - query?: { [key: string]: { [key: string]: any } | any }; - }; - responses: ResponseType; -}; - -type OperationSchema< - Url extends keyof Paths, - Method extends keyof Paths[Url], - Paths = paths, -> = Paths[Url][Method] extends OperationSchemaType ? Paths[Url][Method] : never; - -export type TranslationData = Record< - string, - Record< - string, - Omit & { - translation: string; - } - > ->; diff --git a/src/ui/client/useQueryApi.ts b/src/ui/client/useQueryApi.ts deleted file mode 100644 index dd6a257a..00000000 --- a/src/ui/client/useQueryApi.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { useCallback } from "preact/hooks"; -import { - UseQueryOptions, - useQuery, - useQueryClient, - useMutation, - Query, - QueryClient, - UseMutationOptions, - MutationOptions, -} from "react-query"; -import { useGlobalState } from "../state/GlobalState"; -import { paths } from "./apiSchema.generated"; -import { client, ClientOptions } from "./client"; -import { RequestParamsType, ResponseContent } from "./types"; -import { customPaths } from "./apiSchema.custom"; - -export type QueryProps< - Url extends keyof Paths, - Method extends keyof Paths[Url], - Paths = paths, -> = { - url: Url; - method: Method; - clientOptions?: ClientOptions; - options?: UseQueryOptions>; -} & RequestParamsType; - -export const useApiQuery = < - Url extends keyof Paths, - Method extends keyof Paths[Url], - Paths = paths & customPaths, ->( - props: QueryProps, -) => { - const { url, method, options, clientOptions, ...request } = props; - - const config = useGlobalState((c) => c.config) || {}; - - return useQuery, any>( - [url, (request as any)?.path, (request as any)?.query], - () => - client(url, method, request as any, clientOptions, { - apiKey: config.apiKey, - apiUrl: config.apiUrl, - }), - options, - ); -}; - -export type MutationProps< - Url extends keyof Paths, - Method extends keyof Paths[Url], - Paths = paths, -> = { - url: Url; - method: Method; - clientOptions?: ClientOptions; - options?: UseMutationOptions< - ResponseContent, - any, - RequestParamsType - >; - invalidatePrefix?: string; -}; - -export const useApiMutation = < - Url extends keyof Paths, - Method extends keyof Paths[Url], - Paths = paths, ->( - props: MutationProps, -) => { - const queryClient = useQueryClient(); - const { url, method, options, invalidatePrefix, clientOptions } = props; - const config = useGlobalState((c) => c.config) || {}; - const mutation = useMutation< - ResponseContent, - any, - RequestParamsType - >( - (request) => - client(url, method, request, clientOptions, config), - options, - ); - - // inject custom onSuccess - const customOptions = (options: MutationOptions) => ({ - ...options, - onSuccess: (...args: any) => { - if (invalidatePrefix !== undefined) { - invalidateUrlPrefix(queryClient, invalidatePrefix); - } - // @ts-ignore - options?.onSuccess?.(...args); - }, - }); - - const mutate = useCallback( - (variables, options) => { - return mutation.mutate(variables, customOptions(options as any)); - }, - [mutation.mutate], - ); - - const mutateAsync = useCallback( - (variables, options) => { - return mutation.mutateAsync(variables, customOptions(options as any)); - }, - [mutation.mutateAsync], - ); - - return { ...mutation, mutate, mutateAsync }; -}; - -export const matchUrlPrefix = (prefix: string) => { - return { - predicate: (query: Query) => { - return (query.queryKey[0] as string)?.startsWith(prefix); - }, - }; -}; - -export const invalidateUrlPrefix = (queryClient: QueryClient, prefix: string) => - queryClient.invalidateQueries(matchUrlPrefix(prefix)); diff --git a/src/ui/components/ActionsBottom/ActionsBottom.css b/src/ui/components/ActionsBottom/ActionsBottom.css deleted file mode 100644 index 21e477c6..00000000 --- a/src/ui/components/ActionsBottom/ActionsBottom.css +++ /dev/null @@ -1,23 +0,0 @@ -.container { - position: fixed; - bottom: 0px; - right: 0px; - left: 0px; - display: flex; - flex-direction: column; - align-items: flex-end; - background-color: var(--figma-color-bg); - z-index: 2; -} - -.actions { - display: flex; - gap: 8px; - padding: 12px 16px; - width: 100%; - justify-content: flex-end; -} - -.placeHolder { - height: 40px; -} diff --git a/src/ui/components/ActionsBottom/ActionsBottom.css.d.ts b/src/ui/components/ActionsBottom/ActionsBottom.css.d.ts deleted file mode 100644 index 0ba59994..00000000 --- a/src/ui/components/ActionsBottom/ActionsBottom.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare const styles: { - readonly "actions": string; - readonly "container": string; - readonly "placeHolder": string; -}; -export = styles; - diff --git a/src/ui/components/ActionsBottom/ActionsBottom.tsx b/src/ui/components/ActionsBottom/ActionsBottom.tsx deleted file mode 100644 index ff080f1a..00000000 --- a/src/ui/components/ActionsBottom/ActionsBottom.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Divider } from "@create-figma-plugin/ui"; -import { Fragment, FunctionComponent, h } from "preact"; - -import styles from "./ActionsBottom.css"; - -export const ActionsBottom: FunctionComponent = ({ children }) => { - return ( - -
} /> - - ) : ( - Settings} /> - )} - - - {setupStep == null ? ( - - - {renderSection("project")} - - - {renderSection("strings")} - - - {renderSection("push")} - - - {isLoading ? ( - - ) : error ? ( - }>{error} - ) : null} - - {!isLoading && ( - - {config.documentInfo && ( -
- -
- )} - {!noNavigation && ( - - )} - -
- )} - -
- ) : ( - - {renderSection(setupStep, true)} - - {isLoading ? ( - - ) : error ? ( - }>{error} - ) : null} - - {!isLoading && ( - - {config.documentInfo && ( -
- -
- )} - {setupStep !== "project" && ( - - )} - -
- )} - -
- )} - setForgetDialogOpen(false)} - > -
-

- Project "{projectName}" will be disconnected from the Figma plugin. -

-
- - -
-
-
- - ); -}; diff --git a/src/ui/views/Settings/StringsEditor.css b/src/ui/views/Settings/StringsEditor.css deleted file mode 100644 index ca87a1f4..00000000 --- a/src/ui/views/Settings/StringsEditor.css +++ /dev/null @@ -1,166 +0,0 @@ -.editor-wrapper { - border: 1px solid var(--figma-color-border); - overflow: hidden; - border-radius: var(--border-radius-2); - cursor: text; - background: var(--figma-color-bg); - padding: 1px; -} - -.editor-wrapper:focus-within { - border: 1px solid var(--figma-color-border-brand-strong); - outline: 1px solid var(--figma-color-border-brand-strong); -} - -.cm-scroller { - padding: 2px 0px; -} - -.strings-editor { - display: grid; - font-size: 0.7rem; - overflow: auto; -} - -.strings-editor .cm-editor { - outline: none !important; - white-space: pre-wrap; -} - -.strings-editor .cm-line { - padding: 0px 1px !important; -} - -.strings-editor .cm-content { - padding: 0px !important; -} - -.strings-editor .cm-cursor { - border-color: var(--figma-color-border-primary) !important; -} - -.strings-editor .placeholder-widget { - display: inline-flex; - vertical-align: text-top; - align-items: center; - justify-content: center; - min-width: 15px; - border: 1px solid var(--figma-color-border); - background-color: var(--figma-color-bg-grey); - color: var(--figma-color-text-grey); - font-weight: 500; - border-radius: 10px; - padding: 0px 7px; - margin-top: -2px; - user-select: none; - overflow: hidden; -} - -.strings-editor > * { - display: grid; -} -.strings-editor .placeholder-error { - background: #ff000054; -} - -.strings-editor .placeholder-tagOpen { - border: 1px solid var(--figma-color-border); - background-color: var(--figma-color-bg-brand); - color: var(--figma-color-bg-brand-contrast); - border-radius: 10px 0px 0px 10px; - padding-right: 6px; -} - -.strings-editor .placeholder-tagClose { - border: 1px solid var(--figma-color-border); - background-color: var(--figma-color-bg-brand); - color: var(--figma-color-bg-brand-contrast); - border-radius: 0px 10px 10px 0px; - padding-left: 6px; -} - -.strings-editor .placeholder-tagSelfClosed { - border: 1px solid var(--figma-color-border); - background-color: var(--figma-color-bg-brand); - color: var(--figma-color-bg-brand-contrast); -} - -.strings-editor [dir="rtl"] .placeholder-tagOpen { - border-radius: 0px 10px 10px 0px; - padding-left: 6px; -} -.strings-editor [dir="rtl"] .placeholder-tagClose { - border-radius: 10px 0px 0px 10px; - padding-right: 6px; -} - -.strings-editor .cm-tooltip { - background-color: var(--figma-color-bg); - color: var(--figma-color-bg-contrast); - border-radius: 10px; - padding: 2px 9px; - user-select: none; - text-transform: capitalize; - white-space: nowrap; - font-size: 12px; -} - -.plural-editor-wrapper { - display: grid; - gap: 2px; -} -.plural-editor-wrapper .styled-variants { - display: flex; - flex-direction: column; - gap: 8px; -} -.plural-editor-wrapper .styled-parameters { - color: var(--figma-color-text-brand-secondary); -} -.plural-editor-wrapper .styled-variant-label { - box-sizing: border-box; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - border: 1px solid var(--figma-color-border); - background-color: var(--figma-color-bg); - color: var(--figma-color-text); - border-radius: 12px; - padding: 2px 9px; - user-select: none; - margin: 0px 1px; - text-transform: capitalize; - white-space: nowrap; -} -.plural-editor-wrapper .styled-variant-content { - display: block; -} - -:root { - --syntax-color-function: #007300; - --syntax-color-other: #002bff; -} - -@media (prefers-color-scheme: dark) { - :root { - --syntax-color-function: #9ac99a; - --syntax-color-other: #99aaff; - } -} - -.strings-editor .tolgee-completion-option { - text-transform: none; -} - -/* Placeholder text styling */ -.strings-editor .cm-placeholder { - color: var(--figma-color-text-grey) !important; - pointer-events: none; - opacity: 0.5; -} -@media (prefers-color-scheme: dark) { - .strings-editor .cm-placeholder { - color: var(--figma-color-text-primary) !important; - } -} diff --git a/src/ui/views/Settings/StringsEditor.css.d.ts b/src/ui/views/Settings/StringsEditor.css.d.ts deleted file mode 100644 index 0560cbdf..00000000 --- a/src/ui/views/Settings/StringsEditor.css.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -declare const styles: { - readonly "cm-content": string; - readonly "cm-cursor": string; - readonly "cm-editor": string; - readonly "cm-line": string; - readonly "cm-placeholder": string; - readonly "cm-scroller": string; - readonly "cm-tooltip": string; - readonly "editor-wrapper": string; - readonly "placeholder-error": string; - readonly "placeholder-tagClose": string; - readonly "placeholder-tagOpen": string; - readonly "placeholder-tagSelfClosed": string; - readonly "placeholder-widget": string; - readonly "plural-editor-wrapper": string; - readonly "strings-editor": string; - readonly "styled-parameters": string; - readonly "styled-variant-content": string; - readonly "styled-variant-label": string; - readonly "styled-variants": string; - readonly "tolgee-completion-option": string; -}; -export = styles; - diff --git a/src/ui/views/Settings/StringsEditor.tsx b/src/ui/views/Settings/StringsEditor.tsx deleted file mode 100644 index 9c198ecf..00000000 --- a/src/ui/views/Settings/StringsEditor.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import { h, RefObject } from "preact"; -import { useEffect, useRef } from "preact/hooks"; -import { minimalSetup } from "codemirror"; -import { - EditorSelection, - EditorState, - RangeSetBuilder, - StateField, -} from "@codemirror/state"; -import { - ViewUpdate, - EditorView, - KeyBinding, - Decoration, - DecorationSet, - WidgetType, -} from "@codemirror/view"; -import { getPlaceholders, Placeholder } from "@tginternal/editor"; -import "!./StringsEditor.css"; -import { - autocompletion, - Completion, - CompletionContext, - CompletionResult, - pickedCompletion, - startCompletion, -} from "@codemirror/autocomplete"; - -class PlaceholderBadgeWidget extends WidgetType { - constructor(private value: Placeholder) { - super(); - } - - eq(other: PlaceholderBadgeWidget) { - return ( - other.value.name === this.value.name && - other.value.type === this.value.type && - other.value.error === this.value.error - ); - } - - toDOM() { - const outer = document.createElement("span"); - let classes = `placeholder-widget placeholder-${this.value.type}`; - if (this.value.error) { - classes += ` placeholder-error placeholder-error-${this.value.error}`; - } - outer.className = classes; - const inner = document.createElement("span"); - inner.textContent = this.value.name; - outer.appendChild(inner); - return outer; - } - - ignoreEvent() { - return false; - } -} - -// Renders {placeholder} ranges as badges. Replaces PlaceholderPlugin from -// @tginternal/editor, whose Decoration.widget({side: 1}) ranges place both -// boundary cursor positions inside the widget, so the caret is always drawn -// at the badge's left edge. Non-inclusive Decoration.replace keeps the -// positions before/after the badge, so the caret renders on the correct side. -function placeholderBadges() { - const build = (state: EditorState) => { - const builder = new RangeSetBuilder(); - let placeholders: Placeholder[] | null = []; - try { - placeholders = getPlaceholders(state.doc.toString(), true); - } catch { - placeholders = []; - } - for (const placeholder of placeholders ?? []) { - builder.add( - placeholder.position.start, - placeholder.position.end, - Decoration.replace({ - widget: new PlaceholderBadgeWidget(placeholder), - }), - ); - } - return builder.finish(); - }; - const field = StateField.define({ - create: build, - update: (set, transaction) => - transaction.docChanged ? build(transaction.state) : set, - provide: (f) => [ - EditorView.decorations.from(f), - EditorView.atomicRanges.of((view) => view.state.field(f)), - ], - }); - return field; -} - -// Custom placeholder widget -class PlaceholderWidget extends WidgetType { - constructor(private placeholder: string) { - super(); - } - - toDOM() { - const span = document.createElement("span"); - span.textContent = this.placeholder; - span.className = "cm-placeholder"; - return span; - } - - ignoreEvent() { - return false; - } -} - -// Custom placeholder extension -function placeholderExtension(placeholder: string) { - return EditorView.decorations.compute(["doc"], (state) => { - const doc = state.doc; - if (doc.length > 0) { - return Decoration.none; - } - - const decoration = Decoration.widget({ - widget: new PlaceholderWidget(placeholder), - side: 1, - }); - - return Decoration.set([decoration.range(0)]); - }); -} - -export type EditorProps = { - value: string; - onChange?: (val: string) => void; - background?: string; - autofocus?: boolean; - minHeight?: number | string; - onBlur?: () => void; - onFocus?: () => void; - shortcuts?: KeyBinding[]; - autoScrollIntoView?: boolean; - locale?: string; - examplePluralNum?: number; - nested?: boolean; -}; - -function useRefGroup(value: T): RefObject { - const refObject = useRef(value); - refObject.current = value; - return refObject; -} - -export const StringsEditor = ({ - value, - onChange, - onFocus, - onBlur, - minHeight, -}: EditorProps) => { - const ref = useRef(null); - const editor = useRef(); - const callbacksRef = useRefGroup({ - onChange, - onFocus, - onBlur, - }); - - function createCompletionOption( - label: string, - detail: string, - insertText: string, - ) { - return { - label, - detail, - type: "keyword" as const, - apply( - view: EditorView, - completion: Completion, - from: number, - to: number, - ) { - view.dispatch({ - changes: { from, to, insert: insertText }, - selection: EditorSelection.create([ - EditorSelection.cursor(from + insertText.length, -1), - ]), - annotations: pickedCompletion.of(completion), - scrollIntoView: true, - userEvent: "input.complete", - }); - }, - }; - } - - function completions(context: CompletionContext): CompletionResult | null { - const word = context.matchBefore(/[A-Za-z0-9]*/); - if (!word || (word.from == word.to && !context.explicit)) { - return null; - } - return { - from: word.from, - options: [ - createCompletionOption( - "element name", - "(name of the string)", - "{elementName}", - ), - createCompletionOption( - "element text", - "(displayed text of the string)", - "{elementText}", - ), - createCompletionOption( - "group", - "(name of the nearest group)", - "{group}", - ), - createCompletionOption( - "component", - "(name of the nearest component)", - "{component}", - ), - createCompletionOption( - "frame", - "(name of the nearest frame)", - "{frame}", - ), - createCompletionOption( - "artboard", - "(name of the artboard frame)", - "{artboard}", - ), - createCompletionOption( - "section", - "(name of the nearest section)", - "{section}", - ), - ], - commitCharacters: ["}"], - }; - } - - useEffect(() => { - if (!ref.current) return; - - const extensions = [ - minimalSetup, - EditorView.updateListener.of((v: ViewUpdate) => { - if (v.focusChanged) { - if (v.view.hasFocus) { - callbacksRef.current?.onFocus?.(); - } else { - callbacksRef.current?.onBlur?.(); - } - } - if (v.docChanged) { - callbacksRef.current?.onChange?.(v.state.doc.toString()); - } - }), - EditorView.domEventHandlers({ - click(_event, view) { - startCompletion(view); - return false; - }, - }), - placeholderBadges(), - autocompletion({ - override: [completions], - optionClass: () => { - return "tolgee-completion-option"; - }, - aboveCursor: true, - icons: false, - }), - placeholderExtension("artboard.element"), - ]; - - const instance = new EditorView({ - parent: ref.current!, - state: EditorState.create({ - doc: value, - extensions, - }), - }); - - editor.current = instance; - // set cursor to the end of document - const length = editor.current!.state.doc.length; - editor.current!.dispatch({ selection: { anchor: length } }); - - return () => { - if (editor.current) { - editor.current.destroy(); - editor.current = undefined; - } - }; - }, []); - - useEffect(() => { - const state = editor.current?.state; - const editorValue = state?.doc.toString(); - if (state && editorValue !== value) { - const transaction = state.update({ - changes: { from: 0, to: state.doc.length, insert: value || "" }, - }); - editor.current?.update([transaction]); - } - }, [value]); - - return ( -
-
-
- ); -}; diff --git a/src/ui/views/Settings/StringsSection.tsx b/src/ui/views/Settings/StringsSection.tsx deleted file mode 100644 index c1b23ee6..00000000 --- a/src/ui/views/Settings/StringsSection.tsx +++ /dev/null @@ -1,363 +0,0 @@ -import { h, FunctionComponent, Fragment } from "preact"; -import { useMemo, useState } from "preact/hooks"; -import { - Text, - Muted, - VerticalSpace, - Textbox, - Checkbox, - Bold, - Inline, -} from "@create-figma-plugin/ui"; -import { useQueryClient } from "react-query"; -import styles from "./Settings.css"; -import { TargetedEvent } from "preact/compat"; -import { TolgeeConfig } from "@/types"; -import { formatString } from "@/utilities"; -import { StringsEditor } from "./StringsEditor"; -import { - TOLGEE_KEY_FORMAT_PLACEHOLDERS, - TOLGEE_KEY_FORMAT_PLACEHOLDERS_EXAMPLES, -} from "@/constants"; -import { InfoTooltip } from "../../components/InfoTooltip/InfoTooltip"; -import { clearPrefilledKeysEndpoint } from "@/main/endpoints/clearPrefilledKeys"; -import { getConnectedNodesEndpoint } from "@/main/endpoints/getConnectedNodes"; - -function getPreview( - format: string, - variableCasing: TolgeeConfig["variableCasing"], -) { - let newFormat = format; - for (const key of Object.keys(TOLGEE_KEY_FORMAT_PLACEHOLDERS)) { - const replaceString = formatString( - TOLGEE_KEY_FORMAT_PLACEHOLDERS_EXAMPLES[ - key as keyof typeof TOLGEE_KEY_FORMAT_PLACEHOLDERS - ], - variableCasing, - ); - if (replaceString === "") { - newFormat = newFormat.replace(new RegExp(`{${key}}[^{]*`, "g"), ""); - } else { - newFormat = newFormat.replace( - new RegExp(`\\{${key}\\}`, "g"), - replaceString, - ); - } - } - return newFormat; -} - -export interface StringsSectionProps { - showHeadline: boolean; - tolgeeConfig: Partial & { apiUrl: string }; - setTolgeeConfig: (c: Partial & { apiUrl: string }) => void; -} - -const keyFormatHelpText = ( - -
- Define your key format to be consistent and fast. -
- You can use variables, text and separators. -
- Variables will allow you to use names (values) of Figma structure: -
-
    -
  • element name (name of the string)
  • -
  • ...
  • -
  • section (name of thenearest section)
  • -
  • separtators can be “.” “:” “_” “-” etc.
  • -
-
- Read more in our guide{" "} - - How to name translation keys - - . -
-
-); - -const hiddenLayersHelpText = ( - -
- Skips layers with visibility turned off in Figma. -
-
- With "Including all child texts" enabled, -
- all text layers inside hidden layers are -
- also ignored, even if individually set to visible. -
-
- Otherwise, only the hidden layer itself is ignored. -
-
-); - -const formattingStyleHelpText = ( - -
- This will help you preserve the same format style. -
- Your style will be automatically applied to the variables. -
-
- E.g. style "element_name" -
- "My cool button"→"my_cool_button" -
-
-); - -const variableCasingOptions: Array<{ value: string; text: string }> = [ - { value: "", text: "keep original format" }, - { value: "snake_case", text: "element_name" }, - { value: "snake_case_capitalized", text: "Element_name" }, - { value: "camelCase", text: "elementName" }, - { value: "PascalCase", text: "ElementName" }, - { value: "noSpaces", text: "elementname" }, -]; - -export const StringsSection: FunctionComponent = ({ - showHeadline, - tolgeeConfig, - setTolgeeConfig, -}) => { - const queryClient = useQueryClient(); - const [format, setFormat] = useState(tolgeeConfig.keyFormat || ""); - const [prefill, setPrefill] = useState( - tolgeeConfig.prefillKeyFormat ?? false, - ); - const [ignoreNumbers, setIgnoreNumbers] = useState( - tolgeeConfig.ignoreNumbers ?? true, - ); - const [ignoreHiddenLayers, setIgnoreHiddenLayers] = useState( - tolgeeConfig.ignoreHiddenLayers ?? true, - ); - const [ - ignoreHiddenLayersIncludingChildren, - setIgnoreHiddenLayersIncludingChildren, - ] = useState(tolgeeConfig.ignoreHiddenLayersIncludingChildren ?? false); - - const [ignoreTextLayers, setIgnoreTextLayers] = useState( - tolgeeConfig.ignoreTextLayers ?? false, - ); - const [ignorePrefix, setIgnorePrefix] = useState( - tolgeeConfig.ignorePrefix ?? "", - ); - - const preview = useMemo(() => { - try { - return getPreview(format, tolgeeConfig.variableCasing); - } catch (error) { - console.error("Preview generation error:", error); - return format; - } - }, [format, tolgeeConfig.variableCasing]); - - const handleFormatChange = (val: string) => { - setFormat(val); - setTolgeeConfig({ ...tolgeeConfig, keyFormat: val }); - }; - - const handlePrefillChange = async (e: any) => { - const checked = e.currentTarget.checked; - setPrefill(checked); - setTolgeeConfig({ ...tolgeeConfig, prefillKeyFormat: checked }); - if (!checked) { - // Drop auto-prefilled values that were persisted to node pluginData while - // the toggle was on so they don't keep showing up after disabling. - try { - await clearPrefilledKeysEndpoint.call(); - queryClient.invalidateQueries([getConnectedNodesEndpoint.name]); - } catch (err) { - console.error("Failed to clear prefilled keys", err); - } - } - }; - - const handleVariableCasingChange = (value: string) => { - setTolgeeConfig({ - ...tolgeeConfig, - variableCasing: value as (typeof tolgeeConfig)["variableCasing"], - }); - }; - - function handleIgnorePrefixChange(value: string): void { - setIgnorePrefix(value); - setTolgeeConfig({ ...tolgeeConfig, ignorePrefix: value }); - } - - function handleIgnoreNumbersChange( - event: TargetedEvent, - ): void { - setIgnoreNumbers(event.currentTarget.checked); - setTolgeeConfig({ - ...tolgeeConfig, - ignoreNumbers: event.currentTarget.checked, - }); - } - - function handleIgnoreHiddenLayersChange( - event: TargetedEvent, - ): void { - setIgnoreHiddenLayers(event.currentTarget.checked); - setTolgeeConfig({ - ...tolgeeConfig, - ignoreHiddenLayers: event.currentTarget.checked, - }); - } - - function handleIgnoreHiddenLayersIncludingChildrenChange( - event: TargetedEvent, - ): void { - setIgnoreHiddenLayersIncludingChildren(event.currentTarget.checked); - setTolgeeConfig({ - ...tolgeeConfig, - ignoreHiddenLayersIncludingChildren: event.currentTarget.checked, - }); - } - - function handleTextLayersChange( - event: TargetedEvent, - ): void { - setIgnoreTextLayers(event.currentTarget.checked); - setTolgeeConfig({ - ...tolgeeConfig, - ignoreTextLayers: event.currentTarget.checked, - }); - } - - return ( - - {showHeadline && ( - - - Strings and keys - - - )} - - Key name - - - - prefill key name - - - {prefill && ( - - - -
- - - Key format - - {keyFormatHelpText} - - - -
- -
- - - Formatting style - - {formattingStyleHelpText} - - - - -
- {preview && ( -
- - Preview - - - - {preview} - -
- )} -
- )} - - Ignore strings - - - numbers - - - - - hidden layers - - {hiddenLayersHelpText} - - {ignoreHiddenLayers && ( - - -
- - including all child texts - -
-
- )} -
- - text layers with prefix - - -
-
- ); -}; diff --git a/src/ui/views/StringDetails/StringDetails.css b/src/ui/views/StringDetails/StringDetails.css deleted file mode 100644 index 50bff78c..00000000 --- a/src/ui/views/StringDetails/StringDetails.css +++ /dev/null @@ -1,68 +0,0 @@ -.valuesText { - display: flex; - align-items: center; - gap: 4px; -} -.bottomContainer { - display: flex; - gap: 12px; -} -.paramsContainer { - display: flex; - flex-direction: column; - flex: 1; - gap: 12px; -} -.paramRow { - display: flex; - flex-direction: column; - gap: 12px; - align-items: stretch; - width: 100%; -} -.previewRow { - display: flex; - flex-direction: column; - flex: 1; - gap: 8px; -} -.editorHeadline { - display: flex; - align-items: center; - gap: 4px; -} -.emptyState { - margin: auto; -} -.warningContainer { - display: grid; - gap: 4px; - padding: 8px; - background-color: var(--figma-color-red-bg); - margin-top: 8px; -} -.warningNoticeContainer { - display: grid; - gap: 4px; - padding: 8px; - background-color: var(--figma-color-teal-bg); - margin-top: 8px; -} -.warningContainerIcon { - color: var(--figma-color-red); - transform: rotate(180deg); -} -.warningNoticeContainerIcon { - color: var(--figma-color-teal); -} -.infoLink, -.infoLink * { - color: var(--figma-color-text-brand); - text-decoration: underline; - cursor: pointer; -} -.checkboxWrapper { - display: flex; - align-items: center; - gap: 8px; -} diff --git a/src/ui/views/StringDetails/StringDetails.css.d.ts b/src/ui/views/StringDetails/StringDetails.css.d.ts deleted file mode 100644 index d09bd538..00000000 --- a/src/ui/views/StringDetails/StringDetails.css.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -declare const styles: { - readonly "bottomContainer": string; - readonly "checkboxWrapper": string; - readonly "editorHeadline": string; - readonly "emptyState": string; - readonly "infoLink": string; - readonly "paramRow": string; - readonly "paramsContainer": string; - readonly "previewRow": string; - readonly "valuesText": string; - readonly "warningContainer": string; - readonly "warningContainerIcon": string; - readonly "warningNoticeContainer": string; - readonly "warningNoticeContainerIcon": string; -}; -export = styles; - diff --git a/src/ui/views/StringDetails/StringDetails.tsx b/src/ui/views/StringDetails/StringDetails.tsx deleted file mode 100644 index a89b86d7..00000000 --- a/src/ui/views/StringDetails/StringDetails.tsx +++ /dev/null @@ -1,517 +0,0 @@ -import { Fragment, h } from "preact"; -import { useEffect, useMemo, useState } from "preact/hooks"; -import { - Button, - Checkbox, - Columns, - Container, - Divider, - IconButton, - IconChevronDown16, - Muted, - Text, - Textbox, - TextboxNumeric, - VerticalSpace, -} from "@create-figma-plugin/ui"; -import { useGlobalActions, useGlobalState } from "@/ui/state/GlobalState"; -import { FullPageLoading } from "@/ui/components/FullPageLoading/FullPageLoading"; - -import { TopBar } from "@/ui/components/TopBar/TopBar"; -import { NodeInfo } from "@/types"; -import { PluralEditor } from "@/ui/components/Editor/PluralEditor"; -import { - getTolgeeFormat, - TolgeeFormat, - tolgeeFormatGenerateIcu, -} from "@tginternal/editor"; -import { useTranslation } from "@/ui/hooks/useTranslation"; -import { useSetNodesDataMutation } from "@/ui/hooks/useSetNodesDataMutation"; -import { useSelectedNodes } from "@/ui/hooks/useSelectedNodes"; -import { useFormatText } from "@/ui/hooks/useFormatText"; -import { InfoTooltip } from "@/ui/components/InfoTooltip/InfoTooltip"; -import styles from "./StringDetails.css"; -import { useInterpolatedTranslation } from "@/ui/hooks/useInterpolatedTranslation"; -import { Info } from "@/ui/icons/SvgIcons"; -import { HtmlText } from "../../components/Shared/HtmlText"; - -interface StringDetailsProps { - node: NodeInfo | undefined; -} - -export const StringDetails = ({ node: initialNode }: StringDetailsProps) => { - const { setRoute } = useGlobalActions(); - const config = useGlobalState((c) => c.config); - - const formatText = useFormatText(); - const setNodesDataMutation = useSetNodesDataMutation(); - const selectedNodes = useSelectedNodes(); - - // Used to prevent navigation when changes have been made and not saved - const [needsSubmission, setNeedsSubmission] = useState(false); - - /** The selected node is alway the last one of the currently selected nodes or null, if no nodes are selected */ - const selectedNode = useMemo(() => { - if ((selectedNodes.data?.items.length ?? 0) === 1) { - return { ...selectedNodes.data!.items[0] } as NodeInfo; - } - return ( - selectedNodes.data?.items.find((i) => i.id === initialNode?.id) ?? null - ); - }, [selectedNodes.data?.items]); - - const [currentNode, setCurrentNode] = useState(); - - const isSimpleNode = (node: NodeInfo) => - node && - !node.isPlural && - Object.keys(node.paramsValues ?? {}).length === 0 && - !/<[^>]*>/g.test(node.translation ?? ""); - - const translationLoadable = useTranslation({ - language: config?.language ?? "en", - namespace: config?.namespace ?? "default", - key: currentNode?.key, - }); - const [translation, setTranslation] = useState(); - - const [editorValue, setEditorValue] = useState(); - - useEffect(() => { - if (selectedNode) { - if ( - needsSubmission && - currentNode && - translation && - selectedNode.id !== currentNode?.id - ) { - const currentTranslation = translation; - const res = confirm( - `TOLGEE\nYou have unsaved changes for the translation\n${currentNode.key}\nDo you want to save them?`, - ); - if (res) { - updateNodeDataAndText(currentNode, currentTranslation); - } - } - - const currentTranslation = isSimpleNode(selectedNode) - ? ((selectedNode.characters || selectedNode.translation) ?? "") - : selectedNode?.translation || - translationLoadable.translation?.translation || - ""; - - const newNode = { ...selectedNode, translation: currentTranslation }; - - setTranslation(currentTranslation); - setCurrentNode(newNode); - } else if (!selectedNode) { - setCurrentNode(undefined); - } - }, [selectedNode]); - - useEffect(() => { - const tolgeeValue = getTolgeeFormat( - translation ?? currentNode?.translation ?? currentNode?.characters ?? "", - currentNode?.isPlural ?? false, - true, - ); - - setEditorValue({ - ...tolgeeValue, - parameter: - currentNode?.isPlural && tolgeeValue.parameter == null - ? "value" - : tolgeeValue.parameter, - }); - - if (currentNode) { - refreshTranslation( - currentNode, - currentNode.translation ?? currentNode.characters, - ); - } - }, [currentNode]); - - useEffect(() => { - if (!selectedNode) { - return; - } - const currentTranslation = isSimpleNode(selectedNode) - ? ((selectedNode.characters || selectedNode.translation) ?? "") - : selectedNode?.translation || - translationLoadable.translation?.translation || - ""; - - setNeedsSubmission(currentTranslation !== selectedNode.translation); - - setTranslation(currentTranslation); - }, [selectedNode, translationLoadable.translation?.translation]); - - const refreshTranslation = (node: NodeInfo, currentTranslation: string) => { - updateInterpolatedTranslation({ - currentTranslation, - pluralParamValue: node.pluralParamValue, - paramsValues: node.paramsValues || {}, - }); - }; - - /** Updates the textnode content and the node data */ - const updateNodeDataAndText = ( - node: NodeInfo, - currentTranslation: string, - ): string => { - const newTranslation = getInterpolatedTranslation({ - currentTranslation, - pluralParamValue: node.pluralParamValue, - paramsValues: node.paramsValues || {}, - }); - - if (newTranslation != null) { - formatText.mutate({ - formatted: newTranslation, - nodeInfo: node, - }); - updateNodeData({ - characters: newTranslation, - translation: currentTranslation, - paramsValues: node.paramsValues, - pluralParamValue: node.pluralParamValue, - isPlural: node.isPlural, - }); - setNeedsSubmission(false); - } - return newTranslation || currentTranslation; - }; - - /** Updates nodes properties with the partial data and mutates the data state within figma */ - const updateNodeData = (data: Partial) => { - if (currentNode == null) { - return; - } - setNodesDataMutation.mutate({ - nodes: [{ ...currentNode, ...data }], - }); - }; - - const { - previewText, - previewTextIsError, - translationDiffersFromNode, - placeholders, - tolgeeValue, - getInterpolatedTranslation, - updateInterpolatedTranslation, - } = useInterpolatedTranslation(currentNode); - - if (translationLoadable.isLoading || formatText.isLoading) { - return ; - } - - const infoString = ( - -
- You can use basic HTML tags such as <strong>, <b>, - <em>, -
- <i>, <u>, <br> and also parameters as -
- {parameter} and # as plural placeholder -
- -
-
- ); - - const valuesForFigmaString = `These values will be used to preview\nthe translation for figma.\nThey will not be saved.`; - - return ( - - - - setRoute("index")} - style={{ transform: "rotate(90deg)" }} - > - - - STRING DETAILS - - } - rightPart={ - needsSubmission && currentNode && translation ? ( - - ) : undefined - } - /> - - - {currentNode == null ? ( - - {selectedNodes.data?.items.length === 0 && ( - Please select a node to edit the translation. - )} - {(selectedNodes.data?.items.length ?? 0) > 1 && ( - Please select only one node to edit the translation. - )} - - ) : ( - - {((translationDiffersFromNode && !needsSubmission) || - currentNode.isPlural || - placeholders.length > 0) && ( - - - - - Advanced text format detected. Edit the string via plugin to - preserve formatting. - - - {translationDiffersFromNode && !needsSubmission && ( - - )} - - )} - - - Key name - - - { - setCurrentNode({ ...currentNode, key: e.currentTarget.value }); - }} - variant="border" - /> - -
- { - const newPlural = !currentNode.isPlural; - const newTranslation = tolgeeFormatGenerateIcu( - { - ...tolgeeValue, - parameter: - newPlural && tolgeeValue.parameter == null - ? "value" - : newPlural - ? tolgeeValue.parameter - : undefined, - }, - false, - ); - const newNode: NodeInfo = { - ...currentNode, - isPlural: newPlural, - pluralParamValue: newPlural ? "10" : undefined, - translation: newTranslation, - }; - setNeedsSubmission(true); - setCurrentNode(newNode); - }} - value={currentNode.isPlural} - > - is plural - - {currentNode.connected && ( - - You cannot change this setting here. -
- Head over to you Tolgee platform and change it there. -
- )} -
- - - Translation ({config?.language ?? "Unknown"}) - {infoString} - - - {editorValue && ( - { - const currentVariants = Object.entries(editorValue.variants); - const newVariants = Object.entries(value.variants); - if ( - value.parameter === editorValue.parameter && - currentVariants.length === newVariants.length && - currentVariants.every( - ([key, value], index) => - key === newVariants[index][0] && - value === newVariants[index][1], - ) - ) { - return; - } - - setEditorValue(value); - const generatedIcuString = tolgeeFormatGenerateIcu( - value, - false, - ); - - if (generatedIcuString !== translation) { - setTranslation(generatedIcuString); - setNeedsSubmission(true); - } - }} - onBlur={() => { - if (!editorValue) { - return; - } - - const generatedIcuString = tolgeeFormatGenerateIcu( - editorValue, - false, - ); - - if (generatedIcuString !== currentNode.translation) { - setTranslation(generatedIcuString); - setCurrentNode({ - ...currentNode, - translation: generatedIcuString, - }); - } - }} - value={editorValue} - locale={config?.language ?? "en"} - mode="placeholders" - /> - )} - -
- {(placeholders.length > 0 || tolgeeValue.parameter) && ( -
- - Values for Figma{" "} - {valuesForFigmaString} - - {tolgeeValue.parameter && ( - { - setNeedsSubmission(true); - if (currentTarget.value !== "") { - currentNode.pluralParamValue = currentTarget.value; - const newNode: NodeInfo = { - ...currentNode, - pluralParamValue: currentTarget.value, - }; - if (translation != null) { - setCurrentNode(newNode); - } - } - }} - variant="border" - /> - )} - {placeholders - .filter( - (p) => - tolgeeValue.parameter == null || - p.name != tolgeeValue.parameter, - ) - .map((p) => ( - -
- - Variable {p.name} - - { - setNeedsSubmission(true); - const newNode: NodeInfo = { - ...currentNode, - paramsValues: { - ...currentNode.paramsValues, - [p.name]: currentTarget.value, - }, - }; - if (translation != null) { - setCurrentNode(newNode); - } - }} - variant="border" - /> -
-
- ))} -
- )} - -
- Preview - - - -
-
- - -
- )} - - ); -}; diff --git a/src/ui/views/routes.ts b/src/ui/views/routes.ts deleted file mode 100644 index 0ef06917..00000000 --- a/src/ui/views/routes.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NodeInfo } from "@/types"; - -export type Route = - | ["index"] - | ["settings"] - | ["push"] - | ["string_details", { node: NodeInfo }] - | ["pull", { lang: string }] - | ["connect", { node: NodeInfo }] - | ["create_copy"]; - -export type RouteKey = Route[0]; - -export type RouteParam = Extract[1]; diff --git a/src/ui/vite-env.d.ts b/src/ui/vite-env.d.ts new file mode 100644 index 00000000..4078e747 --- /dev/null +++ b/src/ui/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/src/utilities.ts b/src/utilities.ts deleted file mode 100644 index d9a319c6..00000000 --- a/src/utilities.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - EventHandler, - emit as originalEmit, -} from "@create-figma-plugin/utilities"; -import { TolgeeConfig } from "./types"; -export { on, once } from "@create-figma-plugin/utilities"; - -export const emit = ( - name: Handler["name"], - ...args: Parameters -): void => { - const iframe = document?.getElementById("plugin_iframe") as HTMLIFrameElement; - if (iframe) { - iframe.contentWindow!.postMessage({ pluginMessage: [name, ...args] }); - } else { - originalEmit(name, ...args); - } -}; - -/** - * Formats a string based on the format option by replacing all spaces with the matching - */ -export const formatString = ( - str = "", - formatOption: TolgeeConfig["variableCasing"], -) => { - switch (formatOption) { - case "camelCase": - return str - .split(/\s+/) - .filter(Boolean) - .map((word, index) => - index > 0 - ? word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() - : word.toLowerCase(), - ) - .join(""); - case "PascalCase": - return str - .split(/\s+/) - .filter(Boolean) - .map( - (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), - ) - .join(""); - case "snake_case": - return str - .split(/\s+/) - .filter(Boolean) - .map((word) => word.toLowerCase()) - .join("_"); - case "snake_case_capitalized": - return str - .split(/\s+/) - .filter(Boolean) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join("_"); - case "noSpaces": - return str.replace(/\s/g, ""); - } - return str; -}; diff --git a/src/web/exampleScreenshot.ts b/src/web/exampleScreenshot.ts deleted file mode 100644 index fcfad259..00000000 --- a/src/web/exampleScreenshot.ts +++ /dev/null @@ -1,103 +0,0 @@ -export default { - image: new Uint8Array([ - 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 1, 11, - 0, 0, 0, 163, 8, 6, 0, 0, 0, 157, 109, 59, 101, 0, 0, 0, 9, 112, 72, 89, - 115, 0, 0, 11, 19, 0, 0, 11, 19, 1, 0, 154, 156, 24, 0, 0, 0, 1, 115, 82, - 71, 66, 0, 174, 206, 28, 233, 0, 0, 0, 4, 103, 65, 77, 65, 0, 0, 177, 143, - 11, 252, 97, 5, 0, 0, 4, 224, 73, 68, 65, 84, 120, 1, 237, 219, 239, 145, - 76, 89, 0, 198, 225, 99, 107, 191, 35, 2, 100, 64, 4, 136, 0, 17, 32, 2, 68, - 128, 8, 16, 1, 34, 64, 4, 136, 0, 17, 32, 2, 68, 48, 123, 223, 174, 61, 83, - 183, 199, 204, 236, 59, 179, 165, 106, 170, 250, 121, 170, 186, 232, 190, - 127, 135, 58, 191, 123, 238, 109, 206, 237, 45, 6, 192, 127, 248, 107, 0, - 20, 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, - 0, 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, - 64, 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, - 80, 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, - 84, 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, - 0, 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, - 64, 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, - 80, 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 59, - 25, 139, 15, 31, 62, 140, 59, 119, 238, 140, 43, 87, 174, 140, 155, 55, 111, - 142, 215, 175, 95, 143, 211, 250, 246, 237, 219, 214, 251, 236, 239, 44, - 203, 249, 29, 60, 103, 104, 236, 92, 44, 222, 190, 125, 59, 238, 223, 191, - 63, 174, 95, 191, 62, 222, 191, 127, 63, 30, 60, 120, 48, 158, 60, 121, 50, - 94, 188, 120, 49, 78, 42, 131, 238, 96, 28, 18, 162, 179, 236, 172, 159, 31, - 103, 216, 222, 142, 185, 124, 249, 242, 222, 155, 55, 111, 182, 62, 251, - 250, 245, 235, 222, 133, 11, 23, 246, 126, 252, 248, 177, 121, 191, 196, 99, - 243, 217, 195, 135, 15, 55, 175, 252, 254, 160, 172, 155, 101, 217, 110, - 174, 31, 249, 35, 253, 244, 233, 211, 222, 189, 123, 247, 14, 221, 246, 229, - 203, 151, 71, 46, 155, 114, 126, 75, 200, 54, 251, 125, 245, 234, 213, 254, - 241, 242, 62, 219, 206, 207, 214, 231, 159, 253, 29, 183, 108, 30, 47, 231, - 119, 212, 113, 225, 56, 59, 53, 179, 248, 252, 249, 243, 88, 6, 247, 184, - 125, 251, 246, 214, 231, 75, 64, 198, 213, 171, 87, 199, 187, 119, 239, 54, - 239, 51, 211, 120, 244, 232, 209, 102, 246, 113, 254, 252, 249, 113, 237, - 218, 181, 241, 243, 231, 207, 223, 246, 151, 229, 243, 215, 236, 119, 202, - 109, 205, 173, 91, 183, 54, 219, 102, 230, 49, 183, 205, 62, 115, 140, 44, - 187, 116, 233, 210, 214, 178, 181, 172, 147, 217, 79, 182, 207, 122, 153, - 193, 228, 28, 150, 191, 175, 113, 247, 238, 221, 205, 236, 40, 251, 138, - 185, 44, 235, 206, 101, 79, 159, 62, 221, 95, 150, 99, 100, 89, 142, 57, - 183, 129, 83, 217, 219, 33, 185, 90, 223, 184, 113, 227, 208, 101, 235, 171, - 242, 248, 119, 118, 48, 45, 33, 217, 122, 63, 229, 10, 157, 153, 202, 218, - 56, 112, 229, 206, 242, 108, 123, 216, 186, 75, 180, 126, 155, 9, 204, 115, - 121, 254, 252, 249, 254, 251, 57, 27, 153, 50, 203, 88, 207, 132, 214, 199, - 91, 98, 177, 255, 51, 206, 25, 197, 250, 124, 135, 153, 5, 167, 244, 247, - 216, 33, 185, 250, 31, 118, 37, 143, 124, 158, 43, 240, 122, 221, 102, 187, - 195, 100, 166, 178, 254, 125, 182, 157, 175, 245, 51, 142, 92, 249, 51, 163, - 57, 234, 92, 167, 143, 31, 63, 142, 37, 0, 91, 203, 178, 93, 102, 74, 249, - 60, 207, 33, 50, 27, 201, 251, 245, 241, 243, 254, 241, 227, 199, 91, 231, - 178, 222, 47, 156, 196, 78, 197, 34, 131, 37, 3, 52, 175, 245, 128, 206, 32, - 206, 192, 122, 246, 236, 217, 248, 83, 50, 72, 115, 204, 101, 150, 240, 219, - 231, 205, 182, 71, 197, 106, 153, 153, 108, 110, 59, 178, 223, 25, 144, 121, - 27, 146, 227, 125, 255, 254, 125, 107, 253, 147, 68, 15, 214, 118, 234, 153, - 69, 6, 93, 174, 180, 249, 218, 116, 126, 125, 152, 193, 147, 123, 249, 220, - 211, 175, 3, 210, 106, 7, 95, 6, 114, 214, 157, 161, 202, 43, 223, 192, 52, - 219, 231, 220, 18, 133, 185, 110, 130, 144, 253, 100, 86, 241, 235, 215, - 175, 205, 175, 121, 229, 231, 203, 44, 100, 202, 179, 148, 108, 55, 157, - 230, 27, 31, 152, 118, 106, 102, 17, 203, 61, 252, 102, 208, 229, 161, 224, - 188, 170, 103, 48, 230, 161, 230, 73, 101, 192, 47, 207, 18, 198, 197, 139, - 23, 199, 242, 13, 198, 214, 173, 194, 97, 178, 78, 66, 149, 237, 50, 224, - 219, 64, 101, 191, 89, 119, 158, 115, 206, 63, 251, 138, 60, 212, 92, 158, - 111, 236, 63, 44, 93, 239, 47, 231, 246, 229, 203, 151, 205, 191, 39, 201, - 118, 167, 13, 34, 196, 185, 60, 184, 24, 59, 42, 87, 231, 12, 162, 255, 123, - 31, 159, 65, 122, 146, 125, 156, 246, 184, 243, 185, 199, 97, 3, 254, 184, - 125, 30, 183, 29, 180, 118, 58, 22, 64, 207, 255, 13, 1, 42, 98, 1, 84, 196, - 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, 0, 21, - 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, 69, - 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, 80, 17, - 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, 84, - 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, 0, - 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, - 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, 80, - 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, 84, - 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, 0, - 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, - 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, 80, - 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, 84, - 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, 0, - 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, - 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, 84, 196, 2, 168, 136, 5, 80, - 17, 11, 160, 34, 22, 64, 69, 44, 128, 138, 88, 0, 21, 177, 0, 42, 98, 1, 84, - 196, 2, 168, 136, 5, 80, 17, 11, 160, 34, 22, 64, 69, 44, 128, 202, 63, 83, - 160, 11, 69, 209, 107, 73, 2, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, - ]), - info: { - id: "55:2", - name: "Frame 3", - width: 267, - height: 163, - }, - keys: [ - { - id: "55:3", - name: "Test", - characters: "On the road", - key: "on-the-road-title", - ns: "", - connected: true, - x: 59, - y: 46, - width: 68, - height: 15, - }, - ], -}; diff --git a/src/web/iframeContent.ts b/src/web/iframeContent.ts deleted file mode 100644 index 4fc780ce..00000000 --- a/src/web/iframeContent.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const generateIframeContent = () => { - return ` - - - - - - - -
- - - `; -}; diff --git a/src/web/main.ts b/src/web/main.ts deleted file mode 100644 index 2652c888..00000000 --- a/src/web/main.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - ConfigChangeHandler, - DocumentChangeHandler, - FrameScreenshot, - NodeInfo, - ResizeHandler, - SelectionChangeHandler, - SetupHandle, -} from "@/types"; -import { emit, on } from "@/utilities"; -import { generateIframeContent } from "./iframeContent"; -import { createLinks, getUrlConfig } from "./urlConfig"; -import exampleScreenshot from "./exampleScreenshot"; -import { getScreenshotsEndpoint } from "@/main/endpoints/getScreenshots"; -import { updateNodesEndpoint } from "@/main/endpoints/updateNodes"; -import { setNodesDataEndpoint } from "@/main/endpoints/setNodesData"; -import { getSelectedNodesEndpoint } from "@/main/endpoints/getSelectedNodes"; -import { getConnectedNodesEndpoint } from "@/main/endpoints/getConnectedNodes"; -import { copyPageEndpoint } from "@/main/endpoints/copyPage"; -import { clearPrefilledKeysEndpoint } from "@/main/endpoints/clearPrefilledKeys"; -import { formatTextEndpoint } from "../main/endpoints/formatText"; - -const iframe = document.getElementById("plugin_iframe") as HTMLIFrameElement; -const shortcuts = document.getElementById("shortcuts") as HTMLDivElement; - -function main() { - // populate the iframe the listeners are ready - iframe.contentDocument?.write(generateIframeContent()); - iframe.contentDocument?.close(); - - shortcuts.innerHTML = createLinks(); - - const state = getUrlConfig(); - - function updateNodes(changes: NodeInfo[], notify: boolean) { - const changed = { allNodesChanged: false, selectionChanged: false }; - - const updateNode = (prop: keyof typeof changed) => (node: NodeInfo) => { - const change = changes.find((n) => n.id === node.id); - if (change) { - changed[prop] = true; - } - return { - ...node, - ...change, - }; - }; - - state.allNodes = state.allNodes.map(updateNode("allNodesChanged")); - state.selectedNodes = state.selectedNodes.map( - updateNode("selectionChanged"), - ); - - if (notify) { - if (changed.allNodesChanged) { - emit("DOCUMENT_CHANGE"); - } - emit("SELECTION_CHANGE"); - } - } - - on("SETUP", (data) => { - state.config = { ...data, pageInfo: true, documentInfo: true }; - emit("CONFIG_CHANGE", state.config); - }); - - on("RESIZE", (data) => { - iframe.style.width = `${data.width}px`; - iframe.style.height = `${data.height}px`; - }); - - getScreenshotsEndpoint.mock(() => { - // Build the screenshot's `keys` array from current state so multiple - // nodes that share the same translation key produce multiple entries - // (which is what the real Figma getScreenshots returns and what - // exercises the dedup path in getPushChanges). - const keys = state.allNodes - .filter((n) => n.key) - .map((n, i) => ({ - ...n, - x: 10 + i * 80, - y: 30, - width: 70, - height: 20, - })); - if (keys.length === 0) return []; - return [ - { - image: exampleScreenshot.image, - info: exampleScreenshot.info, - keys, - }, - ] as FrameScreenshot[]; - }); - getSelectedNodesEndpoint.mock(() => ({ - items: state.selectedNodes, - somethingSelected: state.selectedNodes.length > 0, - })); - getConnectedNodesEndpoint.mock(({ ignoreSelection }) => { - const basedOnSelection = !ignoreSelection && state.selectedNodes.length > 0; - const items = basedOnSelection ? state.selectedNodes : state.allNodes; - return { - items: items.filter(({ key }) => key), - basedOnSelection, - }; - }); - copyPageEndpoint.mock(() => { - throw new Error("Not implemented"); - }); - updateNodesEndpoint.mock(({ nodes }) => updateNodes(nodes, true)); - setNodesDataEndpoint.mock(({ nodes }) => updateNodes(nodes, false)); - formatTextEndpoint.mock(({ formatted, nodeInfo }) => { - nodeInfo.characters = formatted; - updateNodes([nodeInfo], false); - }); - clearPrefilledKeysEndpoint.mock(() => { - const clearKey = (n: NodeInfo) => (n.connected ? n : { ...n, key: "" }); - state.allNodes = state.allNodes.map(clearKey); - state.selectedNodes = state.selectedNodes.map(clearKey); - emit("DOCUMENT_CHANGE"); - emit("SELECTION_CHANGE"); - }); -} - -main(); - -export default function () { - // pass -} diff --git a/src/web/ui.tsx b/src/web/ui.tsx deleted file mode 100644 index 6abfc3f6..00000000 --- a/src/web/ui.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { h, render } from "preact"; -import { Plugin } from "../ui/Plugin"; -import "!@create-figma-plugin/ui/lib/css/base.css"; -import { getUrlConfig } from "./urlConfig"; - -const AppWrapper = () => { - const data = getUrlConfig(true); - - return ; -}; - -setTimeout(() => { - render(, document.getElementById("root")!); -}, 0); diff --git a/src/web/urlConfig.ts b/src/web/urlConfig.ts deleted file mode 100644 index ba37de7b..00000000 --- a/src/web/urlConfig.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { ComponentProps } from "preact"; -import { type Plugin } from "../ui/Plugin"; -import { NodeInfo, TolgeeConfig } from "@/types"; - -export type PluginData = ComponentProps; - -export function createShortcutUrl(props: Partial) { - return `/?data=${encodeURIComponent(JSON.stringify(props))}`; -} - -export function getUrlConfig(fromParent?: boolean): PluginData { - const url = fromParent - ? window.parent.parent.location.href - : window.location.href; - const search = new URL(url).search; - const urlData = JSON.parse(new URLSearchParams(search).get("data") || "{}"); - - const data = { - config: {}, - allNodes: [], - selectedNodes: [], - ...urlData, - }; - - return data; -} - -type PluginInitialData = Partial>; - -export function createShortcutLink(name: string, props: PluginInitialData) { - return ``; -} - -export const DEFAULT_CREDENTIALS = { - apiUrl: "http://localhost:8080", - apiKey: "examples-admin-imported-project-implicit", -}; - -export const SIGNED_IN: Partial = { - ...DEFAULT_CREDENTIALS, - language: "en", - namespace: "", - pageInfo: true, - documentInfo: true, -}; - -export const PAGE_COPY: Partial = { - ...DEFAULT_CREDENTIALS, - namespace: "", - documentInfo: true, - pageCopy: true, -}; - -export const PAGE_STRING_DETAILS: Partial = { - ...DEFAULT_CREDENTIALS, - language: "en", - namespace: "", - documentInfo: true, - pageStringDetails: true, -}; - -export const PAGE_COPY_LANGUAGE: Partial = { - ...PAGE_COPY, - language: "cs", -}; - -export const NEW_PAGE: Partial = { - ...DEFAULT_CREDENTIALS, - documentInfo: true, -}; - -type Props = { - text: string; - key?: string; - ns?: string; - connected?: boolean; - translation?: string; - isPlural?: boolean; - id?: string; -}; - -export function createTestNode(props: Props): NodeInfo { - return { - name: props.text, - characters: props.text, - id: Math.random().toString(), - translation: props.translation ?? props.text, - isPlural: props.isPlural ?? false, - key: props.key || "", - ns: props.ns || "", - connected: Boolean(props.connected), - }; -} - -export function createLinks() { - const notConnected = [createTestNode({ text: "Test node" })]; - const connected = [ - createTestNode({ - text: "On the road", - key: "on-the-road-title", - connected: true, - }), - ]; - const connectedKeys = [ - createTestNode({ - text: "on-the-road-title", - key: "on-the-road-title", - connected: true, - }), - ]; - const connectedCzech = [ - createTestNode({ - text: "Na cestě", - key: "on-the-road-title", - connected: true, - }), - ]; - - return [ - createShortcutLink("Credentials", { - config: DEFAULT_CREDENTIALS, - }), - createShortcutLink("Select texts for translation", { - config: SIGNED_IN, - }), - createShortcutLink("One unlinked node", { - config: SIGNED_IN, - selectedNodes: notConnected, - allNodes: notConnected, - }), - createShortcutLink("One linked node", { - config: SIGNED_IN, - selectedNodes: connected, - allNodes: connected, - }), - createShortcutLink("New page", { - config: NEW_PAGE, - }), - - createShortcutLink("Page copy with keys", { - config: PAGE_COPY, - selectedNodes: connectedKeys, - allNodes: connectedKeys, - }), - - createShortcutLink("Page copy with language", { - config: PAGE_COPY_LANGUAGE, - selectedNodes: connectedCzech, - allNodes: connectedCzech, - }), - createShortcutLink("String details with selected node", { - config: { ...PAGE_STRING_DETAILS, nodeInfo: connectedKeys[0] }, - selectedNodes: connectedKeys, - allNodes: connectedKeys, - }), - ].join("\n"); -} diff --git a/tsconfig.json b/tsconfig.json index eb507121..8133b2be 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,40 @@ { - "extends": "@create-figma-plugin/tsconfig", "compilerOptions": { - "target": "es2015", - "typeRoots": ["node_modules/@figma", "node_modules/@types"], - "types": ["plugin-typings", "node"], + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, "skipLibCheck": true, - "module": "commonjs", "noEmit": true, - "baseUrl": "./src", + "typeRoots": ["node_modules/@figma", "node_modules/@types"], + "types": ["plugin-typings", "node"], + "baseUrl": ".", "paths": { - "@/*": ["*"], - "react": ["./node_modules/preact/compat"], - "react-dom": ["./node_modules/preact/compat"] + "$shared/*": ["src/shared/*"], + "$main/*": ["src/main/*"], + "$ui/*": ["src/ui/*"] } }, - "include": ["src/**/*.ts", "src/**/*.tsx"] + "include": [ + "src/**/*.ts", + "src/**/*.svelte", + "vite.config.*.ts", + "scripts/**/*.mjs", + "e2e/**/*.ts", + "e2e/vite.config.host.ts", + "playwright.config.ts" + ], + "exclude": ["node_modules", "dist"] } diff --git a/vite.config.gallery.ts b/vite.config.gallery.ts new file mode 100644 index 00000000..067e8bdd --- /dev/null +++ b/vite.config.gallery.ts @@ -0,0 +1,42 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const rootDir = path.resolve(__dirname, "gallery"); + +/** + * Standalone dev server for the component gallery / design-system page. + * + * Unlike the Figma plugin bundles, this is meant to run in a normal browser + * with full Vite HMR so the UI can be tuned live. The Figma color tokens that + * the components rely on (`--figma-color-*`) don't exist outside Figma, so + * `gallery/theme.css` supplies light/dark fallbacks that approximate Figma's + * own palette. + */ +export default defineConfig({ + root: rootDir, + plugins: [svelte(), tailwindcss()], + resolve: { + alias: { + $shared: path.resolve(__dirname, "src/shared"), + $ui: path.resolve(__dirname, "src/ui"), + // Only used to reuse the pure `shouldIgnoreNode` filter so the gallery + // demonstrates the real ignore behaviour without duplicating its logic. + $main: path.resolve(__dirname, "src/main"), + }, + }, + server: { + port: 5180, + strictPort: true, + open: true, + fs: { + // The gallery imports components from `src/ui`, which sits above `root`. + allow: [__dirname], + }, + }, +}); diff --git a/vite.config.ui.ts b/vite.config.ui.ts new file mode 100644 index 00000000..9f8a6359 --- /dev/null +++ b/vite.config.ui.ts @@ -0,0 +1,89 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; +import tailwindcss from '@tailwindcss/vite'; +import { defineConfig } from 'vite'; +import { viteSingleFile } from 'vite-plugin-singlefile'; +import { visualizer } from 'rollup-plugin-visualizer'; +import fs from 'node:fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const rootDir = path.resolve(__dirname, 'src/ui'); +const outDir = path.resolve(__dirname, 'dist'); + +// Opt-in bundle analysis: `ANALYZE=1 pnpm run build:ui`. Kept off by default so +// regular builds stay fast and don't pollute `dist/` with a stats artifact. +const analyze = process.env.ANALYZE === '1'; + +export default defineConfig({ + root: rootDir, + plugins: [ + svelte(), + tailwindcss(), + viteSingleFile(), + analyze && + visualizer({ + filename: path.join(outDir, 'stats-ui.html'), + template: 'treemap', + gzipSize: true, + brotliSize: false, + sourcemap: false, + emitFile: false, + }), + { + name: 'rename-ui-html', + apply: 'build', + closeBundle() { + const from = path.join(outDir, 'index.html'); + const to = path.join(outDir, 'ui.html'); + if (fs.existsSync(from)) { + fs.renameSync(from, to); + } + }, + }, + { + // The bundle Vite produces is an ES module (uses `import.meta`, top-level + // module scope, etc.) so we MUST keep `type="module"`. We only need to + // drop the `crossorigin` attribute Vite adds, because the Figma plugin + // iframe runs at origin "null" — the crossorigin handshake fails outright + // for an inline classic-ish module otherwise. + name: 'figma-strip-crossorigin', + apply: 'build', + closeBundle() { + const file = path.join(outDir, 'ui.html'); + if (!fs.existsSync(file)) return; + const html = fs.readFileSync(file, 'utf-8'); + const patched = html.replace( + //g, + '' + ); + fs.writeFileSync(file, patched); + }, + }, + ], + resolve: { + alias: { + $shared: path.resolve(__dirname, 'src/shared'), + $ui: path.resolve(__dirname, 'src/ui'), + }, + }, + build: { + outDir, + emptyOutDir: false, + cssCodeSplit: false, + assetsInlineLimit: 100_000_000, + // Figma's plugin iframe runs at origin "null"; the modulepreload polyfill + // Vite injects tries to resolve preload candidates against that origin and + // throws. Disable both the polyfill and the crossorigin attribute Vite + // adds to the inline - - diff --git a/web/package.json b/web/package.json deleted file mode 100644 index d41c808e..00000000 --- a/web/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scripts": { - "watch": "concurrently --kill-others \"npm run build\" \"serve --no-request-logging .\"", - "build": "nodemon --exec \"build-figma-plugin --typecheck\" -e tsx,ts,css --watch ../src/ --watch src/" - }, - "figma-plugin": { - "editorType": [ - "figma" - ], - "main": "src/main.ts", - "ui": "src/ui.tsx", - "id": "1", - "name": "Tolgee" - } -} diff --git a/web/src/main.ts b/web/src/main.ts deleted file mode 100644 index 33b19c96..00000000 --- a/web/src/main.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../src/web/main"; diff --git a/web/src/ui.tsx b/web/src/ui.tsx deleted file mode 100644 index a46db9ba..00000000 --- a/web/src/ui.tsx +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-ignore -window.__FIGMA_COMMAND__ = ""; -// @ts-ignore -window.__SHOW_UI_DATA__ = JSON.stringify({}); -export * from "../../src/web/ui";
-
- -
{children}
-
- - ); -}; diff --git a/src/ui/components/AutocompleteSelect/AutocompleteSelect.css b/src/ui/components/AutocompleteSelect/AutocompleteSelect.css deleted file mode 100644 index 538e00b0..00000000 --- a/src/ui/components/AutocompleteSelect/AutocompleteSelect.css +++ /dev/null @@ -1,61 +0,0 @@ -.autocompleteDropdown { - position: absolute; - left: 0; - right: 0; - background: var(--figma-color-bg); - border: 1px solid var(--figma-color-border); - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07); - z-index: 2; - max-height: 180px; - overflow-y: auto; -} - -.autocompleteDropdown.down { - top: 110%; -} -.autocompleteDropdown.up { - bottom: 110%; -} - -.autocompleteDropdownHeader { - padding: 12px 12px 0 12px; - font-size: 10px; -} -.autocompleteDropdownHeaderEmpty { - padding: 12px; - font-size: 10px; -} - -.autocompleteDropdownItem { - padding: 8px 12px; - cursor: pointer; - font-weight: 500; - background: none; - border: none; - color: var(--figma-color-text); -} -.autocompleteDropdownItem:hover { - background: var(--figma-color-bg-hover); -} - -.autocompleteDropdownAdd { - padding: 12px 12px 12px 12px; - color: var(--figma-color-bg-brand); - font-weight: 500; - cursor: pointer; - border-top: 1px solid var(--figma-color-border); - display: flex; - gap: 4px; - align-items: center; - justify-content: space-between; -} -.autocompleteDropdownAdd > * { - cursor: pointer; -} - -.autocompleteDropdownAddHint { - font-style: italic; - color: var(--figma-color-bg-brand); - font-weight: 400; -} diff --git a/src/ui/components/AutocompleteSelect/AutocompleteSelect.css.d.ts b/src/ui/components/AutocompleteSelect/AutocompleteSelect.css.d.ts deleted file mode 100644 index 632b67c7..00000000 --- a/src/ui/components/AutocompleteSelect/AutocompleteSelect.css.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare const styles: { - readonly "autocompleteDropdown": string; - readonly "autocompleteDropdownAdd": string; - readonly "autocompleteDropdownAddHint": string; - readonly "autocompleteDropdownHeader": string; - readonly "autocompleteDropdownHeaderEmpty": string; - readonly "autocompleteDropdownItem": string; - readonly "down": string; - readonly "up": string; -}; -export = styles; - diff --git a/src/ui/components/AutocompleteSelect/AutocompleteSelect.tsx b/src/ui/components/AutocompleteSelect/AutocompleteSelect.tsx deleted file mode 100644 index bf889696..00000000 --- a/src/ui/components/AutocompleteSelect/AutocompleteSelect.tsx +++ /dev/null @@ -1,345 +0,0 @@ -import { FunctionComponent, h } from "preact"; -import { - useRef, - useState, - useLayoutEffect, - useMemo, - useEffect, -} from "preact/hooks"; -import { Textbox, Muted } from "@create-figma-plugin/ui"; -import { Badge } from "../Badge/Badge"; -import styles from "./AutocompleteSelect.css"; - -type Props = - | { - value: string; - options: string[]; - placeholder?: string; - existingOptionsPlaceholder?: string; - noOptionsPlaceholder?: string; - onChange: (value: string) => void; - displayValue?: (value: string) => string; - dataCy?: string; - singleSelect: true; // When true, shows value in input instead of as badge - inputRef?: preact.RefObject; // Expose input ref for external control - } - | { - value: string[]; - options: string[]; - placeholder?: string; - existingOptionsPlaceholder?: string; - noOptionsPlaceholder?: string; - onChange: (value: string[]) => void; - displayValue?: (value: string) => string; - dataCy?: string; - singleSelect?: false; // Multi-select mode - inputRef?: preact.RefObject; // Expose input ref for external control - }; - -export const AutocompleteSelect: FunctionComponent = (props) => { - const { - options, - placeholder = "Add...", - existingOptionsPlaceholder = "Existing options", - noOptionsPlaceholder = "No options found", - displayValue = (v) => v || "", - dataCy, - singleSelect = false, - inputRef: externalInputRef, - } = props; - - const isMultiSelect = !singleSelect; - const singleValue = singleSelect ? (props.value as string) : ""; - const multiValues = isMultiSelect ? (props.value as string[]) : []; - const singleOnChange = singleSelect - ? (props.onChange as (value: string) => void) - : null; - const multiOnChange = isMultiSelect - ? (props.onChange as (value: string[]) => void) - : null; - - const [inputValue, setInputValue] = useState( - singleSelect ? (singleValue ?? "") : "", - ); - const [isFocused, setIsFocused] = useState(false); - const [showDropdown, setShowDropdown] = useState(false); - const internalInputRef = useRef(null); - const inputRef = externalInputRef || internalInputRef; - const dropdownRef = useRef(null); - const [dropdownDirection, setDropdownDirection] = useState<"down" | "up">( - "down", - ); - - // For single-select mode, show value in input when not focused, otherwise show inputValue - // Ensure we always return a string, not undefined - const displayInputValue = singleSelect - ? isFocused - ? (inputValue ?? "") - : (singleValue ?? "") - : (inputValue ?? ""); - - useLayoutEffect(() => { - if (!showDropdown || !inputRef.current) return; - const inputRect = inputRef.current.getBoundingClientRect(); - const dropdownHeight = dropdownRef.current?.offsetHeight || 180; - const spaceBelow = window.innerHeight - inputRect.bottom - 49; // padding of actionbar - const spaceAbove = inputRect.top; - if (spaceBelow < dropdownHeight && spaceAbove > dropdownHeight) { - setDropdownDirection("up"); - } else { - setDropdownDirection("down"); - } - }, [showDropdown]); - - // Sync inputValue with value prop when not focused (for single-select mode) - useEffect(() => { - if (!singleSelect || isFocused) return; - // Sync inputValue with value when not focused - setInputValue(singleValue ?? ""); - }, [singleSelect, singleValue, isFocused]); - - const trimmedInputValue = useMemo(() => inputValue.trim(), [inputValue]); - const lowerInputTrimmed = useMemo( - () => trimmedInputValue.toLowerCase(), - [trimmedInputValue], - ); - - const normalizedMultiSet = useMemo( - () => new Set(multiValues.map((v) => v.toLowerCase().trim())), - [multiValues], - ); - - const filteredOptions = useMemo(() => { - const filtered = options.filter((opt) => - opt.toLowerCase().includes(lowerInputTrimmed), - ); - // In multi-select mode, exclude already selected values (normalized comparison) - if (isMultiSelect) { - return filtered.filter( - (opt) => !normalizedMultiSet.has(opt.toLowerCase().trim()), - ); - } - return filtered; - }, [options, lowerInputTrimmed, isMultiSelect, normalizedMultiSet]); - - const exactMatch = useMemo(() => { - return filteredOptions.find( - (opt) => opt.toLowerCase() === lowerInputTrimmed, - ); - }, [filteredOptions, lowerInputTrimmed]); - - const alreadyExists = isMultiSelect - ? normalizedMultiSet.has(lowerInputTrimmed) || - options.some((opt) => opt.toLowerCase() === lowerInputTrimmed) - : singleValue === inputValue.trim() || - options.some((opt) => opt.toLowerCase() === lowerInputTrimmed); - - const highlightedOptions = useMemo( - () => - filteredOptions.map((opt) => { - const label = displayValue(opt); - const matchIndex = - lowerInputTrimmed === "" - ? -1 - : label.toLowerCase().indexOf(lowerInputTrimmed); - const isExact = opt.toLowerCase() === lowerInputTrimmed; - return { opt, label, matchIndex, isExact }; - }), - [filteredOptions, displayValue, lowerInputTrimmed], - ); - - const canAdd = inputValue.trim() && !alreadyExists; - - const handleSelect = (selectedValue: string) => { - if (singleSelect && singleOnChange) { - singleOnChange(selectedValue); - setInputValue(selectedValue); - } else if (isMultiSelect && multiOnChange) { - multiOnChange([...multiValues, selectedValue]); - setInputValue(""); - } - setShowDropdown(false); - setIsFocused(false); - }; - - const handleAdd = () => { - const newValue = inputValue.trim(); - if (singleSelect && singleOnChange) { - if (newValue && newValue !== singleValue) { - singleOnChange(newValue); - setInputValue(newValue); - } - } else if (isMultiSelect && multiOnChange) { - if (newValue && !normalizedMultiSet.has(newValue.toLowerCase())) { - multiOnChange([...multiValues, newValue]); - setInputValue(""); - } - } - setShowDropdown(false); - setIsFocused(false); - }; - - const handleRemove = (removedValue?: string) => { - if (singleSelect && singleOnChange) { - singleOnChange(""); - setInputValue(""); - } else if (isMultiSelect && multiOnChange && removedValue) { - multiOnChange(multiValues.filter((v) => v !== removedValue)); - } - }; - - const handleFocus = () => { - setIsFocused(true); - if (singleSelect && !inputValue) { - setInputValue(singleValue ?? ""); - } - setShowDropdown(true); - }; - - const handleBlur = (e: FocusEvent) => { - // Don't close if clicking on the refresh button or its parent - const relatedTarget = e.relatedTarget as HTMLElement; - if (relatedTarget?.closest("[data-refresh-button]")) { - return; - } - - setIsFocused(false); - // Delay to allow click events on dropdown items - setTimeout(() => { - setShowDropdown(false); - if (singleSelect) { - setInputValue(singleValue ?? ""); - } - }, 200); - }; - - return ( -
- {isMultiSelect && - multiValues.map((val) => ( - handleRemove(val)}> - {displayValue(val)} - - ))} -
- setInputValue(e.currentTarget.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - if (exactMatch) { - handleSelect(exactMatch); - } else if (canAdd) { - handleAdd(); - } - } else if (e.key === "Escape") { - if (singleSelect) { - setInputValue(""); - setIsFocused(false); - setShowDropdown(false); - inputRef.current?.blur(); - } - } - }} - /> - {showDropdown && ( -
- {filteredOptions.length > 0 ? ( -
- {existingOptionsPlaceholder} -
- ) : ( -
- {noOptionsPlaceholder} -
- )} - {highlightedOptions.map(({ opt, label, matchIndex, isExact }) => { - if (matchIndex === -1) { - return ( -
handleSelect(opt)} - style={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - }} - > - {label} - {isExact && ( - - Enter - - )} -
- ); - } - const before = label.slice(0, matchIndex); - const match = label.slice( - matchIndex, - matchIndex + trimmedInputValue.length, - ); - const after = label.slice(matchIndex + trimmedInputValue.length); - return ( -
handleSelect(opt)} - style={{ - display: "flex", - justifyContent: "space-between", - alignItems: "center", - }} - > - - {before} - {match} - {after} - - {isExact && ( - - Enter - - )} -
- ); - })} - {canAdd && ( -
handleAdd()} - > - - Add "{inputValue}" - - - Enter - -
- )} -
- )} -
-
- ); -}; diff --git a/src/ui/components/Badge/Badge.css b/src/ui/components/Badge/Badge.css deleted file mode 100644 index 8c5567c4..00000000 --- a/src/ui/components/Badge/Badge.css +++ /dev/null @@ -1,26 +0,0 @@ -.badge { - background-color: var(--figma-color-bg-disabled); - color: rgba(0, 0, 0, 0.7); - font-weight: 400; - display: flex; - align-items: center; - justify-content: center; - padding: 2px 6px; - border-radius: 16px; -} - -.badge button { - color: rgba(0, 0, 0, 0.7); - cursor: pointer; -} - -.badge.bold { - font-weight: 600; - padding: 4px 8px; -} - -@media (prefers-color-scheme: dark) { - .badge { - background-color: var(--figma-color-bg-disabled-secondary); - } -} diff --git a/src/ui/components/Badge/Badge.css.d.ts b/src/ui/components/Badge/Badge.css.d.ts deleted file mode 100644 index f1dd15ec..00000000 --- a/src/ui/components/Badge/Badge.css.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare const styles: { - readonly "badge": string; - readonly "bold": string; -}; -export = styles; - diff --git a/src/ui/components/Badge/Badge.tsx b/src/ui/components/Badge/Badge.tsx deleted file mode 100644 index b33726a5..00000000 --- a/src/ui/components/Badge/Badge.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { h } from "preact"; - -import styles from "./Badge.css"; -import { X } from "../../icons/SvgIcons"; - -type Props = { - children: string; - title?: string; - bold?: boolean; - onClick?: () => void; - onRemove?: () => void; -}; - -export const Badge = ({ onClick, children, title, bold, onRemove }: Props) => { - return ( -
- {children} - {onRemove && ( - - )} -
- ); -}; diff --git a/src/ui/components/BranchSelect/BranchSelect.css b/src/ui/components/BranchSelect/BranchSelect.css deleted file mode 100644 index 1a988069..00000000 --- a/src/ui/components/BranchSelect/BranchSelect.css +++ /dev/null @@ -1,45 +0,0 @@ -.wrapper { - display: flex; - align-items: center; - gap: 4px; - width: 100%; -} - -.wrapper > :first-child { - flex: 1; - min-width: 0; -} -.wrapper > :first-child > :nth-child(2) { - flex-grow: 1; -} - -.refreshButton { - flex-shrink: 0; - width: 16px; - height: 16px; - position: relative; - color: var(--figma-color-text-secondary); - transition: color 0.2s; -} - -.refreshButton:hover:not(:disabled) { - color: var(--figma-color-text); -} - -.refreshButton:disabled { - cursor: not-allowed; - opacity: 0.7; -} - -.refreshButton.rotating { - animation: rotate 1s linear infinite; -} - -@keyframes rotate { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/ui/components/BranchSelect/BranchSelect.css.d.ts b/src/ui/components/BranchSelect/BranchSelect.css.d.ts deleted file mode 100644 index 572f7f79..00000000 --- a/src/ui/components/BranchSelect/BranchSelect.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare const styles: { - readonly "refreshButton": string; - readonly "rotate": string; - readonly "rotating": string; - readonly "wrapper": string; -}; -export = styles; - diff --git a/src/ui/components/BranchSelect/BranchSelect.tsx b/src/ui/components/BranchSelect/BranchSelect.tsx deleted file mode 100644 index 0dd44d73..00000000 --- a/src/ui/components/BranchSelect/BranchSelect.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { FunctionComponent, h } from "preact"; -import { useMemo, useState } from "preact/hooks"; -import { IconButton } from "@create-figma-plugin/ui"; -import { Refresh } from "@/ui/icons/SvgIcons"; -import styles from "./BranchSelect.css"; - -type Props = { - branches: Array<{ name: string; isDefault: boolean }>; - value: string; - onChange: (value: string) => void; - onRefresh?: () => Promise | void; -}; - -export const BranchSelect: FunctionComponent = ({ - value, - branches, - onChange, - onRefresh, -}) => { - const [isRefreshing, setIsRefreshing] = useState(false); - - const options = useMemo<{ text: string; value: string }[]>( - () => - [...branches] - .sort((a, b) => a.name.localeCompare(b.name)) - .map((b) => ({ - text: b.isDefault ? `${b.name} (default)` : b.name, - value: b.name, - })), - [branches], - ); - - const handleRefresh = async (e: MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - if (!onRefresh || isRefreshing) return; - setIsRefreshing(true); - try { - await onRefresh(); - } finally { - setIsRefreshing(false); - } - }; - - return ( -
- - {onRefresh && ( - - - - )} -
- ); -}; diff --git a/src/ui/components/Dialog/Dialog.css b/src/ui/components/Dialog/Dialog.css deleted file mode 100644 index d3829c29..00000000 --- a/src/ui/components/Dialog/Dialog.css +++ /dev/null @@ -1,9 +0,0 @@ -.dialogBody { - z-index: 1300; - position: fixed; - top: 0px; - left: 0px; - right: 0px; - bottom: 0px; - background: var(--figma-color-bg); -} diff --git a/src/ui/components/Dialog/Dialog.css.d.ts b/src/ui/components/Dialog/Dialog.css.d.ts deleted file mode 100644 index 686ea009..00000000 --- a/src/ui/components/Dialog/Dialog.css.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare const styles: { - readonly "dialogBody": string; -}; -export = styles; - diff --git a/src/ui/components/Dialog/Dialog.tsx b/src/ui/components/Dialog/Dialog.tsx deleted file mode 100644 index ef74f66e..00000000 --- a/src/ui/components/Dialog/Dialog.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { ComponentChildren, h } from "preact"; -import { useEffect, useRef } from "preact/hooks"; -import styles from "./Dialog.css"; - -type Props = { - onClose: () => void; - children: ComponentChildren; -}; - -export const Dialog = ({ children, onClose }: Props) => { - const ref = useRef(null); - - useEffect(() => { - const element = ref.current; - const handler = (e: KeyboardEvent) => { - if (e.code === "Escape") { - onClose(); - } - }; - element?.focus(); - element?.addEventListener("keydown", handler); - return () => element?.removeEventListener("keydown", handler); - }, [onClose]); - - return ( -
-
{children}
-
- ); -}; diff --git a/src/ui/components/Editor/Editor.css b/src/ui/components/Editor/Editor.css deleted file mode 100644 index c6223a73..00000000 --- a/src/ui/components/Editor/Editor.css +++ /dev/null @@ -1,149 +0,0 @@ -.editor-wrapper { - border: 1px solid var(--figma-color-border); - overflow: hidden; - border-radius: var(--border-radius-2); - cursor: text; - background: var(--figma-color-bg); - /* color: var(--figma-color-text); */ - padding: 1px; -} - -.editor-wrapper:focus-within { - border: 1px solid var(--figma-color-border-brand-strong); - outline: 1px solid var(--figma-color-border-brand-strong); -} - -.editor-wrapper > * { - padding: 8px 10px; -} - -.editor { - display: grid; -} - -.editor .cm-editor { - outline: none !important; - white-space: pre-wrap; -} - -.editor .cm-line { - padding: 0px 1px !important; -} - -.editor .cm-content { - padding: 0px !important; -} - -.editor .cm-cursor { - border-color: var(--figma-color-border-primary) !important; -} - -.editor .placeholder-widget { - display: inline-flex; - vertical-align: text-top; - align-items: center; - justify-content: center; - min-width: 15px; - border: 1px solid var(--figma-color-border); - background-color: var(--figma-color-bg); - color: var(--figma-color-text); - border-radius: 10px; - padding: 0px 7px; - font-size: 0.8em; - user-select: none; - margin: 0px 1px; - overflow: hidden; -} - -.editor .placeholder-variable, -.editor .placeholder-hash { - background-color: var(--figma-color-bg-editor-variable, #008371); -} - -@media (prefers-color-scheme: light) { - .editor .placeholder-variable, - .editor .placeholder-hash { - color: white; - } -} -.editor > * { - display: grid; -} -.editor .placeholder-error { - background: #ff000054; -} - -.editor .placeholder-tagOpen { - border: 1px solid var(--figma-color-border); - background-color: var(--figma-color-bg-brand); - color: var(--figma-color-bg-brand-contrast); - border-radius: 10px 0px 0px 10px; - padding-right: 6px; -} - -.editor .placeholder-tagClose { - border: 1px solid var(--figma-color-border); - background-color: var(--figma-color-bg-brand); - color: var(--figma-color-bg-brand-contrast); - border-radius: 0px 10px 10px 0px; - padding-left: 6px; -} - -.editor .placeholder-tagSelfClosed { - border: 1px solid var(--figma-color-border); - background-color: var(--figma-color-bg-brand); - color: var(--figma-color-bg-brand-contrast); -} - -.editor [dir="rtl"] .placeholder-tagOpen { - border-radius: 0px 10px 10px 0px; - padding-left: 6px; -} -.editor [dir="rtl"] .placeholder-tagClose { - border-radius: 10px 0px 0px 10px; - padding-right: 6px; -} - -.plural-editor-wrapper { - display: grid; - gap: 2px; -} -.plural-editor-wrapper .styled-variants { - display: flex; - flex-direction: column; - gap: 8px; -} -.plural-editor-wrapper .styled-parameters { - color: var(--figma-color-text-brand-secondary); -} -.plural-editor-wrapper .styled-variant-label { - box-sizing: border-box; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - border: 1px solid var(--figma-color-border); - background-color: var(--figma-color-bg); - color: var(--figma-color-text); - border-radius: 12px; - padding: 2px 9px; - user-select: none; - margin: 0px 1px; - text-transform: capitalize; - white-space: nowrap; -} -.plural-editor-wrapper .styled-variant-content { - display: block; -} - -:root { - --syntax-color-function: #007300; - --syntax-color-other: #002bff; -} - -@media (prefers-color-scheme: dark) { - :root { - --syntax-color-function: #9ac99a; - --syntax-color-other: #99aaff; - } -} diff --git a/src/ui/components/Editor/Editor.css.d.ts b/src/ui/components/Editor/Editor.css.d.ts deleted file mode 100644 index 9841f70b..00000000 --- a/src/ui/components/Editor/Editor.css.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -declare const styles: { - readonly "cm-content": string; - readonly "cm-cursor": string; - readonly "cm-editor": string; - readonly "cm-line": string; - readonly "editor": string; - readonly "editor-wrapper": string; - readonly "placeholder-error": string; - readonly "placeholder-hash": string; - readonly "placeholder-tagClose": string; - readonly "placeholder-tagOpen": string; - readonly "placeholder-tagSelfClosed": string; - readonly "placeholder-variable": string; - readonly "placeholder-widget": string; - readonly "plural-editor-wrapper": string; - readonly "styled-parameters": string; - readonly "styled-variant-content": string; - readonly "styled-variant-label": string; - readonly "styled-variants": string; -}; -export = styles; - diff --git a/src/ui/components/Editor/Editor.tsx b/src/ui/components/Editor/Editor.tsx deleted file mode 100644 index 0c7eef9b..00000000 --- a/src/ui/components/Editor/Editor.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import React, { h, RefObject } from "preact"; -import { useEffect, useRef } from "preact/hooks"; -import { minimalSetup } from "codemirror"; -import { Compartment, EditorState, Prec } from "@codemirror/state"; -import { ViewUpdate, EditorView, keymap, KeyBinding } from "@codemirror/view"; -import { - tolgeeSyntax, - PlaceholderPlugin, - TolgeeHighlight, -} from "@tginternal/editor"; -import "!./Editor.css"; - -const editorSyntaxColors = { - function: "var(--syntax-color-function)", - other: "var(--syntax-color-other)", - main: "var(--figma-color-text)", -}; - -export type EditorProps = { - value: string; - onChange?: (val: string) => void; - background?: string; - mode: "placeholders" | "syntax" | "plain"; - autofocus?: boolean; - minHeight?: number | string; - onBlur?: () => void; - onFocus?: () => void; - shortcuts?: KeyBinding[]; - autoScrollIntoView?: boolean; - locale?: string; - examplePluralNum?: number; - nested?: boolean; - disabled?: boolean; -}; - -function useRefGroup(value: T): RefObject { - const refObject = useRef(value); - refObject.current = value; - return refObject; -} - -export const Editor = ({ - value, - onChange, - onFocus, - onBlur, - mode, - autofocus, - shortcuts, - minHeight, - locale, - examplePluralNum, - nested, - disabled, -}: EditorProps) => { - const ref = useRef(null); - const editor = useRef(); - const placeholders = useRef(new Compartment()); - const isolates = useRef(new Compartment()); - const disabledCompartment = useRef(new Compartment()); - const keyBindings = useRef(shortcuts); - const callbacksRef = useRefGroup({ - onChange, - onFocus, - onBlur, - }); - const languageCompartment = useRef(new Compartment()); - - keyBindings.current = shortcuts; - - useEffect(() => { - const shortcutsUptoDate = shortcuts?.map((value, i) => { - return { - ...value, - run: (val: EditorView) => keyBindings.current?.[i].run?.(val) ?? false, - }; - }); - - const instance = new EditorView({ - parent: ref.current!, - state: EditorState.create({ - doc: value, - extensions: [ - minimalSetup, - Prec.highest(keymap.of(shortcutsUptoDate ?? [])), - EditorView.lineWrapping, - EditorView.updateListener.of((v: ViewUpdate) => { - if (v.focusChanged) { - if (v.view.hasFocus) { - callbacksRef.current?.onFocus?.(); - } else { - callbacksRef.current?.onBlur?.(); - } - } - if (v.docChanged) { - callbacksRef.current?.onChange?.(v.state.doc.toString()); - } - }), - EditorView.contentAttributes.of({ - spellcheck: "true", - lang: locale || "", - }), - TolgeeHighlight(editorSyntaxColors), - languageCompartment.current.of([]), - placeholders.current.of([]), - isolates.current.of([]), - disabledCompartment.current.of([]), - ], - }), - }); - - if (autofocus) { - instance.focus(); - } - - editor.current = instance; - }, []); - - useEffect(() => { - const placholderPlugins = - mode === "placeholders" - ? [ - PlaceholderPlugin({ - examplePluralNum, - nested: Boolean(nested), - tooltips: false, - }), - ] - : []; - const syntaxPlugins = - mode === "plain" ? [] : [tolgeeSyntax(Boolean(nested))]; - editor.current?.dispatch({ - selection: editor.current.state.selection, - effects: [ - placeholders.current?.reconfigure(placholderPlugins), - languageCompartment.current.reconfigure(syntaxPlugins), - ], - }); - }, [mode, nested, examplePluralNum]); - - useEffect(() => { - const state = editor.current?.state; - const editorValue = state?.doc.toString(); - if (state && editorValue !== value) { - const transaction = state.update({ - changes: { from: 0, to: state.doc.length, insert: value || "" }, - }); - editor.current?.update([transaction]); - } - }, [value]); - - useEffect(() => { - // set cursor to the end of document - const length = editor.current!.state.doc.length; - editor.current!.dispatch({ selection: { anchor: length } }); - - return () => { - editor.current!.destroy(); - }; - }, []); - - useEffect(() => { - editor.current?.dispatch({ - effects: disabledCompartment.current.reconfigure( - EditorState.readOnly.of(Boolean(disabled)), - ), - }); - }, [disabled]); - - return ( -
-
-
- ); -}; diff --git a/src/ui/components/Editor/PluralEditor.tsx b/src/ui/components/Editor/PluralEditor.tsx deleted file mode 100644 index ae61b023..00000000 --- a/src/ui/components/Editor/PluralEditor.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { TolgeeFormat } from "@tginternal/editor"; - -import { TranslationPlurals } from "./TranslationPlurals"; -import { Editor, EditorProps } from "./Editor"; -import { h } from "preact"; - -type Props = { - locale: string; - value: TolgeeFormat; - onChange?: (value: TolgeeFormat) => void; - onBlur?: () => void; - editorProps?: Partial; - mode: "placeholders" | "syntax" | "plain"; -}; - -export const PluralEditor = ({ - locale, - value, - onChange, - onBlur, - editorProps, - mode, -}: Props) => { - function handleChange(text: string, variant: string) { - onChange?.({ ...value, variants: { ...value.variants, [variant]: text } }); - } - - return ( - { - const variantOrOther: Intl.LDMLPluralRule = variant || "other"; - return ( -
- handleChange(value, variantOrOther)} - minHeight={value.parameter ? "unset" : "50px"} - locale={locale} - examplePluralNum={exampleValue} - nested={Boolean(variant)} - {...editorProps} - /> -
- ); - }} - /> - ); -}; diff --git a/src/ui/components/Editor/TranslationPlurals.tsx b/src/ui/components/Editor/TranslationPlurals.tsx deleted file mode 100644 index 88b10030..00000000 --- a/src/ui/components/Editor/TranslationPlurals.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - TolgeeFormat, - getVariantExample, - getPluralVariants, -} from "@tginternal/editor"; -import { Fragment, h, JSX } from "preact"; -import { Muted } from "@create-figma-plugin/ui"; -import { useMemo } from "preact/hooks"; - -type RenderProps = { - content: string; - variant: Intl.LDMLPluralRule | undefined; - locale: string; - exampleValue?: number; -}; - -type Props = { - locale: string; - value: TolgeeFormat; - render: (props: RenderProps) => JSX.Element; - showEmpty?: boolean; - variantPaddingTop?: number | string; - setActiveVariant?: (variant: string) => void; -}; - -export const TranslationPlurals = ({ - locale, - render, - value, - showEmpty, -}: Props) => { - const variants = useMemo(() => { - const existing = new Set(Object.keys(value.variants)); - const required = getPluralVariants(locale); - required.forEach((val) => existing.delete(val)); - const result = Array.from(existing).map((value) => { - return [value, getVariantExample(locale, value)] as const; - }); - required.forEach((value) => { - result.push([value, getVariantExample(locale, value)]); - }); - return result; - }, [locale]); - - if (value.parameter) { - return ( -
-
- {variants - .filter( - ([variant]) => - showEmpty || value.variants[variant as Intl.LDMLPluralRule], - ) - .map(([variant, exampleValue]) => ( - - {variant} -
- {render({ - variant: variant as Intl.LDMLPluralRule, - content: - value.variants[variant as Intl.LDMLPluralRule] || "", - exampleValue, - locale, - })} -
-
- ))} -
-
- ); - } - return ( -
- {render({ - content: value.variants["other"] ?? "", - locale, - variant: undefined, - })} -
- ); -}; diff --git a/src/ui/components/FullPageLoading/FullPageLoading.css b/src/ui/components/FullPageLoading/FullPageLoading.css deleted file mode 100644 index ae98df0a..00000000 --- a/src/ui/components/FullPageLoading/FullPageLoading.css +++ /dev/null @@ -1,29 +0,0 @@ -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -.container { - animation: 0.4s ease-in fadeIn; - display: flex; - position: fixed; - align-items: center; - justify-content: center; - top: 0px; - left: 0px; - right: 0px; - bottom: 0px; - z-index: 100; -} - -.content { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; - justify-content: center; -} diff --git a/src/ui/components/FullPageLoading/FullPageLoading.css.d.ts b/src/ui/components/FullPageLoading/FullPageLoading.css.d.ts deleted file mode 100644 index 37f3636c..00000000 --- a/src/ui/components/FullPageLoading/FullPageLoading.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare const styles: { - readonly "container": string; - readonly "content": string; - readonly "fadeIn": string; -}; -export = styles; - diff --git a/src/ui/components/FullPageLoading/FullPageLoading.tsx b/src/ui/components/FullPageLoading/FullPageLoading.tsx deleted file mode 100644 index 63c59a2f..00000000 --- a/src/ui/components/FullPageLoading/FullPageLoading.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { LoadingIndicator, Muted, Text } from "@create-figma-plugin/ui"; -import { FunctionalComponent, h } from "preact"; - -import styles from "./FullPageLoading.css"; - -type Props = { - text?: string; - blocking?: boolean; -}; - -export const FullPageLoading: FunctionalComponent = ({ - text, - blocking = true, -}) => { - return ( -
-
- - - {text} - -
-
- ); -}; diff --git a/src/ui/components/InfoTooltip/InfoTooltip.css b/src/ui/components/InfoTooltip/InfoTooltip.css deleted file mode 100644 index 28f8aa23..00000000 --- a/src/ui/components/InfoTooltip/InfoTooltip.css +++ /dev/null @@ -1,7 +0,0 @@ -.info { - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - color: var(--figma-color-text-primary); -} diff --git a/src/ui/components/InfoTooltip/InfoTooltip.css.d.ts b/src/ui/components/InfoTooltip/InfoTooltip.css.d.ts deleted file mode 100644 index 6168032e..00000000 --- a/src/ui/components/InfoTooltip/InfoTooltip.css.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare const styles: { - readonly "info": string; -}; -export = styles; - diff --git a/src/ui/components/InfoTooltip/InfoTooltip.tsx b/src/ui/components/InfoTooltip/InfoTooltip.tsx deleted file mode 100644 index 0b7b68ee..00000000 --- a/src/ui/components/InfoTooltip/InfoTooltip.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { h } from "preact"; - -import styles from "./InfoTooltip.css"; -import { Info } from "../../icons/SvgIcons"; -import Popover, { ActionItem } from "../Popover/Popover"; -import { useRef } from "preact/hooks"; - -type Props = { - children: unknown; - color?: string; - items?: ActionItem[] | undefined; - rotated?: boolean; -}; - -export const InfoTooltip = ({ children, color, items, rotated }: Props) => { - const popoverTrigger = useRef(null); - - return ( -
- - -
- ); -}; diff --git a/src/ui/components/KeyOptionsButton/KeyOptionsButton.css b/src/ui/components/KeyOptionsButton/KeyOptionsButton.css deleted file mode 100644 index 7a07595d..00000000 --- a/src/ui/components/KeyOptionsButton/KeyOptionsButton.css +++ /dev/null @@ -1,8 +0,0 @@ -.highlightButton { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - width: 20px; - height: 20px; -} diff --git a/src/ui/components/KeyOptionsButton/KeyOptionsButton.css.d.ts b/src/ui/components/KeyOptionsButton/KeyOptionsButton.css.d.ts deleted file mode 100644 index cc19981f..00000000 --- a/src/ui/components/KeyOptionsButton/KeyOptionsButton.css.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare const styles: { - readonly "highlightButton": string; -}; -export = styles; - diff --git a/src/ui/components/KeyOptionsButton/KeyOptionsButton.tsx b/src/ui/components/KeyOptionsButton/KeyOptionsButton.tsx deleted file mode 100644 index 17c6f507..00000000 --- a/src/ui/components/KeyOptionsButton/KeyOptionsButton.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { h } from "preact"; - -import styles from "./KeyOptionsButton.css"; -import { Edit, More, MyLocation } from "@/ui/icons/SvgIcons"; -import { useMemo, useRef } from "preact/hooks"; -import Popover from "../Popover/Popover"; -import { useHighlightNodeMutation } from "../../hooks/useHighlightNodeMutation"; -import { useGlobalActions } from "../../state/GlobalState"; -import { NodeInfo } from "../../../types"; -import { useEditorMode } from "../../hooks/useEditorMode"; - -type Props = { - node: NodeInfo; -}; - -export const KeyOptionsButton = ({ node }: Props) => { - const moreOptionsRef = useRef(null); - - const highlightMutation = useHighlightNodeMutation(); - const { setRoute } = useGlobalActions(); - - const handleHighlight = () => { - highlightMutation.mutate({ id: node.id }); - }; - - const editorMode = useEditorMode(); - - const stringDetailsOption = useMemo( - () => ({ - cy: "string_details_cy", - label: "String details", - icon: , - onClick: () => { - setRoute("string_details", { node }); - }, - }), - [node, setRoute], - ); - - const moveToStringOption = { - label: "Move to String", - icon: , - onClick: () => { - handleHighlight(); - }, - }; - - return ( -
- - , - // onClick: () => { - // console.log("Ignore String"); - // }, - // }, - ] - } - /> -
- ); -}; diff --git a/src/ui/components/LocateNodeButton/LocateNodeButton.css b/src/ui/components/LocateNodeButton/LocateNodeButton.css deleted file mode 100644 index 7de368eb..00000000 --- a/src/ui/components/LocateNodeButton/LocateNodeButton.css +++ /dev/null @@ -1,9 +0,0 @@ -.highlightButton { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - width: 20px; - height: 20px; - color: var(--figma-color-text-secondary); -} diff --git a/src/ui/components/LocateNodeButton/LocateNodeButton.css.d.ts b/src/ui/components/LocateNodeButton/LocateNodeButton.css.d.ts deleted file mode 100644 index cc19981f..00000000 --- a/src/ui/components/LocateNodeButton/LocateNodeButton.css.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare const styles: { - readonly "highlightButton": string; -}; -export = styles; - diff --git a/src/ui/components/LocateNodeButton/LocateNodeButton.tsx b/src/ui/components/LocateNodeButton/LocateNodeButton.tsx deleted file mode 100644 index d1a7f004..00000000 --- a/src/ui/components/LocateNodeButton/LocateNodeButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useHighlightNodeMutation } from "@/ui/hooks/useHighlightNodeMutation"; -import { h } from "preact"; - -import styles from "./LocateNodeButton.css"; -import { MyLocation } from "@/ui/icons/SvgIcons"; - -type Props = { - nodeId: string; -}; - -export const LocateNodeButton = ({ nodeId }: Props) => { - const highlightMutation = useHighlightNodeMutation(); - - const handleHighlight = () => { - highlightMutation.mutate({ id: nodeId }); - }; - - return ( -
- -
- ); -}; diff --git a/src/ui/components/NamespaceSelect/NamespaceSelect.css b/src/ui/components/NamespaceSelect/NamespaceSelect.css deleted file mode 100644 index 2508789a..00000000 --- a/src/ui/components/NamespaceSelect/NamespaceSelect.css +++ /dev/null @@ -1,63 +0,0 @@ -.modalBody { - min-width: 250px; - display: grid; -} - -.actions { - display: flex; - justify-content: flex-end; - margin: 6px 0px; -} -.selectContainer { - padding: 3px 8px; -} - -.container { - width: 100%; -} - -.inputWrapper { - position: relative; - width: 100%; -} - -.inputWrapper > :first-child { - width: 100%; -} - -.refreshButton { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - z-index: 2; - padding: 4px; - cursor: pointer; - color: var(--figma-color-text-secondary); - background: var(--figma-color-bg); - border: none; - transition: color 0.2s; - pointer-events: auto; -} - -.refreshButton:hover:not(:disabled) { - color: var(--figma-color-text); -} - -.refreshButton:disabled { - cursor: not-allowed; - opacity: 0.7; -} - -.refreshButton.rotating { - animation: rotate 1s linear infinite; -} - -@keyframes rotate { - from { - transform: translateY(-50%) rotate(0deg); - } - to { - transform: translateY(-50%) rotate(360deg); - } -} diff --git a/src/ui/components/NamespaceSelect/NamespaceSelect.css.d.ts b/src/ui/components/NamespaceSelect/NamespaceSelect.css.d.ts deleted file mode 100644 index 8511b74b..00000000 --- a/src/ui/components/NamespaceSelect/NamespaceSelect.css.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare const styles: { - readonly "actions": string; - readonly "container": string; - readonly "inputWrapper": string; - readonly "modalBody": string; - readonly "refreshButton": string; - readonly "rotate": string; - readonly "rotating": string; - readonly "selectContainer": string; -}; -export = styles; - diff --git a/src/ui/components/NamespaceSelect/NamespaceSelect.tsx b/src/ui/components/NamespaceSelect/NamespaceSelect.tsx deleted file mode 100644 index 5215b810..00000000 --- a/src/ui/components/NamespaceSelect/NamespaceSelect.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { FunctionComponent, h } from "preact"; -import { useMemo, useState, useRef } from "preact/hooks"; -import { AutocompleteSelect } from "../AutocompleteSelect/AutocompleteSelect"; -import { IconButton } from "@create-figma-plugin/ui"; -import { Refresh } from "@/ui/icons/SvgIcons"; -import styles from "./NamespaceSelect.css"; - -type Props = { - namespaces: string[]; - value: string; - onChange: (value: string) => void; - onRefresh?: () => Promise | void; -}; - -export const NamespaceSelect: FunctionComponent = ({ - value, - namespaces, - onChange, - onRefresh, -}) => { - const [isRefreshing, setIsRefreshing] = useState(false); - const inputRef = useRef(null); - - // Ensure all namespaces are included, plus the current value if it's not in the list - const allNamespaces = useMemo( - () => - [ - ...new Set([ - ...namespaces, - ...(value && !namespaces.includes(value) ? [value] : []), - ]), - ].sort((a, b) => { - if (!a) return 1; - if (!b) return -1; - return a.localeCompare(b); - }), - [namespaces, value], - ); - - const handleRefresh = async (e: MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - if (!onRefresh || isRefreshing) return; - - // Store if input was focused before refresh - const wasFocused = document.activeElement === inputRef.current; - - setIsRefreshing(true); - try { - await onRefresh(); - // Reopen the dropdown if it was open before - if (wasFocused && inputRef.current) { - setTimeout(() => { - inputRef.current?.focus(); - // Small delay to ensure the options are updated - setTimeout(() => { - inputRef.current?.focus(); - }, 50); - }, 100); - } - } finally { - setIsRefreshing(false); - } - }; - - return ( -
-
- v || ""} - dataCy="general_namespace_select_input" - singleSelect={true} - inputRef={inputRef} - /> - {onRefresh && ( - - - - )} -
-
- ); -}; diff --git a/src/ui/components/NodeList/NodeList.css b/src/ui/components/NodeList/NodeList.css deleted file mode 100644 index 756067e4..00000000 --- a/src/ui/components/NodeList/NodeList.css +++ /dev/null @@ -1,7 +0,0 @@ -.container { - display: grid; - margin: 0px 0px; - padding: 8px 8px; - overflow: auto; - align-content: start; -} diff --git a/src/ui/components/NodeList/NodeList.css.d.ts b/src/ui/components/NodeList/NodeList.css.d.ts deleted file mode 100644 index 81abe238..00000000 --- a/src/ui/components/NodeList/NodeList.css.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare const styles: { - readonly "container": string; -}; -export = styles; - diff --git a/src/ui/components/NodeList/NodeList.tsx b/src/ui/components/NodeList/NodeList.tsx deleted file mode 100644 index 67bbcbe9..00000000 --- a/src/ui/components/NodeList/NodeList.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { ComponentChildren, Fragment, h } from "preact"; -import { useEffect, useRef, useState } from "preact/hooks"; - -import styles from "./NodeList.css"; -import { NodeRow } from "./NodeRow"; - -const containerStyle = { - overflow: "auto" as const, - position: "relative" as const, -}; - -const itemStyle = { width: "100%" }; - -type Props = { - items: T[]; - actionCallback?: (item: T) => ComponentChildren; - keyComponent?: (item: T) => ComponentChildren; - nsComponent?: (item: T) => ComponentChildren; - compact?: boolean; - onClick?: (item: T) => void; - row?: (node: T) => ComponentChildren; - virtualized?: boolean; - rowHeight?: number; - minVisibleRows?: number; -}; - -const DEFAULT_ROW_HEIGHT = 60; -const MIN_VISIBLE_ROWS = 8; - -export function NodeList({ - items, - actionCallback, - keyComponent, - nsComponent, - compact, - onClick, - row, - rowHeight = DEFAULT_ROW_HEIGHT, - minVisibleRows = MIN_VISIBLE_ROWS, -}: Props) { - const containerRef = useRef(null); - const [scrollTop, setScrollTop] = useState(0); - const [containerHeight, setContainerHeight] = useState(400); - - // Calculate container height based on available space - useEffect(() => { - if (containerRef.current) { - const rect = containerRef.current.getBoundingClientRect(); - const availableHeight = window.innerHeight - rect.top - 20; // 20px padding - const calculatedHeight = Math.max( - minVisibleRows * rowHeight, - Math.min(availableHeight, items.length * rowHeight), - ); - setContainerHeight(calculatedHeight); - } - }, [items.length, rowHeight, minVisibleRows]); - - // Handle scroll for virtualization - const handleScroll = (e: Event) => { - const target = e.target as HTMLDivElement; - setScrollTop(target.scrollTop); - }; - - useEffect(() => { - const container = containerRef.current; - if (container) { - container.addEventListener("scroll", handleScroll, { passive: true }); - return () => container.removeEventListener("scroll", handleScroll); - } - }, []); - - // Render a single item - const renderItem = (item: T) => { - if (row) { - return {row(item)}; - } - - return ( - onClick?.(item) : undefined} - /> - ); - }; - - // If we have few items, render normally - if (items.length <= minVisibleRows) { - return ( -
- {items?.map(renderItem)} -
- ); - } - - // Virtualized rendering - const overscan = 3; - const visibleStart = Math.floor(scrollTop / rowHeight); - const visibleEnd = Math.min( - items.length - 1, - Math.ceil((scrollTop + containerHeight) / rowHeight), - ); - - const startIndex = Math.max(0, visibleStart - overscan); - const endIndex = Math.min(items.length - 1, visibleEnd + overscan); - const visibleItems = items.slice(startIndex, endIndex + 1); - - const totalHeight = items.length * rowHeight; - const offsetY = startIndex * rowHeight; - - const spacerStyle = { - height: totalHeight, - position: "relative" as const, - }; - const translateStyle = { - transform: `translateY(${offsetY}px)`, - position: "absolute" as const, - top: 0, - left: 0, - right: 0, - }; - - return ( -
-
-
- {visibleItems.map((item) => ( -
- {renderItem(item)} -
- ))} -
-
-
- ); -} diff --git a/src/ui/components/NodeList/NodeRow.css b/src/ui/components/NodeList/NodeRow.css deleted file mode 100644 index de44aa47..00000000 --- a/src/ui/components/NodeList/NodeRow.css +++ /dev/null @@ -1,49 +0,0 @@ -.container { - display: grid; - grid-template-columns: 1fr 1fr auto; - gap: 2px 16px; - padding: 6px 8px; -} - -.container > * { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; -} - -.container:hover { - background-color: var(--figma-color-bg-hover); -} - -.text { - grid-column: 1 / span 2; - min-height: 16px; - font-weight: bold; - cursor: unset; - display: flex; - gap: 8px; - align-items: center; -} - -.action { - grid-column: 3; - grid-row: 1 / span 2; - align-self: center; -} - -.key { - cursor: unset; - grid-column: 1 / span 2; -} - -.ns { - cursor: unset; - overflow: visible; - text-overflow: unset; - margin-top: 4px; - grid-column: 1 / span 2; -} - -.disabled { - color: var(--figma-color-text-disabled); -} diff --git a/src/ui/components/NodeList/NodeRow.css.d.ts b/src/ui/components/NodeList/NodeRow.css.d.ts deleted file mode 100644 index eee1ece3..00000000 --- a/src/ui/components/NodeList/NodeRow.css.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare const styles: { - readonly "action": string; - readonly "container": string; - readonly "disabled": string; - readonly "key": string; - readonly "ns": string; - readonly "text": string; -}; -export = styles; - diff --git a/src/ui/components/NodeList/NodeRow.tsx b/src/ui/components/NodeList/NodeRow.tsx deleted file mode 100644 index 68bd8ce0..00000000 --- a/src/ui/components/NodeList/NodeRow.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { ComponentChildren, h } from "preact"; -import { memo } from "preact/compat"; - -import { PartialNodeInfo } from "@/types"; -import styles from "./NodeRow.css"; -import { Badge } from "../Badge/Badge"; -import { useInterpolatedTranslation } from "@/ui/hooks/useInterpolatedTranslation"; -import { InfoTooltip } from "../InfoTooltip/InfoTooltip"; -import { HtmlText } from "../Shared/HtmlText"; - -type Props = { - node: PartialNodeInfo; - action?: ComponentChildren; - keyComponent?: ComponentChildren; - nsComponent?: ComponentChildren; - showNamespace?: boolean; - compact?: boolean; - onClick?: () => void; -}; - -export const NodeRow = memo( - ({ - node, - action, - keyComponent, - nsComponent, - showNamespace = true, - compact, - onClick, - }: Props) => { - const showText = node.characters || !compact || action; - - const { translationDiffersFromNode } = useInterpolatedTranslation(node); - - const infoString = - "Manual changes have been detected.\nGo to details to resolve conflict."; - - return ( -
- {showText && ( -
- - - {translationDiffersFromNode && ( - - {infoString} - - )} - {node.isPlural && plural} - {Object.keys(node.paramsValues ?? {}).length > 0 && ( - parameters - )} -
- )} -
- {action} -
-
- {keyComponent ? keyComponent : node.key} -
-
- {nsComponent - ? nsComponent - : showNamespace && - node.ns && ( - - Namespace: {node.ns} - - )} -
-
- ); - }, -); diff --git a/src/ui/components/Popover/Popover.css b/src/ui/components/Popover/Popover.css deleted file mode 100644 index 875b493c..00000000 --- a/src/ui/components/Popover/Popover.css +++ /dev/null @@ -1,45 +0,0 @@ -.popover { - position: absolute; - width: max-content; - z-index: 9999; - top: 0; - left: 0; - box-shadow: 0px 2px 8px 0px #00000040; -} - -.popoverHidden { - position: absolute; - width: max-content; - top: 0; - left: 0; - display: none; -} - -.popoverItem { - display: flex; - font-weight: 500; - width: 100%; - gap: 0.5rem; - justify-content: start; - padding: 0.5rem 0.75rem; - background-color: var(--figma-color-bg); - white-space: pre; -} - -.popoverItemAction { - cursor: pointer; -} - -.popoverItemAction:hover { - background-color: var(--figma-color-bg-hover); -} - -.popoverBackdrop { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: red; - z-index: 0; -} diff --git a/src/ui/components/Popover/Popover.css.d.ts b/src/ui/components/Popover/Popover.css.d.ts deleted file mode 100644 index 0bcdb63e..00000000 --- a/src/ui/components/Popover/Popover.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare const styles: { - readonly "popover": string; - readonly "popoverBackdrop": string; - readonly "popoverHidden": string; - readonly "popoverItem": string; - readonly "popoverItemAction": string; -}; -export = styles; - diff --git a/src/ui/components/Popover/Popover.tsx b/src/ui/components/Popover/Popover.tsx deleted file mode 100644 index 34307a73..00000000 --- a/src/ui/components/Popover/Popover.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { Fragment, h, JSX } from "preact"; -import { useCallback, useEffect, useRef, useState } from "preact/hooks"; -import styles from "./Popover.css"; -import { computePosition, offset, autoPlacement } from "@floating-ui/dom"; -import { HtmlText } from "../Shared/HtmlText"; - -export interface ActionItem { - label: string; - icon: JSX.Element; - onClick: () => void; - cy?: string; -} - -interface DropdownProps { - popoverTrigger: preact.RefObject; - items?: ActionItem[]; - text?: unknown; - clampWidth?: boolean; - onClose?: () => void; -} - -const Dropdown = ({ - popoverTrigger, - clampWidth, - items, - text, - onClose, -}: DropdownProps) => { - const dropdownRef = useRef(null); - const [displayPopover, setDisplayPopover] = useState(false); - - const computePositionAndSet = useCallback(() => { - if (!popoverTrigger.current || !dropdownRef.current) { - return; - } - computePosition(popoverTrigger.current, dropdownRef.current, { - placement: "bottom-start", - middleware: [offset(10), autoPlacement()], - }).then(({ x, y }) => { - Object.assign(dropdownRef.current!.style, { - left: `${x}px`, - top: `${y}px`, - maxWidth: clampWidth ? `calc(100% - ${x + 8}px)` : null, - }); - }); - }, [popoverTrigger, clampWidth]); - - const open = useCallback(() => { - if (!popoverTrigger.current || !dropdownRef.current) { - return; - } - setDisplayPopover(true); - computePositionAndSet(); - }, [popoverTrigger, computePositionAndSet]); - - const close = useCallback(() => { - setTimeout(() => { - setDisplayPopover(false); - }); - onClose?.(); - }, [onClose]); - - useEffect(() => { - if (!popoverTrigger.current || !dropdownRef.current) { - return; - } - - popoverTrigger.current.addEventListener("click", open); - window.addEventListener("resize", computePositionAndSet); - - return () => { - popoverTrigger.current?.removeEventListener("click", open); - window.removeEventListener("resize", computePositionAndSet); - }; - }, [popoverTrigger, open, computePositionAndSet]); - - // Close dropdown if clicked outside the dropdown and anchor element. - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) && - popoverTrigger.current && - !popoverTrigger.current.contains(event.target as Node) - ) { - close(); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [popoverTrigger, close]); - - return ( -
- {text && typeof text === "string" && ( - - )} - {text && typeof text !== "string" && ( -
{text}
- )} - {items?.map((item, index) => ( - - ))} - {items == null && !text && } -
- ); -}; - -export default Dropdown; diff --git a/src/ui/components/ResizeHandle/ResizeHandle.css b/src/ui/components/ResizeHandle/ResizeHandle.css deleted file mode 100644 index 31703c2d..00000000 --- a/src/ui/components/ResizeHandle/ResizeHandle.css +++ /dev/null @@ -1,30 +0,0 @@ -.resizeHandle { - position: fixed; - bottom: 0; - right: 0; - width: 20px; - height: 20px; - cursor: se-resize; - z-index: 1; - border-radius: 16px 0 0 0; - transition: all 0.2s ease; - opacity: 0.3; -} - -.resizeHandle:hover { - opacity: 1; - background-color: #7c848f; - color: white; -} - -.resizeHandle:active { - background-color: #7c848f; - color: white; - opacity: 1; -} - -.resizeHandle:focus { - outline: 2px solid #7c848f; - outline-offset: 2px; - opacity: 1; -} diff --git a/src/ui/components/ResizeHandle/ResizeHandle.css.d.ts b/src/ui/components/ResizeHandle/ResizeHandle.css.d.ts deleted file mode 100644 index bec2c859..00000000 --- a/src/ui/components/ResizeHandle/ResizeHandle.css.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare const styles: { - readonly "resizeHandle": string; -}; -export = styles; - diff --git a/src/ui/components/ResizeHandle/ResizeHandle.tsx b/src/ui/components/ResizeHandle/ResizeHandle.tsx deleted file mode 100644 index db12ea91..00000000 --- a/src/ui/components/ResizeHandle/ResizeHandle.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { h } from "preact"; -import { useCallback, useEffect, useRef, useState } from "preact/hooks"; -import { emit } from "@/utilities"; -import { ResizeHandler, WindowSize } from "@/types"; -import { DEFAULT_SIZE, MINIMUM_SIZE } from "@/ui/state/sizes"; -import { useGlobalActions, useGlobalState } from "@/ui/state/GlobalState"; -import { ResizeHandleIcon } from "../../icons/SvgIcons"; -import classes from "./ResizeHandle.css"; - -const MIN_WIDTH = MINIMUM_SIZE.width; -const MIN_HEIGHT = MINIMUM_SIZE.height; -const MAX_WIDTH = 800; -const MAX_HEIGHT = 1000; - -export const ResizeHandle = () => { - const [isResizing, setIsResizing] = useState(false); - const [startSize, setStartSize] = useState(null); - const [startPos, setStartPos] = useState<{ x: number; y: number } | null>( - null, - ); - const handleRef = useRef(null); - const { setSizeStack } = useGlobalActions(); - const sizeStack = useGlobalState((c) => c.sizeStack); - - const handleMouseDown = (e: MouseEvent) => { - e.preventDefault(); - setIsResizing(true); - setStartPos({ x: e.clientX, y: e.clientY }); - const currentSize = sizeStack[sizeStack.length - 1] || DEFAULT_SIZE; - setStartSize(currentSize); - }; - - const handleMouseMove = useCallback( - (e: MouseEvent) => { - if (!isResizing || !startPos || !startSize) return; - - const deltaX = e.clientX - startPos.x; - const deltaY = e.clientY - startPos.y; - - const newWidth = Math.max( - MIN_WIDTH, - Math.min(MAX_WIDTH, startSize.width + deltaX), - ); - const newHeight = Math.max( - MIN_HEIGHT, - Math.min(MAX_HEIGHT, startSize.height + deltaY), - ); - - const newSize: WindowSize = { width: newWidth, height: newHeight }; - - emit("RESIZE", newSize); - - // Update the global size stack to remember this size - setSizeStack((stack) => { - const newStack = [...stack]; - if (newStack.length > 0) { - newStack[newStack.length - 1] = newSize; - } else { - newStack.push(newSize); - } - return newStack; - }); - }, - [isResizing, startPos, startSize, setSizeStack], - ); - - const handleMouseUp = useCallback(() => { - setIsResizing(false); - setStartPos(null); - setStartSize(null); - }, []); - - useEffect(() => { - if (isResizing) { - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - - return () => { - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - }; - } - }, [isResizing, handleMouseMove, handleMouseUp]); - return ( -
{ - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - // Focus the handle to show it's interactive - handleRef.current?.focus(); - return; - } - - // Handle arrow key resizing - const currentSize = sizeStack[sizeStack.length - 1] || DEFAULT_SIZE; - const stepSize = e.shiftKey ? 20 : 10; // Larger step with Shift key - let newSize: WindowSize | null = null; - - switch (e.key) { - case "ArrowRight": - e.preventDefault(); - newSize = { - width: Math.min(MAX_WIDTH, currentSize.width + stepSize), - height: currentSize.height, - }; - break; - case "ArrowLeft": - e.preventDefault(); - newSize = { - width: Math.max(MIN_WIDTH, currentSize.width - stepSize), - height: currentSize.height, - }; - break; - case "ArrowDown": - e.preventDefault(); - newSize = { - width: currentSize.width, - height: Math.min(MAX_HEIGHT, currentSize.height + stepSize), - }; - break; - case "ArrowUp": - e.preventDefault(); - newSize = { - width: currentSize.width, - height: Math.max(MIN_HEIGHT, currentSize.height - stepSize), - }; - break; - } - - if (newSize) { - // Emit resize event - emit("RESIZE", newSize); - - // Update the global size stack - setSizeStack((stack) => { - const newStack = [...stack]; - if (newStack.length > 0) { - newStack[newStack.length - 1] = newSize!; - } else { - newStack.push(newSize!); - } - return newStack; - }); - } - }} - > - -
- ); -}; diff --git a/src/ui/components/Shared/HtmlText.tsx b/src/ui/components/Shared/HtmlText.tsx deleted file mode 100644 index b5ef305f..00000000 --- a/src/ui/components/Shared/HtmlText.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { h } from "preact"; -import DOMPurify from "dompurify"; -import { CSSProperties } from "preact/compat"; - -interface HtmlTextProps { - text?: string; - style: CSSProperties | string | undefined; - dataCy?: string; -} - -export const HtmlText = (props: HtmlTextProps) => { - return ( - - ); -}; diff --git a/src/ui/components/TopBar/TopBar.css b/src/ui/components/TopBar/TopBar.css deleted file mode 100644 index e0619e9a..00000000 --- a/src/ui/components/TopBar/TopBar.css +++ /dev/null @@ -1,30 +0,0 @@ -.container { - display: flex; - justify-content: space-between; - min-height: 30px; -} - -.tabsContainerLeft { - display: grid; - grid-auto-flow: column; - justify-content: start; - align-items: center; - gap: 8px; -} - -.tabsContainerRight { - display: grid; - grid-auto-flow: column; - justify-content: end; - align-items: center; - gap: 8px; -} - -.tabsBackButton { - padding: 0px 0px 0px 10px; - padding-right: 0px; - cursor: pointer; - position: relative; - left: -5px; - margin-right: -10px; -} diff --git a/src/ui/components/TopBar/TopBar.css.d.ts b/src/ui/components/TopBar/TopBar.css.d.ts deleted file mode 100644 index 5f9ee7d9..00000000 --- a/src/ui/components/TopBar/TopBar.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare const styles: { - readonly "container": string; - readonly "tabsBackButton": string; - readonly "tabsContainerLeft": string; - readonly "tabsContainerRight": string; -}; -export = styles; - diff --git a/src/ui/components/TopBar/TopBar.tsx b/src/ui/components/TopBar/TopBar.tsx deleted file mode 100644 index 5a6d29e0..00000000 --- a/src/ui/components/TopBar/TopBar.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { IconChevronLeft32 } from "@create-figma-plugin/ui"; -import { h, JSX } from "preact"; - -import styles from "./TopBar.css"; - -type Props = { - onBack?: () => void; - leftPart?: JSX.Element; - rightPart?: JSX.Element; -}; - -export const TopBar = ({ leftPart, rightPart, onBack }: Props) => { - return ( -
-
- {onBack && ( -
- -
- )} - {leftPart} -
-
{rightPart}
-
- ); -}; diff --git a/src/ui/hooks/useAllTags.ts b/src/ui/hooks/useAllTags.ts deleted file mode 100644 index 9ffd8991..00000000 --- a/src/ui/hooks/useAllTags.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { useState } from "preact/hooks"; -import { useApiMutation } from "../client/useQueryApi"; -import { useFigmaNotify } from "./useFigmaNotify"; - -interface Tag { - id: number; - name: string; -} - -export const useAllTags = () => { - const tagsLoadable = useApiMutation({ - url: "/v2/projects/tags", - method: "get", - }); - const notifier = useFigmaNotify(); - const [tagsData, setTagsData] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - async function loadTags({ - search = "", - sort = [], - }: { - search?: string; - sort?: string[]; - } = {}) { - let page = 0; - const size = 1000; - let hasMore = true; - let allTags: Tag[] = []; - - while (hasMore) { - if (page > 100) { - console.warn("Too many pages, stopping pagination"); - break; - } - try { - const response = await tagsLoadable.mutateAsync({ - query: { - search, - page, - size, - sort, - }, - }); - allTags = allTags.concat(response._embedded?.tags ?? []); - const totalElements = response.page?.totalElements ?? 0; - hasMore = allTags.length < totalElements; - page++; - } catch (e) { - setError(e); - notifier.mutate("Error loading tags"); - throw e; - } - } - setTagsData(allTags); - return allTags; - } - - return { - async getData(params?: { search?: string; sort?: string[] }) { - setIsLoading(true); - setError(null); - try { - return await loadTags(params); - } finally { - setIsLoading(false); - } - }, - tagsData, - isLoading, - error, - }; -}; diff --git a/src/ui/hooks/useAllTranslations.ts b/src/ui/hooks/useAllTranslations.ts deleted file mode 100644 index c12ed9a0..00000000 --- a/src/ui/hooks/useAllTranslations.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { useState, useCallback, useRef, useEffect } from "preact/hooks"; -import { useApiMutation } from "../client/useQueryApi"; -import { components } from "../client/apiSchema.generated"; -import { TranslationData } from "../client/types"; -import { useFigmaNotify } from "./useFigmaNotify"; - -type Props = { - language: string; - namespaces?: string[]; - branch?: string; -}; - -export const useAllTranslations = () => { - const namespacesLoadable = useApiMutation({ - url: "/v2/projects/used-namespaces", - method: "get", - }); - const translationsBaseLoadable = useApiMutation({ - url: "/v2/projects/translations", - method: "get", - }); - - const notifier = useFigmaNotify(); - - const [translationsData, setTranslationsData] = - useState(null); - - const [isLoading, setIsLoading] = useState(false); - - // Use refs to store the latest mutation functions to avoid dependency issues - const namespacesLoadableRef = useRef(namespacesLoadable); - const translationsBaseLoadableRef = useRef(translationsBaseLoadable); - const notifierRef = useRef(notifier); - - useEffect(() => { - namespacesLoadableRef.current = namespacesLoadable; - translationsBaseLoadableRef.current = translationsBaseLoadable; - notifierRef.current = notifier; - }, [namespacesLoadable, translationsBaseLoadable, notifier]); - - async function loadData({ language, namespaces, branch }: Props) { - const nsNames = - namespaces ?? - ( - await namespacesLoadableRef.current.mutateAsync({ - query: { branch: branch || undefined }, - }) - )._embedded?.namespaces?.map((n) => n.name ?? "") ?? - []; - - // Ensure empty string is included if namespaces is provided and contains empty string - // This handles the default namespace case - if (namespaces && namespaces.includes("") && !nsNames.includes("")) { - nsNames.push(""); - } - - const data: TranslationData = {}; - - for (const ns of nsNames) { - let cursor: string | undefined; - let hasMore = true; - - const translationsData: components["schemas"]["KeyWithTranslationsModel"][] = - []; - - do { - const batchOfTranslations = - await translationsBaseLoadableRef.current.mutateAsync({ - query: { - filterNamespace: [ns], - languages: [language], - size: 1000, - cursor, - branch: branch || undefined, - }, - }); - - translationsData.push(...(batchOfTranslations._embedded?.keys ?? [])); - cursor = batchOfTranslations.nextCursor; - - hasMore = - translationsData.length < - (batchOfTranslations.page?.totalElements ?? 0); - } while (hasMore); - - data[ns] = translationsData.reduce( - (acc, key) => { - acc[key.keyName] = { - ...key, - translation: key.translations[language]?.text ?? "", - }; - return acc; - }, - {} as Record, - ); - } - setTranslationsData(data); - return data; - } - - const getData = useCallback(async (props: Props) => { - setIsLoading(true); - try { - return await loadData(props); - } catch (e) { - if (e === "invalid_project_api_key") { - notifierRef.current.mutate("Invalid project API key"); - } - - throw e; - } finally { - setIsLoading(false); - } - }, []); // Empty deps - we use refs for the mutation objects - - const clearCache = useCallback(() => { - setTranslationsData(null); - }, []); - - const error = namespacesLoadable.error || translationsBaseLoadable.error; - - return { - getData, - clearCache, - translationsData, - isLoading, - error, - }; -}; diff --git a/src/ui/hooks/useConnectedMutation.ts b/src/ui/hooks/useConnectedMutation.ts deleted file mode 100644 index 35052b56..00000000 --- a/src/ui/hooks/useConnectedMutation.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - ConnectedNodesProps, - getConnectedNodesEndpoint, -} from "@/main/endpoints/getConnectedNodes"; -import { delayed } from "@/main/utils/delayed"; -import { useMutation } from "react-query"; - -export const useConnectedMutation = (props: ConnectedNodesProps) => { - const result = useMutation( - [getConnectedNodesEndpoint.name], - delayed(() => getConnectedNodesEndpoint.call(props)), - ); - return { ...result }; -}; diff --git a/src/ui/hooks/useConnectedNodes.ts b/src/ui/hooks/useConnectedNodes.ts deleted file mode 100644 index e3b885c9..00000000 --- a/src/ui/hooks/useConnectedNodes.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { - ConnectedNodesProps, - getConnectedNodesEndpoint, -} from "@/main/endpoints/getConnectedNodes"; -import { delayed } from "@/main/utils/delayed"; -import { useQuery } from "react-query"; - -export const useConnectedNodes = (props: ConnectedNodesProps) => { - const queryKey = [ - getConnectedNodesEndpoint.name, - props.ignoreSelection, - ] as const; - return useQuery( - queryKey, - delayed(() => getConnectedNodesEndpoint.call(props)), - { - select: (data) => ({ ...data, items: data.items.filter((n) => n.key) }), - // Keeps data "fresh" long enough that an optimistic cache patch written - // by useSetNodesDataMutation.onSuccess prevents an unnecessary full-page - // rescan when returning to Index right after a connect/disconnect. - staleTime: 30_000, - }, - ); -}; diff --git a/src/ui/hooks/useCopyPage.ts b/src/ui/hooks/useCopyPage.ts deleted file mode 100644 index 6416d1cf..00000000 --- a/src/ui/hooks/useCopyPage.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { copyPageEndpoint } from "@/main/endpoints/copyPage"; -import { delayed } from "@/main/utils/delayed"; -import { NodeInfo } from "@/types"; -import { useMutation } from "react-query"; - -type Props = { - language: string; - nodes: NodeInfo[]; -}; - -export const useCopyPage = () => { - return useMutation( - [copyPageEndpoint.name], - delayed((data: Props | undefined) => copyPageEndpoint.call(data)), - ); -}; diff --git a/src/ui/hooks/useEditorMode.ts b/src/ui/hooks/useEditorMode.ts deleted file mode 100644 index 56b8699e..00000000 --- a/src/ui/hooks/useEditorMode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useQuery } from "react-query"; -import { editorTypeEndpoint } from "../../main/endpoints/editorType"; - -export const useEditorMode = () => { - return useQuery([editorTypeEndpoint.name], () => - editorTypeEndpoint.call(null), - ); -}; diff --git a/src/ui/hooks/useFigmaNotify.ts b/src/ui/hooks/useFigmaNotify.ts deleted file mode 100644 index a3865fcd..00000000 --- a/src/ui/hooks/useFigmaNotify.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useMutation } from "react-query"; -import { notifyEndpoint } from "../../main/endpoints/notify"; -import { delayed } from "../../main/utils/delayed"; - -export const useFigmaNotify = () => { - const result = useMutation( - [notifyEndpoint.name], - delayed((props: string) => notifyEndpoint.call(props)), - ); - - return { ...result }; -}; diff --git a/src/ui/hooks/useFormatText.ts b/src/ui/hooks/useFormatText.ts deleted file mode 100644 index 76ad5872..00000000 --- a/src/ui/hooks/useFormatText.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useMutation } from "react-query"; -import { - formatTextEndpoint, - FormatTextEndpointArgs, -} from "../../main/endpoints/formatText"; -import { delayed } from "../../main/utils/delayed"; - -/** - * Takes the arguments from a node to format the textnode accodingly. Replacing some html tags like - * bold, em, u, or br with the corresponding figma text styles. - * It also replaces the text in the node with the interpolated string. - */ -export const useFormatText = () => { - const result = useMutation( - [formatTextEndpoint.name], - delayed((props: FormatTextEndpointArgs) => formatTextEndpoint.call(props)), - ); - return { ...result }; -}; diff --git a/src/ui/hooks/useHasBranchingEnabled.ts b/src/ui/hooks/useHasBranchingEnabled.ts deleted file mode 100644 index 4b2d20de..00000000 --- a/src/ui/hooks/useHasBranchingEnabled.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useApiQuery } from "../client/useQueryApi"; - -export const useHasBranchingEnabled = () => { - const apiKeyInfo = useApiQuery({ - url: "/v2/api-keys/current", - method: "get", - options: { - cacheTime: 60000, - staleTime: 60000, - }, - }); - - const projectQuery = useApiQuery({ - url: "/v2/projects/{projectId}", - method: "get", - path: { - projectId: apiKeyInfo.data?.projectId ?? 0, - }, - options: { - cacheTime: 60000, - staleTime: 60000, - enabled: apiKeyInfo.data?.projectId !== undefined, - }, - }); - - return projectQuery.data?.useBranching ?? false; -}; diff --git a/src/ui/hooks/useHasNamespacesEnabled.ts b/src/ui/hooks/useHasNamespacesEnabled.ts deleted file mode 100644 index 4ea7a7f6..00000000 --- a/src/ui/hooks/useHasNamespacesEnabled.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useApiQuery } from "../client/useQueryApi"; - -export const useHasNamespacesEnabled = () => { - // First, get the project ID from the API key info (relevant for legacy api keys) - const apiKeyInfo = useApiQuery({ - url: "/v2/api-keys/current", - method: "get", - options: { - cacheTime: 60000, - staleTime: 60000, - }, - }); - - // Then get the project info if we have a project ID - const projectQuery = useApiQuery({ - url: "/v2/projects/{projectId}", - method: "get", - path: { - projectId: apiKeyInfo.data?.projectId ?? 0, - }, - options: { - cacheTime: 60000, - staleTime: 60000, - enabled: apiKeyInfo.data?.projectId !== undefined, - }, - }); - - return projectQuery.data?.useNamespaces ?? false; -}; diff --git a/src/ui/hooks/useHighlightNodeMutation.ts b/src/ui/hooks/useHighlightNodeMutation.ts deleted file mode 100644 index 675fa4b9..00000000 --- a/src/ui/hooks/useHighlightNodeMutation.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { - HighlightNodeProps, - highlightNodeEndpoint, -} from "@/main/endpoints/highlightNode"; -import { useMutation } from "react-query"; - -export const useHighlightNodeMutation = () => { - return useMutation( - [highlightNodeEndpoint.name], - (data: HighlightNodeProps) => highlightNodeEndpoint.call(data), - ); -}; diff --git a/src/ui/hooks/useInterpolatedTranslation.ts b/src/ui/hooks/useInterpolatedTranslation.ts deleted file mode 100644 index 7a02a972..00000000 --- a/src/ui/hooks/useInterpolatedTranslation.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from "preact/hooks"; - -import { - getPlaceholders, - getTolgeeFormat, - Placeholder, - selectPluralRule, - TolgeeFormat, -} from "@tginternal/editor"; -import { createFormatIcu } from "../../createFormatIcu"; -import { useGlobalState } from "../state/GlobalState"; -import { PartialNodeInfo } from "../../types"; - -export const useInterpolatedTranslation = (node?: PartialNodeInfo) => { - const config = useGlobalState((c) => c.config); - const nodeCharacters = node?.characters ?? ""; - const isSimpleNode = - node && - !node.isPlural && - Object.keys(node.paramsValues ?? {}).length === 0 && - !/<[^>]*>/g.test(node.translation ?? ""); - const rawTranslation = isSimpleNode - ? ((node?.characters || node?.translation) ?? "") - : ((node?.translation || node?.characters) ?? ""); - const paramsValues = node?.paramsValues ?? {}; - - const selectedPluralVariant = useMemo(() => { - if (!node?.isPlural) { - return "other"; - } - return selectPluralRule( - config?.language ?? "en", - parseInt(node?.pluralParamValue || "1", 10), - ); - }, [node, node?.pluralParamValue, config?.language]); - - const interpolatedTranslation = useRef(nodeCharacters); - const [previewText, setPreviewText] = useState(""); - const [previewTextIsError, setPreviewTextIsError] = useState(false); - /** Array of all placeholders within the current tolgeeValue plural variant */ - const [placeholders, setPlaceholders] = useState([]); - const [tolgeeValue, setTolgeeValue] = useState({ - variants: {}, - }); - - const getOrUpdateInterpolatedTranslation = ( - args: { - paramsValues: Record; - pluralParamValue?: string; - currentTranslation: string; - }, - updateState = false, - ): string | null => { - const { paramsValues, currentTranslation } = args; - const tolgeeValue = getTolgeeFormat( - currentTranslation, - node?.isPlural ?? false, - false, - ); - - if (updateState) { - setTolgeeValue(tolgeeValue); - } - - const newPlaceholders = - getPlaceholders( - selectedPluralVariant && - tolgeeValue.variants[selectedPluralVariant] != null - ? tolgeeValue.variants[selectedPluralVariant]! - : tolgeeValue.variants["other"] || - Object.values(tolgeeValue.variants)[0] || - "", - )?.filter((p) => p.type === "variable") ?? []; - - if ( - (newPlaceholders.some( - (p) => placeholders.find((p2) => p2.name === p.name) == null, - ) || - placeholders.some( - (p) => newPlaceholders.find((p2) => p2.name === p.name) == null, - )) && - updateState - ) { - setPlaceholders(newPlaceholders); - } - - const formatter = createFormatIcu(); - - const newParamValues: typeof paramsValues = {}; - // remove paramsValues that are not in the current placeholders - Object.keys(paramsValues).forEach((key) => { - if (newPlaceholders.find((p) => p.name === key)) { - newParamValues[key] = paramsValues[key]; - } - }); - - try { - const pluralParam: Record = {}; - if (tolgeeValue.parameter) { - pluralParam[tolgeeValue.parameter] = - args.pluralParamValue ?? (node?.pluralParamValue || "1"); - } - const newInterpolatedTranslation: string = formatter.format({ - language: config?.language ?? "en", - translation: currentTranslation, - params: { - ...newParamValues, - ...pluralParam, - }, - }); - - if (updateState) { - setPreviewText(newInterpolatedTranslation); - setPreviewTextIsError(false); - } - if (updateState) { - interpolatedTranslation.current = newInterpolatedTranslation; - } - return newInterpolatedTranslation; - } catch (e) { - // Add an error message as preview text if the formatting fails - if ("message" in (e as any)) { - setPreviewText((e as any).message); - setPreviewTextIsError(true); - } else { - console.error(e); - } - } - return null; - }; - - const getInterpolatedTranslation = (args: { - paramsValues: Record; - pluralParamValue?: string; - currentTranslation: string; - }) => getOrUpdateInterpolatedTranslation(args, false); - - const updateInterpolatedTranslation = (args: { - paramsValues: Record; - pluralParamValue?: string; - currentTranslation: string; - }) => { - getOrUpdateInterpolatedTranslation(args, true); - }; - const newTranslation = rawTranslation.replace(/\u2028|\u8233/g, "\n\n"); - - const previousCharacters = nodeCharacters.replace(/\u2028|\u8233/g, "\n\n"); - - useEffect(() => { - const newCharacters = getOrUpdateInterpolatedTranslation({ - paramsValues: paramsValues || {}, - currentTranslation: newTranslation, - })?.replace(/<[^>]*>/g, ""); - - if (newCharacters != null && newCharacters !== previousCharacters) { - interpolatedTranslation.current = newCharacters; - } - }, [rawTranslation, node?.isPlural]); - - const translationDiffersFromNode = useMemo(() => { - const res = - !isSimpleNode && - nodeCharacters !== - interpolatedTranslation.current?.replace(/<[^>]*>/g, ""); - return res; - }, [ - interpolatedTranslation.current, - newTranslation, - nodeCharacters, - paramsValues, - selectedPluralVariant, - node, - ]); - - return { - placeholders, - interpolatedTranslation, - previewText, - previewTextIsError, - translationDiffersFromNode, - tolgeeValue, - getInterpolatedTranslation, - updateInterpolatedTranslation, - }; -}; diff --git a/src/ui/hooks/usePrefilledKey.ts b/src/ui/hooks/usePrefilledKey.ts deleted file mode 100644 index 1e5d6f87..00000000 --- a/src/ui/hooks/usePrefilledKey.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useQuery } from "react-query"; -import { preformatKeyEndpoint } from "@/main/endpoints/preformatKey"; -import { TolgeeConfig } from "@/types"; - -/** - * Hook that calculates the prefilled key using the key format. - */ -export function usePrefilledKey( - nodeId: string, - keyFormat: string, - variableCasing: TolgeeConfig["variableCasing"], - enabled: boolean = true, - nodeKey?: string, -) { - const result = useQuery( - [preformatKeyEndpoint.name, nodeId, keyFormat], - async () => { - if (!nodeId || !keyFormat) return ""; - return preformatKeyEndpoint.call({ keyFormat, nodeId, variableCasing }); - }, - { - enabled: enabled && !!nodeId && !!keyFormat && !nodeKey, - structuralSharing: false, - }, - ); - - return { key: result.data, isLoading: result.isLoading }; -} diff --git a/src/ui/hooks/useSelectedNodes.ts b/src/ui/hooks/useSelectedNodes.ts deleted file mode 100644 index 8a1c2bd8..00000000 --- a/src/ui/hooks/useSelectedNodes.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { getSelectedNodesEndpoint } from "@/main/endpoints/getSelectedNodes"; -import { delayed } from "@/main/utils/delayed"; -import { DocumentChangeHandler, SelectionChangeHandler } from "@/types"; -import { on } from "@create-figma-plugin/utilities"; -import { useCallback, useEffect, useRef } from "preact/hooks"; -import { useQuery } from "react-query"; - -export const useSelectedNodes = () => { - const result = useQuery( - [getSelectedNodesEndpoint.name], - delayed(() => getSelectedNodesEndpoint.call()), - { keepPreviousData: true }, - ); - - const refetchTimerRef = useRef>(); - const debouncedRefetch = useCallback(() => { - if (refetchTimerRef.current) clearTimeout(refetchTimerRef.current); - refetchTimerRef.current = setTimeout(() => result.refetch(), 50); - }, [result.refetch]); - - useEffect(() => { - const unsubDoc = on( - "DOCUMENT_CHANGE", - debouncedRefetch, - ); - const unsubSel = on( - "SELECTION_CHANGE", - debouncedRefetch, - ); - return () => { - unsubDoc(); - unsubSel(); - if (refetchTimerRef.current) clearTimeout(refetchTimerRef.current); - }; - }, []); - - return result; -}; diff --git a/src/ui/hooks/useSetNodesDataMutation.ts b/src/ui/hooks/useSetNodesDataMutation.ts deleted file mode 100644 index d1d6324a..00000000 --- a/src/ui/hooks/useSetNodesDataMutation.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - SetNodesDataProps, - setNodesDataEndpoint, -} from "@/main/endpoints/setNodesData"; -import { getConnectedNodesEndpoint } from "@/main/endpoints/getConnectedNodes"; -import { getSelectedNodesEndpoint } from "@/main/endpoints/getSelectedNodes"; -import { delayed } from "@/main/utils/delayed"; -import { NodeInfo } from "@/types"; -import { useMutation, useQueryClient } from "react-query"; - -type CachedNodes = { items: NodeInfo[]; basedOnSelection: boolean }; - -export const useSetNodesDataMutation = () => { - const queryClient = useQueryClient(); - const result = useMutation( - [setNodesDataEndpoint.name], - delayed((props: SetNodesDataProps) => setNodesDataEndpoint.call(props)), - { - onSuccess: (_, { nodes }) => { - // Patch the page-wide cache directly so returning to Index does not - // trigger a full-page rescan. useConnectedNodes uses staleTime:30s, - // so fresh data here prevents the redundant allNodes.refetch() call. - const key = [getConnectedNodesEndpoint.name, true] as const; - const old = queryClient.getQueryData(key); - if (old) { - const patch = new Map(nodes.map((n) => [n.id, n])); - const updated = old.items - .map((item) => - patch.has(item.id) ? { ...item, ...patch.get(item.id)! } : item, - ) - .filter((item) => item.key && item.connected !== false); - // Add newly connected nodes that were not tracked before - for (const n of nodes) { - if ( - n.key && - n.connected !== false && - !old.items.some((i) => i.id === n.id) - ) { - updated.push(n); - } - } - queryClient.setQueryData(key, { - ...old, - items: updated, - }); - } - // Mark selection stale so Index re-fetches node characters after - // navigating back from Pull (Index remounts, mountedRef resets to - // false, so the route-change effect never fires on that remount). - queryClient.invalidateQueries([getSelectedNodesEndpoint.name], { - refetchActive: false, - refetchInactive: false, - }); - }, - }, - ); - return { ...result }; -}; diff --git a/src/ui/hooks/useTranslation.ts b/src/ui/hooks/useTranslation.ts deleted file mode 100644 index d3091a69..00000000 --- a/src/ui/hooks/useTranslation.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useState } from "preact/hooks"; -import { useAllTranslations } from "./useAllTranslations"; - -type Props = { - language: string; - namespace?: string; - /** Key to get as translation value */ - key?: string; -}; - -export const useTranslation = (props: Props) => { - const translationsLoadable = useAllTranslations(); - const [error, setError] = useState(null); - - if ( - !translationsLoadable.isLoading && - translationsLoadable.translationsData == null && - translationsLoadable.error == null && - error == null - ) { - translationsLoadable - .getData({ - language: props.language, - namespaces: [props.namespace ?? "default"], - }) - .catch((e) => { - setError(e); - }); - } - - return { - getData: translationsLoadable.getData, - translation: - props.key != null - ? (translationsLoadable.translationsData?.[ - props.namespace ?? "default" - ]?.[props.key] ?? null) - : null, - isLoading: translationsLoadable.isLoading, - error, - }; -}; diff --git a/src/ui/hooks/useUpdateNodesMutation.ts b/src/ui/hooks/useUpdateNodesMutation.ts deleted file mode 100644 index 0680067e..00000000 --- a/src/ui/hooks/useUpdateNodesMutation.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - UpdateNodeProps, - updateNodesEndpoint, -} from "@/main/endpoints/updateNodes"; -import { delayed } from "@/main/utils/delayed"; -import { useMutation } from "react-query"; - -export const useUpdateNodesMutation = () => { - const result = useMutation( - [updateNodesEndpoint.name], - delayed((props: UpdateNodeProps) => updateNodesEndpoint.call(props)), - ); - return { ...result }; -}; diff --git a/src/ui/hooks/useWindowSize.ts b/src/ui/hooks/useWindowSize.ts deleted file mode 100644 index c11243fd..00000000 --- a/src/ui/hooks/useWindowSize.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect, useRef } from "preact/hooks"; - -import { useGlobalActions, useGlobalState } from "../state/GlobalState"; - -type WindowSize = { - width: number; - height: number; -}; - -export const useWindowSize = (newSize: WindowSize) => { - const lastSize = useRef(); - const { setSizeStack } = useGlobalActions(); - const sizeStack = useGlobalState((c) => c.sizeStack); - useEffect(() => { - const addedSize = { ...newSize }; - if (sizeStack.includes(lastSize.current!)) { - setSizeStack((stack) => - stack.map((i) => { - if (i === lastSize.current) { - return addedSize; - } - return i; - }), - ); - } else { - setSizeStack((stack) => [...stack, addedSize]); - } - lastSize.current = addedSize; - }, [newSize.width, newSize.height]); - - useEffect(() => { - return () => { - setSizeStack((stack) => stack.filter((i) => i !== lastSize.current)); - }; - }, []); -}; diff --git a/src/ui/icons/SvgIcons.tsx b/src/ui/icons/SvgIcons.tsx deleted file mode 100644 index 50d6ab8c..00000000 --- a/src/ui/icons/SvgIcons.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { h } from "preact"; - -// icons are copied from 'node_modules/svg-icon/dist/svg/material' -// add {...props} and fill="currentColor" for more flexibility - -export const Settings = (props: h.JSX.SVGAttributes) => { - return ( - - - - - ); -}; - -export const InsertLink = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const Info = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const More = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const Edit = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const X = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const AddCircleOutline = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const AddCircle = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const MyLocation = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const ChevronDown = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const CheckCircle = (props: h.JSX.SVGAttributes) => { - return ( - - - - - - - - - - - ); -}; - -export const ResizeHandleIcon = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const Branch = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; - -export const Refresh = (props: h.JSX.SVGAttributes) => { - return ( - - - - ); -}; diff --git a/src/ui/index.html b/src/ui/index.html new file mode 100644 index 00000000..0c958a15 --- /dev/null +++ b/src/ui/index.html @@ -0,0 +1,12 @@ + + + + + + Tolgee + + +
+ + + diff --git a/src/ui/lib/__tests__/busRequest.test.ts b/src/ui/lib/__tests__/busRequest.test.ts new file mode 100644 index 00000000..9676e322 --- /dev/null +++ b/src/ui/lib/__tests__/busRequest.test.ts @@ -0,0 +1,110 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { createIdleTimeout } from "$ui/lib/busRequest"; + +beforeEach(() => { + vi.useFakeTimers(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("createIdleTimeout", () => { + it("does not fire before the timeout elapses", () => { + const onTimeout = vi.fn(); + createIdleTimeout(1000, onTimeout); + + vi.advanceTimersByTime(999); + expect(onTimeout).not.toHaveBeenCalled(); + }); + + it("fires after the timeout with no activity (the simple single-result case)", () => { + const onTimeout = vi.fn(); + createIdleTimeout(1000, onTimeout); + + vi.advanceTimersByTime(1000); + expect(onTimeout).toHaveBeenCalledTimes(1); + }); + + it("does not fire if clear() is called before the timeout (resolves normally)", () => { + const onTimeout = vi.fn(); + const watchdog = createIdleTimeout(1000, onTimeout); + + vi.advanceTimersByTime(500); + watchdog.clear(); + vi.advanceTimersByTime(10_000); + + expect(onTimeout).not.toHaveBeenCalled(); + }); + + it("resets the timer on touch() and does not fire while activity keeps arriving (streamed case)", () => { + const onTimeout = vi.fn(); + const watchdog = createIdleTimeout(1000, onTimeout); + + // Simulate progress messages arriving every 700ms — each one resets the + // idle window, so the cumulative elapsed time (2100ms) safely exceeds the + // 1000ms timeout without ever firing, as long as no single gap does. + vi.advanceTimersByTime(700); + watchdog.touch(); + expect(onTimeout).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(700); + watchdog.touch(); + expect(onTimeout).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(700); + watchdog.touch(); + expect(onTimeout).not.toHaveBeenCalled(); + }); + + it("fires if activity stops, even mid-stream", () => { + const onTimeout = vi.fn(); + const watchdog = createIdleTimeout(1000, onTimeout); + + vi.advanceTimersByTime(700); + watchdog.touch(); + expect(onTimeout).not.toHaveBeenCalled(); + + // No further touch() — the stream went quiet. The idle window (armed at + // the last touch) should now elapse and fire. + vi.advanceTimersByTime(1000); + expect(onTimeout).toHaveBeenCalledTimes(1); + }); + + it("fires onTimeout at most once even if the timer somehow ticks again", () => { + const onTimeout = vi.fn(); + createIdleTimeout(1000, onTimeout); + + vi.advanceTimersByTime(1000); + vi.advanceTimersByTime(10_000); + + expect(onTimeout).toHaveBeenCalledTimes(1); + }); + + it("ignores touch() calls after the timeout has already fired", () => { + const onTimeout = vi.fn(); + const watchdog = createIdleTimeout(1000, onTimeout); + + vi.advanceTimersByTime(1000); + expect(onTimeout).toHaveBeenCalledTimes(1); + + // A late, stale message arriving after the watchdog gave up must not + // resurrect it. + watchdog.touch(); + vi.advanceTimersByTime(10_000); + + expect(onTimeout).toHaveBeenCalledTimes(1); + }); + + it("ignores touch() calls after clear()", () => { + const onTimeout = vi.fn(); + const watchdog = createIdleTimeout(1000, onTimeout); + watchdog.clear(); + + watchdog.touch(); + vi.advanceTimersByTime(10_000); + + expect(onTimeout).not.toHaveBeenCalled(); + }); +}); diff --git a/src/ui/lib/__tests__/clipboard.test.ts b/src/ui/lib/__tests__/clipboard.test.ts new file mode 100644 index 00000000..7c43d0ec --- /dev/null +++ b/src/ui/lib/__tests__/clipboard.test.ts @@ -0,0 +1,99 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { copyToClipboard } from "$ui/lib/clipboard"; + +// The test env is `node` (no jsdom); `navigator` is a read-only global there, +// so we install fakes with `vi.stubGlobal` (plain assignment throws). + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +/** + * A fake `document` modelling the copy-event flow. When `fireCopyEvent` is + * true, `execCommand("copy")` synchronously dispatches a `copy` event carrying + * a fake `clipboardData` to the registered capture listener — so we can assert + * the helper writes the payload itself instead of trusting the selection. + */ +function installDocument(opts: { + fireCopyEvent: boolean; + execThrows?: boolean; +}) { + const listeners: Array<(e: unknown) => void> = []; + const clipboardData = { data: null as string | null, setData(_t: string, v: string) { this.data = v; } }; + const textarea = { + value: "", + style: {} as Record, + focus: () => {}, + select: () => {}, + }; + vi.stubGlobal("document", { + createElement: () => textarea, + body: { appendChild: () => {}, removeChild: () => {} }, + getSelection: () => null, + addEventListener: (_type: string, fn: (e: unknown) => void) => listeners.push(fn), + removeEventListener: () => {}, + execCommand: vi.fn(() => { + if (opts.execThrows) throw new Error("execCommand blocked"); + if (opts.fireCopyEvent) { + const evt = { clipboardData, preventDefault: () => {} }; + for (const fn of listeners) fn(evt); + } + return true; + }), + }); + return { clipboardData, textarea }; +} + +describe("copyToClipboard", () => { + it("writes the text into the copy event's clipboardData and returns true", async () => { + const { clipboardData, textarea } = installDocument({ fireCopyEvent: true }); + vi.stubGlobal("navigator", {}); // no async clipboard — event path must carry it + + await expect(copyToClipboard("home.title")).resolves.toBe(true); + // The payload came from OUR copy handler, not execCommand's return value. + expect(clipboardData.data).toBe("home.title"); + expect(textarea.value).toBe("home.title"); + }); + + it("does NOT trust execCommand's `true` when the copy event never fires", async () => { + // The Figma-desktop case: execCommand returns true but writes nothing, and + // there's no async clipboard to fall back to → honest failure, no throw. + installDocument({ fireCopyEvent: false }); + vi.stubGlobal("navigator", {}); + + await expect(copyToClipboard("x")).resolves.toBe(false); + }); + + it("does NOT reject when navigator.clipboard is undefined (the sandbox case)", async () => { + installDocument({ fireCopyEvent: false }); + vi.stubGlobal("navigator", {}); // no `clipboard` property at all + await expect(copyToClipboard("x")).resolves.toBe(false); + }); + + it("falls back to the async Clipboard API when execCommand throws", async () => { + installDocument({ fireCopyEvent: false, execThrows: true }); + const writeText = vi.fn(() => Promise.resolve()); + vi.stubGlobal("navigator", { clipboard: { writeText } }); + + await expect(copyToClipboard("greeting")).resolves.toBe(true); + expect(writeText).toHaveBeenCalledWith("greeting"); + }); + + it("reports FAILURE when the async fallback rejects", async () => { + // The reported bug: the fallback's promise was left un-awaited and success + // returned immediately, so a `writeText` that rejected for lack of focus or + // permission still flashed the caller's confirmation while the clipboard + // kept its old contents. + installDocument({ fireCopyEvent: false, execThrows: true }); + const writeText = vi.fn(() => Promise.reject(new Error("not allowed"))); + vi.stubGlobal("navigator", { clipboard: { writeText } }); + + await expect(copyToClipboard("greeting")).resolves.toBe(false); + }); + + it("returns false when nothing can copy (no event, no writeText)", async () => { + installDocument({ fireCopyEvent: false }); + vi.stubGlobal("navigator", { clipboard: {} }); // clipboard, but no writeText + await expect(copyToClipboard("x")).resolves.toBe(false); + }); +}); diff --git a/src/ui/lib/api/__tests__/auth.test.ts b/src/ui/lib/api/__tests__/auth.test.ts new file mode 100644 index 00000000..dfc6a9bc --- /dev/null +++ b/src/ui/lib/api/__tests__/auth.test.ts @@ -0,0 +1,129 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { REQUIRED_SCOPES, hasRequiredScopes, validateApiKey } from "../auth"; + +type FetchMock = ReturnType; + +const ORIGINAL_FETCH = globalThis.fetch; + +function installFetchMock(impl: (...args: unknown[]) => Promise) { + const mock: FetchMock = vi.fn(impl as never); + globalThis.fetch = mock as unknown as typeof fetch; + return mock; +} + +beforeEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + vi.restoreAllMocks(); +}); + +describe("validateApiKey", () => { + it("returns ok:true with projectId and scopes on success", async () => { + const body = { + projectId: 42, + scopes: ["translations.view", "translations.edit"], + userFullName: "Test User", + branchingEnabled: false, + id: 1, + description: "", + projectName: "Demo", + }; + installFetchMock( + async () => + new Response(JSON.stringify(body), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const result = await validateApiKey("https://app.tolgee.io", "tg-key"); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.projectId).toBe(42); + expect(result.scopes).toEqual(["translations.view", "translations.edit"]); + expect(result.userFullName).toBe("Test User"); + } + }); + + it("returns ok:false with invalid_api_key on 401", async () => { + installFetchMock( + async () => + new Response(JSON.stringify({ code: "invalid_api_key" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }), + ); + + const result = await validateApiKey("https://app.tolgee.io", "bad"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBe("auth.invalid_api_key"); + } + }); + + it("returns ok:false on network error", async () => { + installFetchMock(async () => { + throw new TypeError("Failed to fetch"); + }); + + const result = await validateApiKey("https://app.tolgee.io", "tg-key"); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBe("auth.network_error"); + } + }); + + it("strips trailing slash from apiUrl when building the request", async () => { + const mock = installFetchMock( + async () => + new Response( + JSON.stringify({ + projectId: 1, + scopes: [], + branchingEnabled: false, + id: 1, + description: "", + projectName: "Demo", + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + ); + + await validateApiKey("https://app.tolgee.io/", "tg-key"); + const firstArg = mock.mock.calls[0]?.[0]; + const calledUrl = firstArg instanceof Request ? firstArg.url : String(firstArg ?? ""); + expect(calledUrl).toBe("https://app.tolgee.io/v2/api-keys/current"); + }); +}); + +describe("hasRequiredScopes", () => { + it("returns true when all required scopes are present", () => { + expect( + hasRequiredScopes( + ["translations.view", "translations.edit", "keys.create"], + [...REQUIRED_SCOPES.push], + ), + ).toBe(true); + }); + + it("returns false when a required scope is missing", () => { + expect(hasRequiredScopes(["translations.view"], [...REQUIRED_SCOPES.push])).toBe(false); + }); + + it("returns true when the required list is empty", () => { + expect(hasRequiredScopes([], [])).toBe(true); + }); + + it("exposes the documented scope groups", () => { + expect(REQUIRED_SCOPES.pull).toContain("translations.view"); + expect(REQUIRED_SCOPES.push).toContain("translations.edit"); + expect(REQUIRED_SCOPES.push).toContain("keys.create"); + expect(REQUIRED_SCOPES.screenshots).toContain("screenshots.upload"); + }); +}); diff --git a/src/ui/lib/api/__tests__/branches.test.ts b/src/ui/lib/api/__tests__/branches.test.ts new file mode 100644 index 00000000..51ed63b7 --- /dev/null +++ b/src/ui/lib/api/__tests__/branches.test.ts @@ -0,0 +1,169 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + fetchBranches, + isConfiguredBranchMissing, + pickDefaultBranch, +} from "../branches"; +import { createTolgeeClient } from "../client"; + +type FetchMock = ReturnType; + +const ORIGINAL_FETCH = globalThis.fetch; + +function installFetchMock(impl: (...args: unknown[]) => Promise) { + const mock: FetchMock = vi.fn(impl as never); + globalThis.fetch = mock as unknown as typeof fetch; + return mock; +} + +function okResponse(body: unknown) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +beforeEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + vi.restoreAllMocks(); +}); + +describe("fetchBranches", () => { + it("returns branch names from the response", async () => { + installFetchMock(async () => + okResponse({ + _embedded: { + branches: [ + { name: "main", isDefault: true }, + { name: "feature/x", isDefault: false }, + ], + }, + }), + ); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + const result = await fetchBranches(client); + + expect(result).toEqual([ + { name: "main", isDefault: true }, + { name: "feature/x", isDefault: false }, + ]); + }); + + it("returns [] when _embedded is missing", async () => { + installFetchMock(async () => okResponse({})); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + const result = await fetchBranches(client); + + expect(result).toEqual([]); + }); + + it("filters out entries that have no name", async () => { + installFetchMock(async () => + okResponse({ + _embedded: { + branches: [ + { name: "main", isDefault: true }, + { isDefault: false }, // name undefined + { name: "", isDefault: false }, // name empty string (falsy) + ], + }, + }), + ); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + const result = await fetchBranches(client); + + expect(result).toEqual([{ name: "main", isDefault: true }]); + }); + + it("returns [] when the branches array is empty", async () => { + installFetchMock(async () => okResponse({ _embedded: { branches: [] } })); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + const result = await fetchBranches(client); + + expect(result).toEqual([]); + }); +}); + +describe("pickDefaultBranch", () => { + it("prefers the default branch (isDefault, like the original plugin)", () => { + expect( + pickDefaultBranch([ + { name: "main", isDefault: false }, + { name: "feature/x", isDefault: true }, + ]), + ).toBe("feature/x"); + }); + + it('falls back to "main" when none is marked default', () => { + expect( + pickDefaultBranch([{ name: "dev" }, { name: "main" }]), + ).toBe("main"); + }); + + it('falls back to the first branch when no default and no "main"', () => { + expect(pickDefaultBranch([{ name: "dev" }, { name: "staging" }])).toBe("dev"); + }); + + it("returns an empty string when there are no branches", () => { + expect(pickDefaultBranch([])).toBe(""); + }); +}); + +describe("isConfiguredBranchMissing", () => { + const BRANCHES = [{ name: "main", isDefault: true }, { name: "feature/x" }]; + + it("flags a configured branch absent from the loaded list", () => { + expect(isConfiguredBranchMissing("deleted", BRANCHES, true)).toBe(true); + }); + + it("does not flag a branch present in the loaded list", () => { + expect(isConfiguredBranchMissing("feature/x", BRANCHES, true)).toBe(false); + }); + + it("does not flag when no branch is configured", () => { + expect(isConfiguredBranchMissing("", BRANCHES, true)).toBe(false); + }); + + it("does not flag while branches have not loaded yet (or the fetch failed)", () => { + // The empty list here means "nothing fetched", not "no branches exist" — + // trusting it would warn on every startup before hydration finishes. + expect(isConfiguredBranchMissing("main", [], false)).toBe(false); + }); + + it("flags when the fetch succeeded with an empty branch list", () => { + expect(isConfiguredBranchMissing("main", [], true)).toBe(true); + }); +}); + +describe("fetchBranches — failed request", () => { + it("throws instead of reporting an empty branch list", async () => { + // `openapi-fetch` RESOLVES on 4xx/5xx with an `error` field rather than + // throwing. Discarding it turned a failed request into "this project has + // no branches" on the SUCCESS path — `hydrateBranches` then set + // `loaded: true` and the UI announced that the user's configured branch + // had been deleted, offering an empty picker to replace it. + const client = { + GET: async () => ({ error: { message: "Forbidden" } }), + } as unknown as Parameters[0]; + + await expect(fetchBranches(client)).rejects.toThrow("Forbidden"); + }); + + it("still returns the list on success", async () => { + const client = { + GET: async () => ({ + data: { _embedded: { branches: [{ name: "main", isDefault: true }] } }, + }), + } as unknown as Parameters[0]; + + await expect(fetchBranches(client)).resolves.toEqual([{ name: "main", isDefault: true }]); + }); +}); diff --git a/src/ui/lib/api/__tests__/keyExistence.test.ts b/src/ui/lib/api/__tests__/keyExistence.test.ts new file mode 100644 index 00000000..ec08ad52 --- /dev/null +++ b/src/ui/lib/api/__tests__/keyExistence.test.ts @@ -0,0 +1,174 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTolgeeClient } from "../client"; +import { + connectedKeySig, + effectiveNs, + fetchMissingKeys, +} from "../keyExistence"; + +type FetchMock = ReturnType; + +const ORIGINAL_FETCH = globalThis.fetch; + +function installFetchMock(impl: (...args: unknown[]) => Promise) { + const mock: FetchMock = vi.fn(impl as never); + globalThis.fetch = mock as unknown as typeof fetch; + return mock; +} + +function okResponse(body: unknown) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +function errorResponse(status = 500) { + return new Response(JSON.stringify({ code: "server_error" }), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +function keysInfoResponse(keys: { name: string; namespace?: string }[]) { + return okResponse({ _embedded: { keys } }); +} + +// openapi-fetch invokes fetch with a single Request object. +function sentRequest(mock: FetchMock): Request { + return mock.mock.calls[0]?.[0] as Request; +} + +async function sentBody(mock: FetchMock): Promise> { + return (await sentRequest(mock).clone().json()) as Record; +} + +beforeEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + vi.restoreAllMocks(); +}); + +describe("effectiveNs", () => { + it("keeps a real namespace when the feature is enabled", () => { + expect(effectiveNs("web", true)).toBe("web"); + }); + + it("normalises '' and undefined to undefined when enabled", () => { + expect(effectiveNs("", true)).toBeUndefined(); + expect(effectiveNs(undefined, true)).toBeUndefined(); + }); + + it("drops ANY stored ns when the feature is disabled (matches push)", () => { + expect(effectiveNs("stale-ns", false)).toBeUndefined(); + expect(effectiveNs(undefined, false)).toBeUndefined(); + }); +}); + +describe("fetchMissingKeys", () => { + const client = () => createTolgeeClient("https://app.tolgee.io", "test-key"); + + it("returns an empty set without a request for no keys", async () => { + const mock = installFetchMock(async () => keysInfoResponse([])); + const missing = await fetchMissingKeys(client(), [], "", "en", true); + expect(missing.size).toBe(0); + expect(mock).not.toHaveBeenCalled(); + }); + + it("flags only the keys absent from the response (namespaces enabled)", async () => { + installFetchMock(async () => + keysInfoResponse([{ name: "kept", namespace: "web" }]), + ); + const missing = await fetchMissingKeys( + client(), + [ + { name: "kept", ns: "web" }, + { name: "deleted", ns: "web" }, + ], + "", + "en", + true, + ); + expect(missing).toEqual(new Set([connectedKeySig("web", "deleted")])); + }); + + it("sends the stored ns in the request when namespaces are enabled", async () => { + const mock = installFetchMock(async () => keysInfoResponse([])); + await fetchMissingKeys(client(), [{ name: "a", ns: "web" }], "", "en", true); + const body = await sentBody(mock); + expect(body.keys).toEqual([{ name: "a", namespace: "web" }]); + }); + + it("does NOT false-flag a node carrying a stale ns when namespaces are disabled", async () => { + // The push pipeline ignores `ns` with the feature off, so the key lives in + // the default namespace. The check must look it up there too. + const mock = installFetchMock(async () => + keysInfoResponse([{ name: "Tue.Test" }]), + ); + const missing = await fetchMissingKeys( + client(), + [{ name: "Tue.Test", ns: "stale-ns" }], + "", + "en", + false, + ); + expect(missing.size).toBe(0); + // And the request itself must not mention the stale ns. + const body = await sentBody(mock); + expect(body.keys).toEqual([{ name: "Tue.Test" }]); + }); + + it("still flags a truly deleted key under a stale ns when namespaces are disabled", async () => { + installFetchMock(async () => keysInfoResponse([])); + const missing = await fetchMissingKeys( + client(), + [{ name: "gone", ns: "stale-ns" }], + "", + "en", + false, + ); + // Sig uses the effective (default) namespace — the same one lookups use. + expect(missing).toEqual(new Set([connectedKeySig(undefined, "gone")])); + }); + + it("treats response ns '' and request ns undefined as the same default namespace", async () => { + installFetchMock(async () => + keysInfoResponse([{ name: "a", namespace: "" }]), + ); + const missing = await fetchMissingKeys( + client(), + [{ name: "a", ns: undefined }], + "", + "en", + true, + ); + expect(missing.size).toBe(0); + }); + + it("reports nothing missing on a request error (fail-safe)", async () => { + installFetchMock(async () => errorResponse(500)); + const missing = await fetchMissingKeys( + client(), + [{ name: "a", ns: undefined }], + "", + "en", + true, + ); + expect(missing.size).toBe(0); + }); + + it("passes the branch as a query param when set", async () => { + const mock = installFetchMock(async () => keysInfoResponse([])); + await fetchMissingKeys( + client(), + [{ name: "a", ns: undefined }], + "feature-x", + "en", + true, + ); + expect(sentRequest(mock).url).toContain("branch=feature-x"); + }); +}); diff --git a/src/ui/lib/api/__tests__/keys.test.ts b/src/ui/lib/api/__tests__/keys.test.ts new file mode 100644 index 00000000..4285a06c --- /dev/null +++ b/src/ui/lib/api/__tests__/keys.test.ts @@ -0,0 +1,261 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { NodeInfo } from "$shared/types"; +import { createTolgeeClient } from "../client"; +import { connectInfoFromKey, searchKeys, type KeySearchResult } from "../keys"; + +type FetchMock = ReturnType; + +const ORIGINAL_FETCH = globalThis.fetch; + +function installFetchMock(impl: (...args: unknown[]) => Promise) { + const mock: FetchMock = vi.fn(impl as never); + globalThis.fetch = mock as unknown as typeof fetch; + return mock; +} + +function okResponse(body: unknown) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +function errorResponse(status = 500) { + return new Response(JSON.stringify({ code: "server_error" }), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +beforeEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + vi.restoreAllMocks(); +}); + +describe("searchKeys", () => { + it("returns [] when query is an empty string", async () => { + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const result = await searchKeys(client, "", "en"); + expect(result).toEqual([]); + }); + + it("returns [] when query is only whitespace", async () => { + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const result = await searchKeys(client, " ", "en"); + expect(result).toEqual([]); + }); + + it("returns [] when the API returns an error status", async () => { + installFetchMock(async () => errorResponse(500)); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const result = await searchKeys(client, "hello", "en"); + expect(result).toEqual([]); + }); + + it("maps _embedded.keys to KeySearchResult[]", async () => { + const keys = [ + { + id: 1, + name: "key.one", + namespace: "common", + description: "First key", + translation: "Hello", + baseTranslation: "Hello base", + plural: false, + }, + { + id: 2, + name: "key.two", + namespace: null, + description: undefined, + translation: "World", + baseTranslation: undefined, + plural: true, + }, + ]; + installFetchMock(async () => okResponse({ _embedded: { keys } })); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + const result = await searchKeys(client, "key", "en"); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + id: 1, + name: "key.one", + namespace: "common", + description: "First key", + translation: "Hello", + baseTranslation: "Hello base", + plural: false, + }); + expect(result[1]).toMatchObject({ + id: 2, + name: "key.two", + namespace: null, + plural: true, + }); + }); + + it("passes the correct languageTag as a query param", async () => { + const mock = installFetchMock(async () => okResponse({ _embedded: { keys: [] } })); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + await searchKeys(client, "search-term", "de", 10); + + const calledUrl: string = + mock.mock.calls[0]?.[0] instanceof Request + ? mock.mock.calls[0][0].url + : String(mock.mock.calls[0]?.[0] ?? ""); + + expect(calledUrl).toContain("languageTag=de"); + expect(calledUrl).toContain("search=search-term"); + expect(calledUrl).toContain("size=10"); + }); + + it("passes the branch as a query param when set", async () => { + const mock = installFetchMock(async () => okResponse({ _embedded: { keys: [] } })); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + await searchKeys(client, "term", "en", 20, "feature-x"); + + const calledUrl: string = + mock.mock.calls[0]?.[0] instanceof Request + ? mock.mock.calls[0][0].url + : String(mock.mock.calls[0]?.[0] ?? ""); + + expect(calledUrl).toContain("branch=feature-x"); + }); + + it("omits the branch param when empty (no-branching projects)", async () => { + const mock = installFetchMock(async () => okResponse({ _embedded: { keys: [] } })); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + await searchKeys(client, "term", "en", 20, ""); + + const calledUrl: string = + mock.mock.calls[0]?.[0] instanceof Request + ? mock.mock.calls[0][0].url + : String(mock.mock.calls[0]?.[0] ?? ""); + + expect(calledUrl).not.toContain("branch="); + }); + + it("handles missing optional fields (namespace, description, translation)", async () => { + const keys = [ + { + id: 99, + name: "bare.key", + // namespace, description, translation, baseTranslation, plural all absent + }, + ]; + installFetchMock(async () => okResponse({ _embedded: { keys } })); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + const result = await searchKeys(client, "bare", undefined); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + id: 99, + name: "bare.key", + namespace: null, + description: undefined, + translation: undefined, + baseTranslation: undefined, + plural: undefined, + }); + }); +}); + +describe("connectInfoFromKey", () => { + function node(overrides: Partial = {}): NodeInfo { + return { + id: "1", + name: "Layer", + characters: "Figma text", + key: "", + ns: undefined, + connected: false, + isPlural: false, + ...overrides, + } as NodeInfo; + } + + function result(overrides: Partial = {}): KeySearchResult { + return { id: 1, name: "greeting", namespace: "common", ...overrides }; + } + + it("adopts the key's translation instead of keeping the Figma text", () => { + const info = connectInfoFromKey( + result({ translation: "Ahoj" }), + node({ characters: "Figma text" }), + "en", + ); + expect(info.translation).toBe("Ahoj"); + expect(info).toMatchObject({ + key: "greeting", + ns: "common", + connected: true, + isPlural: false, + }); + expect(info.pluralParamValue).toBeUndefined(); + }); + + it("falls back to the node's characters when the key has no translation yet", () => { + const info = connectInfoFromKey( + result({ translation: undefined, baseTranslation: undefined }), + node({ characters: "Figma text" }), + "en", + ); + expect(info.translation).toBe("Figma text"); + }); + + it("uses '' (not undefined) for a no-namespace key", () => { + const info = connectInfoFromKey(result({ namespace: null }), node(), "en"); + expect(info.ns).toBe(""); + }); + + it("infers the plural sample COUNT from the layer's current text", () => { + const icu = "{count, plural, one {# item} other {# items}}"; + const info = connectInfoFromKey( + result({ plural: true, translation: icu }), + node({ characters: "5 items" }), + "en", + ); + expect(info.isPlural).toBe(true); + expect(info.pluralParamValue).toBe("5"); + }); + + it("infers the count from baseTranslation when the working-language translation is empty", () => { + const info = connectInfoFromKey( + result({ + plural: true, + translation: undefined, + baseTranslation: "{n, plural, one {# item} other {# items}}", + }), + node({ characters: "10 items" }), + "en", + ); + expect(info.pluralParamValue).toBe("10"); + }); + + it("keeps a numeric stored count when the canvas can't be matched, else '1'", () => { + const icu = "{count, plural, one {# item} other {# items}}"; + const withStored = connectInfoFromKey( + result({ plural: true, translation: icu }), + node({ characters: "no match", pluralParamValue: "7" }), + "en", + ); + expect(withStored.pluralParamValue).toBe("7"); + + const bare = connectInfoFromKey( + result({ plural: true, translation: icu }), + node({ characters: "no match" }), + "en", + ); + expect(bare.pluralParamValue).toBe("1"); + }); +}); diff --git a/src/ui/lib/api/__tests__/keysByName.test.ts b/src/ui/lib/api/__tests__/keysByName.test.ts new file mode 100644 index 00000000..48f40757 --- /dev/null +++ b/src/ui/lib/api/__tests__/keysByName.test.ts @@ -0,0 +1,310 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTolgeeClient } from "../client"; +import { fetchRemoteKeys } from "../keysByName"; + +type FetchMock = ReturnType; + +const ORIGINAL_FETCH = globalThis.fetch; + +function installFetchMock(impl: (...args: unknown[]) => Promise) { + const mock: FetchMock = vi.fn(impl as never); + globalThis.fetch = mock as unknown as typeof fetch; + return mock; +} + +function okResponse(body: unknown) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +function errorResponse(status = 500) { + return new Response(JSON.stringify({ code: "server_error" }), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +const SAMPLE_ROW = { + keyName: "key.one", + keyNamespace: "common", + keyIsPlural: false, + keyTags: [{ name: "tag-a" }], + translations: { en: { text: "Hello" } }, +}; + +beforeEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + vi.restoreAllMocks(); +}); + +describe("fetchRemoteKeys", () => { + it("returns [] when filterKeyName is an empty array", async () => { + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const result = await fetchRemoteKeys(client, { filterKeyName: [] }); + expect(result).toEqual([]); + }); + + it("returns keys from _embedded.keys", async () => { + installFetchMock(async () => okResponse({ _embedded: { keys: [SAMPLE_ROW] } })); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + const result = await fetchRemoteKeys(client, { + filterKeyName: ["key.one"], + }); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual(SAMPLE_ROW); + }); + + it("falls back to pagedModel._embedded.keys when _embedded is missing", async () => { + installFetchMock(async () => okResponse({ pagedModel: { _embedded: { keys: [SAMPLE_ROW] } } })); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + const result = await fetchRemoteKeys(client, { + filterKeyName: ["key.one"], + }); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual(SAMPLE_ROW); + }); + + it("throws on API error instead of silently returning []", async () => { + installFetchMock(async () => errorResponse(500)); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + await expect( + fetchRemoteKeys(client, { + filterKeyName: ["key.one"], + }), + ).rejects.toThrow(); + }); + + it("passes branch param in query when provided", async () => { + const mock = installFetchMock(async () => okResponse({ _embedded: { keys: [] } })); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + + await fetchRemoteKeys(client, { + filterKeyName: ["key.one"], + branch: "feature/my-branch", + }); + + const calledUrl: string = + mock.mock.calls[0]?.[0] instanceof Request + ? mock.mock.calls[0][0].url + : String(mock.mock.calls[0]?.[0] ?? ""); + + expect(calledUrl).toContain("branch=feature%2Fmy-branch"); + }); + + // --------------------------------------------------------------------- + // Batching (>200 names split into multiple parallel requests) + // --------------------------------------------------------------------- + + function calledUrl(mock: FetchMock, callIndex: number): URL { + const arg = mock.mock.calls[callIndex]?.[0]; + const raw = arg instanceof Request ? arg.url : String(arg ?? ""); + return new URL(raw); + } + + function rowFor(name: string) { + return { + keyName: name, + keyNamespace: undefined, + keyIsPlural: false, + translations: { en: { text: `text-${name}` } }, + }; + } + + it("splits 450 short key names into batches whose cumulative length stays under the max, and merges the results", async () => { + const mock = installFetchMock(async (input) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + const names = url.searchParams.getAll("filterKeyName"); + const totalChars = names.reduce((sum, n) => sum + n.length, 0); + expect(totalChars).toBeLessThanOrEqual(3000); + return okResponse({ _embedded: { keys: names.map(rowFor) } }); + }); + + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const filterKeyName = Array.from({ length: 450 }, (_, i) => `key-${i}`); + + const result = await fetchRemoteKeys(client, { filterKeyName }); + + // 450 short names (~6-7 chars each, ~3000 chars total) now split by length + // rather than by a fixed count of 200 — with these short names that + // happens to land on 2 batches, not 3 (short names pack far more than + // 200 per batch before the character budget is hit). + expect(mock).toHaveBeenCalledTimes(2); + expect(result).toHaveLength(450); + expect(new Set(result.map((r) => r.keyName))).toEqual(new Set(filterKeyName)); + }); + + it("does not batch a small request (<=200 names) — still a single request", async () => { + const mock = installFetchMock(async () => okResponse({ _embedded: { keys: [] } })); + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const filterKeyName = Array.from({ length: 150 }, (_, i) => `key-${i}`); + + await fetchRemoteKeys(client, { filterKeyName }); + + expect(mock).toHaveBeenCalledTimes(1); + }); + + it("propagates an error from any single batch as a failure of the whole call", async () => { + let callCount = 0; + installFetchMock(async () => { + callCount++; + // Fail the 2nd batch specifically. + if (callCount === 2) return errorResponse(500); + return okResponse({ _embedded: { keys: [] } }); + }); + + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const filterKeyName = Array.from({ length: 450 }, (_, i) => `key-${i}`); + + await expect(fetchRemoteKeys(client, { filterKeyName })).rejects.toThrow(); + }); + + it("follows cursor pagination within a single batch and merges all pages", async () => { + const filterKeyName = ["a", "b"]; + let callCount = 0; + const mock = installFetchMock(async () => { + callCount++; + if (callCount === 1) { + return okResponse({ + _embedded: { keys: [rowFor("a")] }, + nextCursor: "cursor-1", + }); + } + return okResponse({ _embedded: { keys: [rowFor("b")] } }); + }); + + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const result = await fetchRemoteKeys(client, { filterKeyName }); + + expect(mock).toHaveBeenCalledTimes(2); + expect(calledUrl(mock, 1).searchParams.get("cursor")).toBe("cursor-1"); + expect(result.map((r) => r.keyName).sort()).toEqual(["a", "b"]); + }); + + // --------------------------------------------------------------------- + // Length-based batching (task 12: full-sentence camelCase key names can + // overflow the URL budget well under the 200-name count cap) + // --------------------------------------------------------------------- + + function sentenceLikeName(i: number, len: number): string { + const base = `sentenceKeyNumber${i}`; + return base.length >= len ? base.slice(0, len) : base + "x".repeat(len - base.length); + } + + it("batches full-sentence-length key names (~80 chars each) by cumulative length, producing more (smaller) batches than a naive 200-count chunk would", async () => { + const mock = installFetchMock(async (input) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + const names = url.searchParams.getAll("filterKeyName"); + const totalChars = names.reduce((sum, n) => sum + n.length, 0); + expect(totalChars).toBeLessThanOrEqual(3000); + return okResponse({ _embedded: { keys: names.map(rowFor) } }); + }); + + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + // 250 names x ~80 chars: a naive count-based chunk of 200 would produce + // ceil(250/200) = 2 batches. By length (3000-char budget), ~37 such + // names fit per batch, so this must split into noticeably more batches. + const filterKeyName = Array.from({ length: 250 }, (_, i) => sentenceLikeName(i, 80)); + + const result = await fetchRemoteKeys(client, { filterKeyName }); + + expect(mock.mock.calls.length).toBeGreaterThan(2); + expect(result).toHaveLength(250); + expect(new Set(result.map((r) => r.keyName))).toEqual(new Set(filterKeyName)); + }); + + it("still sends a single name longer than MAX_BATCH_CHARS on its own, rather than dropping it", async () => { + const mock = installFetchMock(async (input) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + const names = url.searchParams.getAll("filterKeyName"); + return okResponse({ _embedded: { keys: names.map(rowFor) } }); + }); + + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const hugeName = sentenceLikeName(0, 3500); // longer than the 3000-char batch budget on its own + + const result = await fetchRemoteKeys(client, { filterKeyName: [hugeName] }); + + expect(mock).toHaveBeenCalledTimes(1); + const names = calledUrl(mock, 0).searchParams.getAll("filterKeyName"); + expect(names).toEqual([hugeName]); + expect(result.map((r) => r.keyName)).toEqual([hugeName]); + }); + + // --------------------------------------------------------------------- + // onProgress (task 7f: Push diff-computation progress bar) + // --------------------------------------------------------------------- + + it("reports progress per batch as each resolves (not after Promise.all settles), ending at done === total", async () => { + // Each name alone exceeds MAX_BATCH_CHARS, so each becomes its own + // single-name batch — 3 names -> 3 batches, guaranteed by `chunkByLength`. + const filterKeyName = [ + sentenceLikeName(1, 3500), + sentenceLikeName(2, 3500), + sentenceLikeName(3, 3500), + ]; + + let callCount = 0; + installFetchMock(async (input) => { + callCount++; + const thisCall = callCount; + const url = new URL(input instanceof Request ? input.url : String(input)); + const names = url.searchParams.getAll("filterKeyName"); + // Stagger resolution deliberately out of call order — the 2nd request + // resolves fastest, proving progress isn't just tracking call order. + const delay = thisCall === 1 ? 15 : thisCall === 2 ? 1 : 8; + await new Promise((resolve) => setTimeout(resolve, delay)); + return okResponse({ _embedded: { keys: names.map(rowFor) } }); + }); + + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const onProgress = vi.fn(); + + const result = await fetchRemoteKeys(client, { filterKeyName }, onProgress); + + expect(result).toHaveLength(3); + expect(onProgress).toHaveBeenCalledTimes(3); + const doneValues = onProgress.mock.calls.map((c) => c[0] as number); + expect(doneValues).toEqual([...doneValues].sort((a, b) => a - b)); + expect(new Set(doneValues).size).toBe(doneValues.length); // strictly increasing + expect(onProgress.mock.calls.every((c) => c[1] === 3)).toBe(true); + expect(onProgress.mock.calls.at(-1)).toEqual([3, 3]); + }); + + it("packs a mix of short and long names efficiently — short names group together instead of one-per-batch", async () => { + const mock = installFetchMock(async (input) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + const names = url.searchParams.getAll("filterKeyName"); + const totalChars = names.reduce((sum, n) => sum + n.length, 0); + expect(totalChars).toBeLessThanOrEqual(3000); + return okResponse({ _embedded: { keys: names.map(rowFor) } }); + }); + + const client = createTolgeeClient("https://app.tolgee.io", "test-key"); + const shortNames = Array.from({ length: 50 }, (_, i) => `s${i}`); // ~140 chars total + const longNames = [sentenceLikeName(1, 2000), sentenceLikeName(2, 2000)]; + const filterKeyName = [...shortNames, ...longNames]; + + const result = await fetchRemoteKeys(client, { filterKeyName }); + + // All 50 short names (~140 chars) comfortably pack alongside the first + // 2000-char long name in one batch; the second long name needs its own + // batch since it would push the first over budget. That's 2 requests + // total, not 52 (one-per-name) and not 1 (everything crammed together). + expect(mock).toHaveBeenCalledTimes(2); + const firstBatchNames = calledUrl(mock, 0).searchParams.getAll("filterKeyName"); + expect(firstBatchNames.length).toBeGreaterThan(1); + expect(result).toHaveLength(52); + expect(new Set(result.map((r) => r.keyName))).toEqual(new Set(filterKeyName)); + }); +}); diff --git a/src/ui/lib/api/__tests__/pageNodes.test.ts b/src/ui/lib/api/__tests__/pageNodes.test.ts new file mode 100644 index 00000000..6c55f361 --- /dev/null +++ b/src/ui/lib/api/__tests__/pageNodes.test.ts @@ -0,0 +1,132 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { MainToUi } from "$shared/messages"; + +type Handler = (msg: MainToUi) => void; + +const handlers = new Map>(); +const sent: unknown[] = []; + +function emit(msg: MainToUi): void { + const set = handlers.get(msg.type); + if (!set) return; + for (const handler of [...set]) handler(msg); +} + +vi.mock("$ui/lib/bus", () => ({ + send: (msg: unknown) => { + sent.push(msg); + }, + nextCorrelationId: () => "test-correlation", + on: (type: string, handler: Handler) => { + let set = handlers.get(type); + if (!set) { + set = new Set(); + handlers.set(type, set); + } + set.add(handler); + return () => set?.delete(handler); + }, +})); + +import { requestPageConnectedNodes } from "$ui/lib/api/pageNodes"; + +beforeEach(() => { + vi.useFakeTimers(); + handlers.clear(); + sent.length = 0; +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("requestPageConnectedNodes", () => { + it("resolves with the nodes from a matching page-connected-nodes-result", async () => { + const promise = requestPageConnectedNodes(); + emit({ + type: "page-connected-nodes-result", + correlationId: "test-correlation", + nodes: [{ id: "1:1" } as never], + }); + + await expect(promise).resolves.toEqual([{ id: "1:1" }]); + }); + + it("calls onProgress and resets the idle watchdog on page-connected-nodes-progress", async () => { + const onProgress = vi.fn(); + const promise = requestPageConnectedNodes(undefined, onProgress); + + // Progress arrives every 4 minutes — each one should reset the 5-minute + // idle window, so the cumulative silence between messages never actually + // reaches 5 minutes and the request never times out. + for (let i = 1; i <= 3; i++) { + await vi.advanceTimersByTimeAsync(4 * 60_000); + emit({ + type: "page-connected-nodes-progress", + correlationId: "test-correlation", + done: i * 50, + total: 250, + }); + } + + expect(onProgress).toHaveBeenCalledTimes(3); + expect(onProgress.mock.calls).toEqual([ + [50, 250], + [100, 250], + [150, 250], + ]); + + emit({ + type: "page-connected-nodes-result", + correlationId: "test-correlation", + nodes: [], + }); + await expect(promise).resolves.toEqual([]); + }); + + it("times out after 5 minutes of silence even after earlier progress", async () => { + const onProgress = vi.fn(); + const promise = requestPageConnectedNodes(undefined, onProgress); + // Attach the rejection assertion NOW, before advancing timers — otherwise + // the promise rejects before anything is listening for it and Node flags + // an unhandled rejection even though the test itself would pass. + const rejection = expect(promise).rejects.toThrow( + "Timed out waiting for the page's connected nodes.", + ); + + await vi.advanceTimersByTimeAsync(4 * 60_000); + emit({ + type: "page-connected-nodes-progress", + correlationId: "test-correlation", + done: 50, + total: 250, + }); + + // No further activity — the idle window (reset at the last progress + // message) should now fully elapse and reject the request. + await vi.advanceTimersByTimeAsync(5 * 60_000); + + await rejection; + }); + + it("ignores progress/result messages for a different correlationId", async () => { + const onProgress = vi.fn(); + const promise = requestPageConnectedNodes(undefined, onProgress); + + emit({ + type: "page-connected-nodes-progress", + correlationId: "other-correlation", + done: 10, + total: 500, + }); + expect(onProgress).not.toHaveBeenCalled(); + + emit({ + type: "page-connected-nodes-result", + correlationId: "test-correlation", + nodes: [], + }); + await expect(promise).resolves.toEqual([]); + }); +}); diff --git a/src/ui/lib/api/__tests__/pickers.test.ts b/src/ui/lib/api/__tests__/pickers.test.ts new file mode 100644 index 00000000..7b8f628c --- /dev/null +++ b/src/ui/lib/api/__tests__/pickers.test.ts @@ -0,0 +1,41 @@ +import { auth } from "$ui/lib/stores/auth.svelte"; +import type { TolgeeClient } from "$ui/lib/api/client"; +import { refreshNamespaces } from "$ui/lib/api/pickers"; +import { beforeEach, describe, expect, it } from "vitest"; + +/** Minimal client whose GET resolves to `data` (or throws when `throws`). */ +function fakeClient(opts: { data?: unknown; throws?: boolean }): TolgeeClient { + return { + GET: async () => { + if (opts.throws) throw new Error("network"); + return { data: opts.data }; + }, + } as unknown as TolgeeClient; +} + +beforeEach(() => { + auth.setNamespaces([{ name: "existing" }]); +}); + +describe("refreshNamespaces", () => { + it("replaces the store with the project's used namespaces (incl. a just-pushed one)", async () => { + // The reported bug: after pushing a key under a new namespace, the picker + // must offer it even when no selected node carries it. + await refreshNamespaces( + fakeClient({ data: { _embedded: { namespaces: [{ name: "common" }, { name: "new" }] } } }), + ); + expect(auth.value.namespaces.map((n) => n.name)).toEqual(["common", "new"]); + }); + + it("KEEPS the existing list on a fetch failure (best-effort, unlike startup hydrate)", async () => { + await refreshNamespaces(fakeClient({ throws: true })); + expect(auth.value.namespaces.map((n) => n.name)).toEqual(["existing"]); + }); + + it("drops entries without a name", async () => { + await refreshNamespaces( + fakeClient({ data: { _embedded: { namespaces: [{ name: "a" }, {}, { name: "" }] } } }), + ); + expect(auth.value.namespaces.map((n) => n.name)).toEqual(["a"]); + }); +}); diff --git a/src/ui/lib/api/__tests__/projectMeta.test.ts b/src/ui/lib/api/__tests__/projectMeta.test.ts new file mode 100644 index 00000000..0cdf4fc3 --- /dev/null +++ b/src/ui/lib/api/__tests__/projectMeta.test.ts @@ -0,0 +1,87 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { getProjectMeta } from "../projectMeta"; + +type FetchMock = ReturnType; + +const ORIGINAL_FETCH = globalThis.fetch; + +function installFetchMock(impl: (...args: unknown[]) => Promise) { + const mock: FetchMock = vi.fn(impl as never); + globalThis.fetch = mock as unknown as typeof fetch; + return mock; +} + +function okResponse(body: object) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +beforeEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + vi.restoreAllMocks(); +}); + +describe("getProjectMeta", () => { + it("returns correct metadata from API response", async () => { + installFetchMock(async () => okResponse({ useNamespaces: true, useBranching: true })); + + const result = await getProjectMeta("https://app.tolgee.io", "tg-key", 42); + + expect(result.namespacesFeaturesEnabled).toBe(true); + expect(result.branchingEnabled).toBe(true); + }); + + it("strips trailing slash from apiUrl", async () => { + const mock = installFetchMock(async () => + okResponse({ useNamespaces: false, useBranching: false }), + ); + + await getProjectMeta("https://app.tolgee.io/", "tg-key", 1); + + const firstArg = mock.mock.calls[0]?.[0]; + const calledUrl = firstArg instanceof Request ? firstArg.url : String(firstArg ?? ""); + expect(calledUrl).not.toContain("//v2"); + expect(calledUrl).toMatch(/^https:\/\/app\.tolgee\.io\/v2\//); + }); + + it("sends request to the correct URL /v2/projects/{projectId}", async () => { + const mock = installFetchMock(async () => + okResponse({ useNamespaces: false, useBranching: false }), + ); + + await getProjectMeta("https://app.tolgee.io", "tg-key", 99); + + const firstArg = mock.mock.calls[0]?.[0]; + const calledUrl = firstArg instanceof Request ? firstArg.url : String(firstArg ?? ""); + expect(calledUrl).toBe("https://app.tolgee.io/v2/projects/99"); + }); + + it("throws on non-ok HTTP response", async () => { + installFetchMock( + async () => + new Response(JSON.stringify({ code: "project_not_found" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }), + ); + + await expect(getProjectMeta("https://app.tolgee.io", "tg-key", 1)).rejects.toThrow( + "Failed to load project meta (status 404)", + ); + }); + + it("maps missing/undefined flags as false", async () => { + installFetchMock(async () => okResponse({})); + + const result = await getProjectMeta("https://app.tolgee.io", "tg-key", 1); + + expect(result.namespacesFeaturesEnabled).toBe(false); + expect(result.branchingEnabled).toBe(false); + }); +}); diff --git a/src/ui/lib/api/__tests__/pull.test.ts b/src/ui/lib/api/__tests__/pull.test.ts new file mode 100644 index 00000000..e6c00bf3 --- /dev/null +++ b/src/ui/lib/api/__tests__/pull.test.ts @@ -0,0 +1,519 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTolgeeClient } from "../client"; +import { fetchAllTranslations } from "../pull"; + +// --------------------------------------------------------------------------- +// Fetch mock helpers +// --------------------------------------------------------------------------- + +type FetchMock = ReturnType; + +const ORIGINAL_FETCH = globalThis.fetch; + +function installFetchMock(impl: (...args: unknown[]) => Promise) { + const mock: FetchMock = vi.fn(impl as never); + globalThis.fetch = mock as unknown as typeof fetch; + return mock; +} + +/** Build a well-formed translations API response body. */ +function translationsResponse( + keys: Array<{ + keyName: string; + keyNamespace?: string; + keyIsPlural?: boolean; + translations?: Record; + }>, + opts: { totalElements?: number; nextCursor?: string } = {}, +) { + return { + _embedded: { keys }, + page: { totalElements: opts.totalElements ?? keys.length }, + ...(opts.nextCursor !== undefined ? { nextCursor: opts.nextCursor } : {}), + }; +} + +/** Return a single successful fetch response for a given body. */ +function okResponse(body: unknown) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +/** Extract the URL that was passed in the i-th fetch call. */ +function calledUrl(mock: FetchMock, callIndex = 0): URL { + const arg = mock.mock.calls[callIndex]?.[0]; + const raw = arg instanceof Request ? arg.url : String(arg ?? ""); + return new URL(raw); +} + +/** Extract repeated query-param values (e.g. filterNamespace). */ +function queryAll(url: URL, param: string): string[] { + return url.searchParams.getAll(param); +} + +beforeEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + vi.restoreAllMocks(); +}); + +// --------------------------------------------------------------------------- +// Shared client fixture +// --------------------------------------------------------------------------- + +const API_URL = "https://app.tolgee.io"; +const API_KEY = "tg-test-key"; + +function makeClient() { + return createTolgeeClient(API_URL, API_KEY); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("fetchAllTranslations", () => { + // 1. Returns empty array when API returns no keys + it("returns empty array when API returns no keys", async () => { + installFetchMock(async () => okResponse(translationsResponse([], { totalElements: 0 }))); + + const client = makeClient(); + const result = await fetchAllTranslations(client, { languages: ["en"] }); + + expect(result).toEqual([]); + }); + + // 2. Returns mapped PulledKey items from a single page + it("maps keys from a single page correctly", async () => { + const body = translationsResponse( + [ + { + keyName: "greeting", + keyNamespace: "common", + keyIsPlural: false, + translations: { en: { text: "Hello" }, de: { text: "Hallo" } }, + }, + { + keyName: "items", + keyNamespace: undefined, + keyIsPlural: true, + translations: { en: { text: "{count, plural, one {item} other {items}}" } }, + }, + ], + { totalElements: 2 }, + ); + installFetchMock(async () => okResponse(body)); + + const client = makeClient(); + const result = await fetchAllTranslations(client, { languages: ["en", "de"] }); + + expect(result).toHaveLength(2); + + expect(result[0]).toEqual({ + keyName: "greeting", + keyNamespace: "common", + isPlural: false, + translations: { en: { text: "Hello" }, de: { text: "Hallo" } }, + }); + + expect(result[1]).toEqual({ + keyName: "items", + keyNamespace: undefined, + isPlural: true, + translations: { en: { text: "{count, plural, one {item} other {items}}" } }, + }); + }); + + // 3. Paginates: sends cursor from first page to second page, stops when nextCursor absent + it("paginates using cursor until nextCursor is absent", async () => { + const page1 = translationsResponse( + [{ keyName: "key1", translations: { en: { text: "One" } } }], + { totalElements: 2, nextCursor: "cursor-abc" }, + ); + const page2 = translationsResponse( + [{ keyName: "key2", translations: { en: { text: "Two" } } }], + { totalElements: 2 }, + ); + + let callCount = 0; + const mock = installFetchMock(async () => { + return okResponse(callCount++ === 0 ? page1 : page2); + }); + + const client = makeClient(); + const result = await fetchAllTranslations(client, { languages: ["en"] }); + + // Two fetch calls should have been made + expect(mock).toHaveBeenCalledTimes(2); + + // Second call must include the cursor from the first response + const secondUrl = calledUrl(mock, 1); + expect(secondUrl.searchParams.get("cursor")).toBe("cursor-abc"); + + // Result is the flat union of both pages + expect(result.map((k) => k.keyName)).toEqual(["key1", "key2"]); + }); + + // 4. Calls onProgress after each page with loaded count and total + it("calls onProgress after each page with loaded count and total", async () => { + const page1 = translationsResponse([{ keyName: "a", translations: { en: { text: "A" } } }], { + totalElements: 2, + nextCursor: "cur", + }); + const page2 = translationsResponse([{ keyName: "b", translations: { en: { text: "B" } } }], { + totalElements: 2, + }); + + let callCount = 0; + installFetchMock(async () => okResponse(callCount++ === 0 ? page1 : page2)); + + const onProgress = vi.fn(); + const client = makeClient(); + await fetchAllTranslations(client, { languages: ["en"], onProgress }); + + expect(onProgress).toHaveBeenCalledTimes(2); + // After first page: 1 loaded, total is now known (2) + expect(onProgress).toHaveBeenNthCalledWith(1, 1, 2); + // After second page: 2 loaded, total 2 + expect(onProgress).toHaveBeenNthCalledWith(2, 2, 2); + }); + + // 5. Fetches multiple namespaces in parallel (verify filterNamespace param for each) + it("fetches multiple namespaces in parallel with correct filterNamespace per request", async () => { + // Each namespace gets its own single-page response + const responses: Record = { + ns1: translationsResponse( + [{ keyName: "k1", keyNamespace: "ns1", translations: { en: { text: "K1" } } }], + { totalElements: 1 }, + ), + ns2: translationsResponse( + [{ keyName: "k2", keyNamespace: "ns2", translations: { en: { text: "K2" } } }], + { totalElements: 1 }, + ), + }; + + const mock = installFetchMock(async (input) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + const ns = url.searchParams.get("filterNamespace"); + return okResponse(responses[ns ?? ""] ?? translationsResponse([], {})); + }); + + const client = makeClient(); + const result = await fetchAllTranslations(client, { + languages: ["en"], + namespaces: ["ns1", "ns2"], + }); + + expect(mock).toHaveBeenCalledTimes(2); + + const calledNs = mock.mock.calls.map((call) => { + const arg = call[0]; + const url = new URL(arg instanceof Request ? arg.url : String(arg)); + return url.searchParams.get("filterNamespace"); + }); + expect(calledNs).toContain("ns1"); + expect(calledNs).toContain("ns2"); + + expect(result.map((k) => k.keyName).sort()).toEqual(["k1", "k2"]); + }); + + // 6. namespaces: undefined → no filterNamespace in request + it("sends no filterNamespace when namespaces is undefined", async () => { + const mock = installFetchMock(async () => + okResponse(translationsResponse([], { totalElements: 0 })), + ); + + const client = makeClient(); + await fetchAllTranslations(client, { languages: ["en"], namespaces: undefined }); + + expect(mock).toHaveBeenCalledTimes(1); + const url = calledUrl(mock); + expect(queryAll(url, "filterNamespace")).toEqual([]); + }); + + // 6b. namespaces: [] → also no filterNamespace in request + it("sends no filterNamespace when namespaces is an empty array", async () => { + const mock = installFetchMock(async () => + okResponse(translationsResponse([], { totalElements: 0 })), + ); + + const client = makeClient(); + await fetchAllTranslations(client, { languages: ["en"], namespaces: [] }); + + expect(mock).toHaveBeenCalledTimes(1); + const url = calledUrl(mock); + expect(queryAll(url, "filterNamespace")).toEqual([]); + }); + + // 7. namespaces: [""] → filterNamespace: [""] in request (default namespace) + it('sends filterNamespace="" when namespaces is [""]', async () => { + const mock = installFetchMock(async () => + okResponse(translationsResponse([], { totalElements: 0 })), + ); + + const client = makeClient(); + await fetchAllTranslations(client, { languages: ["en"], namespaces: [""] }); + + expect(mock).toHaveBeenCalledTimes(1); + const url = calledUrl(mock); + expect(queryAll(url, "filterNamespace")).toEqual([""]); + }); + + // 8. Throws error code on API error + it("throws the error code string when the API returns an error object with a code", async () => { + installFetchMock( + async () => + new Response(JSON.stringify({ code: "invalid_project_api_key" }), { + status: 403, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = makeClient(); + await expect(fetchAllTranslations(client, { languages: ["en"] })).rejects.toBe( + "invalid_project_api_key", + ); + }); + + it("throws an Error when the API error has no code field", async () => { + installFetchMock( + async () => + new Response(JSON.stringify({ message: "something went wrong" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = makeClient(); + await expect(fetchAllTranslations(client, { languages: ["en"] })).rejects.toBeInstanceOf(Error); + }); + + // 9. Signal is forwarded to the fetch call + it("forwards the AbortSignal to the underlying fetch call", async () => { + const mock = installFetchMock(async () => + okResponse(translationsResponse([], { totalElements: 0 })), + ); + + const controller = new AbortController(); + const client = makeClient(); + await fetchAllTranslations(client, { languages: ["en"], signal: controller.signal }); + + expect(mock).toHaveBeenCalledTimes(1); + const requestArg: Request = mock.mock.calls[0]?.[0]; + // openapi-fetch may wrap the signal internally; verify it propagates abort. + expect(requestArg.signal).not.toBeNull(); + expect(requestArg.signal.aborted).toBe(false); + controller.abort(); + expect(requestArg.signal.aborted).toBe(true); + }); + + // Additional edge cases + + it("stops paginating when reachedTotal (all keys returned before cursor exhausted)", async () => { + // totalElements = 1, but server returns nextCursor. We should stop after 1 page. + const page1 = translationsResponse( + [{ keyName: "only", translations: { en: { text: "Only" } } }], + { totalElements: 1, nextCursor: "should-never-be-used" }, + ); + + const mock = installFetchMock(async () => okResponse(page1)); + + const client = makeClient(); + const result = await fetchAllTranslations(client, { languages: ["en"] }); + + expect(mock).toHaveBeenCalledTimes(1); + expect(result).toHaveLength(1); + }); + + it("stops paginating when batch is empty even if cursor is present", async () => { + const page1 = translationsResponse([], { totalElements: 0, nextCursor: "ghost-cursor" }); + + const mock = installFetchMock(async () => okResponse(page1)); + + const client = makeClient(); + const result = await fetchAllTranslations(client, { languages: ["en"] }); + + expect(mock).toHaveBeenCalledTimes(1); + expect(result).toEqual([]); + }); + + it("reports total as null until first batch resolves", async () => { + // Two namespaces: ns1 resolves on first call, ns2 resolves on second + // We can only verify the combined onProgress is correct after both finish. + // Use a simpler single-namespace variant and verify first call total is correct. + const page1 = translationsResponse([{ keyName: "x", translations: { en: { text: "X" } } }], { + totalElements: 3, + nextCursor: "c1", + }); + const page2 = translationsResponse( + [ + { keyName: "y", translations: { en: { text: "Y" } } }, + { keyName: "z", translations: { en: { text: "Z" } } }, + ], + { totalElements: 3 }, + ); + + let callCount = 0; + installFetchMock(async () => okResponse(callCount++ === 0 ? page1 : page2)); + + const progressCalls: Array<[number, number | null]> = []; + const client = makeClient(); + await fetchAllTranslations(client, { + languages: ["en"], + onProgress: (loaded, total) => progressCalls.push([loaded, total]), + }); + + expect(progressCalls).toHaveLength(2); + expect(progressCalls[0]).toEqual([1, 3]); // total known after first batch + expect(progressCalls[1]).toEqual([3, 3]); + }); + + it("uses X-API-Key header in requests", async () => { + const mock = installFetchMock(async () => + okResponse(translationsResponse([], { totalElements: 0 })), + ); + + const client = makeClient(); + await fetchAllTranslations(client, { languages: ["en"] }); + + const requestArg: Request = mock.mock.calls[0]?.[0]; + expect(requestArg.headers.get("X-API-Key")).toBe(API_KEY); + }); +}); + +// --------------------------------------------------------------------------- +// fetchAllTranslations — keyNames (task 59: filtered fetch, not full-project) +// --------------------------------------------------------------------------- + +describe("fetchAllTranslations with keyNames", () => { + it("returns [] and never touches the network when keyNames is an empty array", async () => { + const mock = installFetchMock(async () => + okResponse(translationsResponse([], { totalElements: 0 })), + ); + + const client = makeClient(); + const result = await fetchAllTranslations(client, { languages: ["en"], keyNames: [] }); + + expect(result).toEqual([]); + expect(mock).not.toHaveBeenCalled(); + }); + + it("sends filterKeyName and no filterNamespace, merging batch results", async () => { + const mock = installFetchMock(async () => + okResponse( + translationsResponse([ + { keyName: "a", translations: { en: { text: "A" } } }, + { keyName: "b", keyNamespace: "ns1", translations: { en: { text: "B" } } }, + ]), + ), + ); + + const client = makeClient(); + const result = await fetchAllTranslations(client, { + languages: ["en"], + keyNames: ["a", "b"], + }); + + expect(mock).toHaveBeenCalledTimes(1); + const url = calledUrl(mock); + expect(queryAll(url, "filterKeyName")).toEqual(["a", "b"]); + expect(queryAll(url, "filterNamespace")).toEqual([]); + expect(result.map((k) => k.keyName).sort()).toEqual(["a", "b"]); + }); + + it("splits into multiple batches by cumulative name length, one request each", async () => { + // Long names force chunkByLength (MAX_BATCH_CHARS = 3000) to split. + const longName = "x".repeat(1600); + const names = [longName, `${longName}-2`, `${longName}-3`]; + + const mock = installFetchMock(async (input) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + const requested = url.searchParams.getAll("filterKeyName"); + return okResponse( + translationsResponse( + requested.map((keyName) => ({ keyName, translations: { en: { text: keyName } } })), + ), + ); + }); + + const client = makeClient(); + const result = await fetchAllTranslations(client, { languages: ["en"], keyNames: names }); + + // 1600 + 1602 > 3000 -> splits after the first name each time = 3 batches. + expect(mock).toHaveBeenCalledTimes(3); + expect(result.map((k) => k.keyName).sort()).toEqual([...names].sort()); + }); + + it("aggregates onProgress across batches as done key-names / total key-names", async () => { + const longName = "y".repeat(1600); + const names = [longName, `${longName}-2`]; + + installFetchMock(async (input) => { + const url = new URL(input instanceof Request ? input.url : String(input)); + const requested = url.searchParams.getAll("filterKeyName"); + return okResponse( + translationsResponse( + requested.map((keyName) => ({ keyName, translations: { en: { text: keyName } } })), + ), + ); + }); + + const progressCalls: Array<[number, number | null]> = []; + const client = makeClient(); + await fetchAllTranslations(client, { + languages: ["en"], + keyNames: names, + onProgress: (done, total) => progressCalls.push([done, total]), + }); + + expect(progressCalls).toHaveLength(2); + expect(progressCalls.every(([, total]) => total === 2)).toBe(true); + expect(progressCalls.map(([done]) => done).sort((a, b) => a - b)).toEqual([1, 2]); + }); + + it("paginates a single batch via cursor when the server returns one", async () => { + let callCount = 0; + const mock = installFetchMock(async () => { + callCount++; + if (callCount === 1) { + return okResponse( + translationsResponse([{ keyName: "a", translations: { en: { text: "A" } } }], { + nextCursor: "c1", + }), + ); + } + return okResponse( + translationsResponse([{ keyName: "b", translations: { en: { text: "B" } } }]), + ); + }); + + const client = makeClient(); + const result = await fetchAllTranslations(client, { + languages: ["en"], + keyNames: ["a", "b"], + }); + + expect(mock).toHaveBeenCalledTimes(2); + expect(result.map((k) => k.keyName).sort()).toEqual(["a", "b"]); + }); + + it("a failing batch fails the whole call", async () => { + installFetchMock( + async () => + new Response(JSON.stringify({ code: "invalid_project_api_key" }), { + status: 403, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = makeClient(); + await expect( + fetchAllTranslations(client, { languages: ["en"], keyNames: ["a"] }), + ).rejects.toBe("invalid_project_api_key"); + }); +}); diff --git a/src/ui/lib/api/__tests__/screenshots.test.ts b/src/ui/lib/api/__tests__/screenshots.test.ts new file mode 100644 index 00000000..65f300b4 --- /dev/null +++ b/src/ui/lib/api/__tests__/screenshots.test.ts @@ -0,0 +1,120 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { uploadScreenshot } from "../screenshots"; + +type FetchMock = ReturnType; + +const ORIGINAL_FETCH = globalThis.fetch; + +function installFetchMock(impl: (...args: unknown[]) => Promise) { + const mock: FetchMock = vi.fn(impl as never); + globalThis.fetch = mock as unknown as typeof fetch; + return mock; +} + +function makeImage(bytes = [0x89, 0x50, 0x4e, 0x47]): Uint8Array { + return new Uint8Array(bytes); +} + +const DEFAULT_INFO = { location: "figma-frame-1" }; + +beforeEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + vi.restoreAllMocks(); +}); + +describe("uploadScreenshot", () => { + it("returns { id: N } on success", async () => { + installFetchMock( + async () => + new Response(JSON.stringify({ id: 7 }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + const result = await uploadScreenshot( + "https://app.tolgee.io", + "tg-key", + makeImage(), + DEFAULT_INFO, + ); + + expect(result).toEqual({ id: 7 }); + }); + + it("throws when apiUrl is empty string", async () => { + // No fetch mock needed — the guard runs before the request. + await expect(uploadScreenshot("", "tg-key", makeImage(), DEFAULT_INFO)).rejects.toThrow( + "Cannot upload screenshot: API URL is empty", + ); + }); + + it("throws on non-ok HTTP response", async () => { + installFetchMock( + async () => + new Response(JSON.stringify({ code: "unauthorized" }), { + status: 401, + statusText: "Unauthorized", + headers: { "Content-Type": "application/json" }, + }), + ); + + await expect( + uploadScreenshot("https://app.tolgee.io", "bad-key", makeImage(), DEFAULT_INFO), + ).rejects.toThrow("Screenshot upload failed (status 401: Unauthorized)"); + }); + + it("throws when response body has no numeric id", async () => { + installFetchMock( + async () => + new Response(JSON.stringify({}), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + await expect( + uploadScreenshot("https://app.tolgee.io", "tg-key", makeImage(), DEFAULT_INFO), + ).rejects.toThrow("Screenshot upload returned no image id"); + }); + + it("strips trailing slash from apiUrl", async () => { + const mock = installFetchMock( + async () => + new Response(JSON.stringify({ id: 1 }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + await uploadScreenshot("https://app.tolgee.io/", "tg-key", makeImage(), DEFAULT_INFO); + + const firstArg = mock.mock.calls[0]?.[0]; + const calledUrl = firstArg instanceof Request ? firstArg.url : String(firstArg ?? ""); + expect(calledUrl).not.toContain("//v2"); + expect(calledUrl).toMatch(/^https:\/\/app\.tolgee\.io\/v2\//); + }); + + it("sends POST to the correct URL /v2/image-upload", async () => { + const mock = installFetchMock( + async () => + new Response(JSON.stringify({ id: 3 }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + + await uploadScreenshot("https://app.tolgee.io", "tg-key", makeImage(), DEFAULT_INFO); + + const firstArg = mock.mock.calls[0]?.[0]; + const calledUrl = firstArg instanceof Request ? firstArg.url : String(firstArg ?? ""); + expect(calledUrl).toBe("https://app.tolgee.io/v2/image-upload"); + + const options = mock.mock.calls[0]?.[1] as RequestInit | undefined; + expect(options?.method).toBe("POST"); + }); +}); diff --git a/src/ui/lib/api/__tests__/tags.test.ts b/src/ui/lib/api/__tests__/tags.test.ts new file mode 100644 index 00000000..a9492613 --- /dev/null +++ b/src/ui/lib/api/__tests__/tags.test.ts @@ -0,0 +1,77 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTolgeeClient } from "../client"; +import { applyTags } from "../tags"; +import type { TaggableKey } from "../tags"; + +type FetchMock = ReturnType; + +const ORIGINAL_FETCH = globalThis.fetch; + +function installFetchMock(impl: (...args: unknown[]) => Promise) { + const mock: FetchMock = vi.fn(impl as never); + globalThis.fetch = mock as unknown as typeof fetch; + return mock; +} + +const SOME_TAGS: string[] = ["reviewed"]; +const SOME_KEYS: TaggableKey[] = [{ name: "key.one" }, { name: "key.two", namespace: "ns" }]; + +function makeClient() { + return createTolgeeClient("https://app.tolgee.io", "test-key"); +} + +beforeEach(() => { + globalThis.fetch = ORIGINAL_FETCH; +}); + +afterEach(() => { + globalThis.fetch = ORIGINAL_FETCH; + vi.restoreAllMocks(); +}); + +describe("applyTags", () => { + it("returns immediately (no-op) when tagFiltered is empty", async () => { + // No fetch mock — the function must not make any network calls. + await expect(applyTags(makeClient(), [], SOME_KEYS)).resolves.toBeUndefined(); + }); + + it("returns immediately (no-op) when keys is empty", async () => { + await expect(applyTags(makeClient(), SOME_TAGS, [])).resolves.toBeUndefined(); + }); + + it("does not throw on success", async () => { + installFetchMock( + async () => + new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } }), + ); + + await expect(applyTags(makeClient(), SOME_TAGS, SOME_KEYS)).resolves.toBeUndefined(); + }); + + it("throws when the API returns an error response", async () => { + installFetchMock( + async () => + new Response('{"code":"forbidden"}', { + status: 403, + headers: { "Content-Type": "application/json" }, + }), + ); + + await expect(applyTags(makeClient(), SOME_TAGS, SOME_KEYS)).rejects.toThrow( + "Failed to apply tags (status 403)", + ); + }); + + it("sends branch in query params when provided", async () => { + const mock = installFetchMock( + async () => + new Response("{}", { status: 200, headers: { "Content-Type": "application/json" } }), + ); + + await applyTags(makeClient(), SOME_TAGS, SOME_KEYS, "my-branch"); + + const firstArg = mock.mock.calls[0]?.[0]; + const calledUrl = firstArg instanceof Request ? firstArg.url : String(firstArg ?? ""); + expect(calledUrl).toContain("branch=my-branch"); + }); +}); diff --git a/src/ui/lib/api/auth.ts b/src/ui/lib/api/auth.ts new file mode 100644 index 00000000..f01f024e --- /dev/null +++ b/src/ui/lib/api/auth.ts @@ -0,0 +1,91 @@ +import { createTolgeeClient } from "./client"; + +/** + * Best-guess scope sets required by the plugin for different operations. + * Tolgee scopes follow the `{resource}.{action}` pattern. + * + * TODO: revisit once the API surface used by the plugin is finalized — these + * are intentionally conservative and may be refined later. + */ +export const REQUIRED_SCOPES = { + pull: ["translations.view"], + push: ["translations.edit", "keys.create"], + screenshots: ["screenshots.upload"], +} as const; + +export type ValidateApiKeySuccess = { + ok: true; + projectId: number; + scopes: string[]; + userFullName?: string; +}; + +export type ValidateApiKeyFailure = { + ok: false; + error: string; +}; + +export type ValidateApiKeyResult = ValidateApiKeySuccess | ValidateApiKeyFailure; + +/** + * Validates an API key by calling `GET /v2/api-keys/current`. + * + * Returns a discriminated union so callers can branch on `result.ok` without + * dealing with thrown errors. Network failures and unauthorized responses + * are normalized into `{ ok: false, error }` with a stable, translatable + * error key. + */ +export async function validateApiKey( + apiUrl: string, + apiKey: string, +): Promise { + const client = createTolgeeClient(apiUrl, apiKey); + try { + const { data, error, response } = await client.GET("/v2/api-keys/current"); + if (error || !data) { + const status = response?.status; + if (status === 401 || status === 403) { + return { ok: false, error: "auth.invalid_api_key" }; + } + return { ok: false, error: "auth.request_failed" }; + } + + // Schema says `projectId` and `scopes` are required on + // `ApiKeyWithLanguagesModel`, but defensively guard runtime payloads. + const projectId = (data as { projectId?: number }).projectId; + const scopes = (data as { scopes?: string[] }).scopes ?? []; + const userFullName = (data as { userFullName?: string }).userFullName; + + if (typeof projectId !== "number") { + return { ok: false, error: "auth.missing_project_id" }; + } + + return { + ok: true, + projectId, + scopes, + ...(userFullName !== undefined ? { userFullName } : {}), + }; + } catch (err) { + // `openapi-fetch` propagates fetch network errors. Treat any thrown error + // as a network/connectivity failure. + void err; + return { ok: false, error: "auth.network_error" }; + } +} + +/** + * Returns true iff every `required` scope is present in `scopes`. + * + * Note: Tolgee scopes are exact strings such as `translations.view`. We do + * not implement implicit hierarchy expansion here — callers should pass the + * exact scope strings they need. + */ +export function hasRequiredScopes(scopes: string[], required: string[]): boolean { + if (required.length === 0) return true; + const set = new Set(scopes); + for (const r of required) { + if (!set.has(r)) return false; + } + return true; +} diff --git a/src/ui/lib/api/branches.ts b/src/ui/lib/api/branches.ts new file mode 100644 index 00000000..57855248 --- /dev/null +++ b/src/ui/lib/api/branches.ts @@ -0,0 +1,63 @@ +import type { TolgeeClient } from "./client"; + +export type BranchInfo = { name: string; isDefault?: boolean }; + +export async function fetchBranches(client: TolgeeClient): Promise { + // `openapi-fetch` RESOLVES on 4xx/5xx with an `error` field instead of + // throwing. Discarding it turned a failed request into an empty branch list + // on the SUCCESS path: `hydrateBranches` then set `loaded: true`, and + // `isConfiguredBranchMissing` announced that the user's configured branch + // had been deleted — offering an empty picker to replace it — when all that + // had happened was a request failing. + const { data, error } = await client.GET("/v2/projects/branches", {}); + if (error) { + throw new Error( + typeof error === "object" && error && "message" in error + ? String((error as { message?: unknown }).message) + : "Failed to load branches.", + ); + } + const raw = data as { + _embedded?: { branches?: Array<{ name?: string; isDefault?: boolean }> }; + }; + return (raw._embedded?.branches ?? []) + .filter((b): b is { name: string; isDefault?: boolean } => Boolean(b.name)) + .map((b) => ({ name: b.name, isDefault: b.isDefault })); +} + +/** + * Whether the CONFIGURED branch was deleted in Tolgee: it is set, the fetched + * branch list is trustworthy (`branchesLoaded` — a successful fetch, not a + * pending/failed one), and the list doesn't contain it. Mirrors the original + * plugin's `isBranchMissing` warning on the Index view — without it every API + * call would silently target a nonexistent branch. + */ +export function isConfiguredBranchMissing( + configuredBranch: string, + branches: BranchInfo[], + branchesLoaded: boolean, +): boolean { + return ( + branchesLoaded && + configuredBranch !== "" && + !branches.some((b) => b.name === configuredBranch) + ); +} + +/** + * The branch to pre-select when none is chosen yet: the project's default + * branch (`isDefault`, same rule as the original plugin), else the + * conventional "main", else the first — so a branching-enabled project never + * sits on an empty branch. "" only when there are no branches at all. + * NOT `active` — that flag just means "not archived" and can be true for many + * branches at once, so it would pick an arbitrary feature branch. + */ +export function pickDefaultBranch(branches: BranchInfo[]): string { + if (branches.length === 0) return ""; + return ( + branches.find((b) => b.isDefault)?.name ?? + branches.find((b) => b.name === "main")?.name ?? + branches[0]?.name ?? + "" + ); +} diff --git a/src/ui/lib/api/client.ts b/src/ui/lib/api/client.ts new file mode 100644 index 00000000..5979da41 --- /dev/null +++ b/src/ui/lib/api/client.ts @@ -0,0 +1,21 @@ +import createClient from "openapi-fetch"; +import type { paths } from "./schema.generated"; + +export type TolgeeClient = ReturnType; + +/** + * Creates a typed Tolgee API client backed by `openapi-fetch`. + * + * The trailing slash of `apiUrl` is stripped so that callers can pass either + * `https://app.tolgee.io` or `https://app.tolgee.io/` without breaking paths. + */ +export function createTolgeeClient(apiUrl: string, apiKey: string) { + const baseUrl = apiUrl.replace(/\/$/, ""); + return createClient({ + baseUrl, + headers: { + "X-API-Key": apiKey, + "X-Tolgee-SDK-Type": "Figma", + }, + }); +} diff --git a/src/ui/lib/api/keyExistence.ts b/src/ui/lib/api/keyExistence.ts new file mode 100644 index 00000000..6d182588 --- /dev/null +++ b/src/ui/lib/api/keyExistence.ts @@ -0,0 +1,80 @@ +import type { TolgeeClient } from "$ui/lib/api/client"; +import { nsKeyIndex } from "$ui/lib/logic/namespaces"; + +/** A connected key to verify against the server. `ns` undefined = default namespace. */ +export type ConnectedKey = { name: string; ns: string | undefined }; + +/** + * Stable identity for a (namespace, name) pair — used to match a node against + * the "missing" set. ` ` can't appear in a Figma-authored key/namespace, + * so it's a collision-proof separator. `ns` is normalised so undefined and "" + * (both "no namespace" / Tolgee's "") map to the same signature. + */ +export function connectedKeySig(ns: string | undefined, name: string): string { + // Delegates to the shared join so this identity and the `(ns, key)` map + // indexes elsewhere can never drift apart — they answer the same question. + return nsKeyIndex(ns, name); +} + +/** + * The namespace a node's key EFFECTIVELY lives under. When the project has + * namespaces disabled, the whole write pipeline ignores the node's stored `ns` + * (pushFlow/pushDiff strip it), so keys land in the default namespace — a node + * can still carry a stale invisible `ns` (the ns badge is hidden with the + * feature off). Every existence lookup must apply the same rule, or such a + * node gets false-flagged as "key deleted in Tolgee". + */ +export function effectiveNs( + ns: string | undefined, + namespacesEnabled: boolean, +): string | undefined { + return namespacesEnabled ? ns || undefined : undefined; +} + +/** + * Returns the connected keys that NO LONGER EXIST in the Tolgee project, as + * `connectedKeySig` signatures (computed over the EFFECTIVE namespace — see + * `effectiveNs`; lookups against the returned set must use it too). + * + * One `POST /keys/info` with explicit {name, namespace} pairs — the endpoint + * omits keys it can't find, so anything we asked for that isn't in the response + * has been deleted. Explicit pairs make namespace matching exact (including the + * default "" namespace, sent as `namespace: undefined`), with no reliance + * on empty-string query-param quirks. + * + * Fail-safe: on any request error we report nothing missing, so a transient + * failure never false-flags a live link. + */ +export async function fetchMissingKeys( + client: TolgeeClient, + keys: ConnectedKey[], + branch: string, + languageTag: string | undefined, + namespacesEnabled: boolean, +): Promise> { + const missing = new Set(); + if (keys.length === 0) return missing; + + const { data, error } = await client.POST("/v2/projects/keys/info", { + params: { query: { branch: branch || undefined } }, + body: { + keys: keys.map((k) => ({ + name: k.name, + namespace: effectiveNs(k.ns, namespacesEnabled), + })), + // Existence doesn't depend on a language, but the endpoint requires the + // field; send one when we have it, else an empty list. + languageTags: languageTag ? [languageTag] : [], + }, + }); + if (error || !data) return missing; // fail-safe: never flag on error + + const present = new Set( + (data._embedded?.keys ?? []).map((k) => connectedKeySig(k.namespace, k.name)), + ); + for (const k of keys) { + const sig = connectedKeySig(effectiveNs(k.ns, namespacesEnabled), k.name); + if (!present.has(sig)) missing.add(sig); + } + return missing; +} diff --git a/src/ui/lib/api/keys.ts b/src/ui/lib/api/keys.ts new file mode 100644 index 00000000..4c945408 --- /dev/null +++ b/src/ui/lib/api/keys.ts @@ -0,0 +1,114 @@ +import type { TolgeeClient } from "$ui/lib/api/client"; +import type { NodeInfo } from "$shared/types"; +import { inferPluralCount } from "$shared/interpolate"; + +/** + * Search result entry returned by `searchKeys`. + * + * Mirrors the subset of `KeySearchSearchResultModel` from the generated + * OpenAPI schema that the UI cares about. + */ +export type KeySearchResult = { + id: number; + name: string; + namespace: string | null; + description?: string; + translation?: string; + baseTranslation?: string; + plural?: boolean; +}; + +/** + * Calls `GET /v2/projects/keys/search` and normalizes the response into a + * lightweight list of result entries. + * + * Returns an empty array when: + * - `query` is empty/whitespace-only + * - the request fails (callers shouldn't crash on search errors) + */ +export async function searchKeys( + client: TolgeeClient, + query: string, + languageTag: string | undefined, + size = 20, + branch?: string, +): Promise { + const trimmed = query.trim(); + if (!trimmed) return []; + + const { data, error } = await client.GET("/v2/projects/keys/search", { + params: { + query: { + search: trimmed, + size, + languageTag, + // Scope the search to the configured branch (branching projects only) — + // otherwise a key that lives on the working branch wouldn't be found, + // and a default-branch key would wrongly match. Omitted when empty. + branch: branch || undefined, + }, + }, + }); + + if (error || !data) return []; + + // The search endpoint also returns RECENTLY DELETED keys (they carry + // `deletedAt`). Drop them, or callers (auto-connect, Connect) would match and + // link to a key that no longer exists. + const keys = (data._embedded?.keys ?? []).filter((k) => !k.deletedAt); + return keys.map((k) => ({ + id: k.id, + name: k.name, + namespace: k.namespace ?? null, + description: k.description, + translation: k.translation, + baseTranslation: k.baseTranslation, + plural: k.plural, + })); +} + +/** + * Builds the node data to persist when connecting a node to search result `r`. + * + * Mirrors the legacy plugin's connect behaviour: the node adopts the key's + * TRANSLATION, so the canvas layer immediately reflects the Tolgee value. For a + * plural, it stores the sample COUNT (a number, as the original persisted) so + * the key renders and its manual edits are detected; the count is inferred from + * the layer's current text so the layer keeps the plural form it already shows + * (the original stored the arg NAME here, which was a latent bug). Falls back to + * the node's own text / "1" when nothing better is available. + */ +export function connectInfoFromKey( + r: KeySearchResult, + node: Pick, + language: string, +): Partial { + const isPlural = r.plural ?? node.isPlural ?? false; + const icu = r.translation ?? r.baseTranslation ?? ""; + return { + key: r.name, + // "" (not undefined) for a no-namespace key — undefined is dropped by the + // bus's JSON round-trip, leaving the node's old namespace behind. + ns: r.namespace ?? "", + translation: r.translation ?? node.characters ?? "", + isPlural, + pluralParamValue: isPlural + ? (inferPluralCount(icu, node.characters ?? "", language) ?? + (node.pluralParamValue && /^\d+$/.test(node.pluralParamValue) + ? node.pluralParamValue + : "1")) + : undefined, + connected: true, + }; +} + +/** + * Total number of keys in the project (`ProjectStatsModel.keyCount`). Used to + * give the Connect search empty state context — "0 of N keys in Tolgee". + * Returns `null` on any failure so callers can simply omit the count. + */ +export async function fetchProjectKeyCount(client: TolgeeClient): Promise { + const { data, error } = await client.GET("/v2/projects/stats"); + if (error || !data) return null; + return data.keyCount ?? null; +} diff --git a/src/ui/lib/api/keysByName.ts b/src/ui/lib/api/keysByName.ts new file mode 100644 index 00000000..802d4b6d --- /dev/null +++ b/src/ui/lib/api/keysByName.ts @@ -0,0 +1,172 @@ +import type { TolgeeClient } from "$ui/lib/api/client"; + +export type RemoteKeyRow = { + keyName: string; + keyNamespace?: string; + keyIsPlural?: boolean; + keyTags?: Array<{ name: string }>; + translations?: Record; +}; + +export type FetchRemoteKeysOptions = { + filterKeyName: string[]; + filterNamespace?: string[]; + language?: string; + branch?: string; + signal?: AbortSignal; +}; + +const PAGE_SIZE = 1000; + +// `filterKeyName` is a repeated query param — sending too many/too-long names +// in one request risks exceeding proxy/server URL length limits (~8KB), which +// fails outright with a network error (`TypeError: Failed to fetch`). +// +// A fixed name COUNT is not a safe proxy for URL length: key names generated +// from a template like `{elementText}` are the entire camelCased sentence of +// a Figma text layer (often 60-100+ characters each), so even well under a +// count-based cap of 200 the cumulative length can blow the URL budget +// (live-tested: 154 such names succeeded, a bit more started failing). +// Batching by cumulative character length instead keeps every request's URL +// short regardless of how long individual names are. +// +// 3000 is conservative under the typical ~8KB URL/header limit, leaving room +// on the same request for `filterNamespace`, `languages`, `branch`, and for +// URL-encoding overhead (non-ASCII characters can expand to several bytes +// once percent-encoded). +export const MAX_BATCH_CHARS = 3000; + +/** + * Split `names` into batches so that the summed character length of each + * batch stays at or under `maxChars` where possible. A single name that is + * itself longer than `maxChars` still gets sent — alone, in its own batch — + * rather than being dropped or crashing the whole call. + * + * Exported so `pull.ts`'s key-filtered fetch can reuse the exact same + * URL-length-safe batching instead of duplicating the reasoning. + */ +export function chunkByLength(names: string[], maxChars: number): string[][] { + const out: string[][] = []; + let current: string[] = []; + let currentLen = 0; + + for (const name of names) { + if (current.length > 0 && currentLen + name.length > maxChars) { + out.push(current); + current = []; + currentLen = 0; + } + current.push(name); + currentLen += name.length; + } + + if (current.length > 0) out.push(current); + return out; +} + +/** + * Fetch one batch of key names, following `cursor`/`nextCursor` pagination + * until exhausted. A single batch (sized by `chunkByLength`) fits within one + * page in the overwhelming majority of cases, so this loop typically runs + * once — it only keeps going if many namespaces multiply the batch's result + * count past the page size. Mirrors the pagination pattern in `pull.ts`'s + * `fetchAllTranslations`. + */ +async function fetchBatch( + client: TolgeeClient, + filterKeyName: string[], + options: Pick, +): Promise { + const out: RemoteKeyRow[] = []; + let cursor: string | undefined; + + while (true) { + const { data, error } = await client.GET("/v2/projects/translations", { + params: { + query: { + languages: options.language ? [options.language] : undefined, + filterKeyName, + filterNamespace: options.filterNamespace, + size: PAGE_SIZE, + cursor, + branch: options.branch || undefined, + }, + }, + signal: options.signal, + }); + + if (error || !data) { + const code = + (error as { code?: string } | undefined)?.code ?? + (typeof error === "string" ? error : null); + throw code ? new Error(code) : new Error("Failed to fetch remote keys"); + } + + // Response shape: `_embedded.keys` (newer) or `pagedModel._embedded.keys`. + const raw = data as { + _embedded?: { keys?: unknown[] }; + pagedModel?: { _embedded?: { keys?: unknown[] } }; + nextCursor?: string; + }; + const batch = (raw._embedded?.keys ?? raw.pagedModel?._embedded?.keys ?? []) as RemoteKeyRow[]; + out.push(...batch); + + cursor = raw.nextCursor; + if (!cursor || batch.length === 0) { + return out; + } + } +} + +/** + * Fetch a fixed set of keys by name (and optional namespace) for the diff + * preview on the push screen. + * + * `filterKeyName` is split into batches by cumulative character length (see + * `chunkByLength`/`MAX_BATCH_CHARS`), each issued as its own request (in + * parallel — these are independent reads), and each batch follows cursor + * pagination defensively. Results are merged into a single flat array with + * the exact same shape this function returned before batching was + * introduced, so callers (`pushDiff.ts` via `Push.svelte` and + * `pushFlow.ts`) need no changes. + * + * Any batch failing fails the whole call — we throw rather than silently + * returning `[]`, since a swallowed error here previously looked identical + * to "no remote keys found" and made `pushDiff` misreport existing keys as + * deleted on the platform. + * + * `onProgress`, when given, is called once per batch as that SPECIFIC + * batch's `fetchBatch` resolves — not after the whole `Promise.all` settles, + * since batches run in parallel and a large push can have several in flight + * at once. `total` is the full name count, known up front; `done` is the + * cumulative count of names whose batch has already resolved (so it grows + * out of request order when a later batch happens to finish first). + */ +export async function fetchRemoteKeys( + client: TolgeeClient, + options: FetchRemoteKeysOptions, + onProgress?: (done: number, total: number) => void, +): Promise { + if (options.filterKeyName.length === 0) return []; + + const total = options.filterKeyName.length; + const batches = chunkByLength(options.filterKeyName, MAX_BATCH_CHARS); + let done = 0; + const results = await Promise.all( + batches.map(async (filterKeyName) => { + const rows = await fetchBatch(client, filterKeyName, { + filterNamespace: options.filterNamespace, + language: options.language, + branch: options.branch, + signal: options.signal, + }); + // Runs right after THIS batch resolves — safe to mutate the shared + // counter without a lock since JS has no true concurrency here, only + // interleaved microtasks. + done += filterKeyName.length; + onProgress?.(done, total); + return rows; + }), + ); + return results.flat(); +} diff --git a/src/ui/lib/api/pageNodes.ts b/src/ui/lib/api/pageNodes.ts new file mode 100644 index 00000000..1a8eaf3a --- /dev/null +++ b/src/ui/lib/api/pageNodes.ts @@ -0,0 +1,57 @@ +import type { NodeInfo } from "$shared/types"; +import { nextCorrelationId, on, send } from "$ui/lib/bus"; +import { createIdleTimeout } from "$ui/lib/busRequest"; + +// The main thread reports progress via `page-connected-nodes-progress` while +// it scans (only for pages with >100 connected nodes — see `scan.ts`), and +// every such message touches the watchdog below. So this is now a TRUE idle +// timeout: a big-but-alive scan keeps resetting the clock, and only a scan +// that goes silent for the whole 5 minutes trips it. 5 minutes (not 30s) +// because the scan itself reads every connected text node on the page (~5 +// bridge calls each) and a page-wide document at a large company can hold +// thousands of them. +const TIMEOUT_MS = 5 * 60_000; + +/** + * Round-trip to the main thread for every connected text node on the current + * page, independent of the user's selection. Cancels the response listener if + * the caller aborts (svelte-query passes a signal for in-flight cancellation). + * Also gives up after `TIMEOUT_MS` of silence, rejecting with a descriptive + * Error — see `busRequest.ts`. `onProgress`, when given, is called with each + * `page-connected-nodes-progress` message's `done`/`total` (small pages never + * send any, so it may never fire). + */ +export function requestPageConnectedNodes( + signal?: AbortSignal, + onProgress?: (done: number, total: number) => void, +): Promise { + return new Promise((resolve, reject) => { + const correlationId = nextCorrelationId(); + const cleanup = (): void => { + offResult(); + offProgress(); + watchdog.clear(); + signal?.removeEventListener("abort", onAbort); + }; + const watchdog = createIdleTimeout(TIMEOUT_MS, () => { + cleanup(); + reject(new Error("Timed out waiting for the page's connected nodes.")); + }); + const offResult = on("page-connected-nodes-result", (msg) => { + if (msg.correlationId !== correlationId) return; + cleanup(); + resolve(msg.nodes); + }); + const offProgress = on("page-connected-nodes-progress", (msg) => { + if (msg.correlationId !== correlationId) return; + watchdog.touch(); + onProgress?.(msg.done, msg.total); + }); + const onAbort = () => { + cleanup(); + reject(new DOMException("Aborted", "AbortError")); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + send({ type: "request-page-connected-nodes", correlationId }); + }); +} diff --git a/src/ui/lib/api/parentNames.ts b/src/ui/lib/api/parentNames.ts new file mode 100644 index 00000000..40b7836f --- /dev/null +++ b/src/ui/lib/api/parentNames.ts @@ -0,0 +1,35 @@ +import type { KeyParentNames } from "$shared/keyFormat"; +import { nextCorrelationId, on, send } from "$ui/lib/bus"; +import { createIdleTimeout } from "$ui/lib/busRequest"; + +// Single-shot response (no streaming) — a fixed timeout from request time is +// fine, unlike the streamed screenshot/create-copy round-trips. +const TIMEOUT_MS = 30_000; + +/** + * Round-trip to the main thread to resolve the parent placeholder names + * ({component}/{frame}/…) for specific nodes. Used by the bulk "Generate key + * names" action when its template references a parent placeholder the selection + * scan didn't pre-resolve (the saved key format didn't use it). Returns a map + * keyed by node id; a missing id / field means the node has no such ancestor. + * Gives up after `TIMEOUT_MS` of no response, rejecting with a descriptive + * Error — see `busRequest.ts`. + */ +export function resolveParentNames( + nodeIds: string[], +): Promise> { + return new Promise((resolve, reject) => { + const correlationId = nextCorrelationId(); + const watchdog = createIdleTimeout(TIMEOUT_MS, () => { + off(); + reject(new Error("Timed out waiting for parent names to resolve.")); + }); + const off = on("parent-names-result", (msg) => { + if (msg.correlationId !== correlationId) return; + watchdog.clear(); + off(); + resolve(msg.parents); + }); + send({ type: "resolve-parent-names", correlationId, nodeIds }); + }); +} diff --git a/src/ui/lib/api/pickers.ts b/src/ui/lib/api/pickers.ts new file mode 100644 index 00000000..bdfd7a65 --- /dev/null +++ b/src/ui/lib/api/pickers.ts @@ -0,0 +1,85 @@ +import type { TolgeeClient } from "$ui/lib/api/client"; +import { fetchBranches, pickDefaultBranch } from "$ui/lib/api/branches"; +import { auth } from "$ui/lib/stores/auth.svelte"; + +/** + * Fetch the project's languages and used namespaces into the auth store, so the + * header/settings/onboarding pickers are populated without each one re-fetching. + * Errors are swallowed — the UI falls back to "current value only". + * + * Called from BOTH the startup bootstrap (App.svelte, when config already has + * valid credentials) AND a manual Connect (SettingsSectionConnection). Without + * the manual-connect call, a first-time connect (onboarding, or Settings on a + * fresh document) left the language/namespace selects empty until reopen. + */ +export async function hydratePickers(client: TolgeeClient): Promise { + try { + const { data } = await client.GET("/v2/projects/languages", { + params: { query: { size: 1000 } }, + }); + const raw = data as { + _embedded?: { + languages?: Array<{ tag?: string; name?: string; base?: boolean }>; + }; + }; + const list = raw._embedded?.languages ?? []; + const base = list.find((l) => l.base)?.tag ?? ""; + auth.setLanguages( + list + .filter((l): l is { tag: string; name?: string } => Boolean(l.tag)) + .map((l) => ({ tag: l.tag, name: l.name ?? l.tag })), + base, + ); + } catch { + auth.setLanguages([]); + } + // Startup/reconnect: a failed fetch RESETS to [] — the previous project's + // namespaces must not linger after switching projects. + auth.setNamespaces((await fetchUsedNamespaces(client)) ?? []); +} + +/** The project's used namespaces, or null on failure (caller decides whether a + * miss should reset or keep the existing list). */ +async function fetchUsedNamespaces( + client: TolgeeClient, +): Promise<{ name: string }[] | null> { + try { + const { data } = await client.GET("/v2/projects/used-namespaces", {}); + const raw = data as { _embedded?: { namespaces?: Array<{ name?: string }> } }; + return (raw._embedded?.namespaces ?? []) + .filter((n): n is { name: string } => Boolean(n.name)) + .map((n) => ({ name: n.name })); + } catch { + return null; + } +} + +/** + * Re-pull the project's used namespaces into the store — best-effort, so a + * transient failure KEEPS the current list rather than wiping it (unlike the + * startup hydrate). Call after a push: uploading a key under a brand-new + * namespace creates it server-side, and the namespace picker (Index rows, + * bulk "Set namespace") must offer it afterwards even when no currently + * selected node carries it. + */ +export async function refreshNamespaces(client: TolgeeClient): Promise { + const list = await fetchUsedNamespaces(client); + if (list) auth.setNamespaces(list); +} + +/** + * Fetch the project's branches into the auth store and remember the default + * (isDefault → "main" → first). Call only when branching is enabled — so a + * branching-enabled project can pre-fill "main" instead of an empty branch. + * Best-effort; on failure the store is left with an empty list. + */ +export async function hydrateBranches(client: TolgeeClient): Promise { + try { + const branches = await fetchBranches(client); + auth.setBranches(branches, pickDefaultBranch(branches)); + } catch { + // `loaded: false` — a failed fetch says nothing about which branches + // exist, so the missing-branch warning must not fire off this state. + auth.setBranches([], "", false); + } +} diff --git a/src/ui/lib/api/projectMeta.ts b/src/ui/lib/api/projectMeta.ts new file mode 100644 index 00000000..e7e22699 --- /dev/null +++ b/src/ui/lib/api/projectMeta.ts @@ -0,0 +1,46 @@ +export type ProjectMeta = { + name: string; + namespacesFeaturesEnabled: boolean; + branchingEnabled: boolean; +}; + +/** + * Returns project-level metadata required to decide which UI features are + * available (namespaces, branching). + * + * The generated OpenAPI schema (API-key access) does not expose a direct + * `GET /v2/projects/{projectId}` endpoint, so we fall back to raw `fetch`. + */ +export async function getProjectMeta( + apiUrl: string, + apiKey: string, + projectId: number, +): Promise { + const baseUrl = apiUrl.replace(/\/$/, ""); + const url = `${baseUrl}/v2/projects/${projectId}`; + + const response = await fetch(url, { + method: "GET", + headers: { + "X-API-Key": apiKey, + "X-Tolgee-SDK-Type": "Figma", + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to load project meta (status ${response.status})`); + } + + const body = (await response.json()) as { + name?: string; + useNamespaces?: boolean; + useBranching?: boolean; + }; + + return { + name: body.name ?? `Project #${projectId}`, + namespacesFeaturesEnabled: Boolean(body.useNamespaces), + branchingEnabled: Boolean(body.useBranching), + }; +} diff --git a/src/ui/lib/api/pull.ts b/src/ui/lib/api/pull.ts new file mode 100644 index 00000000..74ca3173 --- /dev/null +++ b/src/ui/lib/api/pull.ts @@ -0,0 +1,241 @@ +import type { TolgeeClient } from "$ui/lib/api/client"; +import { MAX_BATCH_CHARS, chunkByLength } from "$ui/lib/api/keysByName"; + +/** + * Flat representation of one Tolgee key + its translations, normalised for + * the diff/apply pipeline. We intentionally keep only what the UI cares about + * so callers don't have to reach into `KeyWithTranslationsModel` fields that + * are unrelated to the pull workflow. + */ +export type PulledKey = { + keyName: string; + keyNamespace?: string; + isPlural: boolean; + translations: Record; +}; + +export type FetchAllTranslationsOptions = { + languages: string[]; + /** + * Namespaces to fetch. Pass `[""]` to filter to the default namespace, + * or an empty array `[]` (or omit) to fetch every namespace in the project + * — no `filterNamespace` is sent in that case, so the server returns keys + * across all namespaces. + */ + namespaces?: string[]; + branch?: string; + pageSize?: number; + /** + * Progress callback. `total` is `null` until the first batch returns + * `page.totalElements`, and may stay `null` if the server omits the field. + */ + onProgress?: (loaded: number, total: number | null) => void; + /** Forwarded to `fetch`; lets svelte-query cancel in-flight requests. */ + signal?: AbortSignal; + /** + * When given (non-empty), fetch ONLY these key names instead of paginating + * the whole project — batched by `chunkByLength`/`MAX_BATCH_CHARS`, the + * same URL-length-safe batching `keysByName.ts`'s `fetchRemoteKeys` uses + * for Push. No `filterNamespace` is sent in this path: callers (Pull, + * CopyView's Download) match each local node to its remote key by the + * node's OWN `ns`, so returning a name across every namespace it exists in + * is correct — `pullDiff` only uses the entries whose `(ns, key)` matches a + * local node either way. + * + * Undefined (or an empty array) falls back to the original unfiltered + * project-wide pagination — still used by CreateCopy/`recreateCopy`, which + * don't know the clone's key set in advance and must fetch broadly. + */ + keyNames?: string[]; +}; + +const DEFAULT_PAGE_SIZE = 1000; + +function toPulledKey(key: { + keyName: string; + keyNamespace?: string; + keyIsPlural?: boolean; + translations?: Record; +}): PulledKey { + const translations: Record = {}; + for (const [lang, view] of Object.entries(key.translations ?? {})) { + translations[lang] = { text: view?.text ?? "" }; + } + return { + keyName: key.keyName, + keyNamespace: key.keyNamespace, + isPlural: Boolean(key.keyIsPlural), + translations, + }; +} + +/** + * Fetch one name-filtered batch, following `cursor`/`nextCursor` pagination + * until exhausted. Mirrors `keysByName.ts`'s `fetchBatch` — a single batch + * (sized by `chunkByLength`) fits within one page in the overwhelming + * majority of cases, so this loop typically runs once. + */ +async function fetchKeyNameBatch( + client: TolgeeClient, + filterKeyName: string[], + languages: string[], + branch: string | undefined, + pageSize: number, + signal: AbortSignal | undefined, +): Promise { + const out: PulledKey[] = []; + let cursor: string | undefined; + + while (true) { + const { data, error } = await client.GET("/v2/projects/translations", { + params: { + query: { + languages, + filterKeyName, + size: pageSize, + cursor, + branch: branch || undefined, + }, + }, + signal, + }); + + if (error) { + const code = + (error as { code?: string }).code ?? (typeof error === "string" ? error : null); + throw code ?? new Error("Failed to fetch translations"); + } + + const batch = data?._embedded?.keys ?? []; + for (const key of batch) out.push(toPulledKey(key)); + + cursor = data?.nextCursor; + if (!cursor || batch.length === 0) return out; + } +} + +/** + * Key-filtered counterpart of the broad pagination below — see `keyNames` on + * `FetchAllTranslationsOptions` for when/why this path is used. + */ +async function fetchTranslationsByKeyNames( + client: TolgeeClient, + keyNames: string[], + options: FetchAllTranslationsOptions, +): Promise { + const { languages, branch, pageSize = DEFAULT_PAGE_SIZE, onProgress, signal } = options; + + const total = keyNames.length; + const batches = chunkByLength(keyNames, MAX_BATCH_CHARS); + let done = 0; + + const results = await Promise.all( + batches.map(async (filterKeyName) => { + const rows = await fetchKeyNameBatch(client, filterKeyName, languages, branch, pageSize, signal); + // Safe without a lock: JS has no true concurrency, only interleaved + // microtasks, and this runs right after THIS batch resolves. + done += filterKeyName.length; + onProgress?.(done, total); + return rows; + }), + ); + return results.flat(); +} + +/** + * Fetches every translation for the requested `languages` across the given + * `namespaces`, paginating via the cursor returned by + * `GET /v2/projects/translations`. + * + * Namespaces are fetched in parallel — each has its own cursor and the server + * returns disjoint key sets. The result is a flat `PulledKey[]`; callers + * disambiguate via `keyName` + `keyNamespace`. + */ +export async function fetchAllTranslations( + client: TolgeeClient, + options: FetchAllTranslationsOptions, +): Promise { + if (options.keyNames) { + if (options.keyNames.length === 0) return []; + return fetchTranslationsByKeyNames(client, options.keyNames, options); + } + + const { + languages, + namespaces, + branch, + pageSize = DEFAULT_PAGE_SIZE, + onProgress, + signal, + } = options; + + // Empty / unspecified `namespaces` means "every namespace" — a single pass + // without `filterNamespace`. The synthetic `null` sentinel drives that + // branch through the same paginated loop. + const nsList: Array = !namespaces || namespaces.length === 0 ? [null] : namespaces; + + // Shared progress aggregator: each namespace pushes its loaded count and + // total; we sum and emit a combined number so the UI sees one bar. + const loadedByNs = new Array(nsList.length).fill(0); + const totalByNs = new Array(nsList.length).fill(null); + + function emitProgress(): void { + const combinedLoaded = loadedByNs.reduce((a, b) => a + b, 0); + const combinedTotal = totalByNs.some((t) => t === null) + ? null + : (totalByNs as number[]).reduce((a, b) => a + b, 0); + onProgress?.(combinedLoaded, combinedTotal); + } + + async function loadNamespace(ns: string | null, index: number): Promise { + const out: PulledKey[] = []; + let cursor: string | undefined; + let nsTotal: number | null = null; + let firstBatch = true; + + while (true) { + const { data, error } = await client.GET("/v2/projects/translations", { + params: { + query: { + languages, + filterNamespace: ns === null ? undefined : [ns], + size: pageSize, + cursor, + branch: branch || undefined, + }, + }, + signal, + }); + + if (error) { + // Surface the error code verbatim so the caller can branch on common + // cases (e.g. "invalid_project_api_key"). + const code = + (error as { code?: string }).code ?? (typeof error === "string" ? error : null); + throw code ?? new Error("Failed to fetch translations"); + } + + const batch = data?._embedded?.keys ?? []; + for (const key of batch) out.push(toPulledKey(key)); + + loadedByNs[index] = out.length; + + if (firstBatch) { + nsTotal = data?.page?.totalElements ?? null; + totalByNs[index] = nsTotal; + firstBatch = false; + } + + emitProgress(); + + cursor = data?.nextCursor; + const reachedTotal = nsTotal !== null && out.length >= nsTotal; + if (!cursor || batch.length === 0 || reachedTotal) { + return out; + } + } + } + + const perNs = await Promise.all(nsList.map((ns, i) => loadNamespace(ns, i))); + return perNs.flat(); +} diff --git a/src/ui/lib/api/push.ts b/src/ui/lib/api/push.ts new file mode 100644 index 00000000..0c68ab61 --- /dev/null +++ b/src/ui/lib/api/push.ts @@ -0,0 +1,98 @@ +import type { TolgeeClient } from "$ui/lib/api/client"; +import type { components } from "$ui/lib/api/schema.generated"; + +export type SingleStepImportResolvableItemRequest = + components["schemas"]["SingleStepImportResolvableItemRequest"]; +export type SimpleImportConflictResult = components["schemas"]["SimpleImportConflictResult"]; +export type RelatedKeyDto = components["schemas"]["RelatedKeyDto"]; + +export type PushOptions = { + branch?: string; + /** + * Controls how Tolgee treats protected REVIEWED translations: + * + * - `RECOMMENDED` (default) — fails for DISABLED translations and + * protected REVIEWED translations. + * - `FORCE_OVERRIDE` — re-submit after explicit user resolution; mapped + * to the server-side `ALL` value (tries to update protected REVIEWED + * translations). + */ + resolutionMode: "RECOMMENDED" | "FORCE_OVERRIDE"; + /** + * When `true`, the server fails the entire import on the first unresolved + * conflict (used for the initial submission). When `false`, the server + * applies what it can and returns the remaining conflicts in the response + * (used for the post-resolution re-submit). + */ + errorOnUnresolvedConflict?: boolean; +}; + +export type PushKeysResult = { + unresolvedConflicts: SimpleImportConflictResult[]; +}; + +/** + * Submit a batch of translation upserts via + * `POST /v2/projects/single-step-import-resolvable`. + * + * The endpoint is designed for "submit-once with conflict resolution": + * callers send a payload, inspect `unresolvedConflicts`, optionally prompt + * the user, and resubmit (typically with `resolutionMode: "FORCE_OVERRIDE"` + * and per-key `resolution` set to `OVERRIDE`). + */ +export async function pushKeys( + client: TolgeeClient, + payload: SingleStepImportResolvableItemRequest[], + options: PushOptions, +): Promise { + const { data, error, response } = await client.POST( + "/v2/projects/single-step-import-resolvable", + { + body: { + keys: payload, + // `errorOnUnresolvedConflict` defaults to `false` so the first call + // collects the conflicts instead of failing fast. + errorOnUnresolvedConflict: options.errorOnUnresolvedConflict ?? false, + overrideMode: options.resolutionMode === "FORCE_OVERRIDE" ? "ALL" : "RECOMMENDED", + branch: options.branch || undefined, + }, + }, + ); + + if (error || !data) { + throw new Error(`Push failed (status ${response?.status ?? "?"})`); + } + + // TODO: tighten when schema refined — the response shape varies in the + // generated schema; this defensively widens it for older API revisions. + const raw = data as { unresolvedConflicts?: SimpleImportConflictResult[] }; + return { + unresolvedConflicts: raw.unresolvedConflicts ?? [], + }; +} + +/** + * Registers "big meta" — the related keys that appear together (in order) in a + * screenshot — via `POST /v2/projects/big-meta`. This feeds Tolgee's in-context + * translation suggestions. + * + * Branch handling deliberately differs from the older published plugin, which + * passed the branch as a `?branch=` query param on this endpoint. The current + * Tolgee API dropped that query param and instead carries the branch PER KEY on + * `RelatedKeyDto.branch`. Verified against the live app.tolgee.io OpenAPI (and + * mirrored in our generated schema): the big-meta operation has NO parameters, + * and `RelatedKeyDto` gained a `branch` field. A query param here would be + * ignored by the current server and silently drop the branch, so the caller + * (`buildRelatedKeys`) sets it on each related key instead. + */ +export async function storeBigMeta( + client: TolgeeClient, + relatedKeysInOrder: RelatedKeyDto[], +): Promise { + const { error, response } = await client.POST("/v2/projects/big-meta", { + body: { relatedKeysInOrder }, + }); + if (error) { + throw new Error(`Storing big-meta failed (status ${response?.status ?? "?"})`); + } +} diff --git a/src/ui/lib/api/schema.generated.ts b/src/ui/lib/api/schema.generated.ts new file mode 100644 index 00000000..fc30ed14 --- /dev/null +++ b/src/ui/lib/api/schema.generated.ts @@ -0,0 +1,18604 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/project/export/jsonZip": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export to ZIP of jsons + * @deprecated + * @description Exports data as ZIP of jsons + */ + get: operations["doExportJsonZip_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/api-keys/current": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get current API key info + * @description Returns info the API key which user currently authenticated with. Otherwise responds with 400 status code. + */ + get: operations["getCurrent_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/api-keys/current-permissions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get current permission info + * @description Returns current PAK or PAT permissions for current user, api-key and project + */ + get: operations["getCurrentPermissions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/auth-provider": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get current third party authentication provider */ + get: operations["getCurrentAuthProvider"]; + put?: never; + post?: never; + /** Initiate provider change to remove current third party authentication provider */ + delete: operations["deleteCurrentAuthProvider"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/auth-provider/change": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get info about authentication provider which can replace the current one */ + get: operations["getChangedAuthProvider"]; + put?: never; + /** Accept change of the third party authentication provider */ + post: operations["acceptChangeAuthProvider"]; + /** Reject change of the third party authentication provider */ + delete: operations["rejectChangeAuthProvider"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/image-upload": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Upload an image for later use */ + post: operations["upload"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/image-upload/{ids}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete uploaded images */ + delete: operations["delete_17"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/notification": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Gets notifications of the currently logged in user, newest is first. */ + get: operations["getNotifications"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/notification-settings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get notification settings + * @description Returns notification settings of the currently logged in user + */ + get: operations["getNotificationsSettings"]; + /** + * Save notification setting + * @description Saves new value for given parameters + */ + put: operations["putNotificationSetting"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/notifications-mark-seen": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Marks notifications of the currently logged in user with given IDs as seen. */ + put: operations["markNotificationsAsSeen"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get all permitted organizations + * @description Returns all organizations, which is current user allowed to view + */ + get: operations["getAll_10"]; + put?: never; + /** Create organization */ + post: operations["create_14"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get one organization */ + get: operations["get_15"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all organization glossaries */ + get: operations["getAll_12"]; + put?: never; + /** Create glossary */ + post: operations["create_15"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries-with-stats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all organization glossaries with some additional statistics */ + get: operations["getAllWithStats"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get glossary */ + get: operations["get_13"]; + /** Update glossary */ + put: operations["update_8"]; + post?: never; + /** Delete glossary */ + delete: operations["delete_7"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}/assigned-projects": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all projects assigned to glossary */ + get: operations["getAssignedProjects"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}/export": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Export glossary terms as CSV */ + get: operations["export"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}/import": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Import glossary terms from CSV */ + post: operations["importCsv"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}/languages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all languages in use by the glossary */ + get: operations["getLanguages"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}/terms": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all glossary terms */ + get: operations["getAll_13"]; + put?: never; + /** Create a new glossary term */ + post: operations["create_16"]; + /** Batch delete multiple terms */ + delete: operations["deleteMultiple"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}/terms/{termId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get glossary term */ + get: operations["get_14"]; + /** Update glossary term */ + put: operations["update_9"]; + post?: never; + /** Delete glossary term */ + delete: operations["delete_8"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}/terms/{termId}/translations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Set a new glossary term translation for language */ + post: operations["update_12"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}/terms/{termId}/translations/{languageTag}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get glossary term translation for language */ + get: operations["get_23"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}/termsIds": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all glossary terms ids */ + get: operations["getAllIds"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/glossaries/{glossaryId}/termsWithTranslations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all glossary terms with translations */ + get: operations["getAllWithTranslations"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{organizationId}/machine-translation-credit-balance": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get credit balance for organization + * @description Returns machine translation credit balance for organization + */ + get: operations["getOrganizationCredits"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/organizations/{slug}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get organization by slug */ + get: operations["get_22"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/pats/current": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Return current PAK + * @description Returns current Personal Access Token. If the request is not authenticated with a Personal Access Token, it will return 400 response status. + */ + get: operations["getCurrent"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get all permitted + * @description Returns all projects where current user has any permission + */ + get: operations["getAll"]; + put?: never; + /** + * Create project + * @description Creates a new project with languages and initial settings. + */ + post: operations["createProject"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/activity": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get project activity */ + get: operations["getActivity_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/activity/revisions/{revisionId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get one revision data */ + get: operations["getSingleRevision_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/batch-jobs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List batch operations */ + get: operations["list_4"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/batch-jobs/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get batch operation */ + get: operations["get_21"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/batch-jobs/{id}/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Stop batch operation + * @description Stops batch operation if possible. + */ + put: operations["cancel_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/big-meta": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Store Big Meta + * @description Stores a bigMeta for a project + */ + post: operations["store_3"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all branches */ + get: operations["all_1"]; + put?: never; + /** Create branch */ + post: operations["create_12"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/find": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get branch by name, or the default branch if name is not provided */ + get: operations["find_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get branch merges */ + get: operations["getBranchMerges_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge/preview": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Creates a merge, dry-runs source branch to target branch and return preview */ + post: operations["dryRunMerge_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge/{mergeId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete branch merge session */ + delete: operations["deleteBranchMerge_1"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge/{mergeId}/apply": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Merge source branch to target branch */ + post: operations["merge_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge/{mergeId}/changes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get branch merge session changes */ + get: operations["getBranchMergeSessionChanges_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge/{mergeId}/changes/{changeId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get single branch merge session change */ + get: operations["getBranchMergeSessionChange_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge/{mergeId}/conflicts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get branch merge session conflicts */ + get: operations["getBranchMergeSessionConflicts_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge/{mergeId}/preview": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get branch merge session preview */ + get: operations["getBranchMergeSessionPreview_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge/{mergeId}/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Refresh branch merge session preview */ + post: operations["refreshBranchMerge_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge/{mergeId}/resolve": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Resolve branch merge session conflicts */ + put: operations["resolveConflict_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/merge/{mergeId}/resolve-all": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Resolve all branch merge session conflicts */ + put: operations["resolveAllConflicts_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/{branchId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Rename branch */ + post: operations["rename_1"]; + /** Delete branch */ + delete: operations["delete_14"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/branches/{branchId}/protected": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Set branch protected flag */ + post: operations["setProtected_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/current-batch-jobs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get all running and pending batch operations + * @description Returns all running and pending batch operations. Completed batch operations are returned only if they are not older than 1 hour. If user doesn't have permission to view all batch operations, only their operations are returned. + */ + get: operations["currentJobs_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/export": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export data + * @description Exports project data in various formats (JSON, properties, YAML, etc.). + * + * ## HTTP Conditional Requests Support + * + * This endpoint supports HTTP conditional requests using both If-Modified-Since and If-None-Match headers: + * + * - **If-Modified-Since header provided**: The server checks if the project data has been modified since the specified date + * - **If-None-Match header provided**: The server checks if the project data has changed by comparing the eTag value + * - **Data not modified**: Returns HTTP 304 Not Modified with empty body + * - **Data modified or no header**: Returns HTTP 200 OK with the exported data, Last-Modified header, and ETag header + * + * The Last-Modified header in the response contains the timestamp of the last project modification, + * and the ETag header contains a unique identifier for the current project state. Both can be used + * for subsequent conditional requests to avoid unnecessary data transfer when the project hasn't changed. + * + * Cache-Control header is set to max-age=0 to ensure validation on each request. + */ + get: operations["exportData_1"]; + put?: never; + /** + * Export data (post) + * @description Exports project data in various formats (JSON, properties, YAML, etc.). + * Useful when exceeding allowed URL size with GET requests. + * + * ## HTTP Conditional Requests Support + * + * This endpoint supports HTTP conditional requests using both If-Modified-Since and If-None-Match headers: + * + * - **If-Modified-Since header provided**: The server checks if the project data has been modified since the specified date + * - **If-None-Match header provided**: The server checks if the project data has changed by comparing the eTag value + * - **Data not modified**: Returns HTTP 304 Not Modified with empty body + * - **Data modified or no header**: Returns HTTP 200 OK with the exported data, Last-Modified header, and ETag header + * + * Note: This endpoint uses a custom implementation that returns 304 Not Modified for all HTTP methods + * (including POST) when conditional headers indicate the data hasn't changed. This differs from Spring's + * default behavior which returns 412 for POST requests, but is appropriate here since POST is used only + * to accommodate large request parameters, not to modify data. + * + * The Last-Modified header in the response contains the timestamp of the last project modification, + * and the ETag header contains a unique identifier for the current project state. Both can be used + * for subsequent conditional requests to avoid unnecessary data transfer when the project hasn't changed. + * + * Cache-Control header is set to max-age=0 to ensure validation on each request. + */ + post: operations["exportPost_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add files + * @description Prepares provided files to import. + */ + post: operations["addFiles_1"]; + /** + * Delete + * @description Deletes prepared import data. + */ + delete: operations["cancelImport_1"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import-settings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Import Settings + * @description Returns import settings for the authenticated user and the project. + */ + get: operations["get_1"]; + /** + * Set Import Settings + * @description Stores import settings for the authenticated user and the project. + */ + put: operations["store_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/all-namespaces": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get namespaces + * @description Returns all existing and imported namespaces + */ + get: operations["getAllNamespaces_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/apply": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Apply import + * @description Imports the data prepared in previous step + */ + put: operations["applyImport_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/apply-streaming": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Apply import (streaming) + * @description Imports the data prepared in previous step. Streams current status. + */ + put: operations["applyImportStreaming_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get result + * @description Returns the result of preparation. + */ + get: operations["getImportResult_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result/files/{fileId}/select-namespace": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Select namespace + * @description Sets namespace for file to import. + */ + put: operations["selectNamespace_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result/files/{importFileId}/issues": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get file issues + * @description Returns issues for uploaded file. + */ + get: operations["getImportFileIssues_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result/languages/{importLanguageId}/reset-existing": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Reset existing language pairing + * @description Resets existing language paired with language to import. + */ + put: operations["resetExistingLanguage_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result/languages/{importLanguageId}/select-existing/{existingLanguageId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Pair existing language + * @description Sets existing language to pair with language to import. Data will be imported to selected existing language when applied. + */ + put: operations["selectExistingLanguage_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result/languages/{languageId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get import language + * @description Returns language prepared to import. + */ + get: operations["getImportLanguage_1"]; + put?: never; + post?: never; + /** + * Delete language + * @description Deletes language prepared to import. + */ + delete: operations["deleteLanguage_3"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result/languages/{languageId}/resolve-all/set-keep-existing": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Resolve all translation conflicts (keep existing) + * @description Resolves all translation conflicts for provided language. The old translations will be kept. + */ + put: operations["resolveTranslationSetKeepExisting_3"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result/languages/{languageId}/resolve-all/set-override": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Resolve all translation conflicts (override) + * @description Resolves all translation conflicts for provided language. The old translations will be overridden. + */ + put: operations["resolveTranslationSetOverride_3"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result/languages/{languageId}/translations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get translations + * @description Returns translations prepared to import. + */ + get: operations["getImportTranslations_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result/languages/{languageId}/translations/{translationId}/resolve/set-keep-existing": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Resolve conflict (keep existing) + * @description Resolves translation conflict. The old translation will be kept. + */ + put: operations["resolveTranslationSetKeepExisting_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/import/result/languages/{languageId}/translations/{translationId}/resolve/set-override": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Resolve conflict (override) + * @description Resolves translation conflict. The old translation will be overridden. + */ + put: operations["resolveTranslationSetOverride_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all keys */ + get: operations["getAll_8"]; + put?: never; + /** Create new key */ + post: operations["create_8"]; + /** + * Delete one or multiple keys (post) + * @description Delete one or multiple keys by their IDs in request body. Useful for larger requests esxceeding allowed URL length. + */ + delete: operations["delete_12"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/create": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create new key */ + post: operations["create_7"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/import": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Import keys + * @description Imports new keys with translations. If key already exists, its translations and tags are not updated. + */ + post: operations["importKeys_3"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/import-resolvable": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Import keys (resolvable) + * @deprecated + * @description Import's new keys with translations. Translations can be updated, when specified.\n\n + * DEPRECATED: Use /v2/projects/{projectId}/single-step-import-resolvable instead. + */ + post: operations["importKeys_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/info": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get key info + * @description Returns information about keys. (KeyData, Screenshots, Translation in specified language)If key is not found, it's not included in the response. + */ + post: operations["getInfo_2"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/search": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Search for keys + * @description This endpoint helps you to find desired key by keyName, base translation or translation in specified language. + * + * Sort is ignored for this request. + */ + get: operations["searchForKey_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/select": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Select keys + * @description Returns all key IDs for specified filter values. This way, you can apply the same filter as in the translation view and get the resulting key IDs for future use. + */ + get: operations["selectKeys_3"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/trash": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List trashed keys */ + get: operations["list_6"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/trash/deleters": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List users who deleted keys */ + get: operations["listDeleters_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/trash/select-all": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Select all trashed key IDs matching the filter */ + get: operations["selectAll_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/trash/{keyId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Permanently delete a trashed key */ + delete: operations["permanentlyDelete_1"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/trash/{keyId}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Restore a trashed key */ + put: operations["restore_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/{ids}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete one or multiple keys */ + delete: operations["delete_16"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get one key */ + get: operations["get_9"]; + /** Edit key name */ + put: operations["edit_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/{id}/big-meta": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Big Meta for key */ + get: operations["getBigMeta_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/{id}/complex-update": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Edit key and related data + * @description Edits key name, translations, tags, screenshots, and other data + */ + put: operations["complexEdit_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/{id}/disabled-languages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get disabled languages + * @description Returns languages, in which key is disabled + */ + get: operations["getDisabledLanguages_1"]; + /** + * Set disabled languages + * @description Sets languages, in which key is disabled + */ + put: operations["setDisabledLanguages_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/{keyId}/auto-translate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Auto translates keys + * @description Uses enabled auto-translation methods. + * You need to set at least one of useMachineTranslation or useTranslationMemory to true. + * + * This will replace the the existing translation with the result obtained from specified source! + */ + put: operations["autoTranslate_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/{keyId}/screenshots": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get screenshots */ + get: operations["getKeyScreenshots"]; + put?: never; + /** Upload screenshot */ + post: operations["uploadScreenshot"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/{keyId}/screenshots/{ids}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete screenshots */ + delete: operations["deleteScreenshots"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/{keyId}/tags": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Tag key + * @description Tags a key with tag. If tag with provided name doesn't exist, it is created + */ + put: operations["tagKey_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/keys/{keyId}/tags/{tagId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Remove tag + * @description Removes tag with provided id from key with provided id + */ + delete: operations["removeTag_1"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/labels": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get available project labels */ + get: operations["getAll_2"]; + put?: never; + /** Create label */ + post: operations["createLabel_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/labels/ids": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get labels by ids */ + get: operations["getLabelsByIds_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/labels/{labelId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update label */ + put: operations["updateLabel_1"]; + post?: never; + /** Delete label */ + delete: operations["deleteLabel_1"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/languages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all languages */ + get: operations["getAll_6"]; + put?: never; + /** Create language */ + post: operations["createLanguage_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/languages/{languageId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get one language */ + get: operations["get_7"]; + /** Update language */ + put: operations["editLanguage_1"]; + post?: never; + /** Delete specific language */ + delete: operations["deleteLanguage_1"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/languages/{languageId}/key/{keyId}/suggestion": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get suggestions */ + get: operations["getSuggestions_1"]; + put?: never; + /** Create translation suggestion */ + post: operations["createSuggestion_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/languages/{languageId}/key/{keyId}/suggestion/{suggestionId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete suggestion + * @description User can only delete suggestion created by them + */ + delete: operations["deleteSuggestion_1"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/languages/{languageId}/key/{keyId}/suggestion/{suggestionId}/accept": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Accept suggestion */ + put: operations["acceptSuggestion_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/languages/{languageId}/key/{keyId}/suggestion/{suggestionId}/decline": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Decline suggestion */ + put: operations["declineSuggestion_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/languages/{languageId}/key/{keyId}/suggestion/{suggestionId}/set-active": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Set suggestion active */ + put: operations["suggestionSetActive_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/my-batch-jobs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List user batch operations + * @description List all batch operations started by current user + */ + get: operations["myList_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/namespace-by-name/{name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get namespace by name + * @description Returns information about a namespace by its name + */ + get: operations["getByName_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/namespaces": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get namespaces */ + get: operations["getAllNamespaces_3"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/namespaces/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update namespace */ + put: operations["update_4"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/single-step-import": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Single step import + * @description Unlike the /v2/projects/{projectId}/import endpoint, imports the data in single request by provided files and parameters. This is useful for automated importing via API or CLI. + */ + post: operations["singleStepFromFiles_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/single-step-import-resolvable": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Single step import from body */ + post: operations["singleStepResolvableImport_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/ai-playground-translate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Translates via llm and stores result in AiPlaygroundResult */ + post: operations["aiPlaygroundTranslate_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/assign-translation-label": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Assign labels to translations */ + post: operations["assignTranslationLabel_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/clear-translations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Clear translation values + * @description Clear translation values for provided keys in selected languages. + */ + post: operations["clearTranslations_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/copy-translations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Copy translation values + * @description Copy translation values from one language to other languages. + */ + post: operations["copyTranslations_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/delete-keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Delete keys */ + post: operations["deleteKeys_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/hard-delete-keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Permanently delete soft-deleted keys */ + post: operations["hardDeleteKeys_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/machine-translate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Machine Translation + * @description Translate provided keys to provided languages through primary MT provider. + */ + post: operations["machineTranslation_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/pre-translate-by-tm": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Pre-translate by TM + * @description Pre-translate provided keys to provided languages by TM. + */ + post: operations["translate_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/qa-check": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Rerun QA checks for translations of selected keys */ + post: operations["qaCheck_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/restore-keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Restore soft-deleted keys */ + post: operations["restoreKeys_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/set-keys-namespace": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Set keys namespace */ + post: operations["setKeysNamespace_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/set-translation-state": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Set translation state */ + post: operations["setTranslationState_3"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/tag-keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Add tags */ + post: operations["tagKeys_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/unassign-translation-label": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Unassign labels from translations */ + post: operations["unassignTranslationLabel_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/start-batch-job/untag-keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Remove tags */ + post: operations["untagKeys_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/stats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get project stats */ + get: operations["getProjectStats_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/stats/daily-activity": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get project daily amount of events */ + get: operations["getProjectDailyActivity_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/stats/qa-issue-counts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get QA issue counts grouped by check type for a language */ + get: operations["getQaIssueCountsByCheckType_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/suggest/machine-translations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get machine translation suggestions + * @description Suggests machine translations from enabled services + */ + post: operations["suggestMachineTranslations_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/suggest/machine-translations-streaming": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get machine translation suggestions (streaming) + * @description Suggests machine translations from enabled services. The results are streamed to the output in ndjson format. If an error occurs when for any service provider used, the error information is returned as a part of the result item, while the response has 200 status code. + */ + post: operations["suggestMachineTranslationsStreaming_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/suggest/translation-memory": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get suggestions from translation memory + * @description Suggests machine translations from translation memory. The result is always sorted by similarity, so sorting is not supported. + */ + post: operations["suggestTranslationMemory_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tag-complex": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Execute complex tag operation */ + put: operations["executeComplexTagOperation_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tags": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get tags */ + get: operations["getAll_15"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get tasks */ + get: operations["getTasks_1"]; + put?: never; + /** Create task */ + post: operations["createTask_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/calculate-scope": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Calculate scope */ + post: operations["calculateScope_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/create-multiple-tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create multiple tasks */ + post: operations["createTasks_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/possible-assignees": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get possible assignees */ + get: operations["getPossibleAssignees_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/{taskNumber}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get task */ + get: operations["getTask_1"]; + /** Update task */ + put: operations["updateTask_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/{taskNumber}/blocking-tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get blocking task numbers + * @description If the tasks is blocked by other tasks, it returns numbers of these tasks. + */ + get: operations["getBlockingTasks_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/{taskNumber}/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Close task */ + put: operations["cancelTask_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/{taskNumber}/close": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Close task + * @deprecated + */ + put: operations["closeTask_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/{taskNumber}/finish": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Finish task */ + put: operations["finishTask_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/{taskNumber}/keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get task keys */ + get: operations["getTaskKeys_1"]; + /** Add or remove task keys */ + put: operations["updateTaskKeys_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/{taskNumber}/keys/{keyId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Update task key + * @description Mark key as done, which updates task progress. + */ + put: operations["updateTaskKey_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/{taskNumber}/per-user-report": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get report + * @description Detailed statistics for every assignee + */ + get: operations["getPerUserReport_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/{taskNumber}/reopen": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Reopen task */ + put: operations["reopenTask_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/tasks/{taskNumber}/xlsx-report": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get report in XLSX + * @description Detailed statistics about the task results + */ + get: operations["getXlsxReport_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get translations in project */ + get: operations["getTranslations_1"]; + /** + * Update translations for existing key + * @description Sets translations for existing key + */ + put: operations["setTranslations_1"]; + /** + * Create key or update translations + * @description Sets translations for existing key or creates new key and sets the translations to it. + */ + post: operations["createOrUpdateTranslations_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/create-comment": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create translation comment + * @description Creates a translation comment. Empty translation is stored, when not exists. + */ + post: operations["create_4"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/label": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Add label to translation by key and language id */ + put: operations["assignLabel_3"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/select-all": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Select keys + * @description Returns all key IDs for specified filter values. This way, you can apply the same filter as in the translation view and get the resulting key IDs for future use. + */ + get: operations["selectKeys_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/{languages}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get all translations + * @description Returns all translations for specified languages + */ + get: operations["getAllTranslations_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/{translationId}/comments": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get translation comments + * @description Returns translation comments of translation + */ + get: operations["getAll_4"]; + put?: never; + /** Create translation comment */ + post: operations["create_2"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/{translationId}/comments/{commentId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get one translation comment */ + get: operations["get_5"]; + /** Update translation comment */ + put: operations["update_2"]; + post?: never; + /** Delete translation comment */ + delete: operations["delete_3"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/{translationId}/comments/{commentId}/set-state/{state}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Set state of translation comment */ + put: operations["setState_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/{translationId}/dismiss-auto-translated-state": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Dismiss auto-translated + * @description Removes "auto translated" indication + */ + put: operations["dismissAutoTranslatedState_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/{translationId}/history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get translation history + * @description Sorting is not supported for supported. It is automatically sorted from newest to oldest. + */ + get: operations["getTranslationHistory_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/{translationId}/label/{labelId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Add label to translation */ + put: operations["assignLabel_1"]; + post?: never; + /** Remove label from translation */ + delete: operations["unassignLabel_1"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/{translationId}/set-outdated-flag/{state}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Set outdated value + * @description Set's "outdated" flag indicating the base translation was changed without updating current translation. + */ + put: operations["setOutdated_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/translations/{translationId}/set-state/{state}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Set translation state */ + put: operations["setTranslationState_1"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/projects/used-namespaces": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get used namespaces + * @description Returns all used project namespaces. Response contains default (null) namespace if used. + */ + get: operations["getUsedNamespaces_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/user": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get user info + * @description Returns information about currently authenticated user. + */ + get: operations["getInfo"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/user-tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get user tasks */ + get: operations["getTasks_2"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v2/user/sso": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get information about SSO configuration + * @description Returns information about sso configuration affecting the user. + */ + get: operations["getSso"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + AcceptAuthProviderChangeRequest: { + id: string; + }; + ApiKeyPermissionsModel: { + project: components["schemas"]["SimpleProjectModel"]; + /** + * Format: int64 + * @description The API key's project id or the one provided as query param + */ + projectId: number; + /** + * @description Granted scopes to the user. When user has type permissions, this field contains permission scopes of the type. + * @example [ + * "KEYS_EDIT", + * "TRANSLATIONS_VIEW" + * ] + */ + scopes: ("translations.view" | "translations.edit" | "translations.suggest" | "keys.edit" | "screenshots.upload" | "screenshots.delete" | "screenshots.view" | "activity.view" | "languages.edit" | "admin" | "project.edit" | "members.view" | "members.edit" | "translation-comments.add" | "translation-comments.edit" | "translation-comments.set-state" | "translations.state-edit" | "keys.view" | "keys.delete" | "keys.create" | "batch-jobs.view" | "batch-jobs.cancel" | "translations.batch-by-tm" | "translations.batch-machine" | "content-delivery.manage" | "content-delivery.publish" | "webhooks.manage" | "tasks.view" | "tasks.edit" | "prompts.view" | "prompts.edit" | "translation-labels.manage" | "translation-labels.assign" | "all.view" | "branch.management" | "branch.protected-modify")[]; + /** + * @description List of languages user can change state to. If null, changing state of all language values is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + stateChangeLanguageIds?: number[]; + /** + * @description List of languages user can suggest to. If null, suggesting to all languages is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + suggestLanguageIds?: number[]; + /** + * @description List of languages user can translate to. If null, all languages editing is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + translateLanguageIds?: number[]; + /** + * @description The user's permission type. This field is null if user has assigned granular permissions or if returning API key's permissions + * @enum {string} + */ + type?: "NONE" | "VIEW" | "TRANSLATE" | "REVIEW" | "EDIT" | "MANAGE"; + /** + * @description List of languages user can view. If null, all languages view is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + viewLanguageIds?: number[]; + }; + ApiKeyWithLanguagesModel: { + /** @description Whether branching is enabled and active on this project. */ + branchingEnabled: boolean; + description: string; + /** Format: int64 */ + expiresAt?: number; + /** Format: int64 */ + id: number; + /** Format: int64 */ + lastUsedAt?: number; + /** + * @deprecated + * @description Languages for which user has translate permission. + */ + permittedLanguageIds?: number[]; + /** Format: int64 */ + projectId: number; + projectName: string; + scopes: string[]; + userFullName?: string; + username?: string; + }; + ApplyBranchMergeRequest: { + deleteBranch: boolean; + }; + AuthProviderDto: { + /** @enum {string} */ + authType?: "GOOGLE" | "GITHUB" | "OAUTH2" | "SSO" | "SSO_GLOBAL"; + id?: string; + ssoDomain?: string; + }; + Avatar: { + large: string; + thumbnail: string; + }; + BatchJobModel: { + /** + * Format: int64 + * @description The activity revision id, that stores the activity details of the job + */ + activityRevisionId?: number; + /** @description The user who started the job */ + author?: components["schemas"]["SimpleUserAccountModel"]; + /** + * Format: int64 + * @description The time when the job created + */ + createdAt: number; + /** @description If the job failed, this is the error message */ + errorMessage?: string; + /** + * Format: int64 + * @description Batch job id + */ + id: number; + /** + * Format: int32 + * @description Total items, that have been processed so far + */ + progress: number; + /** + * @description Status of the batch job + * @enum {string} + */ + status: "PENDING" | "RUNNING" | "SUCCESS" | "FAILED" | "CANCELLED"; + /** + * Format: int32 + * @description Total items + */ + totalItems: number; + /** + * @description Type of the batch job + * @enum {string} + */ + type: "AI_PLAYGROUND_TRANSLATE" | "PRE_TRANSLATE_BT_TM" | "MACHINE_TRANSLATE" | "AUTO_TRANSLATE" | "DELETE_KEYS" | "RESTORE_KEYS" | "HARD_DELETE_KEYS" | "SET_TRANSLATIONS_STATE" | "CLEAR_TRANSLATIONS" | "COPY_TRANSLATIONS" | "TAG_KEYS" | "UNTAG_KEYS" | "SET_KEYS_NAMESPACE" | "AUTOMATION" | "BILLING_TRIAL_EXPIRATION_NOTICE" | "ASSIGN_TRANSLATION_LABEL" | "UNASSIGN_TRANSLATION_LABEL" | "QA_CHECK" | "NO_OP"; + /** + * Format: int64 + * @description The time when the job was last updated (status change) + */ + updatedAt: number; + }; + BigMetaDto: { + /** @description Keys in the document used as a context for machine translation. Keys in the same order as they appear in the document. The order is important! We are using it for graph distance calculation. */ + relatedKeysInOrder?: components["schemas"]["RelatedKeyDto"][]; + }; + BranchMergeChangeModel: { + /** @description Languages changed by the merge */ + changedTranslations?: string[]; + /** + * @description Effective resolution used to compute the merged key + * @enum {string} + */ + effectiveResolution?: "SOURCE" | "TARGET"; + /** + * Format: int64 + * @description Branch merge change id + */ + id: number; + /** @description Merged branch key (post-merge result) */ + mergedKey?: components["schemas"]["BranchMergeKeyModel"]; + /** + * @description Type of key conflict resolution + * @enum {string} + */ + resolution?: "SOURCE" | "TARGET"; + /** @description Source branch key */ + sourceKey?: components["schemas"]["BranchMergeKeyModel"]; + /** @description Target branch key */ + targetKey?: components["schemas"]["BranchMergeKeyModel"]; + /** + * @description Change type + * @enum {string} + */ + type: "ADD" | "UPDATE" | "DELETE" | "CONFLICT"; + }; + BranchMergeConflictModel: { + /** @description Languages changed by the merge */ + changedTranslations?: string[]; + /** + * @description Effective resolution used to compute the merged key + * @enum {string} + */ + effectiveResolution?: "SOURCE" | "TARGET"; + /** + * Format: int64 + * @description Branch merge session id + */ + id: number; + /** @description Merged branch key (post-merge result) */ + mergedKey?: components["schemas"]["BranchMergeKeyModel"]; + /** + * @description Type of key conflict resolution + * @enum {string} + */ + resolution?: "SOURCE" | "TARGET"; + /** @description Source branch key */ + sourceKey: components["schemas"]["BranchMergeKeyModel"]; + /** @description Target branch key */ + targetKey: components["schemas"]["BranchMergeKeyModel"]; + }; + BranchMergeKeyModel: { + /** @description Key description */ + keyDescription?: string; + /** + * Format: int64 + * @description Key id + */ + keyId: number; + /** @description Whether key uses plural forms */ + keyIsPlural: boolean; + /** @description Key name */ + keyName: string; + /** @description Namespace of the key */ + namespace?: string; + /** @description Translations indexed by language tag */ + translations: { + [key: string]: components["schemas"]["BranchMergeTranslationModel"]; + }; + }; + BranchMergeModel: { + /** + * Format: int64 + * @description Branch merge id + */ + id: number; + /** + * Format: int32 + * @description Key additions count + */ + keyAdditionsCount: number; + /** + * Format: int32 + * @description Key deletions count + */ + keyDeletionsCount: number; + /** + * Format: int32 + * @description Key updates count + */ + keyModificationsCount: number; + /** + * Format: int32 + * @description Key resolved conflicts count + */ + keyResolvedConflictsCount: number; + /** + * Format: int32 + * @description Key unresoled conflicts count + */ + keyUnresolvedConflictsCount: number; + /** + * Format: int64 + * @description Date of merge. If null, merge was not applied yet + */ + mergedAt?: number; + /** @description Is merge outdated. If true, it means, that either source or target branch data were changed */ + outdated: boolean; + /** + * Format: int64 + * @description Source branch id + */ + sourceBranchId: number; + /** @description Source branch name */ + sourceBranchName: string; + /** + * Format: int64 + * @description Target branch id + */ + targetBranchId: number; + /** @description Target branch name */ + targetBranchName: string; + /** + * Format: int32 + * @description Uncompleted tasks count on source branch + */ + uncompletedTasksCount: number; + }; + BranchMergeRefModel: { + /** + * Format: int64 + * @description Branch merge ID + */ + id: number; + /** + * Format: int64 + * @description Date of merge. If null, merge was not applied yet + */ + mergedAt?: number; + /** @description Target branch name */ + targetBranchName: string; + }; + BranchMergeTranslationModel: { + /** + * Format: int64 + * @description Translation id + */ + id?: number; + /** @description Language tag */ + language: string; + /** @description Whether translation is outdated */ + outdated: boolean; + /** + * @description Translation state + * @enum {string} + */ + state: "UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED"; + /** @description Translation text */ + text?: string; + }; + BranchModel: { + /** @description Indicates whether this branch is currently active (visible and usable for editing translations and keys). Inactive branches are hidden but still stored in the project. */ + active: boolean; + /** @description User who created or owns this branch. Can be null for system-generated branches. */ + author?: components["schemas"]["SimpleUserAccountModel"]; + /** + * Format: int64 + * @description Date of branch creation + */ + createdAt?: number; + /** + * Format: int64 + * @description Unique identifier of the branch + */ + id: number; + /** @description Is branch default */ + isDefault: boolean; + /** @description Is branch protected */ + isProtected: boolean; + /** @description Ongoing (or applied) merge operation related to this branch. Null when the branch is not being merged yet */ + merge?: components["schemas"]["BranchMergeRefModel"]; + /** @description Human-readable name of the branch. Similar to Git branch names, it identifies the feature or purpose of this branch (e.g. 'feature-login-page') */ + name: string; + /** @description Name of the branch this branch was created from */ + originBranchName?: string; + }; + CalculateScopeRequest: { + branch?: string; + keys: number[]; + /** Format: int64 */ + languageId: number; + /** @enum {string} */ + type: "TRANSLATE" | "REVIEW"; + }; + ClearTranslationsRequest: { + keyIds: number[]; + languageIds: number[]; + }; + CollectionModelBatchJobModel: { + _embedded?: { + batchJobs?: components["schemas"]["BatchJobModel"][]; + }; + }; + CollectionModelGlossaryLanguageDto: { + _embedded?: { + glossaryLanguageDtoList?: components["schemas"]["GlossaryLanguageDto"][]; + }; + }; + CollectionModelImportNamespaceModel: { + _embedded?: { + namespaces?: components["schemas"]["ImportNamespaceModel"][]; + }; + }; + CollectionModelKeyWithBaseTranslationModel: { + _embedded?: { + keys?: components["schemas"]["KeyWithBaseTranslationModel"][]; + }; + }; + CollectionModelKeyWithDataModel: { + _embedded?: { + keys?: components["schemas"]["KeyWithDataModel"][]; + }; + }; + CollectionModelLanguageModel: { + _embedded?: { + languages?: components["schemas"]["LanguageModel"][]; + }; + }; + CollectionModelLong: { + _embedded?: { + longList?: number[]; + }; + }; + CollectionModelScreenshotModel: { + _embedded?: { + screenshots?: components["schemas"]["ScreenshotModel"][]; + }; + }; + CollectionModelSimpleProjectModel: { + _embedded?: { + projects?: components["schemas"]["SimpleProjectModel"][]; + }; + }; + CollectionModelSimpleUserAccountModel: { + _embedded?: { + users?: components["schemas"]["SimpleUserAccountModel"][]; + }; + }; + CollectionModelUsedNamespaceModel: { + _embedded?: { + namespaces?: components["schemas"]["UsedNamespaceModel"][]; + }; + }; + ComplexEditKeyDto: { + /** @description Branch of the key. If not provided, default branch will be used */ + branch?: string; + /** @description Custom values of the key. If not provided, custom values won't be modified */ + custom?: { + [key: string]: unknown; + }; + /** @description Description of the key. It's also used as a context for Tolgee AI translator */ + description?: string; + /** @description If key is pluralized. If it will be reflected in the editor. If null, value won't be modified. */ + isPlural?: boolean; + /** + * Format: int32 + * @description Maximum character limit. Null = don't modify. 0 = remove limit. + */ + maxCharLimit?: number; + /** @description Name of the key */ + name: string; + namespace?: string; + /** @description The argument name for the plural. If null, value won't be modified. If isPlural is false, this value will be ignored. */ + pluralArgName?: string; + /** @description Keys in the document used as a context for machine translation. Keys in the same order as they appear in the document. The order is important! We are using it for graph distance calculation. */ + relatedKeysInOrder?: components["schemas"]["RelatedKeyDto"][]; + /** @description IDs of screenshots to delete */ + screenshotIdsToDelete?: number[]; + /** + * @deprecated + * @description Ids of screenshots uploaded with /v2/image-upload endpoint + */ + screenshotUploadedImageIds?: number[]; + screenshotsToAdd?: components["schemas"]["KeyScreenshotDto"][]; + /** @description Translation states to update, if not provided states won't be modified */ + states?: { + [key: string]: "TRANSLATED" | "REVIEWED"; + }; + /** @description Tags of the key. If not provided tags won't be modified */ + tags?: string[]; + /** @description Translations to update */ + translations?: { + [key: string]: string; + }; + /** @description If true, it will fail with 400 (with code plural_forms_data_loss) if plural is disabled and there are plural forms, which would be lost by the action. You can get rid of this warning by setting this value to false. */ + warnOnDataLoss?: boolean; + }; + ComplexTagKeysRequest: { + /** @description Include keys filtered by the provided key information */ + filterKeys?: components["schemas"]["KeyId"][]; + /** @description Exclude keys filtered by the provided key information */ + filterKeysNot?: components["schemas"]["KeyId"][]; + /** @description Include keys filtered by the provided tag information. This filter supports wildcards. For example, `draft-*` will match all tags starting with `draft-`. */ + filterTag?: string[]; + /** @description Exclude keys filtered by the provided tag information. This filter supports wildcards. For example, `draft-*` will match all tags starting with `draft-`. */ + filterTagNot?: string[]; + /** @description Specified tags will be added to filtered keys */ + tagFiltered?: string[]; + /** @description Specified tags will be added to keys not filtered by any of the specified filters. */ + tagOther?: string[]; + /** @description Specified tags will be removed from filtered keys. It supports wildcards. For example, `draft-*` will remove all tags starting with `draft-`. */ + untagFiltered?: string[]; + /** @description Specified tags will be removed from keys not filtered by any of the specified filters. It supports wildcards. For example, `draft-*` will remove all tags starting with `draft-`. */ + untagOther?: string[]; + }; + ComputedPermissionModel: { + /** @enum {string} */ + origin: "ORGANIZATION_BASE" | "DIRECT" | "ORGANIZATION_OWNER" | "NONE" | "SERVER_ADMIN" | "SERVER_SUPPORTER"; + permissionModel?: components["schemas"]["PermissionModel"]; + /** + * @deprecated + * @description Deprecated (use translateLanguageIds). + * + * List of languages current user has TRANSLATE permission to. If null, all languages edition is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + permittedLanguageIds?: number[]; + /** + * @description Granted scopes to the user. When user has type permissions, this field contains permission scopes of the type. + * @example [ + * "KEYS_EDIT", + * "TRANSLATIONS_VIEW" + * ] + */ + scopes: ("translations.view" | "translations.edit" | "translations.suggest" | "keys.edit" | "screenshots.upload" | "screenshots.delete" | "screenshots.view" | "activity.view" | "languages.edit" | "admin" | "project.edit" | "members.view" | "members.edit" | "translation-comments.add" | "translation-comments.edit" | "translation-comments.set-state" | "translations.state-edit" | "keys.view" | "keys.delete" | "keys.create" | "batch-jobs.view" | "batch-jobs.cancel" | "translations.batch-by-tm" | "translations.batch-machine" | "content-delivery.manage" | "content-delivery.publish" | "webhooks.manage" | "tasks.view" | "tasks.edit" | "prompts.view" | "prompts.edit" | "translation-labels.manage" | "translation-labels.assign" | "all.view" | "branch.management" | "branch.protected-modify")[]; + /** + * @description List of languages user can change state to. If null, changing state of all language values is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + stateChangeLanguageIds?: number[]; + /** + * @description List of languages user can suggest to. If null, suggesting to all languages is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + suggestLanguageIds?: number[]; + /** + * @description List of languages user can translate to. If null, all languages editing is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + translateLanguageIds?: number[]; + /** + * @description The user's permission type. This field is null if uses granular permissions + * @enum {string} + */ + type?: "NONE" | "VIEW" | "TRANSLATE" | "REVIEW" | "EDIT" | "MANAGE"; + /** + * @description List of languages user can view. If null, all languages view is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + viewLanguageIds?: number[]; + }; + CopyTranslationRequest: { + keyIds: number[]; + /** Format: int64 */ + sourceLanguageId: number; + targetLanguageIds: number[]; + }; + CreateBranchModel: { + /** + * @description Branch name + * @example feature/new-branch + */ + name: string; + /** + * Format: int64 + * @description Origin branch id + */ + originBranchId: number; + }; + CreateGlossaryRequest: { + /** @description IDs of projects to be assigned to glossary */ + assignedProjectIds: number[]; + /** + * @description Language tag according to BCP 47 definition + * @example cs-CZ + */ + baseLanguageTag: string; + /** + * @description Glossary name + * @example My glossary + */ + name: string; + }; + CreateGlossaryTermWithTranslationRequest: { + /** + * @description A detailed explanation or definition of the glossary term + * @example It's trademark + */ + description: string; + /** @description Specifies whether the term represents a shortened form of a word or phrase */ + flagAbbreviation: boolean; + /** @description When true, the term matching considers uppercase and lowercase characters as distinct */ + flagCaseSensitive: boolean; + /** @description When true, marks this term as prohibited or not recommended for use in translations */ + flagForbiddenTerm: boolean; + /** @description When true, this term will have the same translation across all target languages */ + flagNonTranslatable: boolean; + text: string; + }; + CreateKeyDto: { + branch?: string; + /** + * @description Description of the key + * @example This key is used on homepage. It's a label of sign up button. + */ + description?: string; + /** @description If key is pluralized. If it will be reflected in the editor */ + isPlural: boolean; + /** + * Format: int32 + * @description Maximum character limit for translations of this key. Null means no limit. + */ + maxCharLimit?: number; + /** @description Name of the key */ + name: string; + namespace?: string; + /** @description The argument name for the plural. If null, value will be guessed from the values provided in translations. */ + pluralArgName?: string; + /** @description Keys in the document used as a context for machine translation. Keys in the same order as they appear in the document. The order is important! We are using it for graph distance calculation. */ + relatedKeysInOrder?: components["schemas"]["RelatedKeyDto"][]; + /** + * @deprecated + * @description Ids of screenshots uploaded with /v2/image-upload endpoint + */ + screenshotUploadedImageIds?: number[]; + screenshots?: components["schemas"]["KeyScreenshotDto"][]; + /** @description Translation states to update, if not provided states won't be modified */ + states?: { + [key: string]: "TRANSLATED" | "REVIEWED"; + }; + tags?: string[]; + translations?: { + [key: string]: string; + }; + }; + CreateMultipleTasksRequest: { + tasks: components["schemas"]["CreateTaskRequest"][]; + }; + CreateProjectRequest: { + /** @description Tag of one of created languages, to select it as base language. If not provided, first language will be selected as base. */ + baseLanguageTag?: string; + /** @description Whether to use ICU placeholder visualization in the editor and it's support. */ + icuPlaceholders: boolean; + languages: components["schemas"]["LanguageRequest"][]; + name: string; + /** + * Format: int64 + * @description Organization to create the project in + */ + organizationId: number; + /** @description Slug of your project used in url e.g. "/v2/projects/what-a-project". If not provided, it will be generated */ + slug?: string; + }; + CreateTaskRequest: { + assignees: number[]; + /** @description Branch name. If empty or null, default branch is used. */ + branch?: string; + description: string; + /** + * Format: int64 + * @description Due to date in epoch format (milliseconds). + * @example 1661172869000 + */ + dueDate?: number; + keys: number[]; + /** + * Format: int64 + * @description Id of language, this task is attached to. + * @example 1 + */ + languageId: number; + name?: string; + /** @enum {string} */ + type: "TRANSLATE" | "REVIEW"; + }; + CreateTranslationSuggestionRequest: { + translation: string; + }; + CreateUpdateGlossaryTermResponse: { + term: components["schemas"]["SimpleGlossaryTermModel"]; + translation?: components["schemas"]["GlossaryTermTranslationModel"]; + }; + CreditBalanceModel: { + /** Format: int64 */ + bucketSize: number; + /** Format: int64 */ + creditBalance: number; + /** + * Format: int64 + * @deprecated + * @description Customers were able to buy extra credits separately in the past. + * + * This option is not available anymore and this field is kept only for backward compatibility purposes and is always 0. + */ + extraCreditBalance: number; + }; + DeleteKeysDto: { + /** @description IDs of keys to delete */ + ids: number[]; + }; + DeleteKeysRequest: { + keyIds: number[]; + }; + DeleteMultipleGlossaryTermsRequest: { + termIds: number[]; + }; + DryRunMergeBranchRequest: { + /** + * Format: int64 + * @description Source branch id + */ + sourceBranchId: number; + }; + EditKeyDto: { + /** @description The branch of the key. (When empty or null default branch will be used) */ + branch?: string; + /** + * @description Description of the key + * @example This key is used on homepage. It's a label of sign up button. + */ + description?: string; + /** + * Format: int32 + * @description Maximum character limit for translations of this key. Null means no limit. + */ + maxCharLimit?: number; + name: string; + namespace?: string; + }; + EntityDescriptionWithRelations: { + data: { + [key: string]: unknown; + }; + entityClass: string; + /** Format: int64 */ + entityId: number; + }; + ErrorResponseBody: { + code: string; + params?: unknown[]; + }; + ErrorResponseTyped: { + /** @enum {string} */ + code: "unauthenticated" | "api_access_forbidden" | "api_key_not_found" | "invalid_api_key" | "invalid_project_api_key" | "project_api_key_expired" | "bad_credentials" | "mfa_enabled" | "invalid_otp_code" | "mfa_not_enabled" | "can_not_revoke_own_permissions" | "data_corrupted" | "invitation_code_does_not_exist_or_expired" | "invitation_email_mismatch" | "language_tag_exists" | "language_name_exists" | "language_not_found" | "operation_not_permitted" | "registrations_not_allowed" | "project_not_found" | "resource_not_found" | "scope_not_found" | "key_exists" | "third_party_auth_error_message" | "third_party_auth_no_email" | "third_party_auth_non_matching_email" | "third_party_auth_no_sub" | "third_party_auth_unknown_error" | "email_already_verified" | "third_party_unauthorized" | "third_party_google_workspace_mismatch" | "third_party_switch_initiated" | "third_party_switch_conflict" | "username_already_exists" | "username_or_password_invalid" | "user_already_has_permissions" | "user_already_has_role" | "user_not_found" | "file_not_image" | "file_too_big" | "invalid_timestamp" | "email_not_verified" | "missing_callback_url" | "invalid_jwt_token" | "expired_jwt_token" | "general_jwt_error" | "cannot_find_suitable_address_part" | "slug_not_unique" | "user_is_not_member_of_organization" | "organization_has_no_other_owner" | "user_has_no_project_access" | "user_is_organization_owner" | "cannot_set_your_own_permissions" | "user_is_organization_member" | "property_not_mutable" | "import_language_not_from_project" | "existing_language_not_selected" | "conflict_is_not_resolved" | "language_already_selected" | "cannot_parse_file" | "could_not_resolve_property" | "cannot_add_more_then_100_languages" | "no_languages_provided" | "language_with_base_language_tag_not_found" | "language_not_from_project" | "namespace_not_from_project" | "cannot_delete_base_language" | "key_not_from_project" | "max_screenshots_exceeded" | "translation_not_from_project" | "can_edit_only_own_comment" | "request_parse_error" | "request_validation_error" | "filter_by_value_state_not_valid" | "import_has_expired" | "tag_not_from_project" | "translation_text_too_long" | "invalid_recaptcha_token" | "cannot_leave_owning_project" | "cannot_leave_project_with_organization_role" | "dont_have_direct_permissions" | "tag_too_log" | "too_many_uploaded_images" | "one_or_more_images_not_found" | "screenshot_not_of_key" | "service_not_found" | "too_many_requests" | "translation_not_found" | "out_of_credits" | "key_not_found" | "organization_not_found" | "cannot_find_base_language" | "base_language_not_found" | "no_exported_result" | "cannot_set_your_own_role" | "only_translate_review_or_view_permission_accepts_view_languages" | "oauth2_token_url_not_set" | "oauth2_user_url_not_set" | "email_already_invited_or_member" | "price_not_found" | "invoice_not_from_organization" | "invoice_not_found" | "plan_not_found" | "plan_not_available_any_more" | "no_auto_translation_method" | "cannot_translate_base_language" | "pat_not_found" | "invalid_pat" | "pat_expired" | "operation_unavailable_for_account_type" | "validation_email_is_not_valid" | "current_password_required" | "cannot_create_organization" | "wrong_current_password" | "wrong_param_type" | "user_missing_password" | "expired_super_jwt_token" | "cannot_delete_your_own_account" | "cannot_sort_by_this_column" | "namespace_not_found" | "namespace_exists" | "invalid_authentication_method" | "unknown_sort_property" | "only_review_permission_accepts_state_change_languages" | "only_translate_or_review_permission_accepts_translate_languages" | "cannot_set_language_permissions_for_admin_scope" | "cannot_set_view_languages_without_translations_view_scope" | "cannot_set_translate_languages_without_translations_edit_scope" | "cannot_set_state_change_languages_without_translations_state_edit_scope" | "language_not_permitted" | "scopes_has_to_be_set" | "set_exactly_one_of_scopes_or_type" | "translation_exists" | "import_keys_error" | "provide_only_one_of_screenshots_and_screenshot_uploaded_image_ids" | "multiple_projects_not_supported" | "plan_translation_limit_exceeded" | "feature_not_enabled" | "license_key_not_found" | "cannot_set_view_languages_without_for_level_based_permissions" | "cannot_set_different_translate_and_state_change_languages_for_level_based_permissions" | "cannot_disable_your_own_account" | "subscription_not_found" | "invoice_does_not_have_usage" | "customer_not_found" | "subscription_not_active" | "organization_already_subscribed" | "organization_not_subscribed" | "license_key_used_by_another_instance" | "translation_spending_limit_exceeded" | "credit_spending_limit_exceeded" | "seats_spending_limit_exceeded" | "this_instance_is_already_licensed" | "big_meta_not_from_project" | "mt_service_not_enabled" | "project_not_selected" | "organization_not_selected" | "plan_has_subscribers" | "translation_failed" | "batch_job_not_found" | "no_translations_to_recheck" | "key_exists_in_namespace" | "tag_is_blank" | "execution_failed_on_management_error" | "translation_api_rate_limit" | "cannot_finalize_activity" | "formality_not_supported_by_service" | "language_not_supported_by_service" | "rate_limited" | "pat_access_not_allowed" | "pak_access_not_allowed" | "cannot_modify_disabled_translation" | "azure_config_required" | "s3_config_required" | "content_storage_config_required" | "content_storage_test_failed" | "content_storage_config_invalid" | "invalid_connection_string" | "cannot_create_azure_storage_client" | "s3_access_key_required" | "azure_connection_string_required" | "s3_secret_key_required" | "cannot_store_file_to_content_storage" | "unexpected_error_while_publishing_to_content_storage" | "webhook_responded_with_non_200_status" | "unexpected_error_while_executing_webhook" | "content_storage_is_in_use" | "cannot_set_state_for_missing_translation" | "no_project_id_provided" | "license_key_not_provided" | "subscription_already_canceled" | "user_is_subscribed_to_paid_plan" | "cannot_create_free_plan_without_fixed_type" | "cannot_modify_plan_free_status" | "key_id_not_provided" | "free_self_hosted_seat_limit_exceeded" | "advanced_params_not_supported" | "plural_forms_not_found_for_language" | "nested_plurals_not_supported" | "message_is_not_plural" | "content_outside_plural_forms" | "invalid_plural_form" | "multiple_plurals_not_supported" | "custom_values_json_too_long" | "unsupported_po_message_format" | "plural_forms_data_loss" | "current_user_does_not_own_image" | "user_cannot_view_this_organization" | "user_is_not_owner_of_organization" | "user_is_not_owner_or_maintainer_of_organization" | "pak_created_for_different_project" | "custom_slug_is_only_applicable_for_custom_storage" | "invalid_slug_format" | "batch_job_cancellation_timeout" | "import_failed" | "cannot_add_more_then_1000_languages" | "no_data_to_import" | "multiple_namespaces_mapped_to_single_file" | "multiple_mappings_for_same_file_language_name" | "multiple_mappings_for_null_file_language_name" | "too_many_mappings_for_file" | "missing_placeholder_in_template" | "tag_not_found" | "cannot_parse_encrypted_slack_login_data" | "slack_workspace_not_found" | "cannot_fetch_user_details_from_slack" | "slack_missing_scope" | "slack_not_connected_to_your_account" | "slack_invalid_command" | "slack_not_subscribed_yet" | "slack_connection_failed" | "tolgee_account_already_connected" | "slack_not_configured" | "slack_workspace_already_connected" | "slack_connection_error" | "email_verification_code_not_valid" | "cannot_subscribe_to_free_plan" | "plan_auto_assignment_only_for_free_plans" | "plan_auto_assignment_only_for_private_plans" | "task_not_found" | "task_not_finished" | "task_not_open" | "translation_agency_not_found" | "this_feature_is_not_implemented_in_oss" | "sso_token_exchange_failed" | "sso_user_info_retrieval_failed" | "sso_id_token_expired" | "sso_user_cannot_create_organization" | "sso_cant_verify_user" | "sso_auth_missing_domain" | "sso_domain_not_found_or_disabled" | "authentication_method_disabled" | "native_authentication_disabled" | "invitation_organization_mismatch" | "user_is_managed_by_organization" | "cannot_set_sso_provider_missing_fields" | "namespaces_cannot_be_disabled_when_namespace_exists" | "namespace_cannot_be_used_when_feature_is_disabled" | "sso_domain_not_allowed" | "sso_login_forced_for_this_account" | "use_sso_for_authentication_instead" | "date_has_to_be_in_the_future" | "custom_plan_and_plan_id_cannot_be_set_together" | "specify_plan_id_or_custom_plan" | "custom_plans_has_to_be_private" | "cannot_create_free_plan_with_prices" | "subscription_not_scheduled_for_cancellation" | "cannot_cancel_trial" | "cannot_update_without_modification" | "current_subscription_is_not_trialing" | "sorting_and_paging_is_not_supported_when_using_cursor" | "strings_metric_are_not_supported" | "plan_key_limit_exceeded" | "keys_spending_limit_exceeded" | "plan_seat_limit_exceeded" | "instance_not_using_license_key" | "invalid_path" | "llm_provider_not_found" | "llm_provider_error" | "prompt_not_found" | "llm_provider_not_returned_json" | "llm_template_parsing_error" | "llm_rate_limited" | "llm_provider_timeout" | "no_llm_provider_configured" | "glossary_not_found" | "glossary_term_not_found" | "glossary_term_translation_not_found" | "glossary_non_translatable_term_cannot_be_translated" | "llm_content_filter" | "llm_provider_empty_response" | "label_not_found" | "label_not_from_project" | "label_already_exists" | "filter_by_value_label_not_valid" | "suggestion_not_found" | "user_can_only_delete_his_suggestions" | "cannot_modify_reviewed_translation" | "cannot_modify_keys" | "expect_no_conflict_failed" | "suggestion_cant_be_plural" | "suggestion_must_be_plural" | "duplicate_suggestion" | "unsupported_media_type" | "impersonation_of_admin_by_supporter_not_allowed" | "already_impersonating_user" | "operation_not_permitted_in_read_only_mode" | "file_processing_failed" | "multiple_items_in_chunk_failed" | "branch_not_found" | "cannot_delete_default_branch" | "cannot_delete_branch_with_children" | "branch_already_exists" | "origin_branch_not_found" | "branch_merge_not_found" | "branch_merge_change_not_found" | "branch_merge_revision_not_valid" | "branch_merge_conflicts_not_resolved" | "branch_merge_already_merged" | "feature_not_enabled_for_project" | "export_key_plural_suffix_collision" | "translation_exceeds_char_limit" | "url_not_valid" | "qa_checks_not_enabled"; + params?: unknown[]; + }; + ExistenceEntityDescription: { + data: { + [key: string]: unknown; + }; + entityClass: string; + /** Format: int64 */ + entityId: number; + exists?: boolean; + relations: { + [key: string]: components["schemas"]["EntityDescriptionWithRelations"]; + }; + }; + ExportParams: { + /** + * @description If true, HTML tags are escaped in the exported file. (Supported in the XLIFF format only). + * + * e.g. Key hello will be exported as <b>hello</b> + */ + escapeHtml?: boolean; + /** + * @description This is a template that defines the structure of the resulting .zip file content. + * + * The template is a string that can contain the following placeholders: {namespace}, {languageTag}, + * {androidLanguageTag}, {snakeLanguageTag}, {extension}. + * + * For example, when exporting to JSON with the template `{namespace}/{languageTag}.{extension}`, + * the English translations of the `home` namespace will be stored in `home/en.json`. + * + * The `{snakeLanguageTag}` placeholder is the same as `{languageTag}` but in snake case. (e.g., en_US). + * + * The Android specific `{androidLanguageTag}` placeholder is the same as `{languageTag}` + * but in Android format. (e.g., en-rUS) + */ + fileStructureTemplate?: string; + /** + * @description Filter translations with branch. + * + * By default, default branch is exported. + */ + filterBranch?: string; + /** @description Filter key IDs to be contained in export */ + filterKeyId?: number[]; + /** @description Filter key IDs not to be contained in export */ + filterKeyIdNot?: number[]; + /** @description Filter keys with prefix */ + filterKeyPrefix?: string; + /** @description Filter translations with namespace. By default, all namespaces everything are exported. To export default namespace, use empty string. */ + filterNamespace?: string[]; + /** @description Filter translations with state. By default, all states except untranslated is exported. */ + filterState?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED")[]; + /** + * @description Filter keys tagged by. + * + * This filter works the same as `filterTagIn` but in this cases it accepts single tag only. + */ + filterTag?: string; + /** @description Filter keys tagged by one of provided tags */ + filterTagIn?: string[]; + /** @description Filter keys not tagged by one of provided tags */ + filterTagNotIn?: string[]; + /** + * @description Format to export to + * @enum {string} + */ + format: "JSON" | "JSON_TOLGEE" | "XLIFF" | "PO" | "APPLE_STRINGS_STRINGSDICT" | "APPLE_XLIFF" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" | "PROPERTIES" | "YAML_RUBY" | "YAML" | "JSON_I18NEXT" | "CSV" | "RESX_ICU" | "XLSX" | "APPLE_XCSTRINGS" | "ANDROID_SDK" | "APPLE_SDK"; + /** + * @description Languages to be contained in export. + * + * If null, all languages are exported + * @example en + */ + languages?: string[]; + /** + * @description Message format to be used for export. + * + * e.g. PHP_PO: Hello %s, ICU: Hello {name}. + * + * This property is honored only for generic formats like JSON or YAML. + * For specific formats like `YAML_RUBY` it's ignored. + * @enum {string} + */ + messageFormat?: "C_SPRINTF" | "PHP_SPRINTF" | "JAVA_STRING_FORMAT" | "APPLE_SPRINTF" | "RUBY_SPRINTF" | "I18NEXT" | "ICU" | "PYTHON_PERCENT"; + /** + * @description Delimiter to structure file content. + * + * e.g. For key "home.header.title" would result in {"home": {"header": "title": {"Hello"}}} structure. + * + * When null, resulting file won't be structured. Works only for generic structured formats (e.g. JSON, YAML), + * specific formats like `YAML_RUBY` don't honor this parameter. + */ + structureDelimiter?: string; + /** + * @description If true, for structured formats (like JSON) arrays are supported. + * + * e.g. Key hello[0] will be exported as {"hello": ["..."]} + */ + supportArrays: boolean; + zip: boolean; + }; + GetKeysRequestDto: { + keys: components["schemas"]["KeyDefinitionDto"][]; + /** @description Tags to return language translations in */ + languageTags: string[]; + }; + GlossaryImportResult: { + /** + * Format: int32 + * @description Number of imported terms + * @example 42 + */ + imported: number; + }; + GlossaryLanguageDto: { + /** + * @description Indicates if this is the base (main) language of the glossary + * @example true + */ + base: boolean; + /** + * @description The language code (e.g., 'en' for English) + * @example en + */ + tag: string; + }; + GlossaryModel: { + /** + * @description Language tag for default translations for terms + * @example en + */ + baseLanguageTag: string; + /** Format: int64 */ + id: number; + name: string; + organizationOwner: components["schemas"]["SimpleOrganizationModel"]; + }; + GlossaryTermModel: { + description: string; + /** @description Specifies whether the term represents a shortened form of a word or phrase */ + flagAbbreviation: boolean; + /** @description When true, the term matching considers uppercase and lowercase characters as distinct */ + flagCaseSensitive: boolean; + /** @description When true, marks this term as prohibited or not recommended for use in translations */ + flagForbiddenTerm: boolean; + /** @description When true, this term has the same translation across all target languages */ + flagNonTranslatable: boolean; + glossary: components["schemas"]["GlossaryModel"]; + /** Format: int64 */ + id: number; + translations: components["schemas"]["GlossaryTermTranslationModel"][]; + }; + GlossaryTermTranslationModel: { + languageTag: string; + text: string; + }; + HardDeleteKeysRequest: { + keyIds: number[]; + }; + ImageUploadInfoDto: { + location?: string; + }; + ImportAddFilesResultModel: { + errors: components["schemas"]["ErrorResponseBody"][]; + result?: components["schemas"]["PagedModelImportLanguageModel"]; + warnings: components["schemas"]["ErrorResponseBody"][]; + }; + ImportFileIssueModel: { + /** Format: int64 */ + id: number; + params: components["schemas"]["ImportFileIssueParamModel"][]; + /** @enum {string} */ + type: "KEY_IS_NOT_STRING" | "MULTIPLE_VALUES_FOR_KEY_AND_LANGUAGE" | "VALUE_IS_NOT_STRING" | "KEY_IS_EMPTY" | "VALUE_IS_EMPTY" | "PO_MSGCTXT_NOT_SUPPORTED" | "ID_ATTRIBUTE_NOT_PROVIDED" | "TARGET_NOT_PROVIDED" | "TRANSLATION_TOO_LONG" | "KEY_IS_BLANK" | "TRANSLATION_DEFINED_IN_ANOTHER_FILE" | "INVALID_CUSTOM_VALUES" | "DESCRIPTION_TOO_LONG" | "TRANSLATION_EXCEEDS_CHAR_LIMIT"; + }; + ImportFileIssueParamModel: { + /** @enum {string} */ + type: "KEY_NAME" | "KEY_ID" | "LANGUAGE_ID" | "KEY_INDEX" | "VALUE" | "LINE" | "FILE_NODE_ORIGINAL" | "LANGUAGE_NAME"; + value?: string; + }; + ImportFileMapping: { + /** @description Name of the file to import. This is the name of the file provided in `files` request part or in uploaded archive. */ + fileName: string; + /** + * @description Format of the file. If not provided, Tolgee will try to guess the format from the file name or file contents. + * + * It is recommended to provide these values to prevent any issues with format detection. + * @enum {string} + */ + format?: "CSV_ICU" | "CSV_JAVA" | "CSV_PHP" | "CSV_RUBY" | "JSON_I18NEXT" | "JSON_ICU" | "JSON_JAVA" | "JSON_PHP" | "JSON_RUBY" | "JSON_C" | "PO_PHP" | "PO_C" | "PO_JAVA" | "PO_ICU" | "PO_RUBY" | "PO_PYTHON" | "STRINGS" | "STRINGSDICT" | "APPLE_XLIFF" | "APPLE_XCSTRINGS" | "PROPERTIES_ICU" | "PROPERTIES_JAVA" | "PROPERTIES_UNKNOWN" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" | "YAML_RUBY" | "YAML_JAVA" | "YAML_ICU" | "YAML_PHP" | "YAML_UNKNOWN" | "XLIFF_ICU" | "XLIFF_JAVA" | "XLIFF_PHP" | "XLIFF_RUBY" | "RESX_ICU" | "XLSX_ICU" | "XLSX_JAVA" | "XLSX_PHP" | "XLSX_RUBY"; + /** + * @description The existing language tag in the Tolgee platform to which the imported language should be mapped. + * + * When null, Tolgee will try to guess the language from the file contents or file name. + */ + languageTag?: string; + /** + * @description Tags of languages to be imported. When null, all languages from will be imported. + * + * This field is useful when the file contains multiple languages and you want to import only some of them. For example when importing Apple String Catalog (APPLE_XCSTRINGS), you might want only to import the base language. + */ + languageTagsToImport?: string[]; + /** @description Namespace to import the file to. If not provided, the key will be imported without namespace. */ + namespace?: string; + }; + ImportKeysDto: { + keys: components["schemas"]["ImportKeysItemDto"][]; + }; + ImportKeysItemDto: { + /** + * @description Description of key + * @example This key is used on homepage. It's a label of sign up button. + */ + description?: string; + /** + * @description Key name to set translations for + * @example what_a_key_to_translate + */ + name: string; + /** @description The namespace of the key. (When empty or null default namespace will be used) */ + namespace?: string; + /** + * @description Tags of the key + * @example [ + * "homepage", + * "user-profile" + * ] + */ + tags?: string[]; + /** + * @description Object mapping language tag to translation + * @example { + * "en": "What a translated value!", + * "cs": "Jaká to přeložená hodnota!" + * } + */ + translations: { + [key: string]: string; + }; + }; + ImportKeysResolvableDto: { + keys: components["schemas"]["ImportKeysResolvableItemDto"][]; + }; + ImportKeysResolvableItemDto: { + branch?: string; + /** + * @description Key name to set translations for + * @example what_a_key_to_translate + */ + name: string; + /** @description The namespace of the key. (When empty or null default namespace will be used) */ + namespace?: string; + screenshots?: components["schemas"]["KeyScreenshotDto"][]; + /** @description Object mapping language tag to translation */ + translations: { + [key: string]: components["schemas"]["ImportTranslationResolvableDto"]; + }; + }; + ImportLanguageModel: { + /** Format: int32 */ + conflictCount: number; + /** + * @deprecated + * @description Use existingLanguageTag + */ + existingLanguageAbbreviation?: string; + /** Format: int64 */ + existingLanguageId?: number; + existingLanguageName?: string; + existingLanguageTag?: string; + /** Format: int64 */ + id: number; + /** Format: int64 */ + importFileId: number; + /** Format: int32 */ + importFileIssueCount: number; + importFileName: string; + name: string; + namespace?: string; + /** Format: int32 */ + resolvedCount: number; + /** Format: int32 */ + totalCount: number; + }; + ImportNamespaceModel: { + /** + * Format: int64 + * @description The id of namespace. When null, namespace doesn't exist and will be created by import. + * @example 10000048 + */ + id?: number; + /** @example homepage */ + name: string; + }; + ImportResult: { + unresolvedConflicts?: components["schemas"]["SimpleImportConflictResult"][]; + }; + ImportSettingsModel: { + /** @description If true, placeholders from other formats will be converted to ICU when possible */ + convertPlaceholdersToIcu: boolean; + /** @description If false, only updates keys, skipping the creation of new keys */ + createNewKeys: boolean; + /** @description If true, key descriptions will be overridden by the import */ + overrideKeyDescriptions: boolean; + }; + ImportSettingsRequest: { + /** @description If true, placeholders from other formats will be converted to ICU when possible */ + convertPlaceholdersToIcu: boolean; + /** @description If false, only updates keys, skipping the creation of new keys */ + createNewKeys: boolean; + /** @description If true, key descriptions will be overridden by the import */ + overrideKeyDescriptions: boolean; + }; + ImportTranslationModel: { + /** Format: int64 */ + conflictId?: number; + conflictText?: string; + /** @enum {string} */ + conflictType?: "SHOULD_NOT_EDIT_REVIEWED" | "CANNOT_EDIT_REVIEWED" | "CANNOT_EDIT_DISABLED"; + existingKeyIsPlural: boolean; + /** Format: int64 */ + id: number; + isOverridable: boolean; + isPlural: boolean; + keyDescription?: string; + /** Format: int64 */ + keyId: number; + keyName: string; + override: boolean; + resolved: boolean; + text?: string; + }; + ImportTranslationResolvableDto: { + /** + * @description Determines, how conflict is resolved. + * - KEEP: Translation is not changed + * - OVERRIDE: Translation is overridden + * - NEW: New translation is created + * - FORCE_OVERRIDE: Translation is updated, created or kept. + * @example OVERRIDE + * @enum {string} + */ + resolution: "KEEP" | "OVERRIDE" | "NEW" | "FORCE_OVERRIDE"; + /** + * @description Translation text + * @example Hello! I am a translation! + */ + text: string; + }; + JwtAuthenticationResponse: { + accessToken?: string; + tokenType?: string; + }; + KeyDefinitionDto: { + name: string; + namespace?: string; + }; + KeyId: { + /** + * Format: int64 + * @description If key id is provided, name and namespace are ignored. + */ + id?: number; + name?: string; + namespace?: string; + }; + KeyImportResolvableResultModel: { + /** @description List of keys */ + keys: components["schemas"]["KeyModel"][]; + /** @description Map uploadedImageId to screenshot */ + screenshots: { + [key: string]: components["schemas"]["ScreenshotModel"]; + }; + }; + KeyInScreenshotModel: { + /** Format: int64 */ + keyId: number; + keyName: string; + keyNamespace?: string; + originalText?: string; + position?: components["schemas"]["KeyInScreenshotPosition"]; + }; + KeyInScreenshotPosition: { + /** Format: int32 */ + height: number; + /** Format: int32 */ + width: number; + /** Format: int32 */ + x: number; + /** Format: int32 */ + y: number; + }; + KeyInScreenshotPositionDto: { + /** Format: int32 */ + height: number; + /** Format: int32 */ + width: number; + /** Format: int32 */ + x: number; + /** Format: int32 */ + y: number; + }; + KeyModel: { + /** + * @description Branch of key + * @example dev + */ + branch?: string; + /** @description Custom values of the key */ + custom?: { + [key: string]: unknown; + }; + /** + * @description Description of key + * @example This key is used on homepage. It's a label of sign up button. + */ + description?: string; + /** + * Format: int64 + * @description Id of key record + */ + id: number; + /** + * Format: int32 + * @description Maximum character limit for translations of this key + */ + maxCharLimit?: number; + /** + * @description Name of key + * @example this_is_super_key + */ + name: string; + /** + * @description Namespace of key + * @example homepage + */ + namespace?: string; + }; + KeyScreenshotDto: { + positions?: components["schemas"]["KeyInScreenshotPositionDto"][]; + text?: string; + /** + * Format: int64 + * @description Ids of screenshot uploaded with /v2/image-upload endpoint + */ + uploadedImageId: number; + }; + KeySearchResultView: { + baseTranslation?: string; + /** Format: date-time */ + deletedAt?: string; + deletedByUserAvatarHash?: string; + deletedByUserDeleted?: boolean; + /** Format: int64 */ + deletedByUserId?: number; + deletedByUserName?: string; + deletedByUserUsername?: string; + description?: string; + /** Format: int64 */ + id: number; + name: string; + namespace?: string; + plural?: boolean; + translation?: string; + }; + KeySearchSearchResultModel: { + baseTranslation?: string; + /** Format: date-time */ + deletedAt?: string; + deletedByUserAvatarHash?: string; + deletedByUserDeleted?: boolean; + /** Format: int64 */ + deletedByUserId?: number; + deletedByUserName?: string; + deletedByUserUsername?: string; + description?: string; + /** Format: int64 */ + id: number; + name: string; + namespace?: string; + plural?: boolean; + translation?: string; + view?: components["schemas"]["KeySearchResultView"]; + }; + KeyTaskViewModel: { + done: boolean; + /** Format: int64 */ + languageId: number; + languageTag: string; + /** Format: int64 */ + number: number; + /** @enum {string} */ + type: "TRANSLATE" | "REVIEW"; + userAssigned: boolean; + }; + KeyWithBaseTranslationModel: { + /** + * @description Base translation + * @example This is translation + */ + baseTranslation?: string; + /** + * Format: int64 + * @description Id of key record + */ + id: number; + /** + * @description Name of key + * @example this_is_super_key + */ + name: string; + /** + * @description Namespace of key + * @example homepage + */ + namespace?: string; + }; + KeyWithDataModel: { + /** @description Branch of the key */ + branch?: string; + /** @description Custom values of the key */ + custom: { + [key: string]: unknown; + }; + /** + * @description Description of key + * @example This key is used on homepage. It's a label of sign up button. + */ + description?: string; + /** + * Format: int64 + * @description Id of key record + */ + id: number; + /** @description If key is pluralized. If it will be reflected in the editor */ + isPlural: boolean; + /** + * Format: int32 + * @description Maximum character limit for translations of this key + */ + maxCharLimit?: number; + /** + * @description Name of key + * @example this_is_super_key + */ + name: string; + /** + * @description Namespace of key + * @example homepage + */ + namespace?: string; + /** @description The argument name for the plural */ + pluralArgName?: string; + /** @description Screenshots of the key */ + screenshots: components["schemas"]["ScreenshotModel"][]; + /** @description Tags of key */ + tags: components["schemas"]["TagModel"][]; + /** + * @description Translations object containing values updated in this request + * @example { + * "en": { + * "id": 100000003, + * "text": "This is super translation!" + * } + * } + */ + translations: { + [key: string]: components["schemas"]["TranslationModel"]; + }; + }; + KeyWithTranslationsModel: { + /** + * @description Branch of key + * @example feature-branch + */ + branch?: string; + /** @description There is a context available for this key */ + contextPresent: boolean; + /** + * Format: int64 + * @description The time when the key was created + */ + createdAt: number; + /** + * @description The namespace of the key + * @example homepage + */ + keyDescription?: string; + /** + * Format: int64 + * @description Id of key record + */ + keyId: number; + /** + * @description Is this key a plural? + * @example true + */ + keyIsPlural: boolean; + /** + * Format: int32 + * @description Maximum character limit for translations of this key + */ + keyMaxCharLimit?: number; + /** + * @description Name of key + * @example this_is_super_key + */ + keyName: string; + /** + * @description The namespace of the key + * @example homepage + */ + keyNamespace?: string; + /** + * Format: int64 + * @description The namespace id of the key + * @example 100000282 + */ + keyNamespaceId?: number; + /** + * @description The placeholder name for plural parameter + * @example value + */ + keyPluralArgName?: string; + /** @description Tags of key */ + keyTags: components["schemas"]["TagModel"][]; + /** + * Format: int64 + * @description Count of screenshots provided for the key + * @example 1 + */ + screenshotCount: number; + /** @description Key screenshots. Not provided when API key hasn't screenshots.view scope permission. */ + screenshots?: components["schemas"]["ScreenshotModel"][]; + /** @description Tasks related to this key */ + tasks?: components["schemas"]["KeyTaskViewModel"][]; + /** + * @description Translations object + * @example { + * "en": { + * "id": 100000003, + * "text": "This is super translation!" + * "state": "TRANSLATED", + * "commentCount": 1 + * } + * } + */ + translations: { + [key: string]: components["schemas"]["TranslationViewModel"]; + }; + }; + KeysScopeView: { + /** Format: int64 */ + characterCount: number; + /** Format: int64 */ + keyCount: number; + /** Format: int64 */ + keyCountIncludingConflicts: number; + /** Format: int64 */ + wordCount: number; + }; + KeysWithTranslationsPageModel: { + _embedded?: { + keys?: components["schemas"]["KeyWithTranslationsModel"][]; + }; + /** + * @description Cursor to get next data + * @example eyJrZXlJZCI6eyJkaXJlY3Rpb24iOiJBU0MiLCJ2YWx1ZSI6IjEwMDAwMDAxMjAifX0= + */ + nextCursor?: string; + page?: components["schemas"]["PageMetadata"]; + pagedModel?: components["schemas"]["PagedModelKeyWithTranslationsModel"]; + /** @description Provided languages data */ + selectedLanguages: components["schemas"]["LanguageModel"][]; + }; + LabelModel: { + color: string; + description?: string; + /** Format: int64 */ + id: number; + name: string; + }; + LabelRequest: { + /** + * @description Hex color in format #RRGGBB. + * @example #FF5733 + */ + color: string; + description?: string; + name: string; + }; + LabelTranslationsRequest: { + keyIds: number[]; + labelIds: number[]; + languageIds: number[]; + }; + LanguageMapping: { + /** + * @description The language from the imported file. + * + * For xliff files, this is the `source-language` or the `target-language` attribute value of `file` element. + * @example en-US + */ + importLanguage: string; + /** + * @description The tag of language existing in the Tolgee platform to which the imported language should be mapped. + * @example en-US + */ + platformLanguageTag: string; + }; + LanguageModel: { + /** + * @description Whether is base language of project + * @example false + */ + base: boolean; + /** + * @description Language flag emoji as UTF-8 emoji + * @example 🇨🇿 + */ + flagEmoji?: string; + /** Format: int64 */ + id: number; + /** + * @description Language name in english + * @example Czech + */ + name: string; + /** + * @description Language name in this language + * @example čeština + */ + originalName?: string; + /** + * @description Language tag according to BCP 47 definition + * @example cs-CZ + */ + tag: string; + }; + LanguageRequest: { + /** + * @description Language flag emoji as UTF-8 emoji + * @example 🇨🇿 + */ + flagEmoji?: string; + /** + * @description Language name in english + * @example Czech + */ + name: string; + /** + * @description Language name in this language + * @example čeština + */ + originalName: string; + /** + * @description Language tag according to BCP 47 definition + * @example cs-CZ + */ + tag: string; + }; + LanguageStatsModel: { + languageFlagEmoji?: string; + /** Format: int64 */ + languageId?: number; + languageName?: string; + languageOriginalName?: string; + languageTag?: string; + /** Format: int64 */ + qaChecksStaleCount: number; + /** Format: int64 */ + qaIssueCount: number; + /** Format: int64 */ + reviewedKeyCount: number; + /** Format: double */ + reviewedPercentage: number; + /** Format: int64 */ + reviewedWordCount: number; + /** Format: int64 */ + translatedKeyCount: number; + /** Format: double */ + translatedPercentage: number; + /** Format: int64 */ + translatedWordCount: number; + /** Format: date-time */ + translationsUpdatedAt?: string; + /** Format: int64 */ + untranslatedKeyCount: number; + /** Format: double */ + untranslatedPercentage: number; + /** Format: int64 */ + untranslatedWordCount: number; + }; + MachineTranslationRequest: { + keyIds: number[]; + llmPrompt?: components["schemas"]["PromptDto"]; + targetLanguageIds: number[]; + }; + ModifiedEntityModel: { + description?: { + [key: string]: unknown; + }; + entityClass: string; + /** Format: int64 */ + entityId: number; + exists?: boolean; + modifications?: { + [key: string]: components["schemas"]["PropertyModification"]; + }; + relations?: { + [key: string]: components["schemas"]["ExistenceEntityDescription"]; + }; + }; + NamespaceModel: { + /** + * Format: int64 + * @description The id of namespace + * @example 10000048 + */ + id: number; + /** @example homepage */ + name: string; + }; + NotificationModel: { + /** Format: date-time */ + createdAt?: string; + /** Format: int64 */ + id: number; + linkedTask?: components["schemas"]["TaskModel"]; + originatingUser?: components["schemas"]["SimpleUserAccountModel"]; + project?: components["schemas"]["SimpleProjectModel"]; + /** @enum {string} */ + type: "TASK_ASSIGNED" | "TASK_FINISHED" | "TASK_CANCELED" | "MFA_ENABLED" | "MFA_DISABLED" | "PASSWORD_CHANGED"; + }; + NotificationSettingGroupModel: { + email: boolean; + inApp: boolean; + }; + NotificationSettingModel: { + accountSecurity: components["schemas"]["NotificationSettingGroupModel"]; + tasks: components["schemas"]["NotificationSettingGroupModel"]; + }; + NotificationSettingsRequest: { + /** + * @example IN_APP + * @enum {string} + */ + channel: "IN_APP" | "EMAIL"; + /** + * @description True if the setting should be enabled, false for disabled + * @example false + */ + enabled: boolean; + /** + * @example TASKS + * @enum {string} + */ + group: "ACCOUNT_SECURITY" | "TASKS"; + }; + NotificationsMarkSeenRequest: { + /** + * @description Notification IDs to be marked as seen + * @example [ + * 1, + * 2, + * 3 + * ] + */ + notificationIds: number[]; + }; + OrganizationDto: { + /** @example This is a beautiful organization full of beautiful and clever people */ + description?: string; + /** @example Beautiful organization */ + name: string; + /** @example btforg */ + slug?: string; + }; + OrganizationModel: { + /** @example Links to avatar images */ + avatar?: components["schemas"]["Avatar"]; + basePermissions: components["schemas"]["PermissionModel"]; + /** + * @description The role of currently authorized user. + * + * Can be null when user has direct access to one of the projects owned by the organization. + * @enum {string} + */ + currentUserRole?: "MEMBER" | "OWNER" | "MAINTAINER"; + /** @example This is a beautiful organization full of beautiful and clever people */ + description?: string; + /** Format: int64 */ + id: number; + /** @example Beautiful organization */ + name: string; + /** @example btforg */ + slug: string; + }; + PageMetadata: { + /** Format: int64 */ + number?: number; + /** Format: int64 */ + size?: number; + /** Format: int64 */ + totalElements?: number; + /** Format: int64 */ + totalPages?: number; + }; + PagedModelBatchJobModel: { + _embedded?: { + batchJobs?: components["schemas"]["BatchJobModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelBranchMergeChangeModel: { + _embedded?: { + branchMergeChanges?: components["schemas"]["BranchMergeChangeModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelBranchMergeConflictModel: { + _embedded?: { + branchMergeConflicts?: components["schemas"]["BranchMergeConflictModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelBranchMergeModel: { + _embedded?: { + branchMerges?: components["schemas"]["BranchMergeModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelBranchModel: { + _embedded?: { + branches?: components["schemas"]["BranchModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelImportFileIssueModel: { + _embedded?: { + importFileIssues?: components["schemas"]["ImportFileIssueModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelImportLanguageModel: { + _embedded?: { + languages?: components["schemas"]["ImportLanguageModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelImportTranslationModel: { + _embedded?: { + translations?: components["schemas"]["ImportTranslationModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelKeyModel: { + _embedded?: { + keys?: components["schemas"]["KeyModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelKeySearchSearchResultModel: { + _embedded?: { + keys?: components["schemas"]["KeySearchSearchResultModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelKeyWithTranslationsModel: { + _embedded?: { + keys?: components["schemas"]["KeyWithTranslationsModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelLabelModel: { + _embedded?: { + labels?: components["schemas"]["LabelModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelLanguageModel: { + _embedded?: { + languages?: components["schemas"]["LanguageModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelNamespaceModel: { + _embedded?: { + namespaces?: components["schemas"]["NamespaceModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelNotificationModel: { + _embedded?: { + notificationModelList?: components["schemas"]["NotificationModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelOrganizationModel: { + _embedded?: { + organizations?: components["schemas"]["OrganizationModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelProjectActivityModel: { + _embedded?: { + activities?: components["schemas"]["ProjectActivityModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelProjectModel: { + _embedded?: { + projects?: components["schemas"]["ProjectModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelSimpleGlossaryModel: { + _embedded?: { + glossaries?: components["schemas"]["SimpleGlossaryModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelSimpleGlossaryTermModel: { + _embedded?: { + glossaryTerms?: components["schemas"]["SimpleGlossaryTermModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelSimpleGlossaryTermWithTranslationsModel: { + _embedded?: { + glossaryTerms?: components["schemas"]["SimpleGlossaryTermWithTranslationsModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelSimpleGlossaryWithStatsModel: { + _embedded?: { + glossaries?: components["schemas"]["SimpleGlossaryWithStatsModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelSimpleUserAccountModel: { + _embedded?: { + users?: components["schemas"]["SimpleUserAccountModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelTagModel: { + _embedded?: { + tags?: components["schemas"]["TagModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelTaskModel: { + _embedded?: { + tasks?: components["schemas"]["TaskModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelTaskWithProjectModel: { + _embedded?: { + tasks?: components["schemas"]["TaskWithProjectModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelTranslationCommentModel: { + _embedded?: { + translationComments?: components["schemas"]["TranslationCommentModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelTranslationHistoryModel: { + _embedded?: { + revisions?: components["schemas"]["TranslationHistoryModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelTranslationMemoryItemModel: { + _embedded?: { + translationMemoryItems?: components["schemas"]["TranslationMemoryItemModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelTranslationSuggestionModel: { + _embedded?: { + suggestions?: components["schemas"]["TranslationSuggestionModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelTrashedKeyWithTranslationsModel: { + _embedded?: { + keys?: components["schemas"]["TrashedKeyWithTranslationsModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + PagedModelWithNextCursorNotificationModel: { + _embedded?: { + notificationModelList?: components["schemas"]["NotificationModel"][]; + }; + /** + * @description Cursor to get next data + * @example eyJrZXlJZCI6eyJkaXJlY3Rpb24iOiJBU0MiLCJ2YWx1ZSI6IjEwMDAwMDAxMjAifX0= + */ + nextCursor?: string; + page?: components["schemas"]["PageMetadata"]; + pagedModel?: components["schemas"]["PagedModelNotificationModel"]; + }; + PatWithUserModel: { + /** Format: int64 */ + createdAt: number; + description: string; + /** Format: int64 */ + expiresAt?: number; + /** Format: int64 */ + id: number; + /** Format: int64 */ + lastUsedAt?: number; + /** Format: int64 */ + updatedAt: number; + user: components["schemas"]["SimpleUserAccountModel"]; + }; + PermissionModel: { + /** + * @deprecated + * @description Deprecated (use translateLanguageIds). + * + * List of languages current user has TRANSLATE permission to. If null, all languages edition is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + permittedLanguageIds?: number[]; + /** + * @description Granted scopes to the user. When user has type permissions, this field contains permission scopes of the type. + * @example [ + * "KEYS_EDIT", + * "TRANSLATIONS_VIEW" + * ] + */ + scopes: ("translations.view" | "translations.edit" | "translations.suggest" | "keys.edit" | "screenshots.upload" | "screenshots.delete" | "screenshots.view" | "activity.view" | "languages.edit" | "admin" | "project.edit" | "members.view" | "members.edit" | "translation-comments.add" | "translation-comments.edit" | "translation-comments.set-state" | "translations.state-edit" | "keys.view" | "keys.delete" | "keys.create" | "batch-jobs.view" | "batch-jobs.cancel" | "translations.batch-by-tm" | "translations.batch-machine" | "content-delivery.manage" | "content-delivery.publish" | "webhooks.manage" | "tasks.view" | "tasks.edit" | "prompts.view" | "prompts.edit" | "translation-labels.manage" | "translation-labels.assign" | "all.view" | "branch.management" | "branch.protected-modify")[]; + /** + * @description List of languages user can change state to. If null, changing state of all language values is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + stateChangeLanguageIds?: number[]; + /** + * @description List of languages user can suggest to. If null, suggesting to all languages is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + suggestLanguageIds?: number[]; + /** + * @description List of languages user can translate to. If null, all languages editing is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + translateLanguageIds?: number[]; + /** + * @description The user's permission type. This field is null if uses granular permissions + * @enum {string} + */ + type?: "NONE" | "VIEW" | "TRANSLATE" | "REVIEW" | "EDIT" | "MANAGE"; + /** + * @description List of languages user can view. If null, all languages view is permitted. + * @example [ + * 200001, + * 200004 + * ] + */ + viewLanguageIds?: number[]; + }; + PreTranslationByTmRequest: { + keyIds: number[]; + targetLanguageIds: number[]; + }; + PrivateUserAccountModel: { + /** @enum {string} */ + accountType: "LOCAL" | "MANAGED" | "THIRD_PARTY"; + avatar?: components["schemas"]["Avatar"]; + deletable: boolean; + domain?: string; + emailAwaitingVerification?: string; + /** @enum {string} */ + globalServerRole: "USER" | "ADMIN" | "SUPPORTER"; + /** Format: int64 */ + id: number; + mfaEnabled: boolean; + name?: string; + needsSuperJwtToken: boolean; + /** @enum {string} */ + thirdPartyAuthType?: "GOOGLE" | "GITHUB" | "OAUTH2" | "SSO" | "SSO_GLOBAL"; + username: string; + }; + ProjectActivityAuthorModel: { + avatar?: components["schemas"]["Avatar"]; + deleted: boolean; + /** Format: int64 */ + id: number; + name?: string; + username?: string; + }; + ProjectActivityModel: { + author?: components["schemas"]["ProjectActivityAuthorModel"]; + counts?: { + [key: string]: number; + }; + meta?: { + [key: string]: unknown; + }; + modifiedEntities?: { + [key: string]: components["schemas"]["ModifiedEntityModel"][]; + }; + params?: unknown; + /** Format: int64 */ + revisionId: number; + /** Format: int64 */ + timestamp: number; + /** @enum {string} */ + type: "UNKNOWN" | "SET_TRANSLATION_STATE" | "SET_TRANSLATIONS" | "DISMISS_AUTO_TRANSLATED_STATE" | "SET_OUTDATED_FLAG" | "TRANSLATION_COMMENT_ADD" | "TRANSLATION_COMMENT_DELETE" | "TRANSLATION_COMMENT_EDIT" | "TRANSLATION_COMMENT_SET_STATE" | "SCREENSHOT_DELETE" | "SCREENSHOT_ADD" | "KEY_TAGS_EDIT" | "KEY_NAME_EDIT" | "KEY_CHARACTER_LIMIT_EDIT" | "KEY_DELETE" | "KEY_SOFT_DELETE" | "KEY_RESTORE" | "KEY_HARD_DELETE" | "BATCH_KEY_RESTORE" | "BATCH_KEY_HARD_DELETE" | "CREATE_KEY" | "COMPLEX_EDIT" | "IMPORT" | "CREATE_LANGUAGE" | "EDIT_LANGUAGE" | "DELETE_LANGUAGE" | "HARD_DELETE_LANGUAGE" | "CREATE_PROJECT" | "EDIT_PROJECT" | "NAMESPACE_EDIT" | "BATCH_PRE_TRANSLATE_BY_TM" | "BATCH_MACHINE_TRANSLATE" | "AUTO_TRANSLATE" | "BATCH_CLEAR_TRANSLATIONS" | "BATCH_COPY_TRANSLATIONS" | "BATCH_SET_TRANSLATION_STATE" | "BATCH_TAG_KEYS" | "BATCH_UNTAG_KEYS" | "BATCH_SET_KEYS_NAMESPACE" | "BATCH_ASSIGN_TRANSLATION_LABEL" | "BATCH_UNASSIGN_TRANSLATION_LABEL" | "AUTOMATION" | "CONTENT_DELIVERY_CONFIG_CREATE" | "CONTENT_DELIVERY_CONFIG_UPDATE" | "CONTENT_DELIVERY_CONFIG_DELETE" | "CONTENT_STORAGE_CREATE" | "CONTENT_STORAGE_UPDATE" | "CONTENT_STORAGE_DELETE" | "WEBHOOK_CONFIG_CREATE" | "WEBHOOK_CONFIG_UPDATE" | "WEBHOOK_CONFIG_DELETE" | "COMPLEX_TAG_OPERATION" | "TASKS_CREATE" | "TASK_CREATE" | "TASK_UPDATE" | "TASK_KEYS_UPDATE" | "TASK_FINISH" | "TASK_CLOSE" | "TASK_REOPEN" | "TASK_KEY_UPDATE" | "ORDER_TRANSLATION" | "GLOSSARY_CREATE" | "GLOSSARY_UPDATE" | "GLOSSARY_DELETE" | "GLOSSARY_IMPORT" | "GLOSSARY_TERM_CREATE" | "GLOSSARY_TERM_UPDATE" | "GLOSSARY_TERM_DELETE" | "GLOSSARY_TERM_TRANSLATION_UPDATE" | "TRANSLATION_LABELS_EDIT" | "TRANSLATION_LABEL_ASSIGN" | "TRANSLATION_LABEL_CREATE" | "TRANSLATION_LABEL_UPDATE" | "TRANSLATION_LABEL_DELETE" | "CREATE_SUGGESTION" | "DECLINE_SUGGESTION" | "ACCEPT_SUGGESTION" | "REVERSE_SUGGESTION" | "DELETE_SUGGESTION" | "SUGGESTION_SET_ACTIVE" | "AI_PROMPT_CREATE" | "AI_PROMPT_UPDATE" | "AI_PROMPT_DELETE" | "BRANCH_CREATE" | "BRANCH_RENAME" | "BRANCH_DELETE" | "BRANCH_PROTECTION_CHANGE" | "BRANCH_MERGE" | "QA_ISSUE_IGNORE" | "QA_ISSUE_UNIGNORE"; + }; + ProjectModel: { + avatar?: components["schemas"]["Avatar"]; + baseLanguage?: components["schemas"]["LanguageModel"]; + computedPermission: components["schemas"]["ComputedPermissionModel"]; + defaultNamespace?: components["schemas"]["NamespaceModel"]; + description?: string; + /** + * @description Current user's direct permission + * @example MANAGE + */ + directPermission?: components["schemas"]["PermissionModel"]; + /** @description Whether to disable ICU placeholder visualization in the editor and it's support. */ + icuPlaceholders: boolean; + /** Format: int64 */ + id: number; + name: string; + organizationOwner?: components["schemas"]["SimpleOrganizationModel"]; + /** @enum {string} */ + organizationRole?: "MEMBER" | "OWNER" | "MAINTAINER"; + slug?: string; + /** + * @description Suggestions for translations + * @enum {string} + */ + suggestionsMode: "DISABLED" | "ENABLED"; + /** + * @description Level of protection of translations + * @enum {string} + */ + translationProtection: "NONE" | "PROTECT_REVIEWED"; + useBranching: boolean; + useNamespaces: boolean; + useQaChecks: boolean; + }; + ProjectStatsModel: { + /** Format: int64 */ + baseWordsCount: number; + /** Format: int64 */ + keyCount: number; + /** Format: int32 */ + languageCount: number; + languageStats: components["schemas"]["LanguageStatsModel"][]; + /** Format: int64 */ + membersCount: number; + /** Format: int64 */ + projectId: number; + /** Format: double */ + reviewedPercentage: number; + /** Format: int64 */ + tagCount: number; + /** Format: int64 */ + taskCount: number; + /** Format: double */ + translatedPercentage: number; + }; + PromptDto: { + basicPromptOptions?: ("KEY_NAME" | "KEY_DESCRIPTION" | "KEY_CONTEXT" | "PROJECT_DESCRIPTION" | "LANGUAGE_NOTES" | "TM_SUGGESTIONS" | "SCREENSHOT" | "GLOSSARY")[]; + name: string; + providerName: string; + template?: string; + }; + PropertyModification: { + new?: unknown; + old?: unknown; + }; + PublicSsoTenantModel: { + domain: string; + force: boolean; + global: boolean; + }; + QaIssueModel: { + /** Format: int64 */ + id: number; + /** @enum {string} */ + message: "qa_empty_translation" | "qa_empty_plural_variant" | "qa_check_failed" | "qa_spaces_leading_added" | "qa_spaces_leading_removed" | "qa_spaces_trailing_added" | "qa_spaces_trailing_removed" | "qa_spaces_doubled" | "qa_spaces_non_breaking_added" | "qa_spaces_non_breaking_removed" | "qa_punctuation_add" | "qa_punctuation_remove" | "qa_punctuation_replace" | "qa_case_capitalize" | "qa_case_lowercase" | "qa_numbers_missing" | "qa_leading_spaces" | "qa_trailing_spaces" | "qa_leading_newlines" | "qa_trailing_newlines" | "qa_newlines_missing" | "qa_newlines_extra" | "qa_newlines_too_many_sections" | "qa_newlines_too_few_sections" | "qa_brackets_missing" | "qa_brackets_extra" | "qa_brackets_unclosed" | "qa_brackets_unmatched_close" | "qa_special_char_missing" | "qa_special_char_added" | "qa_url_missing" | "qa_url_extra" | "qa_url_replace" | "qa_repeated_word" | "qa_placeholders_missing" | "qa_placeholders_extra" | "qa_html_tag_missing" | "qa_html_tag_extra" | "qa_html_unclosed_tag" | "qa_html_unopened_tag" | "qa_icu_syntax_error" | "qa_spelling_error" | "qa_grammar_error" | "qa_key_length_limit_exceeded"; + params?: { + [key: string]: string; + }; + pluralVariant?: string; + /** Format: int32 */ + positionEnd?: number; + /** Format: int32 */ + positionStart?: number; + replacement?: string; + /** @enum {string} */ + state: "OPEN" | "IGNORED"; + /** @enum {string} */ + type: "EMPTY_TRANSLATION" | "SPACES_MISMATCH" | "UNMATCHED_NEWLINES" | "TRIM_CHECK" | "CHARACTER_CASE_MISMATCH" | "MISSING_NUMBERS" | "REPEATED_WORDS" | "PUNCTUATION_MISMATCH" | "SPELLING" | "GRAMMAR" | "BRACKETS_MISMATCH" | "BRACKETS_UNBALANCED" | "SPECIAL_CHARACTER_MISMATCH" | "DIFFERENT_URLS" | "KEY_LENGTH_LIMIT" | "INCONSISTENT_PLACEHOLDERS" | "INCONSISTENT_HTML" | "HTML_SYNTAX" | "ICU_SYNTAX"; + }; + QaRecheckByKeysRequest: { + keyIds: number[]; + languageIds?: number[]; + }; + RelatedKeyDto: { + branch?: string; + keyName: string; + namespace?: string; + }; + RenameBranchModel: { + /** + * @description New branch name + * @example feature/rename-branch + */ + name: string; + }; + ResolveAllBranchMergeConflictsRequest: { + /** @enum {string} */ + resolve: "SOURCE" | "TARGET"; + }; + ResolveBranchMergeConflictRequest: { + /** + * Format: int64 + * @description Merge change id + */ + changeId: number; + /** + * @description Type of resolution + * @enum {string} + */ + resolve: "SOURCE" | "TARGET"; + }; + RestoreKeysRequest: { + keyIds: number[]; + }; + ScreenshotInfoDto: { + location?: string; + positions?: components["schemas"]["KeyInScreenshotPositionDto"][]; + text?: string; + }; + ScreenshotModel: { + /** Format: date-time */ + createdAt?: string; + fileUrl: string; + /** + * @description File name, which may be downloaded from the screenshot path. + * + * When images are secured. Encrypted timestamp is appended to the filename. + */ + filename: string; + /** Format: int32 */ + height?: number; + /** Format: int64 */ + id: number; + keyReferences: components["schemas"]["KeyInScreenshotModel"][]; + location?: string; + middleSized?: string; + middleSizedUrl?: string; + /** + * @description Thumbnail file name, which may be downloaded from the screenshot path. + * + * When images are secured. Encrypted timestamp is appended to the filename. + */ + thumbnail: string; + thumbnailUrl: string; + /** Format: int32 */ + width?: number; + }; + SelectAllResponse: { + ids: number[]; + }; + SetBranchProtectedModel: { + /** + * @description Whether the branch is protected + * @example true + */ + isProtected: boolean; + }; + SetDisabledLanguagesRequest: { + languageIds: number[]; + }; + SetFileNamespaceRequest: { + namespace?: string; + }; + SetKeysNamespaceRequest: { + keyIds: number[]; + namespace?: string; + }; + SetTranslationsResponseModel: { + /** + * Format: int64 + * @description Id of key record + */ + keyId: number; + keyIsPlural: boolean; + /** + * @description Name of key + * @example this_is_super_key + */ + keyName: string; + /** + * @description The namespace of the key + * @example homepage + */ + keyNamespace?: string; + /** + * @description Translations object containing values updated in this request + * @example { + * "en": { + * "id": 100000003, + * "text": "This is super translation!" + * } + * } + */ + translations: { + [key: string]: components["schemas"]["TranslationModel"]; + }; + }; + SetTranslationsStateStateRequest: { + keyIds: number[]; + languageIds: number[]; + /** @enum {string} */ + state: "UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED"; + }; + SetTranslationsWithKeyDto: { + /** @description Branch name to set translations for */ + branch?: string; + /** + * @description Key name to set translations for + * @example what_a_key_to_translate + */ + key: string; + /** + * @description List of languages to return translations for. + * + * If not provided, only modified translation will be provided. + * @example [ + * "en", + * "de", + * "fr" + * ] + */ + languagesToReturn?: string[]; + /** @description The namespace of the key. (When empty or null default namespace will be used) */ + namespace?: string; + /** + * @description Object mapping language tag to translation + * @example { + * "en": "What a translated value!", + * "cs": "Jaká to přeložená hodnota!" + * } + */ + translations: { + [key: string]: string; + }; + }; + SimpleGlossaryModel: { + /** + * @description Language tag for default translations for terms + * @example en + */ + baseLanguageTag: string; + /** Format: int64 */ + id: number; + name: string; + }; + SimpleGlossaryTermModel: { + description: string; + /** @description Specifies whether the term represents a shortened form of a word or phrase */ + flagAbbreviation: boolean; + /** @description When true, the term matching considers uppercase and lowercase characters as distinct */ + flagCaseSensitive: boolean; + /** @description When true, marks this term as prohibited or not recommended for use in translations */ + flagForbiddenTerm: boolean; + /** @description When true, this term has the same translation across all target languages */ + flagNonTranslatable: boolean; + /** Format: int64 */ + id: number; + }; + SimpleGlossaryTermWithTranslationsModel: { + /** + * @description A detailed explanation or definition of the glossary term + * @example It's trademark + */ + description: string; + /** @description Specifies whether the term represents a shortened form of a word or phrase */ + flagAbbreviation: boolean; + /** @description When true, the term matching considers uppercase and lowercase characters as distinct */ + flagCaseSensitive: boolean; + /** @description When true, marks this term as prohibited or not recommended for use in translations */ + flagForbiddenTerm: boolean; + /** @description When true, this term has the same translation across all target languages */ + flagNonTranslatable: boolean; + /** Format: int64 */ + id: number; + translations: components["schemas"]["GlossaryTermTranslationModel"][]; + }; + SimpleGlossaryWithStatsModel: { + /** + * Format: int64 + * @description Total number of projects currently using this glossary + * @example 69 + */ + assignedProjectsCount: number; + /** + * @description The primary language code used for terms (e.g., 'en' for English) + * @example en + */ + baseLanguageTag: string; + /** + * @description The name of one project using this glossary (can be shown as a preview) + * @example My Project + */ + firstAssignedProjectName?: string; + /** Format: int64 */ + id: number; + name: string; + }; + SimpleImportConflictResult: { + isOverridable: boolean; + keyName: string; + keyNamespace?: string; + language: string; + }; + SimpleOrganizationModel: { + /** @example Links to avatar images */ + avatar?: components["schemas"]["Avatar"]; + basePermissions: components["schemas"]["PermissionModel"]; + /** @example This is a beautiful organization full of beautiful and clever people */ + description?: string; + /** Format: int64 */ + id: number; + /** @example Beautiful organization */ + name: string; + /** @example btforg */ + slug: string; + }; + SimpleProjectModel: { + avatar?: components["schemas"]["Avatar"]; + baseLanguage?: components["schemas"]["LanguageModel"]; + description?: string; + icuPlaceholders: boolean; + /** Format: int64 */ + id: number; + name: string; + slug?: string; + }; + SimpleUserAccountModel: { + avatar?: components["schemas"]["Avatar"]; + deleted: boolean; + /** Format: int64 */ + id: number; + name?: string; + username: string; + }; + SingleStepImportRequest: { + /** + * @description Branch to which files will be imported + * @example main + */ + branch?: string; + /** @description If true, placeholders from other formats will be converted to ICU when possible */ + convertPlaceholdersToIcu: boolean; + /** @description If false, only updates keys, skipping the creation of new keys */ + createNewKeys: boolean; + /** + * @description If `false`, import will apply all `non-failed` overrides and reports `unresolvedConflict` + * .If `true`, import will fail completely on unresolved conflict and won't apply any changes. Unresolved conflicts are reported in the `params` of the error response + */ + errorOnUnresolvedConflict?: boolean; + /** @description Definition of mapping for each file to import. */ + fileMappings: components["schemas"]["ImportFileMapping"][]; + /** + * @description Whether to override existing translation data. + * + * When set to `KEEP`, existing translations will be kept. + * When set to `NO_FORCE`, error will be thrown on conflict. + * When set to `OVERRIDE`, existing translations will be overwritten + * @enum {string} + */ + forceMode: "OVERRIDE" | "KEEP" | "NO_FORCE"; + /** + * @description Maps the languages from imported files to languages existing in the Tolgee platform. + * + * Use this field only when your files contain multiple languages (e.g., XLIFF files). + * + * Otherwise, use the `languageTag` property of `fileMappings`. + * + * Example: In xliff files, there are `source-language` and `target-language` attributes defined on `file` element. Using this field you can map source and target values to languages stored in the Tolgee Platform. + */ + languageMappings?: components["schemas"]["LanguageMapping"][]; + /** @description If true, key descriptions will be overridden by the import */ + overrideKeyDescriptions: boolean; + /** + * @description Some translations are forbidden or protected: + * + * When set to `RECOMMENDED` it will fail for DISABLED translations and protected REVIEWED translations. + * When set to `ALL` it will fail for DISABLED translations, but will try to update protected REVIEWED translations (fails only if user has no permission) + * @enum {string} + */ + overrideMode?: "RECOMMENDED" | "ALL"; + /** @description If yes, keys from project that were not included in import will be deleted (only within namespaces which are included in the import). */ + removeOtherKeys?: boolean; + /** + * @description When importing files in structured formats (e.g., JSON, YAML), this field defines the delimiter which will be used in names of imported keys. + * @example . + */ + structureDelimiter?: string; + /** @description Keys created by this import will be tagged with these tags. It add tags only to new keys. The keys that already exist will not be tagged. */ + tagNewKeys: string[]; + }; + SingleStepImportResolvableItemRequest: { + /** + * @description Key name to set translations for + * @example what_a_key_to_translate + */ + name: string; + /** @description The namespace of the key. (When empty or null default namespace will be used) */ + namespace?: string; + screenshots?: components["schemas"]["KeyScreenshotDto"][]; + /** @description Object mapping language tag to translation */ + translations: { + [key: string]: components["schemas"]["SingleStepImportResolvableTranslationRequest"]; + }; + }; + SingleStepImportResolvableRequest: { + /** @description Branch to import keys into. If not specified, default branch is used. */ + branch?: string; + /** + * @description If `false`, import will apply all `non-failed` overrides and reports `unresolvedConflict` + * .If `true`, import will fail completely on unresolved conflict and won't apply any changes. Unresolved conflicts are reported in the `params` of the error response + */ + errorOnUnresolvedConflict?: boolean; + /** @description List of keys to import */ + keys: components["schemas"]["SingleStepImportResolvableItemRequest"][]; + /** + * @description Some translations are forbidden or protected: + * + * When set to `RECOMMENDED` it will fail for DISABLED translations and protected REVIEWED translations. + * When set to `ALL` it will fail for DISABLED translations, but will try to update protected REVIEWED translations (fails only if user has no permission) + * @enum {string} + */ + overrideMode?: "RECOMMENDED" | "ALL"; + }; + SingleStepImportResolvableTranslationRequest: { + /** + * @description To ensure the import doesn't override something that should not be (in case data have changed unexpectedly), + * you can specify what do you "expect": + * - EXPECT_NO_CONFLICT: There should be no conflict, if there is, import fails + * - OVERRIDE: New translation is applied over the existing in every case (Default) + * @example OVERRIDE + * @enum {string} + */ + resolution?: "EXPECT_NO_CONFLICT" | "OVERRIDE"; + /** + * @description Translation text + * @example Hello! I am a translation! + */ + text: string; + }; + StreamingResponseBody: unknown; + SuggestRequestDto: { + /** @description Text value of base translation. Useful, when base translation is not stored yet. */ + baseText?: string; + /** @description Whether base text is plural. This value is ignored if baseText is null. */ + isPlural?: boolean; + /** + * Format: int64 + * @description Key Id to get results for. Use when key is stored already. + */ + keyId?: number; + plural?: boolean; + /** @description List of services to use. If null, then all enabled services are used. */ + services?: ("GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "PROMPT")[]; + /** Format: int64 */ + targetLanguageId: number; + }; + SuggestResultModel: { + /** @description If true, the base translation was empty and no translation was provided. */ + baseBlank: boolean; + /** + * @deprecated + * @description String translations provided by enabled services. (deprecated, use `result` instead) + * @example { + * "GOOGLE": "This was translated by Google", + * "TOLGEE": "This was translated by Tolgee Translator", + * } + */ + machineTranslations?: { + [key: string]: string; + }; + /** + * @description Results provided by enabled services. + * @example { + * "GOOGLE": { + * "output": "This was translated by Google", + * "contextDescription": null + * }, + * "TOLGEE": { + * "output": "This was translated by Tolgee Translator", + * "contextDescription": "This is an example in swagger" + * } + * } + */ + result?: { + [key: string]: components["schemas"]["TranslationItemModel"]; + }; + }; + TagKeyDto: { + name: string; + }; + TagKeysRequest: { + keyIds: number[]; + tags: string[]; + }; + TagModel: { + /** Format: int64 */ + id: number; + name: string; + }; + TaskKeysResponse: { + keys: number[]; + }; + TaskModel: { + agency?: components["schemas"]["TranslationAgencySimpleModel"]; + assignees: components["schemas"]["SimpleUserAccountModel"][]; + author?: components["schemas"]["SimpleUserAccountModel"]; + /** Format: int64 */ + baseCharacterCount: number; + /** Format: int64 */ + baseWordCount: number; + branchName?: string; + /** Format: int64 */ + closedAt?: number; + /** Format: int64 */ + createdAt?: number; + description: string; + /** Format: int64 */ + doneItems: number; + /** Format: int64 */ + dueDate?: number; + language: components["schemas"]["LanguageModel"]; + name?: string; + /** Format: int64 */ + number: number; + originBranchName?: string; + /** @enum {string} */ + state: "NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED"; + /** Format: int64 */ + totalItems: number; + /** @enum {string} */ + type: "TRANSLATE" | "REVIEW"; + }; + TaskPerUserReportModel: { + /** Format: int64 */ + baseCharacterCount: number; + /** Format: int64 */ + baseWordCount: number; + /** Format: int64 */ + doneItems: number; + user: components["schemas"]["SimpleUserAccountModel"]; + }; + TaskWithProjectModel: { + agency?: components["schemas"]["TranslationAgencySimpleModel"]; + assignees: components["schemas"]["SimpleUserAccountModel"][]; + author?: components["schemas"]["SimpleUserAccountModel"]; + /** Format: int64 */ + baseCharacterCount: number; + /** Format: int64 */ + baseWordCount: number; + branchName?: string; + /** Format: int64 */ + closedAt?: number; + /** Format: int64 */ + createdAt?: number; + description: string; + /** Format: int64 */ + doneItems: number; + /** Format: int64 */ + dueDate?: number; + language: components["schemas"]["LanguageModel"]; + name?: string; + /** Format: int64 */ + number: number; + originBranchName?: string; + project: components["schemas"]["SimpleProjectModel"]; + /** @enum {string} */ + state: "NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED"; + /** Format: int64 */ + totalItems: number; + /** @enum {string} */ + type: "TRANSLATE" | "REVIEW"; + }; + TranslationAgencySimpleModel: { + avatar?: components["schemas"]["Avatar"]; + /** Format: int64 */ + id: number; + name: string; + url?: string; + }; + TranslationCommentDto: { + /** @enum {string} */ + state: "RESOLUTION_NOT_NEEDED" | "NEEDS_RESOLUTION" | "RESOLVED"; + text: string; + }; + TranslationCommentModel: { + /** @description User who created the comment */ + author: components["schemas"]["SimpleUserAccountModel"]; + /** + * Format: date-time + * @description Date when it was created + */ + createdAt: string; + /** + * Format: int64 + * @description Id of translation comment record + */ + id: number; + /** + * @description State of translation + * @enum {string} + */ + state: "RESOLUTION_NOT_NEEDED" | "NEEDS_RESOLUTION" | "RESOLVED"; + /** @description Text of comment */ + text: string; + /** + * Format: date-time + * @description Date when it was updated + */ + updatedAt: string; + }; + TranslationCommentWithLangKeyDto: { + /** Format: int64 */ + keyId: number; + /** Format: int64 */ + languageId: number; + /** @enum {string} */ + state: "RESOLUTION_NOT_NEEDED" | "NEEDS_RESOLUTION" | "RESOLVED"; + text: string; + }; + TranslationHistoryModel: { + /** @description Author of the change */ + author?: components["schemas"]["SimpleUserAccountModel"]; + /** @description Modified fields */ + modifications?: { + [key: string]: components["schemas"]["PropertyModification"]; + }; + /** @enum {string} */ + revisionType: "ADD" | "MOD" | "DEL"; + /** + * Format: int64 + * @description Unix timestamp of the revision + */ + timestamp: number; + }; + TranslationItemModel: { + contextDescription?: string; + output: string; + }; + TranslationLabelRequest: { + /** Format: int64 */ + keyId: number; + /** Format: int64 */ + labelId: number; + /** Format: int64 */ + languageId: number; + }; + TranslationMemoryItemModel: { + baseText: string; + keyName: string; + /** Format: float */ + similarity: number; + targetText: string; + }; + TranslationModel: { + /** @description Was translated using Translation Memory or Machine translation service? */ + auto: boolean; + /** + * Format: int64 + * @description Id of translation record + */ + id: number; + /** + * @description Which machine translation service was used to auto translate this + * @enum {string} + */ + mtProvider?: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "PROMPT"; + /** @description Whether base language translation was changed after this translation was updated */ + outdated: boolean; + /** + * @description State of translation + * @enum {string} + */ + state: "UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED"; + /** @description Translation text */ + text?: string; + }; + TranslationSuggestionAcceptResponse: { + accepted: components["schemas"]["TranslationSuggestionModel"]; + declined: number[]; + }; + TranslationSuggestionModel: { + author: components["schemas"]["SimpleUserAccountModel"]; + /** Format: date-time */ + createdAt: string; + /** Format: int64 */ + id: number; + isPlural: boolean; + /** Format: int64 */ + keyId: number; + /** Format: int64 */ + languageId: number; + /** @enum {string} */ + state: "ACTIVE" | "ACCEPTED" | "DECLINED"; + translation?: string; + /** Format: date-time */ + updatedAt: string; + }; + TranslationSuggestionSimpleModel: { + author: components["schemas"]["SimpleUserAccountModel"]; + /** Format: int64 */ + id: number; + isPlural: boolean; + /** @enum {string} */ + state: "ACTIVE" | "ACCEPTED" | "DECLINED"; + translation?: string; + }; + TranslationViewModel: { + /** + * Format: int64 + * @description Number of active suggestions + */ + activeSuggestionCount: number; + /** @description Was translated using Translation Memory or Machine translation service? */ + auto: boolean; + /** + * Format: int64 + * @description Count of translation comments + */ + commentCount: number; + /** @description Was translation memory used to translate this? */ + fromTranslationMemory: boolean; + /** + * Format: int64 + * @description Id of translation record + */ + id?: number; + /** @description Labels assigned to this translation */ + labels?: components["schemas"]["LabelModel"][]; + /** + * @description Which machine translation service was used to auto translate this + * @enum {string} + */ + mtProvider?: "GOOGLE" | "AWS" | "DEEPL" | "AZURE" | "BAIDU" | "PROMPT"; + /** @description Whether base language translation was changed after this translation was updated */ + outdated: boolean; + /** @description Whether QA checks are stale and need re-running */ + qaChecksStale: boolean; + /** + * Format: int64 + * @description Number of open QA issues + */ + qaIssueCount: number; + /** @description Detailed QA issues for inline highlighting (only when includeQaIssues=true) */ + qaIssues?: components["schemas"]["QaIssueModel"][]; + /** + * @description State of translation + * @enum {string} + */ + state: "UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED"; + /** @description First suggestion */ + suggestions?: components["schemas"]["TranslationSuggestionSimpleModel"][]; + /** @description Translation text */ + text?: string; + /** + * Format: int64 + * @description Number of all suggestions + */ + totalSuggestionCount: number; + /** + * Format: int64 + * @description Count of unresolved translation comments + */ + unresolvedCommentCount: number; + }; + TranslationWithCommentModel: { + comment: components["schemas"]["TranslationCommentModel"]; + translation: components["schemas"]["TranslationModel"]; + }; + TrashedKeyWithTranslationsModel: { + /** + * Format: date-time + * @description When the key was deleted + */ + deletedAt: string; + /** @description User who deleted the key */ + deletedBy?: components["schemas"]["SimpleUserAccountModel"]; + /** @description Description of the key */ + description?: string; + /** + * Format: int64 + * @description Id of key record + */ + id: number; + /** @description Whether the key is plural */ + isPlural: boolean; + /** + * @description Name of key + * @example this_is_super_key + */ + name: string; + /** + * @description Namespace of key + * @example homepage + */ + namespace?: string; + /** + * Format: date-time + * @description When the key will be permanently deleted + */ + permanentDeleteAt: string; + /** @description Screenshots of the key */ + screenshots: components["schemas"]["ScreenshotModel"][]; + /** @description Tags of key */ + tags: components["schemas"]["TagModel"][]; + /** @description Translations object keyed by language tag */ + translations: { + [key: string]: components["schemas"]["TranslationViewModel"]; + }; + }; + UntagKeysRequest: { + keyIds: number[]; + tags: string[]; + }; + UpdateGlossaryRequest: { + /** @description Projects assigned to glossary; when null, assigned projects will be kept unchanged. */ + assignedProjectIds?: number[]; + /** + * @description Language tag according to BCP 47 definition + * @example cs-CZ + */ + baseLanguageTag: string; + /** + * @description Glossary name + * @example My glossary + */ + name: string; + }; + UpdateGlossaryTermTranslationRequest: { + /** + * @description Language tag according to BCP 47 definition + * @example cs-CZ + */ + languageTag: string; + /** + * @description Translation text + * @example Translated text to language of languageTag + */ + text: string; + }; + UpdateGlossaryTermWithTranslationRequest: { + description?: string; + /** @description Specifies whether the term represents a shortened form of a word or phrase */ + flagAbbreviation?: boolean; + /** @description When true, the term matching considers uppercase and lowercase characters as distinct */ + flagCaseSensitive?: boolean; + /** @description When true, marks this term as prohibited or not recommended for use in translations */ + flagForbiddenTerm?: boolean; + /** @description When true, this term will have the same translation across all target languages */ + flagNonTranslatable?: boolean; + text?: string; + }; + UpdateNamespaceDto: { + name: string; + }; + UpdateTaskKeyRequest: { + done: boolean; + }; + UpdateTaskKeyResponse: { + /** @description Task key is marked as done */ + done: boolean; + /** @description Task progress is 100% */ + taskFinished: boolean; + }; + UpdateTaskKeysRequest: { + /** @description Keys to add to task */ + addKeys?: number[]; + /** @description Keys to remove from task */ + removeKeys?: number[]; + }; + UpdateTaskRequest: { + assignees: number[]; + description: string; + /** + * Format: int64 + * @description Due to date in epoch format (milliseconds). + * @example 1661172869000 + */ + dueDate?: number; + name?: string; + }; + UploadedImageModel: { + /** Format: date-time */ + createdAt: string; + fileUrl: string; + filename: string; + /** Format: int64 */ + id: number; + location?: string; + requestFilename: string; + }; + UsedNamespaceModel: { + /** + * Format: int64 + * @description The id of namespace. Null for default namespace. + * @example 10000048 + */ + id?: number; + /** + * @description Name of namespace. Null if default. + * @example homepage + */ + name?: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + doExportJsonZip_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/zip": components["schemas"]["StreamingResponseBody"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getCurrent_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiKeyWithLanguagesModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getCurrentPermissions: { + parameters: { + query?: { + /** @description Required when using with PAT */ + projectId?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiKeyPermissionsModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getCurrentAuthProvider: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthProviderDto"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + deleteCurrentAuthProvider: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getChangedAuthProvider: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthProviderDto"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + acceptChangeAuthProvider: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AcceptAuthProviderChangeRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["JwtAuthenticationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + rejectChangeAuthProvider: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + upload: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "multipart/form-data": { + /** Format: binary */ + image: string; + info?: components["schemas"]["ImageUploadInfoDto"]; + }; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["UploadedImageModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + delete_17: { + parameters: { + query?: never; + header?: never; + path: { + ids: number[]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getNotifications: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + /** + * @description Filter by the `seen` parameter. + * + * no value = request everything + * + * true = only seen + * + * false = only unseen + */ + filterSeen?: boolean; + cursor?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelWithNextCursorNotificationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getNotificationsSettings: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NotificationSettingModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + putNotificationSetting: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NotificationSettingsRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + markNotificationsAsSeen: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NotificationsMarkSeenRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAll_10: { + parameters: { + query: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + filterCurrentUserOwner: boolean; + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelOrganizationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + create_14: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrganizationDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + get_15: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAll_12: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + search?: string; + }; + header?: never; + path: { + organizationId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelSimpleGlossaryModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + create_15: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateGlossaryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAllWithStats: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + search?: string; + }; + header?: never; + path: { + organizationId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelSimpleGlossaryWithStatsModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + get_13: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + update_8: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateGlossaryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + delete_7: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAssignedProjects: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelSimpleProjectModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + export: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StreamingResponseBody"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + importCsv: { + parameters: { + query?: { + removeExistingTerms?: boolean; + }; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + "multipart/form-data": { + /** Format: binary */ + file: string; + }; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryImportResult"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getLanguages: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelGlossaryLanguageDto"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAll_13: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + search?: string; + languageTags?: string[]; + }; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelSimpleGlossaryTermModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + create_16: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateGlossaryTermWithTranslationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateUpdateGlossaryTermResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + deleteMultiple: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DeleteMultipleGlossaryTermsRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + get_14: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + termId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + update_9: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + termId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateGlossaryTermWithTranslationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateUpdateGlossaryTermResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + delete_8: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + termId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + update_12: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + termId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateGlossaryTermTranslationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermTranslationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + get_23: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + glossaryId: number; + termId: number; + languageTag: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermTranslationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAllIds: { + parameters: { + query?: { + search?: string; + languageTags?: string[]; + }; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelLong"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAllWithTranslations: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + search?: string; + languageTags?: string[]; + }; + header?: never; + path: { + organizationId: number; + glossaryId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelSimpleGlossaryTermWithTranslationsModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getOrganizationCredits: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreditBalanceModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + get_22: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OrganizationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getCurrent: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatWithUserModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAll: { + parameters: { + query?: { + /** @description Filter projects by id */ + filterId?: number[]; + /** @description Filter projects without id */ + filterNotId?: number[]; + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelProjectModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + createProject: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateProjectRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProjectModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getActivity_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelProjectActivityModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getSingleRevision_1: { + parameters: { + query?: { + branch?: string; + }; + header?: never; + path: { + revisionId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProjectActivityModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + list_4: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelBatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + get_21: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + cancel_1: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + store_3: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BigMetaDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + all_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelBranchModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + create_12: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateBranchModel"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BranchModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + find_1: { + parameters: { + query?: { + name?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BranchModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getBranchMerges_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelBranchMergeModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + dryRunMerge_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DryRunMergeBranchRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BranchMergeRefModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + deleteBranchMerge_1: { + parameters: { + query?: never; + header?: never; + path: { + mergeId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + merge_1: { + parameters: { + query?: never; + header?: never; + path: { + mergeId: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["ApplyBranchMergeRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getBranchMergeSessionChanges_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + type?: "ADD" | "UPDATE" | "DELETE" | "CONFLICT"; + }; + header?: never; + path: { + mergeId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelBranchMergeChangeModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getBranchMergeSessionChange_1: { + parameters: { + query?: never; + header?: never; + path: { + mergeId: number; + changeId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BranchMergeChangeModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getBranchMergeSessionConflicts_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path: { + mergeId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelBranchMergeConflictModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getBranchMergeSessionPreview_1: { + parameters: { + query?: never; + header?: never; + path: { + mergeId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BranchMergeModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + refreshBranchMerge_1: { + parameters: { + query?: never; + header?: never; + path: { + mergeId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BranchMergeModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + resolveConflict_1: { + parameters: { + query?: never; + header?: never; + path: { + mergeId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ResolveBranchMergeConflictRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + resolveAllConflicts_1: { + parameters: { + query?: never; + header?: never; + path: { + mergeId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ResolveAllBranchMergeConflictsRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + rename_1: { + parameters: { + query?: never; + header?: never; + path: { + branchId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RenameBranchModel"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BranchModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + delete_14: { + parameters: { + query?: never; + header?: never; + path: { + branchId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + setProtected_1: { + parameters: { + query?: never; + header?: never; + path: { + branchId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetBranchProtectedModel"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BranchModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + currentJobs_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelBatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + exportData_1: { + parameters: { + query: { + /** + * @description Languages to be contained in export. + * + * If null, all languages are exported + * @example en + */ + languages?: string[]; + /** @description Format to export to */ + format: "JSON" | "JSON_TOLGEE" | "XLIFF" | "PO" | "APPLE_STRINGS_STRINGSDICT" | "APPLE_XLIFF" | "ANDROID_XML" | "COMPOSE_XML" | "FLUTTER_ARB" | "PROPERTIES" | "YAML_RUBY" | "YAML" | "JSON_I18NEXT" | "CSV" | "RESX_ICU" | "XLSX" | "APPLE_XCSTRINGS" | "ANDROID_SDK" | "APPLE_SDK"; + /** + * @description Delimiter to structure file content. + * + * e.g. For key "home.header.title" would result in {"home": {"header": "title": {"Hello"}}} structure. + * + * When null, resulting file won't be structured. Works only for generic structured formats (e.g. JSON, YAML), + * specific formats like `YAML_RUBY` don't honor this parameter. + */ + structureDelimiter?: string; + /** @description Filter key IDs to be contained in export */ + filterKeyId?: number[]; + /** @description Filter key IDs not to be contained in export */ + filterKeyIdNot?: number[]; + /** + * @description Filter keys tagged by. + * + * This filter works the same as `filterTagIn` but in this cases it accepts single tag only. + */ + filterTag?: string; + /** @description Filter keys tagged by one of provided tags */ + filterTagIn?: string[]; + /** @description Filter keys not tagged by one of provided tags */ + filterTagNotIn?: string[]; + /** @description Filter keys with prefix */ + filterKeyPrefix?: string; + /** @description Filter translations with state. By default, all states except untranslated is exported. */ + filterState?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED")[]; + /** @description Filter translations with namespace. By default, all namespaces everything are exported. To export default namespace, use empty string. */ + filterNamespace?: string[]; + /** + * @description If false, it doesn't return zip of files, but it returns single file. + * + * This is possible only when single language is exported. Otherwise it returns "400 - Bad Request" response. + */ + zip: boolean; + /** + * @description Message format to be used for export. + * + * e.g. PHP_PO: Hello %s, ICU: Hello {name}. + * + * This property is honored only for generic formats like JSON or YAML. + * For specific formats like `YAML_RUBY` it's ignored. + */ + messageFormat?: "C_SPRINTF" | "PHP_SPRINTF" | "JAVA_STRING_FORMAT" | "APPLE_SPRINTF" | "RUBY_SPRINTF" | "I18NEXT" | "ICU" | "PYTHON_PERCENT"; + /** + * @description This is a template that defines the structure of the resulting .zip file content. + * + * The template is a string that can contain the following placeholders: {namespace}, {languageTag}, + * {androidLanguageTag}, {snakeLanguageTag}, {extension}. + * + * For example, when exporting to JSON with the template `{namespace}/{languageTag}.{extension}`, + * the English translations of the `home` namespace will be stored in `home/en.json`. + * + * The `{snakeLanguageTag}` placeholder is the same as `{languageTag}` but in snake case. (e.g., en_US). + * + * The Android specific `{androidLanguageTag}` placeholder is the same as `{languageTag}` + * but in Android format. (e.g., en-rUS) + */ + fileStructureTemplate?: string; + /** + * @description If true, for structured formats (like JSON) arrays are supported. + * + * e.g. Key hello[0] will be exported as {"hello": ["..."]} + */ + supportArrays: boolean; + /** + * @description If true, HTML tags are escaped in the exported file. (Supported in the XLIFF format only). + * + * e.g. Key hello will be exported as <b>hello</b> + */ + escapeHtml?: boolean; + /** + * @description Filter translations with branch. + * + * By default, default branch is exported. + */ + filterBranch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** + * @description When multiple files are exported, they are zipped and returned as a single zip file. + * When a single file is exported, it is returned directly. + */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/*": unknown; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + exportPost_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ExportParams"]; + }; + }; + responses: { + /** + * @description When multiple files are exported, they are zipped and returned as a single zip file. + * When a single file is exported, it is returned directly. + */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/*": unknown; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + addFiles_1: { + parameters: { + query?: { + /** + * @description When importing files in structured formats (e.g., JSON, YAML), this field defines the delimiter which will be used in names of imported keys. + * @example . + */ + structureDelimiter?: string; + /** + * @description Branch to which files will be imported + * @example main + */ + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "multipart/form-data": { + files: string[]; + }; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ImportAddFilesResultModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + cancelImport_1: { + parameters: { + query?: { + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + get_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ImportSettingsModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + store_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ImportSettingsRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ImportSettingsModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAllNamespaces_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelImportNamespaceModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + applyImport_1: { + parameters: { + query?: { + /** @description Whether override or keep all translations with unresolved conflicts */ + forceMode?: "OVERRIDE" | "KEEP" | "NO_FORCE"; + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + applyImportStreaming_1: { + parameters: { + query?: { + /** @description Whether override or keep all translations with unresolved conflicts */ + forceMode?: "OVERRIDE" | "KEEP" | "NO_FORCE"; + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/x-ndjson": components["schemas"]["StreamingResponseBody"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getImportResult_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelImportLanguageModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + selectNamespace_1: { + parameters: { + query?: never; + header?: never; + path: { + fileId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetFileNamespaceRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getImportFileIssues_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path: { + importFileId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelImportFileIssueModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + resetExistingLanguage_1: { + parameters: { + query?: never; + header?: never; + path: { + importLanguageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + selectExistingLanguage_1: { + parameters: { + query?: never; + header?: never; + path: { + importLanguageId: number; + existingLanguageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getImportLanguage_1: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ImportLanguageModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + deleteLanguage_3: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + resolveTranslationSetKeepExisting_3: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + resolveTranslationSetOverride_3: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getImportTranslations_1: { + parameters: { + query?: { + /** @description Whether only translations, which are in conflict with existing translations should be returned */ + onlyConflicts?: boolean; + /** @description Whether only translations with unresolved conflictswith existing translations should be returned */ + onlyUnresolved?: boolean; + /** @description String to search in translation text or key */ + search?: string; + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path: { + languageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelImportTranslationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + resolveTranslationSetKeepExisting_1: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + translationId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + resolveTranslationSetOverride_1: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + translationId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAll_8: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelKeyModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + create_8: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateKeyDto"]; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["KeyWithDataModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + delete_12: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DeleteKeysDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + create_7: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateKeyDto"]; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["KeyWithDataModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + importKeys_3: { + parameters: { + query?: { + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ImportKeysDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + importKeys_1: { + parameters: { + query?: { + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ImportKeysResolvableDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KeyImportResolvableResultModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getInfo_2: { + parameters: { + query?: { + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GetKeysRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelKeyWithDataModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + searchForKey_1: { + parameters: { + query: { + /** @description Search query */ + search: string; + /** @description Language to search in */ + languageTag?: string; + branch?: string; + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelKeySearchSearchResultModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + selectKeys_3: { + parameters: { + query?: { + /** + * @description Translation state in the format: languageTag,state. You can use this parameter multiple times. + * + * When used with multiple states for same language it is applied with logical OR. + * + * When used with multiple languages, it is applied with logical AND. + */ + filterState?: string[]; + /** + * @description Languages to be contained in response. + * + * To add multiple languages, repeat this param (eg. ?languages=en&languages=de) + * @example en + */ + languages?: string[]; + /** @description String to search in key name or translation text */ + search?: string; + /** @description Selects key with provided names. Use this param multiple times to fetch more keys. */ + filterKeyName?: string[]; + /** @description Selects key with provided ID. Use this param multiple times to fetch more keys. */ + filterKeyId?: number[]; + /** @description Selects only keys for which the translation is missing in any returned language. It only filters for translations included in returned languages. */ + filterUntranslatedAny?: boolean; + /** @description Selects only keys, where translation is provided in any language */ + filterTranslatedAny?: boolean; + /** + * @description Selects only keys where the translation is missing for the specified language. The specified language must be included in the returned languages. Otherwise, this filter doesn't apply. + * @example en-US + */ + filterUntranslatedInLang?: string; + /** + * @description Selects only keys, where translation is provided in specified language + * @example en-US + */ + filterTranslatedInLang?: string; + /** + * @description Selects only keys, where translation was auto translated for specified languages. + * @example en-US + */ + filterAutoTranslatedInLang?: string[]; + /** @description Selects only keys with screenshots */ + filterHasScreenshot?: boolean; + /** @description Selects only keys without screenshots */ + filterHasNoScreenshot?: boolean; + /** + * @description Selects only keys with provided namespaces. + * + * To filter default namespace, set to empty string. + */ + filterNamespace?: string[]; + /** + * @description Selects only keys without provided namespaces. + * + * To filter default namespace, set to empty string. + */ + filterNoNamespace?: string[]; + /** @description Selects only keys with provided tag */ + filterTag?: string[]; + /** @description Selects only keys without provided tag */ + filterNoTag?: string[]; + /** + * @description Selects only keys, where translation in provided langs is in outdated state + * @example en-US + */ + filterOutdatedLanguage?: string[]; + /** + * @description Selects only keys, where translation in provided langs is not in outdated state + * @example en-US + */ + filterNotOutdatedLanguage?: string[]; + /** + * @description Selects only key affected by activity with specidfied revision ID + * @example 1234567 + */ + filterRevisionId?: number[]; + /** @description Select only keys which were not successfully translated by batch job with provided id */ + filterFailedKeysOfJob?: number; + /** @description Select only keys which are in specified task */ + filterTaskNumber?: number[]; + /** @description Filter task keys which are `not done` */ + filterTaskKeysNotDone?: boolean; + /** @description Filter task keys which are `done` */ + filterTaskKeysDone?: boolean; + /** @description Filter keys with unresolved comments in lang */ + filterHasUnresolvedCommentsInLang?: string[]; + /** @description Filter keys with any comments in lang */ + filterHasCommentsInLang?: string[]; + /** + * @description Filter key translations with labels + * @example labelId1,labelId2 + */ + filterLabel?: string[]; + /** @description Filter keys with open QA issues in lang */ + filterHasQaIssuesInLang?: string[]; + /** @description Filter keys with specific QA check type issues */ + filterQaCheckType?: ("EMPTY_TRANSLATION" | "SPACES_MISMATCH" | "UNMATCHED_NEWLINES" | "TRIM_CHECK" | "CHARACTER_CASE_MISMATCH" | "MISSING_NUMBERS" | "REPEATED_WORDS" | "PUNCTUATION_MISMATCH" | "SPELLING" | "GRAMMAR" | "BRACKETS_MISMATCH" | "BRACKETS_UNBALANCED" | "SPECIAL_CHARACTER_MISMATCH" | "DIFFERENT_URLS" | "KEY_LENGTH_LIMIT" | "INCONSISTENT_PLACEHOLDERS" | "INCONSISTENT_HTML" | "HTML_SYNTAX" | "ICU_SYNTAX")[]; + /** @description Filter keys with any suggestions in lang */ + filterHasSuggestionsInLang?: string[]; + /** @description Filter keys with no suggestions in lang */ + filterHasNoSuggestionsInLang?: string[]; + /** @description Selects only keys from specified branch */ + branch?: string; + /** @description Filter trashed keys by who deleted them (user IDs) */ + filterDeletedByUserId?: number[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SelectAllResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + list_6: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + /** + * @description Translation state in the format: languageTag,state. You can use this parameter multiple times. + * + * When used with multiple states for same language it is applied with logical OR. + * + * When used with multiple languages, it is applied with logical AND. + */ + filterState?: string[]; + /** + * @description Languages to be contained in response. + * + * To add multiple languages, repeat this param (eg. ?languages=en&languages=de) + * @example en + */ + languages?: string[]; + /** @description String to search in key name or translation text */ + search?: string; + /** @description Selects key with provided names. Use this param multiple times to fetch more keys. */ + filterKeyName?: string[]; + /** @description Selects key with provided ID. Use this param multiple times to fetch more keys. */ + filterKeyId?: number[]; + /** @description Selects only keys for which the translation is missing in any returned language. It only filters for translations included in returned languages. */ + filterUntranslatedAny?: boolean; + /** @description Selects only keys, where translation is provided in any language */ + filterTranslatedAny?: boolean; + /** + * @description Selects only keys where the translation is missing for the specified language. The specified language must be included in the returned languages. Otherwise, this filter doesn't apply. + * @example en-US + */ + filterUntranslatedInLang?: string; + /** + * @description Selects only keys, where translation is provided in specified language + * @example en-US + */ + filterTranslatedInLang?: string; + /** + * @description Selects only keys, where translation was auto translated for specified languages. + * @example en-US + */ + filterAutoTranslatedInLang?: string[]; + /** @description Selects only keys with screenshots */ + filterHasScreenshot?: boolean; + /** @description Selects only keys without screenshots */ + filterHasNoScreenshot?: boolean; + /** + * @description Selects only keys with provided namespaces. + * + * To filter default namespace, set to empty string. + */ + filterNamespace?: string[]; + /** + * @description Selects only keys without provided namespaces. + * + * To filter default namespace, set to empty string. + */ + filterNoNamespace?: string[]; + /** @description Selects only keys with provided tag */ + filterTag?: string[]; + /** @description Selects only keys without provided tag */ + filterNoTag?: string[]; + /** + * @description Selects only keys, where translation in provided langs is in outdated state + * @example en-US + */ + filterOutdatedLanguage?: string[]; + /** + * @description Selects only keys, where translation in provided langs is not in outdated state + * @example en-US + */ + filterNotOutdatedLanguage?: string[]; + /** + * @description Selects only key affected by activity with specidfied revision ID + * @example 1234567 + */ + filterRevisionId?: number[]; + /** @description Select only keys which were not successfully translated by batch job with provided id */ + filterFailedKeysOfJob?: number; + /** @description Select only keys which are in specified task */ + filterTaskNumber?: number[]; + /** @description Filter task keys which are `not done` */ + filterTaskKeysNotDone?: boolean; + /** @description Filter task keys which are `done` */ + filterTaskKeysDone?: boolean; + /** @description Filter keys with unresolved comments in lang */ + filterHasUnresolvedCommentsInLang?: string[]; + /** @description Filter keys with any comments in lang */ + filterHasCommentsInLang?: string[]; + /** + * @description Filter key translations with labels + * @example labelId1,labelId2 + */ + filterLabel?: string[]; + /** @description Filter keys with open QA issues in lang */ + filterHasQaIssuesInLang?: string[]; + /** @description Filter keys with specific QA check type issues */ + filterQaCheckType?: ("EMPTY_TRANSLATION" | "SPACES_MISMATCH" | "UNMATCHED_NEWLINES" | "TRIM_CHECK" | "CHARACTER_CASE_MISMATCH" | "MISSING_NUMBERS" | "REPEATED_WORDS" | "PUNCTUATION_MISMATCH" | "SPELLING" | "GRAMMAR" | "BRACKETS_MISMATCH" | "BRACKETS_UNBALANCED" | "SPECIAL_CHARACTER_MISMATCH" | "DIFFERENT_URLS" | "KEY_LENGTH_LIMIT" | "INCONSISTENT_PLACEHOLDERS" | "INCONSISTENT_HTML" | "HTML_SYNTAX" | "ICU_SYNTAX")[]; + /** @description Filter keys with any suggestions in lang */ + filterHasSuggestionsInLang?: string[]; + /** @description Filter keys with no suggestions in lang */ + filterHasNoSuggestionsInLang?: string[]; + /** @description Selects only keys from specified branch */ + branch?: string; + /** @description Filter trashed keys by who deleted them (user IDs) */ + filterDeletedByUserId?: number[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelTrashedKeyWithTranslationsModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + listDeleters_1: { + parameters: { + query?: { + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelSimpleUserAccountModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + selectAll_1: { + parameters: { + query?: { + /** + * @description Translation state in the format: languageTag,state. You can use this parameter multiple times. + * + * When used with multiple states for same language it is applied with logical OR. + * + * When used with multiple languages, it is applied with logical AND. + */ + filterState?: string[]; + /** + * @description Languages to be contained in response. + * + * To add multiple languages, repeat this param (eg. ?languages=en&languages=de) + * @example en + */ + languages?: string[]; + /** @description String to search in key name or translation text */ + search?: string; + /** @description Selects key with provided names. Use this param multiple times to fetch more keys. */ + filterKeyName?: string[]; + /** @description Selects key with provided ID. Use this param multiple times to fetch more keys. */ + filterKeyId?: number[]; + /** @description Selects only keys for which the translation is missing in any returned language. It only filters for translations included in returned languages. */ + filterUntranslatedAny?: boolean; + /** @description Selects only keys, where translation is provided in any language */ + filterTranslatedAny?: boolean; + /** + * @description Selects only keys where the translation is missing for the specified language. The specified language must be included in the returned languages. Otherwise, this filter doesn't apply. + * @example en-US + */ + filterUntranslatedInLang?: string; + /** + * @description Selects only keys, where translation is provided in specified language + * @example en-US + */ + filterTranslatedInLang?: string; + /** + * @description Selects only keys, where translation was auto translated for specified languages. + * @example en-US + */ + filterAutoTranslatedInLang?: string[]; + /** @description Selects only keys with screenshots */ + filterHasScreenshot?: boolean; + /** @description Selects only keys without screenshots */ + filterHasNoScreenshot?: boolean; + /** + * @description Selects only keys with provided namespaces. + * + * To filter default namespace, set to empty string. + */ + filterNamespace?: string[]; + /** + * @description Selects only keys without provided namespaces. + * + * To filter default namespace, set to empty string. + */ + filterNoNamespace?: string[]; + /** @description Selects only keys with provided tag */ + filterTag?: string[]; + /** @description Selects only keys without provided tag */ + filterNoTag?: string[]; + /** + * @description Selects only keys, where translation in provided langs is in outdated state + * @example en-US + */ + filterOutdatedLanguage?: string[]; + /** + * @description Selects only keys, where translation in provided langs is not in outdated state + * @example en-US + */ + filterNotOutdatedLanguage?: string[]; + /** + * @description Selects only key affected by activity with specidfied revision ID + * @example 1234567 + */ + filterRevisionId?: number[]; + /** @description Select only keys which were not successfully translated by batch job with provided id */ + filterFailedKeysOfJob?: number; + /** @description Select only keys which are in specified task */ + filterTaskNumber?: number[]; + /** @description Filter task keys which are `not done` */ + filterTaskKeysNotDone?: boolean; + /** @description Filter task keys which are `done` */ + filterTaskKeysDone?: boolean; + /** @description Filter keys with unresolved comments in lang */ + filterHasUnresolvedCommentsInLang?: string[]; + /** @description Filter keys with any comments in lang */ + filterHasCommentsInLang?: string[]; + /** + * @description Filter key translations with labels + * @example labelId1,labelId2 + */ + filterLabel?: string[]; + /** @description Filter keys with open QA issues in lang */ + filterHasQaIssuesInLang?: string[]; + /** @description Filter keys with specific QA check type issues */ + filterQaCheckType?: ("EMPTY_TRANSLATION" | "SPACES_MISMATCH" | "UNMATCHED_NEWLINES" | "TRIM_CHECK" | "CHARACTER_CASE_MISMATCH" | "MISSING_NUMBERS" | "REPEATED_WORDS" | "PUNCTUATION_MISMATCH" | "SPELLING" | "GRAMMAR" | "BRACKETS_MISMATCH" | "BRACKETS_UNBALANCED" | "SPECIAL_CHARACTER_MISMATCH" | "DIFFERENT_URLS" | "KEY_LENGTH_LIMIT" | "INCONSISTENT_PLACEHOLDERS" | "INCONSISTENT_HTML" | "HTML_SYNTAX" | "ICU_SYNTAX")[]; + /** @description Filter keys with any suggestions in lang */ + filterHasSuggestionsInLang?: string[]; + /** @description Filter keys with no suggestions in lang */ + filterHasNoSuggestionsInLang?: string[]; + /** @description Selects only keys from specified branch */ + branch?: string; + /** @description Filter trashed keys by who deleted them (user IDs) */ + filterDeletedByUserId?: number[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SelectAllResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + permanentlyDelete_1: { + parameters: { + query?: never; + header?: never; + path: { + keyId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + restore_1: { + parameters: { + query?: never; + header?: never; + path: { + keyId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KeyModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + delete_16: { + parameters: { + query?: never; + header?: never; + path: { + ids: number[]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + get_9: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KeyModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + edit_1: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EditKeyDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KeyModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getBigMeta_1: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelKeyWithBaseTranslationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + complexEdit_1: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ComplexEditKeyDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KeyWithDataModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getDisabledLanguages_1: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelLanguageModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + setDisabledLanguages_1: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetDisabledLanguagesRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelLanguageModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + autoTranslate_1: { + parameters: { + query?: { + /** + * @description Tags of languages to auto-translate. + * When no languages provided, it translates only untranslated languages. + */ + languages?: string[]; + useMachineTranslation?: boolean; + useTranslationMemory?: boolean; + }; + header?: never; + path: { + keyId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getKeyScreenshots: { + parameters: { + query?: never; + header?: never; + path: { + keyId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelScreenshotModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + uploadScreenshot: { + parameters: { + query?: never; + header?: never; + path: { + keyId: number; + }; + cookie?: never; + }; + requestBody?: { + content: { + "multipart/form-data": { + /** Format: binary */ + screenshot: string; + info?: components["schemas"]["ScreenshotInfoDto"]; + }; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ScreenshotModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + deleteScreenshots: { + parameters: { + query?: never; + header?: never; + path: { + ids: number[]; + keyId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + tagKey_1: { + parameters: { + query?: never; + header?: never; + path: { + keyId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TagKeyDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TagModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + removeTag_1: { + parameters: { + query?: never; + header?: never; + path: { + keyId: number; + tagId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAll_2: { + parameters: { + query?: { + search?: string; + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelLabelModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + createLabel_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LabelRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LabelModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getLabelsByIds_1: { + parameters: { + query: { + id: number[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LabelModel"][]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + updateLabel_1: { + parameters: { + query?: never; + header?: never; + path: { + labelId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LabelRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LabelModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + deleteLabel_1: { + parameters: { + query?: never; + header?: never; + path: { + labelId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAll_6: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + /** @description Filter languages by id */ + filterId?: number[]; + /** @description Filter languages without id */ + filterNotId?: number[]; + /** @description Filter languages by name or tag */ + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelLanguageModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + createLanguage_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LanguageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LanguageModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + get_7: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LanguageModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + editLanguage_1: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LanguageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LanguageModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + deleteLanguage_1: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getSuggestions_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + /** @description Filter by suggestion state */ + filterState?: ("ACTIVE" | "ACCEPTED" | "DECLINED")[]; + }; + header?: never; + path: { + languageId: number; + keyId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelTranslationSuggestionModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + createSuggestion_1: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + keyId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateTranslationSuggestionRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranslationSuggestionModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + deleteSuggestion_1: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + keyId: number; + suggestionId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + acceptSuggestion_1: { + parameters: { + query?: { + declineOther?: boolean; + }; + header?: never; + path: { + languageId: number; + keyId: number; + suggestionId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranslationSuggestionAcceptResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + declineSuggestion_1: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + keyId: number; + suggestionId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranslationSuggestionModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + suggestionSetActive_1: { + parameters: { + query?: never; + header?: never; + path: { + languageId: number; + keyId: number; + suggestionId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranslationSuggestionModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + myList_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelBatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getByName_1: { + parameters: { + query?: never; + header?: never; + path: { + name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NamespaceModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAllNamespaces_3: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelNamespaceModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + update_4: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateNamespaceDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NamespaceModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + singleStepFromFiles_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "multipart/form-data": { + files: string[]; + params: components["schemas"]["SingleStepImportRequest"]; + }; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ImportResult"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + singleStepResolvableImport_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SingleStepImportResolvableRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ImportResult"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + aiPlaygroundTranslate_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["MachineTranslationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + assignTranslationLabel_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LabelTranslationsRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + clearTranslations_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ClearTranslationsRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + copyTranslations_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CopyTranslationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + deleteKeys_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DeleteKeysRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + hardDeleteKeys_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["HardDeleteKeysRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + machineTranslation_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["MachineTranslationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + translate_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PreTranslationByTmRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + qaCheck_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["QaRecheckByKeysRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + restoreKeys_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RestoreKeysRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + setKeysNamespace_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetKeysNamespaceRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + setTranslationState_3: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetTranslationsStateStateRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + tagKeys_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TagKeysRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + unassignTranslationLabel_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LabelTranslationsRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + untagKeys_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UntagKeysRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchJobModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getProjectStats_1: { + parameters: { + query?: { + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ProjectStatsModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getProjectDailyActivity_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: number; + }; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getQaIssueCountsByCheckType_1: { + parameters: { + query: { + languageId: number; + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: number; + }; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + suggestMachineTranslations_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SuggestRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SuggestResultModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + suggestMachineTranslationsStreaming_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SuggestRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/x-ndjson": components["schemas"]["StreamingResponseBody"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + suggestTranslationMemory_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SuggestRequestDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelTranslationMemoryItemModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + executeComplexTagOperation_1: { + parameters: { + query?: { + branch?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ComplexTagKeysRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAll_15: { + parameters: { + query?: { + search?: string; + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelTagModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getTasks_1: { + parameters: { + query?: { + /** @description Filter tasks by state */ + filterState?: ("NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED")[]; + /** @description Filter tasks without state */ + filterNotState?: ("NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED")[]; + /** @description Filter tasks by assignee */ + filterAssignee?: number[]; + /** @description Filter tasks by type */ + filterType?: ("TRANSLATE" | "REVIEW")[]; + /** @description Filter tasks by id */ + filterId?: number[]; + /** @description Filter tasks without id */ + filterNotId?: number[]; + /** @description Filter tasks by project */ + filterProject?: number[]; + /** @description Filter tasks without project */ + filterNotProject?: number[]; + /** @description Filter tasks by language */ + filterLanguage?: number[]; + /** @description Filter tasks by key */ + filterKey?: number[]; + /** @description Filter tasks by agency */ + filterAgency?: number[]; + /** @description Exclude tasks which were closed before specified timestamp */ + filterNotClosedBefore?: number; + /** @description Filter tasks by branch name. Defaults to project's default branch. */ + branch?: string; + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelTaskModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + createTask_1: { + parameters: { + query?: { + filterState?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED")[]; + filterOutdated?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateTaskRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + calculateScope_1: { + parameters: { + query?: { + filterState?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED")[]; + filterOutdated?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CalculateScopeRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KeysScopeView"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + createTasks_1: { + parameters: { + query?: { + filterState?: ("UNTRANSLATED" | "TRANSLATED" | "REVIEWED" | "DISABLED")[]; + filterOutdated?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateMultipleTasksRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getPossibleAssignees_1: { + parameters: { + query?: { + /** @description Filter users by id */ + filterId?: number[]; + /** @description Filter only users that have at least following scopes */ + filterMinimalScope?: string; + /** @description Filter only users that can view language */ + filterViewLanguageId?: number; + /** @description Filter only users that can edit language */ + filterEditLanguageId?: number; + /** @description Filter only users that can edit state of language */ + filterStateLanguageId?: number; + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelSimpleUserAccountModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getTask_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + updateTask_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateTaskRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getBlockingTasks_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number[]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + cancelTask_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + closeTask_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + finishTask_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getTaskKeys_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskKeysResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + updateTaskKeys_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateTaskKeysRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + updateTaskKey_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + keyId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateTaskKeyRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UpdateTaskKeyResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getPerUserReport_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskPerUserReportModel"][]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + reopenTask_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TaskModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getXlsxReport_1: { + parameters: { + query?: never; + header?: never; + path: { + taskNumber: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getTranslations_1: { + parameters: { + query?: { + /** @description Cursor to get next data */ + cursor?: string; + /** @description Include detailed QA issues for inline highlighting */ + includeQaIssues?: boolean; + /** + * @description Translation state in the format: languageTag,state. You can use this parameter multiple times. + * + * When used with multiple states for same language it is applied with logical OR. + * + * When used with multiple languages, it is applied with logical AND. + */ + filterState?: string[]; + /** + * @description Languages to be contained in response. + * + * To add multiple languages, repeat this param (eg. ?languages=en&languages=de) + * @example en + */ + languages?: string[]; + /** @description String to search in key name or translation text */ + search?: string; + /** @description Selects key with provided names. Use this param multiple times to fetch more keys. */ + filterKeyName?: string[]; + /** @description Selects key with provided ID. Use this param multiple times to fetch more keys. */ + filterKeyId?: number[]; + /** @description Selects only keys for which the translation is missing in any returned language. It only filters for translations included in returned languages. */ + filterUntranslatedAny?: boolean; + /** @description Selects only keys, where translation is provided in any language */ + filterTranslatedAny?: boolean; + /** + * @description Selects only keys where the translation is missing for the specified language. The specified language must be included in the returned languages. Otherwise, this filter doesn't apply. + * @example en-US + */ + filterUntranslatedInLang?: string; + /** + * @description Selects only keys, where translation is provided in specified language + * @example en-US + */ + filterTranslatedInLang?: string; + /** + * @description Selects only keys, where translation was auto translated for specified languages. + * @example en-US + */ + filterAutoTranslatedInLang?: string[]; + /** @description Selects only keys with screenshots */ + filterHasScreenshot?: boolean; + /** @description Selects only keys without screenshots */ + filterHasNoScreenshot?: boolean; + /** + * @description Selects only keys with provided namespaces. + * + * To filter default namespace, set to empty string. + */ + filterNamespace?: string[]; + /** + * @description Selects only keys without provided namespaces. + * + * To filter default namespace, set to empty string. + */ + filterNoNamespace?: string[]; + /** @description Selects only keys with provided tag */ + filterTag?: string[]; + /** @description Selects only keys without provided tag */ + filterNoTag?: string[]; + /** + * @description Selects only keys, where translation in provided langs is in outdated state + * @example en-US + */ + filterOutdatedLanguage?: string[]; + /** + * @description Selects only keys, where translation in provided langs is not in outdated state + * @example en-US + */ + filterNotOutdatedLanguage?: string[]; + /** + * @description Selects only key affected by activity with specidfied revision ID + * @example 1234567 + */ + filterRevisionId?: number[]; + /** @description Select only keys which were not successfully translated by batch job with provided id */ + filterFailedKeysOfJob?: number; + /** @description Select only keys which are in specified task */ + filterTaskNumber?: number[]; + /** @description Filter task keys which are `not done` */ + filterTaskKeysNotDone?: boolean; + /** @description Filter task keys which are `done` */ + filterTaskKeysDone?: boolean; + /** @description Filter keys with unresolved comments in lang */ + filterHasUnresolvedCommentsInLang?: string[]; + /** @description Filter keys with any comments in lang */ + filterHasCommentsInLang?: string[]; + /** + * @description Filter key translations with labels + * @example labelId1,labelId2 + */ + filterLabel?: string[]; + /** @description Filter keys with open QA issues in lang */ + filterHasQaIssuesInLang?: string[]; + /** @description Filter keys with specific QA check type issues */ + filterQaCheckType?: ("EMPTY_TRANSLATION" | "SPACES_MISMATCH" | "UNMATCHED_NEWLINES" | "TRIM_CHECK" | "CHARACTER_CASE_MISMATCH" | "MISSING_NUMBERS" | "REPEATED_WORDS" | "PUNCTUATION_MISMATCH" | "SPELLING" | "GRAMMAR" | "BRACKETS_MISMATCH" | "BRACKETS_UNBALANCED" | "SPECIAL_CHARACTER_MISMATCH" | "DIFFERENT_URLS" | "KEY_LENGTH_LIMIT" | "INCONSISTENT_PLACEHOLDERS" | "INCONSISTENT_HTML" | "HTML_SYNTAX" | "ICU_SYNTAX")[]; + /** @description Filter keys with any suggestions in lang */ + filterHasSuggestionsInLang?: string[]; + /** @description Filter keys with no suggestions in lang */ + filterHasNoSuggestionsInLang?: string[]; + /** @description Selects only keys from specified branch */ + branch?: string; + /** @description Filter trashed keys by who deleted them (user IDs) */ + filterDeletedByUserId?: number[]; + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KeysWithTranslationsPageModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + setTranslations_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetTranslationsWithKeyDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetTranslationsResponseModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + createOrUpdateTranslations_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetTranslationsWithKeyDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetTranslationsResponseModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + create_4: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TranslationCommentWithLangKeyDto"]; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["TranslationWithCommentModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + assignLabel_3: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TranslationLabelRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LabelModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + selectKeys_1: { + parameters: { + query?: { + /** + * @description Translation state in the format: languageTag,state. You can use this parameter multiple times. + * + * When used with multiple states for same language it is applied with logical OR. + * + * When used with multiple languages, it is applied with logical AND. + */ + filterState?: string[]; + /** + * @description Languages to be contained in response. + * + * To add multiple languages, repeat this param (eg. ?languages=en&languages=de) + * @example en + */ + languages?: string[]; + /** @description String to search in key name or translation text */ + search?: string; + /** @description Selects key with provided names. Use this param multiple times to fetch more keys. */ + filterKeyName?: string[]; + /** @description Selects key with provided ID. Use this param multiple times to fetch more keys. */ + filterKeyId?: number[]; + /** @description Selects only keys for which the translation is missing in any returned language. It only filters for translations included in returned languages. */ + filterUntranslatedAny?: boolean; + /** @description Selects only keys, where translation is provided in any language */ + filterTranslatedAny?: boolean; + /** + * @description Selects only keys where the translation is missing for the specified language. The specified language must be included in the returned languages. Otherwise, this filter doesn't apply. + * @example en-US + */ + filterUntranslatedInLang?: string; + /** + * @description Selects only keys, where translation is provided in specified language + * @example en-US + */ + filterTranslatedInLang?: string; + /** + * @description Selects only keys, where translation was auto translated for specified languages. + * @example en-US + */ + filterAutoTranslatedInLang?: string[]; + /** @description Selects only keys with screenshots */ + filterHasScreenshot?: boolean; + /** @description Selects only keys without screenshots */ + filterHasNoScreenshot?: boolean; + /** + * @description Selects only keys with provided namespaces. + * + * To filter default namespace, set to empty string. + */ + filterNamespace?: string[]; + /** + * @description Selects only keys without provided namespaces. + * + * To filter default namespace, set to empty string. + */ + filterNoNamespace?: string[]; + /** @description Selects only keys with provided tag */ + filterTag?: string[]; + /** @description Selects only keys without provided tag */ + filterNoTag?: string[]; + /** + * @description Selects only keys, where translation in provided langs is in outdated state + * @example en-US + */ + filterOutdatedLanguage?: string[]; + /** + * @description Selects only keys, where translation in provided langs is not in outdated state + * @example en-US + */ + filterNotOutdatedLanguage?: string[]; + /** + * @description Selects only key affected by activity with specidfied revision ID + * @example 1234567 + */ + filterRevisionId?: number[]; + /** @description Select only keys which were not successfully translated by batch job with provided id */ + filterFailedKeysOfJob?: number; + /** @description Select only keys which are in specified task */ + filterTaskNumber?: number[]; + /** @description Filter task keys which are `not done` */ + filterTaskKeysNotDone?: boolean; + /** @description Filter task keys which are `done` */ + filterTaskKeysDone?: boolean; + /** @description Filter keys with unresolved comments in lang */ + filterHasUnresolvedCommentsInLang?: string[]; + /** @description Filter keys with any comments in lang */ + filterHasCommentsInLang?: string[]; + /** + * @description Filter key translations with labels + * @example labelId1,labelId2 + */ + filterLabel?: string[]; + /** @description Filter keys with open QA issues in lang */ + filterHasQaIssuesInLang?: string[]; + /** @description Filter keys with specific QA check type issues */ + filterQaCheckType?: ("EMPTY_TRANSLATION" | "SPACES_MISMATCH" | "UNMATCHED_NEWLINES" | "TRIM_CHECK" | "CHARACTER_CASE_MISMATCH" | "MISSING_NUMBERS" | "REPEATED_WORDS" | "PUNCTUATION_MISMATCH" | "SPELLING" | "GRAMMAR" | "BRACKETS_MISMATCH" | "BRACKETS_UNBALANCED" | "SPECIAL_CHARACTER_MISMATCH" | "DIFFERENT_URLS" | "KEY_LENGTH_LIMIT" | "INCONSISTENT_PLACEHOLDERS" | "INCONSISTENT_HTML" | "HTML_SYNTAX" | "ICU_SYNTAX")[]; + /** @description Filter keys with any suggestions in lang */ + filterHasSuggestionsInLang?: string[]; + /** @description Filter keys with no suggestions in lang */ + filterHasNoSuggestionsInLang?: string[]; + /** @description Selects only keys from specified branch */ + branch?: string; + /** @description Filter trashed keys by who deleted them (user IDs) */ + filterDeletedByUserId?: number[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SelectAllResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAllTranslations_1: { + parameters: { + query?: { + /** @description Namespace to return */ + ns?: string; + /** + * @description Delimiter to structure response content. + * + * e.g. For key "home.header.title" would result in {"home": {"header": {"title": "Hello"}}} structure. + * + * When null, resulting file will be a flat key-value object. + */ + structureDelimiter?: string; + /** + * @description Enables filtering of returned keys by their tags. + * Only keys with at least one provided tag will be returned. + * Optional, filtering is not applied if not specified. + * @example [ + * "productionReady", + * "nextRelease" + * ] + */ + filterTag?: string[]; + /** @description Branch name to return translations from */ + branch?: string; + }; + header?: never; + path: { + /** + * @description Comma-separated language tags to return translations in. Languages you are not permitted to see will be silently dropped and not returned. + * @example [ + * "en", + * "de", + * "fr" + * ] + */ + languages: string[]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getAll_4: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path: { + translationId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelTranslationCommentModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + create_2: { + parameters: { + query?: never; + header?: never; + path: { + translationId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TranslationCommentDto"]; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["TranslationCommentModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + get_5: { + parameters: { + query?: never; + header?: never; + path: { + translationId: number; + commentId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranslationCommentModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + update_2: { + parameters: { + query?: never; + header?: never; + path: { + commentId: number; + translationId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TranslationCommentDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranslationCommentModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + delete_3: { + parameters: { + query?: never; + header?: never; + path: { + translationId: number; + commentId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + setState_1: { + parameters: { + query?: never; + header?: never; + path: { + translationId: number; + commentId: number; + state: "RESOLUTION_NOT_NEEDED" | "NEEDS_RESOLUTION" | "RESOLVED"; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranslationCommentModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + dismissAutoTranslatedState_1: { + parameters: { + query?: never; + header?: never; + path: { + translationId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranslationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getTranslationHistory_1: { + parameters: { + query?: { + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + }; + header?: never; + path: { + translationId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelTranslationHistoryModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + assignLabel_1: { + parameters: { + query?: never; + header?: never; + path: { + translationId: number; + labelId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LabelModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + unassignLabel_1: { + parameters: { + query?: never; + header?: never; + path: { + translationId: number; + labelId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + setOutdated_1: { + parameters: { + query?: never; + header?: never; + path: { + translationId: number; + state: boolean; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranslationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + setTranslationState_1: { + parameters: { + query?: never; + header?: never; + path: { + translationId: number; + state: "TRANSLATED" | "REVIEWED"; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TranslationModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getUsedNamespaces_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionModelUsedNamespaceModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getInfo: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PrivateUserAccountModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getTasks_2: { + parameters: { + query?: { + /** @description Filter tasks by state */ + filterState?: ("NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED")[]; + /** @description Filter tasks without state */ + filterNotState?: ("NEW" | "IN_PROGRESS" | "FINISHED" | "CANCELED")[]; + /** @description Filter tasks by assignee */ + filterAssignee?: number[]; + /** @description Filter tasks by type */ + filterType?: ("TRANSLATE" | "REVIEW")[]; + /** @description Filter tasks by id */ + filterId?: number[]; + /** @description Filter tasks without id */ + filterNotId?: number[]; + /** @description Filter tasks by project */ + filterProject?: number[]; + /** @description Filter tasks without project */ + filterNotProject?: number[]; + /** @description Filter tasks by language */ + filterLanguage?: number[]; + /** @description Filter tasks by key */ + filterKey?: number[]; + /** @description Filter tasks by agency */ + filterAgency?: number[]; + /** @description Exclude tasks which were closed before specified timestamp */ + filterNotClosedBefore?: number; + /** @description Filter tasks by branch name. Defaults to project's default branch. */ + branch?: string; + /** @description Zero-based page index (0..N) */ + page?: number; + /** @description The size of the page to be returned */ + size?: number; + /** @description Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ + sort?: string[]; + search?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PagedModelTaskWithProjectModel"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; + getSso: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PublicSsoTenantModel"]; + }; + }; + /** @description No SSO configuration available for this user */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string & (components["schemas"]["ErrorResponseTyped"] | components["schemas"]["ErrorResponseBody"]); + }; + }; + }; + }; +} diff --git a/src/ui/lib/api/screenshots.ts b/src/ui/lib/api/screenshots.ts new file mode 100644 index 00000000..5323e4eb --- /dev/null +++ b/src/ui/lib/api/screenshots.ts @@ -0,0 +1,68 @@ +export type UploadScreenshotInfo = { + /** + * Stable per-source identifier used by Tolgee to deduplicate uploads from + * the same Figma frame across multiple pushes. The legacy plugin uses + * `figma-${frameId}`. + */ + location: string; +}; + +export type UploadedScreenshot = { + /** Image id Tolgee assigns to the uploaded blob. */ + id: number; +}; + +/** + * Uploads a PNG screenshot to Tolgee via `POST /v2/image-upload`. + * + * Implemented with raw `fetch` rather than `openapi-fetch`: the generated + * schema types the multipart body in a way that is awkward to satisfy from + * TypeScript (it expects a `string` for the `image` part and a typed object + * for `info`), and the actual wire format Tolgee accepts is the standard + * multipart payload built below. `Blob` and `FormData` are part of the + * iframe's DOM and are therefore safe to use here (they are not available + * inside the plugin sandbox). + */ +export async function uploadScreenshot( + apiUrl: string, + apiKey: string, + image: Uint8Array, + info: UploadScreenshotInfo, +): Promise { + const baseUrl = apiUrl.replace(/\/$/, ""); + if (!baseUrl) { + throw new Error("Cannot upload screenshot: API URL is empty"); + } + + const formData = new FormData(); + // `Uint8Array` -> `Blob` so the underlying ArrayBuffer is properly framed + // as multipart binary data on the wire. We narrow the buffer to a fresh + // `ArrayBuffer` slice so TypeScript's strict `BlobPart` typing (which + // forbids `SharedArrayBuffer`-backed views) accepts the value. + const imageBuffer = image.buffer.slice( + image.byteOffset, + image.byteOffset + image.byteLength, + ) as ArrayBuffer; + formData.append("image", new Blob([imageBuffer], { type: "image/png" }), "screenshot.png"); + formData.append("info", new Blob([JSON.stringify(info)], { type: "application/json" })); + + const response = await fetch(`${baseUrl}/v2/image-upload`, { + method: "POST", + headers: { + "X-API-Key": apiKey, + "X-Tolgee-SDK-Type": "Figma", + Accept: "application/json", + }, + body: formData, + }); + + if (!response.ok) { + throw new Error(`Screenshot upload failed (status ${response.status}: ${response.statusText})`); + } + + const body = (await response.json()) as { id?: number }; + if (typeof body.id !== "number") { + throw new Error("Screenshot upload returned no image id"); + } + return { id: body.id }; +} diff --git a/src/ui/lib/api/tags.ts b/src/ui/lib/api/tags.ts new file mode 100644 index 00000000..378354c6 --- /dev/null +++ b/src/ui/lib/api/tags.ts @@ -0,0 +1,61 @@ +import type { TolgeeClient } from "$ui/lib/api/client"; + +export type TaggableKey = { + name: string; + namespace?: string; +}; + +/** + * Existing tag names in the project (`GET /v2/projects/tags`), for the "Add + * tags" autocomplete. Optional `search` filters server-side. Returns an empty + * array on any failure so the picker just shows no suggestions. + */ +export async function fetchProjectTags( + client: TolgeeClient, + search = "", + size = 20, +): Promise { + const { data, error } = await client.GET("/v2/projects/tags", { + params: { query: { search: search.trim() || undefined, size } }, + }); + if (error || !data) return []; + return (data._embedded?.tags ?? []) + .map((t) => t.name) + .filter((n): n is string => Boolean(n)); +} + +/** + * Apply a fixed set of tags to a list of keys via `PUT /v2/projects/tag-complex`. + * + * Mirrors the call shape used in the legacy plugin: we use the + * `filterKeys` + `tagFiltered` combo so only the explicitly listed keys are + * tagged. The endpoint accepts an optional `branch` query parameter. + * + * Throws if the API returns an error so the caller can surface it; if you + * want partial-failure tolerance, handle it at the call site. + */ +export async function applyTags( + client: TolgeeClient, + tagFiltered: string[], + keys: TaggableKey[], + branch?: string, +): Promise { + if (tagFiltered.length === 0 || keys.length === 0) return; + + const { error, response } = await client.PUT("/v2/projects/tag-complex", { + params: { + query: branch ? { branch } : {}, + }, + body: { + tagFiltered, + filterKeys: keys.map((k) => ({ + name: k.name, + namespace: k.namespace || undefined, + })), + }, + }); + + if (error) { + throw new Error(`Failed to apply tags (status ${response?.status ?? "?"})`); + } +} diff --git a/src/ui/lib/bus.ts b/src/ui/lib/bus.ts new file mode 100644 index 00000000..dac063de --- /dev/null +++ b/src/ui/lib/bus.ts @@ -0,0 +1,67 @@ +import type { MainToUi, UiToMain } from "$shared/messages"; + +type Handler = (msg: MainToUi) => void | Promise; + +/** + * Multiple listeners per message type. Result messages have both a global + * subscriber (App patches the selection from `nodes-set-result` / + * `apply-translations-result`) and per-flow ones (Pull awaits its own + * correlationId), so a single-slot registry would let one silently replace — + * or worse, unsubscribe — the other. + */ +const handlers = new Map>(); + +/** + * UI → Main messages are routed through `parent.postMessage`, whose + * structured-clone algorithm rejects Svelte 5 `$state` proxies with a + * DataCloneError. Callers tend to pass `$state`-tracked objects directly + * (config form snapshots, paramsValues, payloads built from reactive state), + * so we strip the proxy layer here once. JSON-roundtrip is safe for our + * payloads — we never send `Uint8Array`, `Map`, `Set`, `Date`, or other + * non-JSON values from the UI; screenshots travel in the opposite direction. + */ +export function send(msg: UiToMain): void { + const plain = JSON.parse(JSON.stringify(msg)) as UiToMain; + parent.postMessage({ pluginMessage: plain }, "*"); +} + +export function on( + type: K, + handler: (msg: Extract) => void | Promise, +): () => void { + let set = handlers.get(type); + if (!set) { + set = new Set(); + handlers.set(type, set); + } + const h = handler as Handler; + set.add(h); + return () => { + set.delete(h); + }; +} + +export function attachBus(): void { + window.addEventListener("message", async (event) => { + const msg = event.data?.pluginMessage as MainToUi | undefined; + if (!msg) return; + const set = handlers.get(msg.type); + if (!set) return; + // Snapshot so a handler that unsubscribes (or subscribes) mid-dispatch + // doesn't mutate the iteration. Each handler is isolated — one throwing + // must not skip the others (e.g. a flow-specific result listener failing + // would otherwise silently drop App's global selection patch). + for (const handler of [...set]) { + try { + await handler(msg); + } catch (err) { + console.error(`[tolgee:ui] handler for "${msg.type}" failed`, err); + } + } + }); +} + +let counter = 0; +export function nextCorrelationId(): string { + return `c${Date.now()}-${counter++}`; +} diff --git a/src/ui/lib/busRequest.ts b/src/ui/lib/busRequest.ts new file mode 100644 index 00000000..9906ace9 --- /dev/null +++ b/src/ui/lib/busRequest.ts @@ -0,0 +1,77 @@ +/** + * Timeout watchdog for UI -> main round-trips over `bus.ts`. + * + * Every UI -> main request (`send()` + a correlated `on()` listener waiting + * for the matching result) would otherwise wait forever. Main-thread error + * handling covers the expected failures (the bus dispatch wraps handlers and + * answers with `handler-error`; `applyTranslations`/`setNodesData` collect + * per-node errors instead of throwing) — this is the safety net for the rest: + * if a response is ever lost for a reason NOT covered by that handling (Figma + * killing the sandboxed process, a future regression), the UI hangs on a + * spinner with no recovery except force-quitting the plugin. + * + * (That claim about the bus was written before the wrapping existed — the + * dispatch had no try/catch at all, which is exactly how a thrown handler + * could strand a request with no trace.) + * + * This module is intentionally just the "give up after N seconds of + * silence" timer. Call sites keep wiring their own `on()` listeners for the + * message types they care about — the shapes differ too much (single result + * vs. streamed progress + terminal message) to force through one generic + * `request()` function. A shared timeout MECHANISM matters more than a + * shared request SHAPE. + */ + +export interface RequestWatchdog { + /** + * (Re)start the idle timer. Call once when the request is sent, and again + * on every intermediate/progress message for streamed round-trips so a + * slow-but-progressing operation isn't killed while it's still active. + */ + touch(): void; + /** + * Stop the timer for good. Call as soon as the round-trip settles + * (terminal message received, or the caller otherwise gives up) so the + * timeout can never fire after the fact. + */ + clear(): void; +} + +/** + * Starts an idle timeout that invokes `onTimeout` if `touch()` isn't called + * again within `timeoutMs`. The timer starts armed immediately (equivalent to + * an implicit `touch()` at request time) — callers don't need to call + * `touch()` before the first wait. + * + * `onTimeout` fires at most once; after it fires (or after `clear()`) further + * `touch()` calls are ignored to avoid resurrecting a settled/timed-out + * request. + */ +export function createIdleTimeout( + timeoutMs: number, + onTimeout: () => void, +): RequestWatchdog { + let handle: ReturnType | undefined; + let settled = false; + + const arm = (): void => { + if (settled) return; + if (handle !== undefined) clearTimeout(handle); + handle = setTimeout(() => { + settled = true; + handle = undefined; + onTimeout(); + }, timeoutMs); + }; + + const clear = (): void => { + settled = true; + if (handle !== undefined) { + clearTimeout(handle); + handle = undefined; + } + }; + + arm(); + return { touch: arm, clear }; +} diff --git a/src/ui/lib/clipboard.ts b/src/ui/lib/clipboard.ts new file mode 100644 index 00000000..4562583e --- /dev/null +++ b/src/ui/lib/clipboard.ts @@ -0,0 +1,96 @@ +/** + * Copy text to the clipboard from Figma's sandboxed plugin iframe. + * + * Two sandbox quirks make this harder than it looks: + * 1. `navigator.clipboard` is frequently UNDEFINED (the async Clipboard API + * isn't granted), so `navigator.clipboard.writeText(...)` throws a + * TypeError SYNCHRONOUSLY on the `.writeText` access. + * 2. `document.execCommand("copy")` often returns `true` WITHOUT actually + * writing — the checkmark flashes but the real clipboard keeps its old + * contents (observed on Figma desktop / macOS). + * + * So we don't trust either. We register a one-shot `copy` event listener and + * write the text into `clipboardData` OURSELVES, then trigger the event with a + * selected off-screen textarea. Success is defined as "our copy handler + * actually fired", not execCommand's return value. The async Clipboard API is + * only a last-resort fallback for environments that expose it. + * + * Must be called synchronously from a user gesture (a menu-item click) — the + * `async` keyword doesn't weaken that: everything up to the first `await` (the + * whole `execCommand` path) still runs inside the gesture's own tick. The + * promise exists only so the last-resort fallback can be awaited instead of + * guessed at. + * + * Returns whether the copy is believed to have succeeded. + */ +export async function copyToClipboard(text: string): Promise { + let copied = false; + + const onCopy = (event: Event): void => { + const e = event as ClipboardEvent; + // Set the payload ourselves so it doesn't depend on the textarea selection + // being copyable — this is what makes the write actually land. + e.clipboardData?.setData("text/plain", text); + e.preventDefault(); + copied = true; + }; + + try { + // Capture phase so we see the event before anything else can stop it. + document.addEventListener("copy", onCopy, true); + + const textarea = document.createElement("textarea"); + textarea.value = text; + // Off-screen but still SELECTABLE — the proven Figma-forum positioning. + // NOT display:none / visibility:hidden / readonly, all of which can stop + // the textarea being selected and suppress the copy event. + textarea.style.position = "fixed"; + textarea.style.top = "-9999px"; + textarea.style.left = "-9999px"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + + // Preserve any selection we're about to clobber, so copying a key doesn't + // wipe out text the user had highlighted elsewhere. + const selection = document.getSelection(); + const savedRange = + selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null; + + textarea.focus(); + textarea.select(); + // Fires the `copy` event (→ onCopy writes the data). Its boolean is + // deliberately ignored; `copied` is the source of truth. + document.execCommand("copy"); + document.body.removeChild(textarea); + + if (savedRange && selection) { + selection.removeAllRanges(); + selection.addRange(savedRange); + } + } catch { + // fall through to the async fallback + } finally { + document.removeEventListener("copy", onCopy, true); + } + + if (copied) return true; + + // Last resort: the async Clipboard API when the environment exposes it + // (never the pure Figma sandbox, but harmless elsewhere). Guarded so an + // undefined `clipboard` can't throw synchronously. + try { + const clip = navigator.clipboard; + if (clip && typeof clip.writeText === "function") { + // AWAIT it. Returning `true` on the un-awaited promise reported success + // the instant the call was made — but `writeText` rejects whenever the + // document lacks focus or clipboard permission, so the caller flashed + // its confirmation while the clipboard still held the old value. + await clip.writeText(text); + return true; + } + } catch { + // ignore — reported as a failure below + } + + return false; +} diff --git a/src/ui/lib/components/domain/ErrorBanner.svelte b/src/ui/lib/components/domain/ErrorBanner.svelte new file mode 100644 index 00000000..ddbc5998 --- /dev/null +++ b/src/ui/lib/components/domain/ErrorBanner.svelte @@ -0,0 +1,18 @@ + + +
+ {banner.message} +
diff --git a/src/ui/lib/components/domain/Header.svelte b/src/ui/lib/components/domain/Header.svelte new file mode 100644 index 00000000..2e4984f0 --- /dev/null +++ b/src/ui/lib/components/domain/Header.svelte @@ -0,0 +1,149 @@ + + +
+

Strings

+ + {#if auth.value.authenticated} +
+ {#if isDev} + + {:else if languageOptions.length > 0} + + {/if} +
+ {:else} + Not connected + {/if} + + + {#if auth.value.authenticated && !isDev} + + + {#snippet child({ props })} + + + + {/snippet} + + + Create a duplicate of this page
(per language or with raw keys) +
+
+ {/if} + + + {#snippet child({ props })} + + + + {/snippet} + + + Open plugin settings + + +
+
diff --git a/src/ui/lib/components/domain/IcuEditor.svelte b/src/ui/lib/components/domain/IcuEditor.svelte new file mode 100644 index 00000000..3f0b3a49 --- /dev/null +++ b/src/ui/lib/components/domain/IcuEditor.svelte @@ -0,0 +1,39 @@ + + + diff --git a/src/ui/lib/components/domain/IcuPreview.svelte b/src/ui/lib/components/domain/IcuPreview.svelte new file mode 100644 index 00000000..cd41aa74 --- /dev/null +++ b/src/ui/lib/components/domain/IcuPreview.svelte @@ -0,0 +1,34 @@ + + +
+
Preview
+
{@html safeResult}
+ {#if preview.error} +
+ ICU error: {preview.error.message} +
+ {/if} +
diff --git a/src/ui/lib/components/domain/IcuTextarea.svelte b/src/ui/lib/components/domain/IcuTextarea.svelte new file mode 100644 index 00000000..96e989ef --- /dev/null +++ b/src/ui/lib/components/domain/IcuTextarea.svelte @@ -0,0 +1,250 @@ + + +
+ + + +
+ + diff --git a/src/ui/lib/components/domain/NodeList.svelte b/src/ui/lib/components/domain/NodeList.svelte new file mode 100644 index 00000000..45a5b261 --- /dev/null +++ b/src/ui/lib/components/domain/NodeList.svelte @@ -0,0 +1,153 @@ + + +{#if nodes.length === 0} +
+ {emptyText} +
+{:else} + +
(scrollTop = viewport?.scrollTop ?? 0)} + class="h-full overflow-auto py-1" + > + {#if padTop > 0} + + {/if} +
    + {#each visible as node (node.id)} + onFilterText?.(node)} + onFilterKey={() => onFilterKey?.(node)} + selected={selectedIds?.has(node.id) ?? false} + onToggleSelect={() => onToggleSelect?.(node.id)} + {namespaceNames} + /> + {/each} +
+ {#if padBottom > 0} + + {/if} +
+
+{/if} diff --git a/src/ui/lib/components/domain/NodeListItem.svelte b/src/ui/lib/components/domain/NodeListItem.svelte new file mode 100644 index 00000000..df5191f7 --- /dev/null +++ b/src/ui/lib/components/domain/NodeListItem.svelte @@ -0,0 +1,527 @@ + + +
  • (interactive = true)} + onfocusin={() => (interactive = true)} +> +
    + +
    + + + + + {#if node.isPlural} + Plural + {/if} + {#if hasRichFormat(node)} + + Formatted + {/if} + {#if duplicateCount > 1} + + + + {#snippet child({ props })} + + {/snippet} + + Click to filter identical strings + + {/if} + {#if conflictCount > 1} + + + + + {/if} + {#if manualChange} + + + + + {/if} +
    + + +
    + + + {#if node.connected} + {#if interactive} + + + {#snippet child({ props })} + + {/snippet} + + + {#if keyMissing} + This key is no longer in Tolgee: {formatConnected(node)} + {:else} + Connected to key: {formatConnected(node)} + {/if} + + + + {#if auth.value.namespacesEnabled} + + + {#snippet child({ props })} + ns:{formatNamespace(node)} + {/snippet} + + + {node.ns ? `Namespace: ${node.ns}` : "No namespace"} + + + {/if} + {:else} + + + {#if auth.value.namespacesEnabled} + ns:{formatNamespace(node)} + {/if} + {/if} + {:else} + + (editing = true)} + onblur={() => (editing = false)} + /> + {#if auth.value.namespacesEnabled} + + + {/if} + {/if} +
    +
    + + +
    + {#if interactive} + {#if node.connected} + + + + {:else} + + + + {/if} + + + + {#snippet child({ props })} + + {#if justCopied} + + {:else} + + {/if} + + {/snippet} + + + {#if !isDev} + + + String details + + {/if} + + Move to string + + {#if node.connected} + + Connection detail + + {/if} + {#if isDev} + + {#if node.key} + + Copy key + + {/if} + {#if node.translation} + + Copy string + + {/if} + {#if node.connected} + + Open in Tolgee + + {/if} + {/if} + + + {:else} + + {#if node.connected} + + + + {:else} + + + + {/if} + + + + {/if} +
    +
  • diff --git a/src/ui/lib/components/domain/ParamsEditor.svelte b/src/ui/lib/components/domain/ParamsEditor.svelte new file mode 100644 index 00000000..17e2156c --- /dev/null +++ b/src/ui/lib/components/domain/ParamsEditor.svelte @@ -0,0 +1,90 @@ + + +{#if placeholders.length > 0} +
    + +
    + + + + {#snippet child({ props })} + + + + {/snippet} + + + These values are only used to preview the translation in Figma. They + won't be saved. + + +
    +
    + {#each placeholders as p (p)} +
    + {p} + + set(p, (e.currentTarget as HTMLInputElement).value)} + placeholder="example" + class="flex-1" + /> +
    + {/each} +
    +{/if} diff --git a/src/ui/lib/components/domain/PluralEditor.svelte b/src/ui/lib/components/domain/PluralEditor.svelte new file mode 100644 index 00000000..93f4bdaa --- /dev/null +++ b/src/ui/lib/components/domain/PluralEditor.svelte @@ -0,0 +1,83 @@ + + +
    + {#each variantOrder as variant (variant)} + {@const exampleValue = getVariantExample(locale, variant)} + {@const content = + (parsed.variants as Record)[variant] ?? ""} + +
    + + + {variant} + +
    + commitVariant(variant, next)} + hashLabel={exampleValue !== undefined ? `#${exampleValue}` : "#"} + {placeholder} + /> +
    +
    + {/each} +
    diff --git a/src/ui/lib/components/domain/PluralVariantInput.svelte b/src/ui/lib/components/domain/PluralVariantInput.svelte new file mode 100644 index 00000000..e8c59bd3 --- /dev/null +++ b/src/ui/lib/components/domain/PluralVariantInput.svelte @@ -0,0 +1,164 @@ + + + +
    { + // Single-line field: never insert a hard newline. + if (e.key === "Enter") { + e.preventDefault(); + return; + } + // A typed `#` becomes a chip right away (same as a loaded value). + if (e.key === "#") { + e.preventDefault(); + insertHashChip(); + } + }} +>
    + + diff --git a/src/ui/lib/components/domain/ProjectMetaSync.svelte b/src/ui/lib/components/domain/ProjectMetaSync.svelte new file mode 100644 index 00000000..2ae528ac --- /dev/null +++ b/src/ui/lib/components/domain/ProjectMetaSync.svelte @@ -0,0 +1,48 @@ + diff --git a/src/ui/lib/components/domain/PullSummary.svelte b/src/ui/lib/components/domain/PullSummary.svelte new file mode 100644 index 00000000..20db5358 --- /dev/null +++ b/src/ui/lib/components/domain/PullSummary.svelte @@ -0,0 +1,39 @@ + + + +
    + Download summary ({language || "—"}) +
    +
      +
    • +
      + {changedCount} +
      +
      Changed
      +
    • +
    • +
      + {missingCount} +
      +
      Missing
      +
    • +
    • +
      + {unchangedCount} +
      +
      Unchanged
      +
    • +
    +
    diff --git a/src/ui/lib/components/domain/PushConflictItem.svelte b/src/ui/lib/components/domain/PushConflictItem.svelte new file mode 100644 index 00000000..260afa04 --- /dev/null +++ b/src/ui/lib/components/domain/PushConflictItem.svelte @@ -0,0 +1,93 @@ + + +
    +
    +
    + {keyNamespace ? `${keyNamespace}.${keyName}` : keyName} + · {language} +
    +
    + {figmaText || "(empty)"} +
    +
    Figma
    +
    + +
    +
    + {remoteText || "(empty)"} +
    +
    Tolgee
    +
    + +
    + + {#if showHttpWarning} +

    + Using insecure http:// — consider https:// in production. +

    + {/if} +
    + +
    + +
    + + (showKey = !showKey)} + aria-label={showKey ? "Hide API key" : "Show API key"} + > + {#if showKey} + + {:else} + + {/if} + +
    +
    + + {#if auth.value.authenticated} + + +
    + + + + was successfully connected + + +
    +
    + {:else} + + {/if} + + {#if errorMsg} + {errorMsg} + {/if} + + {#if missingPullScope} + + API key is missing required scope: + {REQUIRED_SCOPES.pull.join(", ")} + + {/if} + diff --git a/src/ui/lib/components/domain/SettingsSectionKeys.svelte b/src/ui/lib/components/domain/SettingsSectionKeys.svelte new file mode 100644 index 00000000..ba3df498 --- /dev/null +++ b/src/ui/lib/components/domain/SettingsSectionKeys.svelte @@ -0,0 +1,168 @@ + + +{#snippet infoHint(label: string)} + + {#snippet child({ props })} + + + + {/snippet} + +{/snippet} + + +
    +

    + Key name +

    + + (form.prefillKeyFormat = v)} + /> + + {#if prefillKeyFormat} + +
    +
    +
    + + + {@render infoHint("What key format does")} + +

    Define your key format to be consistent and fast.

    +

    You can use variables, text and separators.

    +

    Variables use names from your Figma structure:

    +
      +
    • element name, element text
    • +
    • frame, group, component, instance, artboard, section
    • +
    • + separators like . : + _ - +
    • +
    +

    + Read more in our guide + . +

    +
    +
    +
    + (form.keyFormat = v)} + class="w-full" + /> +
    + +
    +
    + + + {@render infoHint("What formatting style does")} + +

    This will help you preserve the same format style.

    +

    Your style is automatically applied to the variables.

    +

    + E.g. style element_name: +
    + "My cool button" → my_cool_button +

    +
    +
    +
    + +
    +
    + +{#if showAdvanced} +
    +

    + Advanced +

    + + {#if auth.value.namespacesEnabled} +
    +
    + + + + + {#snippet child({ props })} + + + + {/snippet} + + + Applied to newly created keys. To change existing keys, select them + and use bulk “Set namespace”. + + + +
    + (form.namespace = v)} + class="w-full" + /> +
    + {:else if !hideDisabledNotes} +

    + + Namespaces are disabled for this project +

    + {/if} + + {#if auth.value.branchingEnabled} +
    +
    + + + + + {#snippet child({ props })} + + + + {/snippet} + + + Uploads and downloads target this branch. Defaults to the main + branch. + + + +
    + + {/if} + {/snippet} + +
    +
    diff --git a/src/ui/lib/components/domain/SyncButton.svelte b/src/ui/lib/components/domain/SyncButton.svelte new file mode 100644 index 00000000..780cd271 --- /dev/null +++ b/src/ui/lib/components/domain/SyncButton.svelte @@ -0,0 +1,62 @@ + + + diff --git a/src/ui/lib/components/domain/ViewFooter.svelte b/src/ui/lib/components/domain/ViewFooter.svelte new file mode 100644 index 00000000..ad3f216b --- /dev/null +++ b/src/ui/lib/components/domain/ViewFooter.svelte @@ -0,0 +1,69 @@ + + +
    + {#if leftContent} + {@render leftContent()} + {/if} +
    + {#if children} + {@render children()} + {:else} + {#if onCancel} + + {/if} + {#if onConfirm} + + {/if} + {/if} +
    +
    diff --git a/src/ui/lib/components/domain/ViewHeader.svelte b/src/ui/lib/components/domain/ViewHeader.svelte new file mode 100644 index 00000000..08d67f99 --- /dev/null +++ b/src/ui/lib/components/domain/ViewHeader.svelte @@ -0,0 +1,60 @@ + + +
    + + + +

    + {title}{#if subtitle}{subtitle}{/if} +

    + {#if actions} +
    {@render actions()}
    + {/if} +
    diff --git a/src/ui/lib/components/icons/Tolgee.svelte b/src/ui/lib/components/icons/Tolgee.svelte new file mode 100644 index 00000000..ef9d7961 --- /dev/null +++ b/src/ui/lib/components/icons/Tolgee.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/src/ui/lib/components/ui/badge.svelte b/src/ui/lib/components/ui/badge.svelte new file mode 100644 index 00000000..16f2bc15 --- /dev/null +++ b/src/ui/lib/components/ui/badge.svelte @@ -0,0 +1,26 @@ + + + + {@render children?.()} + diff --git a/src/ui/lib/components/ui/button.svelte b/src/ui/lib/components/ui/button.svelte new file mode 100644 index 00000000..13ad5cc6 --- /dev/null +++ b/src/ui/lib/components/ui/button.svelte @@ -0,0 +1,69 @@ + + + diff --git a/src/ui/lib/components/ui/buttonContext.ts b/src/ui/lib/components/ui/buttonContext.ts new file mode 100644 index 00000000..ccf740a4 --- /dev/null +++ b/src/ui/lib/components/ui/buttonContext.ts @@ -0,0 +1,6 @@ +/** + * Context key a container can set to pin the default size of any `Button` + * rendered inside it (unless the button passes an explicit `size`). Lets e.g. + * `ViewFooter` size a whole row of action buttons from one place. + */ +export const BUTTON_SIZE_CTX = Symbol("button-size"); diff --git a/src/ui/lib/components/ui/card.svelte b/src/ui/lib/components/ui/card.svelte new file mode 100644 index 00000000..ae806f73 --- /dev/null +++ b/src/ui/lib/components/ui/card.svelte @@ -0,0 +1,11 @@ + + +
    + {@render children?.()} +
    diff --git a/src/ui/lib/components/ui/checkbox.svelte b/src/ui/lib/components/ui/checkbox.svelte new file mode 100644 index 00000000..41c8d5fd --- /dev/null +++ b/src/ui/lib/components/ui/checkbox.svelte @@ -0,0 +1,33 @@ + + + + {#if checked} + + {:else if indeterminate} + + {/if} + diff --git a/src/ui/lib/components/ui/checkboxField.svelte b/src/ui/lib/components/ui/checkboxField.svelte new file mode 100644 index 00000000..a0395c52 --- /dev/null +++ b/src/ui/lib/components/ui/checkboxField.svelte @@ -0,0 +1,47 @@ + + +
    + + {#if trailing} + {@render trailing()} + {/if} +
    diff --git a/src/ui/lib/components/ui/dialog/Content.svelte b/src/ui/lib/components/ui/dialog/Content.svelte new file mode 100644 index 00000000..559c57ef --- /dev/null +++ b/src/ui/lib/components/ui/dialog/Content.svelte @@ -0,0 +1,45 @@ + + +{#if portal} + + {#if overlay}{/if} + + {@render children?.()} + + +{:else} + {#if overlay}{/if} + + {@render children?.()} + +{/if} diff --git a/src/ui/lib/components/ui/dialog/Description.svelte b/src/ui/lib/components/ui/dialog/Description.svelte new file mode 100644 index 00000000..d319b0c8 --- /dev/null +++ b/src/ui/lib/components/ui/dialog/Description.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/src/ui/lib/components/ui/dialog/Overlay.svelte b/src/ui/lib/components/ui/dialog/Overlay.svelte new file mode 100644 index 00000000..0c5cfe86 --- /dev/null +++ b/src/ui/lib/components/ui/dialog/Overlay.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/ui/lib/components/ui/dialog/Title.svelte b/src/ui/lib/components/ui/dialog/Title.svelte new file mode 100644 index 00000000..fb7e2279 --- /dev/null +++ b/src/ui/lib/components/ui/dialog/Title.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/src/ui/lib/components/ui/dialog/index.ts b/src/ui/lib/components/ui/dialog/index.ts new file mode 100644 index 00000000..5d34b16a --- /dev/null +++ b/src/ui/lib/components/ui/dialog/index.ts @@ -0,0 +1,11 @@ +import { Dialog as DialogPrimitive } from "bits-ui"; + +export const Root = DialogPrimitive.Root; +export const Trigger = DialogPrimitive.Trigger; +export const Close = DialogPrimitive.Close; +export const Portal = DialogPrimitive.Portal; + +export { default as Content } from "./Content.svelte"; +export { default as Overlay } from "./Overlay.svelte"; +export { default as Title } from "./Title.svelte"; +export { default as Description } from "./Description.svelte"; diff --git a/src/ui/lib/components/ui/dropdown-menu/Content.svelte b/src/ui/lib/components/ui/dropdown-menu/Content.svelte new file mode 100644 index 00000000..23330059 --- /dev/null +++ b/src/ui/lib/components/ui/dropdown-menu/Content.svelte @@ -0,0 +1,21 @@ + + + + + {@render children?.()} + + diff --git a/src/ui/lib/components/ui/dropdown-menu/Item.svelte b/src/ui/lib/components/ui/dropdown-menu/Item.svelte new file mode 100644 index 00000000..37ba534f --- /dev/null +++ b/src/ui/lib/components/ui/dropdown-menu/Item.svelte @@ -0,0 +1,18 @@ + + + + {@render children?.()} + diff --git a/src/ui/lib/components/ui/dropdown-menu/Separator.svelte b/src/ui/lib/components/ui/dropdown-menu/Separator.svelte new file mode 100644 index 00000000..a651941c --- /dev/null +++ b/src/ui/lib/components/ui/dropdown-menu/Separator.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/ui/lib/components/ui/dropdown-menu/index.ts b/src/ui/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 00000000..61cc3d00 --- /dev/null +++ b/src/ui/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,9 @@ +import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; + +export const Root = DropdownMenuPrimitive.Root; +export const Trigger = DropdownMenuPrimitive.Trigger; +export const Group = DropdownMenuPrimitive.Group; + +export { default as Content } from "./Content.svelte"; +export { default as Item } from "./Item.svelte"; +export { default as Separator } from "./Separator.svelte"; diff --git a/src/ui/lib/components/ui/emptyState.svelte b/src/ui/lib/components/ui/emptyState.svelte new file mode 100644 index 00000000..663843f8 --- /dev/null +++ b/src/ui/lib/components/ui/emptyState.svelte @@ -0,0 +1,41 @@ + + +
    + {#if Icon} + + {/if} +
    +

    {title}

    + {#if description} +

    {description}

    + {/if} +
    +
    diff --git a/src/ui/lib/components/ui/filterChip.svelte b/src/ui/lib/components/ui/filterChip.svelte new file mode 100644 index 00000000..86038893 --- /dev/null +++ b/src/ui/lib/components/ui/filterChip.svelte @@ -0,0 +1,31 @@ + + + + {@render children()} + + diff --git a/src/ui/lib/components/ui/iconButton.svelte b/src/ui/lib/components/ui/iconButton.svelte new file mode 100644 index 00000000..80e0e15d --- /dev/null +++ b/src/ui/lib/components/ui/iconButton.svelte @@ -0,0 +1,41 @@ + + + diff --git a/src/ui/lib/components/ui/index.ts b/src/ui/lib/components/ui/index.ts new file mode 100644 index 00000000..77d6a691 --- /dev/null +++ b/src/ui/lib/components/ui/index.ts @@ -0,0 +1,26 @@ +export { default as Button } from "./button.svelte"; +export { default as IconButton } from "./iconButton.svelte"; +export { default as TooltipIconButton } from "./tooltipIconButton.svelte"; +export { default as StatusMarker } from "./statusMarker.svelte"; +export { default as FilterChip } from "./filterChip.svelte"; +export { default as Stat } from "./stat.svelte"; +export { default as Input } from "./input.svelte"; +export { default as SearchInput } from "./searchInput.svelte"; +export { default as Label } from "./label.svelte"; +export { default as Switch } from "./switch.svelte"; +export { default as Checkbox } from "./checkbox.svelte"; +export { default as CheckboxField } from "./checkboxField.svelte"; +export { default as KeyFormatInput } from "./keyFormatInput.svelte"; +export { default as TagInput } from "./tagInput.svelte"; +export { default as TruncatedText } from "./truncatedText.svelte"; +export { default as Select } from "./select.svelte"; +export { default as Card } from "./card.svelte"; +export { default as Badge } from "./badge.svelte"; +export { default as Message } from "./message.svelte"; +export { default as EmptyState } from "./emptyState.svelte"; +export { default as ProgressBar } from "./progressBar.svelte"; + +export * as Dialog from "./dialog"; +export * as Tabs from "./tabs"; +export * as Tooltip from "./tooltip"; +export * as DropdownMenu from "./dropdown-menu"; diff --git a/src/ui/lib/components/ui/input.svelte b/src/ui/lib/components/ui/input.svelte new file mode 100644 index 00000000..ca503ab7 --- /dev/null +++ b/src/ui/lib/components/ui/input.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/ui/lib/components/ui/keyFormatInput.svelte b/src/ui/lib/components/ui/keyFormatInput.svelte new file mode 100644 index 00000000..4e5a8208 --- /dev/null +++ b/src/ui/lib/components/ui/keyFormatInput.svelte @@ -0,0 +1,316 @@ + + + + + +
    +
    { + emit(); + refresh(); + }} + onkeyup={(e) => { + // Arrows/clicks move the caret without an input event → keep the menu in sync. + if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) refresh(); + }} + onkeydown={onKeydown} + onmouseup={refresh} + onfocus={refresh} + onblur={() => setTimeout(() => (open = false), 120)} + >
    + + {#if open && filtered.length > 0} + +
      + {#each filtered as p, i (p.token)} +
    • + +
    • + {/each} +
    + {/if} +
    + + diff --git a/src/ui/lib/components/ui/label.svelte b/src/ui/lib/components/ui/label.svelte new file mode 100644 index 00000000..7a9ec234 --- /dev/null +++ b/src/ui/lib/components/ui/label.svelte @@ -0,0 +1,12 @@ + + + diff --git a/src/ui/lib/components/ui/message.svelte b/src/ui/lib/components/ui/message.svelte new file mode 100644 index 00000000..4cd547c2 --- /dev/null +++ b/src/ui/lib/components/ui/message.svelte @@ -0,0 +1,66 @@ + + +
    + +
    {@render children?.()}
    + {#if onDismiss} + + {/if} +
    diff --git a/src/ui/lib/components/ui/namespaceInput.svelte b/src/ui/lib/components/ui/namespaceInput.svelte new file mode 100644 index 00000000..cb4e4b08 --- /dev/null +++ b/src/ui/lib/components/ui/namespaceInput.svelte @@ -0,0 +1,184 @@ + + +
    + { + inputValue = e.currentTarget.value; + openMenu(); + }} + onfocus={onFocus} + onkeydown={onKeydown} + onblur={() => setTimeout(() => ((open = false), (focused = false)), 120)} + /> + + {#if open && items.length > 0} +
      + {#if available.length > 0} +
    • + Existing namespaces +
    • + {/if} + {#each items as opt, i (opt.kind + opt.name)} +
    • + +
    • + {/each} +
    + {/if} +
    diff --git a/src/ui/lib/components/ui/progressBar.svelte b/src/ui/lib/components/ui/progressBar.svelte new file mode 100644 index 00000000..a1efd344 --- /dev/null +++ b/src/ui/lib/components/ui/progressBar.svelte @@ -0,0 +1,85 @@ + + +
    + {#if label} +
    + {label} + {#if count}{count}{/if} +
    + {/if} +
    + {#if pct !== null} +
    + {:else} +
    + {/if} +
    +
    + + diff --git a/src/ui/lib/components/ui/searchInput.svelte b/src/ui/lib/components/ui/searchInput.svelte new file mode 100644 index 00000000..c244c3aa --- /dev/null +++ b/src/ui/lib/components/ui/searchInput.svelte @@ -0,0 +1,36 @@ + + +
    + + + {#if value} + + {/if} +
    diff --git a/src/ui/lib/components/ui/select.svelte b/src/ui/lib/components/ui/select.svelte new file mode 100644 index 00000000..a473f914 --- /dev/null +++ b/src/ui/lib/components/ui/select.svelte @@ -0,0 +1,60 @@ + + +
    + + + +
    diff --git a/src/ui/lib/components/ui/stat.svelte b/src/ui/lib/components/ui/stat.svelte new file mode 100644 index 00000000..c7b65216 --- /dev/null +++ b/src/ui/lib/components/ui/stat.svelte @@ -0,0 +1,40 @@ + + +{#if onclick} + +{:else} +
    +
    {value}
    +
    {label}
    +
    +{/if} diff --git a/src/ui/lib/components/ui/statusMarker.svelte b/src/ui/lib/components/ui/statusMarker.svelte new file mode 100644 index 00000000..7d777689 --- /dev/null +++ b/src/ui/lib/components/ui/statusMarker.svelte @@ -0,0 +1,48 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + {label} + + diff --git a/src/ui/lib/components/ui/switch.svelte b/src/ui/lib/components/ui/switch.svelte new file mode 100644 index 00000000..817cc106 --- /dev/null +++ b/src/ui/lib/components/ui/switch.svelte @@ -0,0 +1,36 @@ + + + + + diff --git a/src/ui/lib/components/ui/tabs/Content.svelte b/src/ui/lib/components/ui/tabs/Content.svelte new file mode 100644 index 00000000..2cbef73c --- /dev/null +++ b/src/ui/lib/components/ui/tabs/Content.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/src/ui/lib/components/ui/tabs/List.svelte b/src/ui/lib/components/ui/tabs/List.svelte new file mode 100644 index 00000000..0b03df90 --- /dev/null +++ b/src/ui/lib/components/ui/tabs/List.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/src/ui/lib/components/ui/tabs/Trigger.svelte b/src/ui/lib/components/ui/tabs/Trigger.svelte new file mode 100644 index 00000000..86c6485a --- /dev/null +++ b/src/ui/lib/components/ui/tabs/Trigger.svelte @@ -0,0 +1,18 @@ + + + + {@render children?.()} + diff --git a/src/ui/lib/components/ui/tabs/index.ts b/src/ui/lib/components/ui/tabs/index.ts new file mode 100644 index 00000000..63a0fe48 --- /dev/null +++ b/src/ui/lib/components/ui/tabs/index.ts @@ -0,0 +1,6 @@ +import { Tabs as TabsPrimitive } from "bits-ui"; + +export const Root = TabsPrimitive.Root; +export { default as List } from "./List.svelte"; +export { default as Trigger } from "./Trigger.svelte"; +export { default as Content } from "./Content.svelte"; diff --git a/src/ui/lib/components/ui/tagInput.svelte b/src/ui/lib/components/ui/tagInput.svelte new file mode 100644 index 00000000..92479175 --- /dev/null +++ b/src/ui/lib/components/ui/tagInput.svelte @@ -0,0 +1,213 @@ + + +
    +
    + {#each value as tag (tag)} + + {tag} + + + {/each} + setTimeout(() => (open = false), 120)} + /> +
    + + {#if open && options.length > 0} +
      + {#if available.length > 0} +
    • + Tags existing in project +
    • + {/if} + {#each options as opt, i (opt.kind + opt.name)} +
    • + +
    • + {/each} +
    + {/if} +
    diff --git a/src/ui/lib/components/ui/tooltip/Content.svelte b/src/ui/lib/components/ui/tooltip/Content.svelte new file mode 100644 index 00000000..f35d2713 --- /dev/null +++ b/src/ui/lib/components/ui/tooltip/Content.svelte @@ -0,0 +1,21 @@ + + + + + {@render children?.()} + + diff --git a/src/ui/lib/components/ui/tooltip/Root.svelte b/src/ui/lib/components/ui/tooltip/Root.svelte new file mode 100644 index 00000000..cc7c2ef7 --- /dev/null +++ b/src/ui/lib/components/ui/tooltip/Root.svelte @@ -0,0 +1,25 @@ + + + + {@render children?.()} + diff --git a/src/ui/lib/components/ui/tooltip/__tests__/dismiss.test.ts b/src/ui/lib/components/ui/tooltip/__tests__/dismiss.test.ts new file mode 100644 index 00000000..f7755f75 --- /dev/null +++ b/src/ui/lib/components/ui/tooltip/__tests__/dismiss.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it, vi } from "vitest"; +import { dismissAllTooltips, onTooltipDismiss } from "$ui/lib/components/ui/tooltip/dismiss"; + +describe("tooltip dismiss channel", () => { + it("closes every registered tooltip", () => { + // The reported symptom is a tooltip left hanging after the pointer leaves + // the iframe, so a dismiss has to reach ALL of them, not just the last. + const a = vi.fn(); + const b = vi.fn(); + const offA = onTooltipDismiss(a); + const offB = onTooltipDismiss(b); + + dismissAllTooltips(); + + expect(a).toHaveBeenCalledTimes(1); + expect(b).toHaveBeenCalledTimes(1); + offA(); + offB(); + }); + + it("stops calling a tooltip once it unsubscribes", () => { + // `$effect` returns the unsubscribe on unmount; a leak here would call + // into a destroyed component on every pointer exit. + const closed = vi.fn(); + const off = onTooltipDismiss(closed); + off(); + + dismissAllTooltips(); + + expect(closed).not.toHaveBeenCalled(); + }); + + it("survives a subscriber that unsubscribes while being closed", () => { + // Setting `open = false` can tear the component down synchronously, which + // unregisters mid-iteration — over a live Set that would skip the next + // subscriber. + const second = vi.fn(); + // Held in an object so the callback can reach its own unsubscribe. + const first: { off?: () => void } = {}; + first.off = onTooltipDismiss(() => first.off?.()); + const offSecond = onTooltipDismiss(second); + + expect(() => dismissAllTooltips()).not.toThrow(); + expect(second).toHaveBeenCalledTimes(1); + offSecond(); + }); + + it("is a no-op when nothing is registered", () => { + expect(() => dismissAllTooltips()).not.toThrow(); + }); +}); diff --git a/src/ui/lib/components/ui/tooltip/dismiss.ts b/src/ui/lib/components/ui/tooltip/dismiss.ts new file mode 100644 index 00000000..9da064e0 --- /dev/null +++ b/src/ui/lib/components/ui/tooltip/dismiss.ts @@ -0,0 +1,47 @@ +/** + * Force-close channel for tooltips. + * + * The plugin UI runs inside Figma's iframe. When the pointer leaves that iframe + * in one motion — out of the Figma window, or onto the canvas beside the panel + * — the trigger element never receives `pointerleave`, so the tooltip library + * has no event to close on and the tooltip stays up indefinitely (reported + * live: "every tooltip sometimes stays forever, when the mouse leaves the + * area/iframe completely"). + * + * The DOM wiring is one line with no branching; the registry below is the part + * with actual logic, so it is kept separate and unit-tested. `ensureListening` + * is a no-op without a `window`, which is also what lets those tests run in + * this repo's `node` environment. + */ + +const subscribers = new Set<() => void>(); +let listening = false; + +/** Close every currently-registered tooltip. */ +export function dismissAllTooltips(): void { + // Snapshot first: a subscriber that unregisters itself while closing would + // otherwise mutate the set mid-iteration. + for (const close of [...subscribers]) close(); +} + +function ensureListening(): void { + if (listening || typeof window === "undefined") return; + listening = true; + // `blur` catches the pointer (or focus) leaving the iframe altogether; + // `pointerleave` on the document catches it exiting through an edge while + // the iframe keeps focus. Either alone leaves a gap. + window.addEventListener("blur", dismissAllTooltips); + document.addEventListener("pointerleave", dismissAllTooltips); +} + +/** + * Register `close` to run whenever tooltips must be dismissed. Returns an + * unsubscribe function suitable for returning straight out of an `$effect`. + */ +export function onTooltipDismiss(close: () => void): () => void { + ensureListening(); + subscribers.add(close); + return () => { + subscribers.delete(close); + }; +} diff --git a/src/ui/lib/components/ui/tooltip/index.ts b/src/ui/lib/components/ui/tooltip/index.ts new file mode 100644 index 00000000..4bd6ee07 --- /dev/null +++ b/src/ui/lib/components/ui/tooltip/index.ts @@ -0,0 +1,8 @@ +import { Tooltip as TooltipPrimitive } from "bits-ui"; + +export const Provider = TooltipPrimitive.Provider; +export { default as Root } from "./Root.svelte"; +export const Trigger = TooltipPrimitive.Trigger; +export const Portal = TooltipPrimitive.Portal; + +export { default as Content } from "./Content.svelte"; diff --git a/src/ui/lib/components/ui/tooltipIconButton.svelte b/src/ui/lib/components/ui/tooltipIconButton.svelte new file mode 100644 index 00000000..0c0daddb --- /dev/null +++ b/src/ui/lib/components/ui/tooltipIconButton.svelte @@ -0,0 +1,46 @@ + + + + + {#snippet child({ props })} + + {@render children?.()} + + {/snippet} + + {tooltip ?? label} + diff --git a/src/ui/lib/components/ui/truncatedText.svelte b/src/ui/lib/components/ui/truncatedText.svelte new file mode 100644 index 00000000..fa85b26c --- /dev/null +++ b/src/ui/lib/components/ui/truncatedText.svelte @@ -0,0 +1,62 @@ + + + + + + {#snippet child({ props })} + {#if onclick} + + {:else} + + {text} + + {/if} + {/snippet} + + + {text} + + + diff --git a/src/ui/lib/logic/__tests__/asyncPool.test.ts b/src/ui/lib/logic/__tests__/asyncPool.test.ts new file mode 100644 index 00000000..d325c3e5 --- /dev/null +++ b/src/ui/lib/logic/__tests__/asyncPool.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { runWithConcurrency } from "../asyncPool"; + +describe("runWithConcurrency", () => { + it("runs every item exactly once", async () => { + const seen: number[] = []; + await runWithConcurrency([1, 2, 3, 4, 5], 2, async (n) => { + seen.push(n); + }); + expect(seen.slice().sort((a, b) => a - b)).toEqual([1, 2, 3, 4, 5]); + }); + + it("never exceeds the concurrency limit, and actually overlaps work", async () => { + let inFlight = 0; + let maxInFlight = 0; + const items = Array.from({ length: 12 }, (_, i) => i); + await runWithConcurrency(items, 3, async () => { + inFlight++; + maxInFlight = Math.max(maxInFlight, inFlight); + await Promise.resolve(); + await Promise.resolve(); + inFlight--; + }); + expect(maxInFlight).toBe(3); + }); + + it("caps the worker count at the item count when concurrency exceeds it", async () => { + let inFlight = 0; + let maxInFlight = 0; + await runWithConcurrency([1, 2], 10, async () => { + inFlight++; + maxInFlight = Math.max(maxInFlight, inFlight); + await Promise.resolve(); + inFlight--; + }); + expect(maxInFlight).toBe(2); + }); + + it("resolves immediately for an empty list", async () => { + let called = false; + await runWithConcurrency([], 5, async () => { + called = true; + }); + expect(called).toBe(false); + }); + + it("propagates a worker error", async () => { + await expect( + runWithConcurrency([1, 2, 3], 2, async (n) => { + if (n === 2) throw new Error("boom"); + }), + ).rejects.toThrow("boom"); + }); +}); diff --git a/src/ui/lib/logic/__tests__/authBootstrap.test.ts b/src/ui/lib/logic/__tests__/authBootstrap.test.ts new file mode 100644 index 00000000..9fbfff63 --- /dev/null +++ b/src/ui/lib/logic/__tests__/authBootstrap.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { decideAuthBootstrap } from "$ui/lib/logic/authBootstrap"; + +describe("decideAuthBootstrap", () => { + it("a successful validation never clears and remembers the fingerprint", () => { + expect(decideAuthBootstrap({ ok: true }, false)).toEqual({ + clearAuth: false, + rememberFingerprint: true, + }); + expect(decideAuthBootstrap({ ok: true }, true)).toEqual({ + clearAuth: false, + rememberFingerprint: true, + }); + }); + + it("an invalid API key clears an authenticated session and remembers the fingerprint", () => { + expect( + decideAuthBootstrap({ ok: false, error: "auth.invalid_api_key" }, true), + ).toEqual({ clearAuth: true, rememberFingerprint: true }); + }); + + it("an invalid API key with nothing to clear still remembers the fingerprint", () => { + expect( + decideAuthBootstrap({ ok: false, error: "auth.invalid_api_key" }, false), + ).toEqual({ clearAuth: false, rememberFingerprint: true }); + }); + + it("REGRESSION: a network blip never clears an already-authenticated session, and is not remembered", () => { + expect( + decideAuthBootstrap({ ok: false, error: "auth.network_error" }, true), + ).toEqual({ clearAuth: false, rememberFingerprint: false }); + }); + + it("a non-401/403 request failure is treated the same as a network error", () => { + expect( + decideAuthBootstrap({ ok: false, error: "auth.request_failed" }, true), + ).toEqual({ clearAuth: false, rememberFingerprint: false }); + expect( + decideAuthBootstrap({ ok: false, error: "auth.request_failed" }, false), + ).toEqual({ clearAuth: false, rememberFingerprint: false }); + }); + + it("an unrecognized error code is treated as a soft failure, not an invalid key", () => { + expect( + decideAuthBootstrap({ ok: false, error: "auth.missing_project_id" }, true), + ).toEqual({ clearAuth: false, rememberFingerprint: false }); + }); +}); diff --git a/src/ui/lib/logic/__tests__/copyApply.test.ts b/src/ui/lib/logic/__tests__/copyApply.test.ts new file mode 100644 index 00000000..cad2edb6 --- /dev/null +++ b/src/ui/lib/logic/__tests__/copyApply.test.ts @@ -0,0 +1,223 @@ +import type { NodeInfo } from "$shared/types"; +import { plainCanvasText } from "$shared/richText"; +import { buildCopyUpdates, type CopyTranslations } from "$ui/lib/logic/copyApply"; +import { pullDiff } from "$ui/lib/logic/pullDiff"; +import type { PulledKey } from "$ui/lib/api/pull"; +import { describe, expect, it } from "vitest"; +import { nsKeyIndex } from "$ui/lib/logic/namespaces"; + +// Ported from the former main-thread `resolveCopyNodeText` tests — the render +// moved here (the UI) because Figma's main-thread sandbox has no `Intl`, so +// every `{param}`/plural render silently failed there and language copies came +// out half in the source language (the "polo španělská kopie" report). + +function makeNode(overrides: Partial = {}): NodeInfo { + return { + id: overrides.id ?? "n1", + name: "Layer", + characters: "", + translation: "", + isPlural: false, + key: "k", + ns: undefined, + connected: true, + ...overrides, + }; +} + +/** Single-node convenience: the update's text, or null when skipped. */ +function resolvedText( + node: NodeInfo, + remote: { text: string; isPlural: boolean } | undefined, + language: string, +): string | null { + const translations: CopyTranslations = remote + ? { [nsKeyIndex(node.ns, node.key)]: remote } + : {}; + const updates = buildCopyUpdates([node], translations, language); + return updates[0]?.text ?? null; +} + +describe("buildCopyUpdates", () => { + const PLURAL = "{count, plural, one {# apple} other {# apples}}"; + + it("renders a plural remote translation using the NODE's own sample count, not the raw ICU", () => { + const node = makeNode({ isPlural: true, pluralParamValue: "9" }); + expect(resolvedText(node, { text: PLURAL, isPlural: true }, "en")).toBe("9 apples"); + }); + + it("renders a parametrized remote translation using the node's own paramsValues", () => { + const node = makeNode({ paramsValues: { name: "Zuzana" } }); + expect(resolvedText(node, { text: "Hello {name}!", isPlural: false }, "en")).toBe( + "Hello Zuzana!", + ); + }); + + it("trusts the remote isPlural flag over the node's own (possibly stale) one", () => { + const node = makeNode({ isPlural: false, pluralParamValue: "3" }); + expect(resolvedText(node, { text: PLURAL, isPlural: true }, "en")).toBe("3 apples"); + }); + + it("renders correctly even when BOTH isPlural flags say false but the ICU is a plural", () => { + const node = makeNode({ isPlural: false, pluralParamValue: "9" }); + expect( + resolvedText( + node, + { + text: "{value, plural, one {J'ai # pomme} many {J'ai # pommes} other {J'ai # pommes}}", + isPlural: false, + }, + "fr", + ), + ).toBe("J'ai 9 pommes"); + }); + + it("falls back to the node's persisted translation when there's no remote match", () => { + const node = makeNode({ isPlural: true, pluralParamValue: "2", translation: PLURAL }); + expect(resolvedText(node, undefined, "en")).toBe("2 apples"); + }); + + it("skips a node with neither a remote match nor a persisted translation", () => { + const node = makeNode({ translation: "" }); + expect(resolvedText(node, undefined, "en")).toBeNull(); + }); + + it("keeps inline HTML tags in the rendered output for applyRichText to consume", () => { + const node = makeNode(); + expect(resolvedText(node, { text: "Bold text", isPlural: false }, "en")).toBe( + "Bold text", + ); + }); + + it("skips an unrenderable ICU instead of dumping the raw pattern", () => { + const node = makeNode(); + expect(resolvedText(node, { text: "Ahoj {name", isPlural: false }, "cs")).toBeNull(); + }); + + it("persists the RAW remote translation alongside the rendered text", () => { + // The whole point of routing copies through apply-translations: the raw + // translation lands in plugin data too, so a Download right after a fresh + // copy diffs to "No changes found" instead of rewriting every node. + const node = makeNode({ paramsValues: { name: "Zuzana" } }); + const updates = buildCopyUpdates( + [node], + { [nsKeyIndex(undefined, "k")]: { text: "¡Hola {name}!", isPlural: false } }, + "es", + ); + expect(updates).toEqual([ + { id: "n1", text: "¡Hola Zuzana!", translation: "¡Hola {name}!", isPlural: false }, + ]); + }); + + it("matches remote keys by namespace and skips unconnected nodes", () => { + const inNs = makeNode({ id: "a", key: "greet", ns: "web" }); + const noNs = makeNode({ id: "b", key: "greet" }); + const unconnected = makeNode({ id: "c", key: "greet", connected: false }); + const updates = buildCopyUpdates( + [inNs, noNs, unconnected], + { + [nsKeyIndex("web", "greet")]: { text: "Hola web", isPlural: false }, + [nsKeyIndex(undefined, "greet")]: { text: "Hola", isPlural: false }, + }, + "es", + ); + expect(updates.map((u) => [u.id, u.text])).toEqual([ + ["a", "Hola web"], + ["b", "Hola"], + ]); + }); +}); + +describe("recreate -> immediate Download round trip", () => { + // Zuzka reported that clicking "Recreate copy" and then immediately + // "Download all" still shows a nonzero downloaded count, as if Recreate + // hadn't actually applied the translations. This reproduces the FULL + // pipeline end to end (buildCopyUpdates -> simulated apply-translations + // write -> pullDiff) using the exact remote data Recreate itself would + // have used, to check whether the two are genuinely idempotent or whether + // there's a real logic bug hiding in the gap between them. + function simulateWriteThenRediff( + node: NodeInfo, + remoteKey: PulledKey, + language: string, + ): ReturnType { + const translations: CopyTranslations = { + [nsKeyIndex(node.ns, node.key)]: { + text: remoteKey.translations[language]?.text ?? "", + isPlural: remoteKey.isPlural, + }, + }; + const [update] = buildCopyUpdates([node], translations, language); + if (!update) throw new Error("expected buildCopyUpdates to produce an update"); + + // Exactly what applyTranslations (src/main/nodes/selection.ts) persists: + // translation + isPlural from the update, characters via the SAME + // plainCanvasText transform applyRichText uses. pluralParamValue/ + // paramsValues are untouched (buildCopyUpdates never sends them), so the + // clone keeps whatever it inherited from the source at clone time. + const postWrite: NodeInfo = { + ...node, + translation: update.translation, + isPlural: update.isPlural ?? false, + characters: plainCanvasText(update.text), + }; + + return pullDiff([postWrite], [remoteKey], language); + } + + it("a simple string is unchanged after recreate + immediate download", () => { + const node = makeNode({ translation: "old text", characters: "old text" }); + const remoteKey: PulledKey = { + keyName: "k", + isPlural: false, + translations: { en: { text: "Hello there" } }, + }; + + const diff = simulateWriteThenRediff(node, remoteKey, "en"); + + expect(diff.changedNodes).toEqual([]); + expect(diff.unchangedNodes).toHaveLength(1); + }); + + it("a plural string is unchanged after recreate + immediate download", () => { + const node = makeNode({ isPlural: false, pluralParamValue: "9" }); + const remoteKey: PulledKey = { + keyName: "k", + isPlural: true, + translations: { en: { text: "{count, plural, one {# apple} other {# apples}}" } }, + }; + + const diff = simulateWriteThenRediff(node, remoteKey, "en"); + + expect(diff.changedNodes).toEqual([]); + expect(diff.unchangedNodes).toHaveLength(1); + }); + + it("a parametrized string is unchanged after recreate + immediate download", () => { + const node = makeNode({ paramsValues: { name: "Zuzana" } }); + const remoteKey: PulledKey = { + keyName: "k", + isPlural: false, + translations: { es: { text: "¡Hola {name}!" } }, + }; + + const diff = simulateWriteThenRediff(node, remoteKey, "es"); + + expect(diff.changedNodes).toEqual([]); + expect(diff.unchangedNodes).toHaveLength(1); + }); + + it("a rich-text (bold/italic) string is unchanged after recreate + immediate download", () => { + const node = makeNode(); + const remoteKey: PulledKey = { + keyName: "k", + isPlural: false, + translations: { en: { text: "Bold and italic" } }, + }; + + const diff = simulateWriteThenRediff(node, remoteKey, "en"); + + expect(diff.changedNodes).toEqual([]); + expect(diff.unchangedNodes).toHaveLength(1); + }); +}); diff --git a/src/ui/lib/logic/__tests__/copyLanguage.test.ts b/src/ui/lib/logic/__tests__/copyLanguage.test.ts new file mode 100644 index 00000000..b5de0cf3 --- /dev/null +++ b/src/ui/lib/logic/__tests__/copyLanguage.test.ts @@ -0,0 +1,115 @@ +import type { TolgeeConfig } from "$shared/types"; +import { copyLanguageFromPageName, resolveCopyLanguage } from "$ui/lib/logic/copyLanguage"; +import { describe, expect, it } from "vitest"; + +const TAGS = new Set(["en", "cs", "de", "pt-BR"]); +const NO_TAGS = new Set(); + +describe("copyLanguageFromPageName", () => { + it("reads the language from the '…- ' suffix", () => { + expect(copyLanguageFromPageName("Page 1 - cs", TAGS)).toBe("cs"); + expect(copyLanguageFromPageName("Home - pt-BR", TAGS)).toBe("pt-BR"); + }); + + it("works even before the project language list has loaded (tag-shape match)", () => { + // The bug: on a copy page the language list is often still in flight, so + // validating only against loaded tags failed and recreate fell back to en. + expect(copyLanguageFromPageName("Page 1 - cs", NO_TAGS)).toBe("cs"); + expect(copyLanguageFromPageName("Home - zh-Hans", NO_TAGS)).toBe("zh-Hans"); + }); + + it("takes the LAST segment when the source name itself contains ' - '", () => { + expect(copyLanguageFromPageName("Docs - API - de", TAGS)).toBe("de"); + }); + + it("ignores a suffix that is neither a known tag nor a language shape", () => { + // A keys copy is named "… - keys"; and a plain source name ending in " - X". + expect(copyLanguageFromPageName("Page 1 - keys", NO_TAGS)).toBeUndefined(); + expect(copyLanguageFromPageName("Docs - API", NO_TAGS)).toBeUndefined(); + expect(copyLanguageFromPageName("Docs - Landing", NO_TAGS)).toBeUndefined(); + }); + + it("returns undefined with no suffix / no name", () => { + expect(copyLanguageFromPageName("Page 1", TAGS)).toBeUndefined(); + expect(copyLanguageFromPageName(undefined, TAGS)).toBeUndefined(); + }); +}); + +describe("resolveCopyLanguage — page name wins (self-healing, poison-proof)", () => { + const cfg = (o: Partial) => o as Partial; + + it("uses the page-name suffix even when config.language was repointed to the main language", () => { + // The reported bug: the copy is "Page 1 - cs" but the selectable language + // reads 'en' — recreate must still target Czech, from the name. + expect(resolveCopyLanguage(cfg({ language: "en" }), "Page 1 - cs", NO_TAGS)).toBe("cs"); + }); + + it("lets the correct name override a stale/poisoned copyLanguage marker", () => { + expect( + resolveCopyLanguage(cfg({ copyLanguage: "en", language: "en" }), "Page 1 - cs", TAGS), + ).toBe("cs"); + }); + + it("falls back to the marker when the copy page was renamed off the pattern", () => { + expect(resolveCopyLanguage(cfg({ copyLanguage: "cs", language: "en" }), "Renamed", TAGS)).toBe( + "cs", + ); + }); + + it("uses the selectable language only as a last resort", () => { + expect(resolveCopyLanguage(cfg({ language: "de" }), "Renamed", TAGS)).toBe("de"); + }); + + it("returns undefined for a keys copy (name '…- keys', no marker/language)", () => { + expect(resolveCopyLanguage(cfg({}), "Page 1 - keys", TAGS)).toBeUndefined(); + }); +}); + +describe("name suffix vs the project's language list", () => { + it("rejects a rename that merely LOOKS like a tag, once the list has loaded", () => { + // "wip" matches the BCP-47 shape, so renaming "Home - cs" to "Home - wip" + // used to be read as a language — silently overriding the immutable + // `copyLanguage` marker and breaking Download for that page. + const known = new Set(["en", "cs", "de"]); + expect(copyLanguageFromPageName("Home - wip", known)).toBeUndefined(); + expect(copyLanguageFromPageName("Home - cs", known)).toBe("cs"); + }); + + it("still accepts a well-shaped tag while the list is still loading", () => { + // The regression this must not reintroduce: on a copy page the language + // list is usually still in flight when Recreate/Download runs. Requiring + // membership there rejected a legitimate "cs" and fell back to the main + // language — the exact bug the name-first order was introduced to fix. + const loading = new Set(); + expect(copyLanguageFromPageName("Home - cs", loading)).toBe("cs"); + expect(copyLanguageFromPageName("Home - pt-BR", loading)).toBe("pt-BR"); + }); + + it("falls through to the marker for a renamed copy instead of guessing", () => { + const known = new Set(["en", "cs"]); + expect( + resolveCopyLanguage({ copyLanguage: "cs", language: "en" }, "Home - wip", known), + ).toBe("cs"); + }); + + it("prefers the name over a marker poisoned to the main language", () => { + // Self-healing: copies made before the resolution was fixed carry + // `copyLanguage: "en"`. Marker-first would keep them broken forever. + const known = new Set(["en", "cs"]); + expect( + resolveCopyLanguage({ copyLanguage: "en", language: "en" }, "Home - cs", known), + ).toBe("cs"); + }); + + it("reads a copy made by the published plugin, which writes no marker", () => { + // `copyPage.ts` stores `{pageCopy, pageInfo, language}` and nothing else, + // so the name is the only stable signal such a copy leaves. + const known = new Set(["en", "cs"]); + expect(resolveCopyLanguage({ language: "en" }, "Home - cs", known)).toBe("cs"); + }); + + it("still treats a keys copy as having no language", () => { + const known = new Set(["en", "cs"]); + expect(resolveCopyLanguage({}, "Home - keys", known)).toBeUndefined(); + }); +}); diff --git a/src/ui/lib/logic/__tests__/copyRecreate.test.ts b/src/ui/lib/logic/__tests__/copyRecreate.test.ts new file mode 100644 index 00000000..e66a55bd --- /dev/null +++ b/src/ui/lib/logic/__tests__/copyRecreate.test.ts @@ -0,0 +1,168 @@ +import type { MainToUi, UiToMain } from "$shared/messages"; +import type { NodeInfo } from "$shared/types"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +// Module-level bus mock: handlers registered via `on` are triggerable from the +// test through `emit`, mimicking main→UI messages. Crucially there is NO +// component lifecycle here — exactly the property finishCopyRecreate needs +// (CopyView unmounts mid-recreate; the job must keep working regardless). +const sent: UiToMain[] = []; +type Handler = (msg: MainToUi) => void; +const handlers = new Map>(); +let correlationCounter = 0; + +vi.mock("$ui/lib/bus", () => ({ + send: (msg: UiToMain) => { + sent.push(msg); + }, + nextCorrelationId: () => `apply-${++correlationCounter}`, + on: (type: string, handler: Handler) => { + let set = handlers.get(type); + if (!set) { + set = new Set(); + handlers.set(type, set); + } + set.add(handler); + return () => set?.delete(handler); + }, +})); + +import { nsKeyIndex } from "$ui/lib/logic/namespaces"; +import { + type CopyTranslations, + finishCopyRecreate, + isCopyRecreateInFlight, +} from "$ui/lib/logic/copyApply"; + +function emit(type: string, msg: Record): void { + for (const h of [...(handlers.get(type) ?? [])]) h({ type, ...msg } as MainToUi); +} + +/** Let the job's queued async work (the post-result apply chain) run. */ +async function flushAsync(): Promise { + for (let i = 0; i < 10; i++) await Promise.resolve(); +} + +function makeNode(overrides: Partial = {}): NodeInfo { + return { + id: overrides.id ?? "n1", + name: "Node", + characters: overrides.characters ?? "Hello", + translation: overrides.translation ?? "", + isPlural: false, + key: overrides.key ?? "greeting", + ns: overrides.ns, + connected: overrides.connected ?? true, + }; +} + +afterEach(() => { + sent.length = 0; + handlers.clear(); +}); + +describe("finishCopyRecreate — survives the initiating view unmounting", () => { + it("applies the fetched language onto the clone and toasts, with no component alive", async () => { + const translations: Record = { + cs: { [nsKeyIndex(undefined, "greeting")]: { text: "Ahoj", isPlural: false } }, + }; + const done = vi.fn(); + finishCopyRecreate({ correlationId: "rc1", translations, onDone: done }); + + // The result arrives AFTER CopyView would have unmounted (page switch) — + // nothing here but the module job is listening. + emit("create-copy-result", { + correlationId: "rc1", + ok: true, + pages: [{ pageId: "p2", language: "cs", nodes: [makeNode()] }], + }); + await flushAsync(); + + // The job rendered + sent the apply batch for the clone's node… + const apply = sent.find((m) => m.type === "apply-translations") as Extract< + UiToMain, + { type: "apply-translations" } + >; + expect(apply).toBeDefined(); + expect(apply.updates).toEqual([ + { id: "n1", text: "Ahoj", translation: "Ahoj", isPlural: false }, + ]); + + // …and once the write confirms, it toasts and reports success. + emit("apply-translations-result", { correlationId: apply.correlationId, ok: true, errors: [] }); + await flushAsync(); + expect(sent.some((m) => m.type === "notify" && m.text === "Copy recreated.")).toBe(true); + expect(done).toHaveBeenCalledWith({ ok: true }); + }); + + it("a keys copy (null translations) skips the apply and still completes", async () => { + const done = vi.fn(); + finishCopyRecreate({ correlationId: "rc2", translations: null, onDone: done }); + emit("create-copy-result", { + correlationId: "rc2", + ok: true, + pages: [{ pageId: "p2", language: "", nodes: [makeNode()] }], + }); + await flushAsync(); + expect(sent.some((m) => m.type === "apply-translations")).toBe(false); + expect(sent.some((m) => m.type === "notify" && m.text === "Copy recreated.")).toBe(true); + expect(done).toHaveBeenCalledWith({ ok: true }); + }); + + it("surfaces a failed clone through onDone", async () => { + const done = vi.fn(); + finishCopyRecreate({ correlationId: "rc3", translations: null, onDone: done }); + emit("create-copy-result", { correlationId: "rc3", ok: false, error: "boom" }); + await flushAsync(); + expect(done).toHaveBeenCalledWith({ ok: false, error: "boom" }); + expect(sent.some((m) => m.type === "notify")).toBe(false); + }); + + it("ignores results for other correlation ids", async () => { + const done = vi.fn(); + finishCopyRecreate({ correlationId: "rc4", translations: null, onDone: done }); + emit("create-copy-result", { correlationId: "other", ok: true, pages: [] }); + await flushAsync(); + expect(done).not.toHaveBeenCalled(); + // Complete it properly so the job's watchdog is cleaned up. + emit("create-copy-result", { correlationId: "rc4", ok: true, pages: [] }); + await flushAsync(); + expect(done).toHaveBeenCalledWith({ ok: true }); + }); +}); + +describe("recreate re-entrancy", () => { + it("reports itself in flight until the job settles", () => { + // The guard has to live here, not in the view: recreating unmounts + // CopyView, so a second click lands on a fresh instance whose own state + // says "idle" — and a second run deletes the page the first is writing. + expect(isCopyRecreateInFlight()).toBe(false); + + finishCopyRecreate({ correlationId: "r-1", translations: null }); + expect(isCopyRecreateInFlight()).toBe(true); + + emit("create-copy-result", { correlationId: "r-1", ok: true, pages: [] }); + expect(isCopyRecreateInFlight()).toBe(false); + }); + + it("clears the flag when the job fails", () => { + finishCopyRecreate({ correlationId: "r-2", translations: null }); + + emit("create-copy-result", { correlationId: "r-2", ok: false, error: "nope" }); + + expect(isCopyRecreateInFlight()).toBe(false); + }); + + it("an older job settling does not clear a newer one's flag", () => { + finishCopyRecreate({ correlationId: "r-3", translations: null }); + finishCopyRecreate({ correlationId: "r-4", translations: null }); + + // r-3 answers late; r-4 is the current job and must stay marked. + emit("create-copy-result", { correlationId: "r-3", ok: true, pages: [] }); + + expect(isCopyRecreateInFlight()).toBe(true); + + emit("create-copy-result", { correlationId: "r-4", ok: true, pages: [] }); + expect(isCopyRecreateInFlight()).toBe(false); + }); +}); diff --git a/src/ui/lib/logic/__tests__/deeplink.test.ts b/src/ui/lib/logic/__tests__/deeplink.test.ts new file mode 100644 index 00000000..0c6aa3a3 --- /dev/null +++ b/src/ui/lib/logic/__tests__/deeplink.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "vitest"; +import type { NodeInfo } from "$shared/types"; +import { buildKeyDeepLink, buildProjectDashboardLink } from "../deeplink"; + +// Real tests for the module moved out of the deleted ui-inspect minipanel +// (task 22b) — replacing the skipped placeholder that shipped with it. The +// cases below are exactly what that placeholder promised to cover. + +function node(overrides: Partial = {}): NodeInfo { + return { + id: "1:1", + name: "Layer", + characters: "Hello", + translation: "Hello", + isPlural: false, + key: "home.title", + ns: undefined, + connected: true, + ...overrides, + }; +} + +describe("buildKeyDeepLink", () => { + it("builds a project-scoped translations search URL (happy path)", () => { + const url = buildKeyDeepLink({ apiUrl: "https://app.tolgee.io", projectId: 42 }, node()); + expect(url).toBe("https://app.tolgee.io/projects/42/translations?search=home.title"); + }); + + it("adds the namespace filter when the node has one", () => { + const url = buildKeyDeepLink( + { apiUrl: "https://app.tolgee.io", projectId: 42 }, + node({ ns: "web" }), + ); + expect(url).toBe( + "https://app.tolgee.io/projects/42/translations?search=home.title&filterNamespace=web", + ); + }); + + it("falls back to the project-selector view when no projectId is persisted", () => { + const url = buildKeyDeepLink({ apiUrl: "https://app.tolgee.io" }, node()); + expect(url).toBe("https://app.tolgee.io/projects/translations?search=home.title"); + }); + + it("trims trailing slashes off apiUrl instead of producing //", () => { + const url = buildKeyDeepLink({ apiUrl: "https://app.tolgee.io///", projectId: 1 }, node()); + expect(url).toBe("https://app.tolgee.io/projects/1/translations?search=home.title"); + }); + + it("returns null without an apiUrl or without a key", () => { + expect(buildKeyDeepLink(null, node())).toBeNull(); + expect(buildKeyDeepLink({}, node())).toBeNull(); + expect(buildKeyDeepLink({ apiUrl: "https://app.tolgee.io" }, node({ key: "" }))).toBeNull(); + }); + + it("URL-encodes keys with reserved characters", () => { + const url = buildKeyDeepLink( + { apiUrl: "https://app.tolgee.io", projectId: 1 }, + node({ key: "a b&c" }), + ); + expect(url).toBe("https://app.tolgee.io/projects/1/translations?search=a+b%26c"); + }); +}); + +describe("buildProjectDashboardLink", () => { + it("targets the project dashboard when projectId is known", () => { + expect(buildProjectDashboardLink({ apiUrl: "https://app.tolgee.io", projectId: 7 })).toBe( + "https://app.tolgee.io/projects/7", + ); + }); + + it("falls back to the project list without a projectId", () => { + expect(buildProjectDashboardLink({ apiUrl: "https://app.tolgee.io/" })).toBe( + "https://app.tolgee.io/projects", + ); + }); + + it("returns null without an apiUrl", () => { + expect(buildProjectDashboardLink(null)).toBeNull(); + expect(buildProjectDashboardLink({})).toBeNull(); + }); +}); diff --git a/src/ui/lib/logic/__tests__/icuParams.test.ts b/src/ui/lib/logic/__tests__/icuParams.test.ts new file mode 100644 index 00000000..b5ed4141 --- /dev/null +++ b/src/ui/lib/logic/__tests__/icuParams.test.ts @@ -0,0 +1,90 @@ +import type { NodeInfo } from "$shared/types"; +import { extractParamNames, hasRichFormat } from "$ui/lib/logic/icuParams"; +import { describe, expect, it } from "vitest"; + +describe("extractParamNames", () => { + it("returns named parameters in order", () => { + expect(extractParamNames("Hi {name}")).toEqual(["name"]); + expect(extractParamNames("{firstName} {lastName}")).toEqual(["firstName", "lastName"]); + }); + + it("keeps params that carry a non-selector format (e.g. number/date)", () => { + expect(extractParamNames("You have {count, number} left")).toEqual(["count"]); + expect(extractParamNames("on {when, date, short}")).toEqual(["when"]); + }); + + it("skips positional/numeric args", () => { + expect(extractParamNames("{0} then {1}")).toEqual([]); + }); + + it("skips plural and selectordinal selectors — their `#` bodies aren't params", () => { + expect(extractParamNames("{count, plural, one {# item} other {# items}}")).toEqual([]); + expect(extractParamNames("{n, selectordinal, one {#st} other {#th}}")).toEqual([]); + }); + + it("skips the `select` selector itself; its word-form variant bodies are a known limitation", () => { + // The selector `g` is correctly skipped (its tail matches the guard, which + // also swallows the first variant `{he}`). But `{she}` / `{they}` are plain + // `{word}` tokens, so this naive regex mis-reads them as parameters. + // Harmless for the Format badge (a `select` string is "rich" either way); + // captured here so the edge case is visible, not silently relied upon. + // Plural/selectordinal avoid it because their bodies start with `#`. + expect(extractParamNames("{g, select, male {he} female {she} other {they}}")).toEqual([ + "she", + "they", + ]); + }); + + it("returns nothing for plain text", () => { + expect(extractParamNames("Just some words.")).toEqual([]); + }); + + it("collects the fillable param alongside a plural selector", () => { + // The selector `count` is skipped; the standalone `{name}` is a real value. + expect(extractParamNames("{name} has {count, plural, one {# msg} other {# msgs}}")).toEqual([ + "name", + ]); + }); +}); + +/** Minimal NodeInfo stand-in — only `translation` (and, for the negative + * contract test, `characters`) matter to hasRichFormat. */ +function node(fields: Partial): NodeInfo { + return fields as NodeInfo; +} + +describe("hasRichFormat", () => { + it("is true when the translation carries a fillable parameter", () => { + expect(hasRichFormat(node({ translation: "Hi {name}" }))).toBe(true); + }); + + it("is true when the translation carries inline markup", () => { + expect(hasRichFormat(node({ translation: "This is bold" }))).toBe(true); + expect(hasRichFormat(node({ translation: "line one
    line two" }))).toBe(true); + expect(hasRichFormat(node({ translation: "an emphasis" }))).toBe(true); + }); + + it("is false for plain text", () => { + expect(hasRichFormat(node({ translation: "Just some words." }))).toBe(false); + }); + + it("is false when there is no translation", () => { + expect(hasRichFormat(node({}))).toBe(false); + }); + + it("does not mistake a stray `<` comparison for markup", () => { + expect(hasRichFormat(node({ translation: "keep if a < b and c > d" }))).toBe(false); + }); + + it("is false for a bare plural with no params or markup", () => { + expect(hasRichFormat(node({ translation: "{count, plural, one {# item} other {# items}}" }))).toBe( + false, + ); + }); + + it("reads the stored translation, not the rendered characters", () => { + // `characters` is the canvas text with formatting already applied; the + // source `{…}` / tags live only on `translation`. + expect(hasRichFormat(node({ translation: "plain", characters: "rendered" }))).toBe(false); + }); +}); diff --git a/src/ui/lib/logic/__tests__/manualChange.test.ts b/src/ui/lib/logic/__tests__/manualChange.test.ts new file mode 100644 index 00000000..06c942cd --- /dev/null +++ b/src/ui/lib/logic/__tests__/manualChange.test.ts @@ -0,0 +1,91 @@ +import type { NodeInfo } from "$shared/types"; +import { describe, expect, it } from "vitest"; +import { hasManualChange, isAdvancedString } from "$ui/lib/logic/manualChange"; + +const PLURAL = "{value, plural, one {# woman} other {# women}}"; + +function makeNode(overrides: Partial = {}): NodeInfo { + return { + id: "n", + name: "Layer", + characters: "", + translation: "", + isPlural: false, + key: "plural-W", + ns: undefined, + connected: false, + ...overrides, + } as NodeInfo; +} + +describe("hasManualChange", () => { + it("flags an UNCONNECTED plural whose canvas was edited (matches the original)", () => { + // Regression: the rewrite gated on `connected`, so a locally-created plural + // edited on canvas ("9 women-" vs the rendered "9 women") showed no warning. + const node = makeNode({ + isPlural: true, + connected: false, + translation: PLURAL, + pluralParamValue: "9", + characters: "9 women-", + }); + expect(hasManualChange(node, "en")).toBe(true); + }); + + it("does NOT flag a plural whose canvas matches its render", () => { + const node = makeNode({ + isPlural: true, + connected: false, + translation: PLURAL, + pluralParamValue: "9", + characters: "9 women", + }); + expect(hasManualChange(node, "en")).toBe(false); + }); + + it("does NOT flag a plain string (only advanced strings can diverge)", () => { + const node = makeNode({ + connected: true, + translation: "Hello", + characters: "Hello, edited", + }); + expect(hasManualChange(node, "en")).toBe(false); + }); + + it("does NOT flag a node with no stored translation", () => { + const node = makeNode({ isPlural: true, translation: "", characters: "10 women" }); + expect(hasManualChange(node, "en")).toBe(false); + }); +}); + +describe("isAdvancedString", () => { + it("does NOT treat ICU-escaped literal braces as advanced", () => { + // Regression: the check was a bare `\{[^}]*\}` brace-pair regex. Tolgee + // stores a plain translation containing literal braces as ICU-escaped text, + // which has no argument in it — reading it as advanced makes `textOfNode` + // prefer the stale stored value over the designer's canvas edit, and the + // Push diff then reports the key as unchanged. + expect(isAdvancedString(makeNode({ translation: "Use '{'braces'}' here" }))).toBe(false); + expect(isAdvancedString(makeNode({ translation: "a '{' b" }))).toBe(false); + }); + + it("still treats a real ICU argument as advanced", () => { + expect(isAdvancedString(makeNode({ translation: "Hello, {name}!" }))).toBe(true); + expect(isAdvancedString(makeNode({ translation: PLURAL }))).toBe(true); + }); + + it("still treats inline markup as advanced", () => { + expect(isAdvancedString(makeNode({ translation: "bold" }))).toBe(true); + }); + + it("treats a stray unbalanced brace as plain text, not an argument", () => { + expect(isAdvancedString(makeNode({ translation: "co{unt" }))).toBe(false); + }); + + it("still honours the isPlural flag and sample params", () => { + expect(isAdvancedString(makeNode({ isPlural: true, translation: "plain" }))).toBe(true); + expect(isAdvancedString(makeNode({ translation: "plain", paramsValues: { n: "1" } }))).toBe( + true, + ); + }); +}); diff --git a/src/ui/lib/logic/__tests__/namespaces.test.ts b/src/ui/lib/logic/__tests__/namespaces.test.ts new file mode 100644 index 00000000..a95df5c3 --- /dev/null +++ b/src/ui/lib/logic/__tests__/namespaces.test.ts @@ -0,0 +1,50 @@ +import type { NodeInfo } from "$shared/types"; +import { collectNamespaceNames, namespacedKeyLabel, nsKeyIndex } from "$ui/lib/logic/namespaces"; +import { describe, expect, it } from "vitest"; + +describe("namespacedKeyLabel", () => { + it("prefixes the namespace when namespaces are enabled", () => { + expect(namespacedKeyLabel("common", "hello", true)).toBe("common.hello"); + }); + + it("omits the prefix when there is no namespace", () => { + expect(namespacedKeyLabel(undefined, "hello", true)).toBe("hello"); + }); + + it("omits the prefix when namespaces are disabled, even with a namespace present", () => { + expect(namespacedKeyLabel("common", "hello", false)).toBe("hello"); + }); +}); + +describe("collectNamespaceNames", () => { + it("merges server, node, and default namespaces, deduped and sorted", () => { + const names = collectNamespaceNames( + [{ name: "b" }, { name: "a" }], + [{ ns: "c" }, { ns: "a" }] as NodeInfo[], + "d", + ); + expect(names).toEqual(["a", "b", "c", "d"]); + }); +}); + +describe("nsKeyIndex", () => { + it("keeps pairs distinct when a name contains the old separator", () => { + // The reported collision: joining on `|` made (ns "a|b", key "c") and + // (ns "a", key "b|c") both spell `a|b|c`. The later entry overwrote the + // earlier one, and in the copy flow that wrote one translation onto BOTH + // cloned nodes. Neither the key nor the namespace input forbids `|`. + expect(nsKeyIndex("a|b", "c")).not.toBe(nsKeyIndex("a", "b|c")); + }); + + it("treats an absent namespace as empty, not as the string 'undefined'", () => { + expect(nsKeyIndex(undefined, "greeting")).toBe(nsKeyIndex("", "greeting")); + }); + + it("distinguishes a namespaced key from a bare one", () => { + expect(nsKeyIndex("web", "greeting")).not.toBe(nsKeyIndex(undefined, "greeting")); + }); + + it("round-trips the same pair to the same index", () => { + expect(nsKeyIndex("web", "greeting")).toBe(nsKeyIndex("web", "greeting")); + }); +}); diff --git a/src/ui/lib/logic/__tests__/prefillKey.test.ts b/src/ui/lib/logic/__tests__/prefillKey.test.ts new file mode 100644 index 00000000..b26c0f02 --- /dev/null +++ b/src/ui/lib/logic/__tests__/prefillKey.test.ts @@ -0,0 +1,155 @@ +import { beforeEach, describe, expect, it } from "vitest"; + +import type { NodeInfo, TolgeeConfig } from "$shared/types"; +import { pendingPrefills, resetPrefillSettled } from "$ui/lib/logic/prefillKey"; + +function makeNode(overrides: Partial = {}): NodeInfo { + return { + id: overrides.id ?? "1:1", + name: "Layer", + characters: "Sign in", + key: "", + ns: undefined, + translation: "", + isPlural: false, + connected: false, + ...overrides, + }; +} + +const CONFIG: Partial = { + prefillKeyFormat: true, + keyFormat: "{elementText}", + namespace: "", +}; + +beforeEach(() => { + resetPrefillSettled(); +}); + +describe("pendingPrefills", () => { + it("fills empty-key unconnected nodes once, then settles", () => { + const nodes = [makeNode({ id: "1:1" }), makeNode({ id: "1:2", characters: "Log out" })]; + const first = pendingPrefills(nodes, CONFIG); + expect(first.map((p) => p.id)).toEqual(["1:1", "1:2"]); + expect(first[0]?.key).toBe("Sign in"); + // Once the caller has persisted the generated keys, a re-scan is a no-op + // (the keys are now non-empty, so nothing regenerates). + const persisted = [ + makeNode({ id: "1:1", key: "Sign in" }), + makeNode({ id: "1:2", key: "Log out", characters: "Log out" }), + ]; + expect(pendingPrefills(persisted, CONFIG)).toEqual([]); + }); + + it("plugin open never rewrites keys persisted earlier (boot is not a format change)", () => { + // Fresh session memory = reopen. Keyed nodes stay untouched, empty fill. + const nodes = [ + makeNode({ id: "1:1", key: "my.manual.key" }), + makeNode({ id: "1:2", characters: "Log out" }), + makeNode({ id: "1:3", key: "linked", connected: true }), + ]; + expect(pendingPrefills(nodes, CONFIG)).toEqual([ + { id: "1:2", key: "Log out", ns: "" }, + ]); + }); + + it("a cleared key REGENERATES on the same format (clearing = 'give me a fresh auto value')", () => { + const node = makeNode({ id: "1:1" }); + pendingPrefills([node], CONFIG); // prefill applied → settled + const cleared = makeNode({ id: "1:1", key: "" }); // user deleted the key + // Matches upstream (fills any keyless unconnected node); a clear no longer + // "sticks" — the empty key is refilled with the current-format value. + expect(pendingPrefills([cleared], CONFIG)).toEqual([{ id: "1:1", key: "Sign in", ns: "" }]); + }); + + it("a manual (non-empty) key is NOT overwritten on re-scan under the same format", () => { + const node = makeNode({ id: "1:1" }); + pendingPrefills([node], CONFIG); // generated "Sign in", settled + const edited = makeNode({ id: "1:1", key: "my.custom" }); // user edited it + // The non-empty edit is protected: regeneration only touches empty keys. + expect(pendingPrefills([edited], CONFIG)).toEqual([]); + }); + + it("on a format change, regenerates UNTOUCHED auto keys but preserves manual edits", () => { + const nodes = [ + // Auto key: still equals the marker we generated → follows the new format. + makeNode({ id: "1:1", key: "Sign in", prefilledKey: "Sign in" }), + // Manual edit: key differs from the marker → left alone. + makeNode({ id: "1:2", key: "manual.key", prefilledKey: "Log out", characters: "Log out" }), + makeNode({ id: "1:3", key: "linked", connected: true }), + ]; + pendingPrefills(nodes, CONFIG); // decisions made under {elementText} + const regenerated = pendingPrefills(nodes, { ...CONFIG, keyFormat: "{elementName}" }); + // Only the untouched auto key reflows; the manual edit and the connected + // node are never rewritten. + expect(regenerated).toEqual([{ id: "1:1", key: "Layer", ns: "" }]); + }); + + it("a format change also refills keys the user had cleared", () => { + const node = makeNode({ id: "1:1" }); + pendingPrefills([node], CONFIG); + const cleared = makeNode({ id: "1:1", key: "" }); + pendingPrefills([cleared], CONFIG); // still cleared under the same format + expect(pendingPrefills([cleared], { ...CONFIG, keyFormat: "{elementName}" })).toEqual([ + { id: "1:1", key: "Layer", ns: "" }, + ]); + }); + + it("regenerates an untouched auto key when variable casing changes", () => { + const node = makeNode({ id: "1:1" }); + pendingPrefills([node], CONFIG); + // Auto key (equals the marker) reflows on a casing change too. + const keyed = makeNode({ id: "1:1", key: "Sign in", prefilledKey: "Sign in" }); + const regenerated = pendingPrefills([keyed], { ...CONFIG, variableCasing: "camelCase" }); + expect(regenerated).toEqual([{ id: "1:1", key: "signIn", ns: "" }]); + }); + + it("preserves a manual edit when the style changes (the reported bug)", () => { + const node = makeNode({ id: "1:1" }); + const [applied] = pendingPrefills([node], CONFIG); // auto-generated "Sign in", marker set + // User hand-edits the key; the marker still holds the old auto value. + const edited = makeNode({ id: "1:1", key: "my.custom", prefilledKey: applied?.key }); + // Changing the format must NOT overwrite the manual edit… + expect(pendingPrefills([edited], { ...CONFIG, keyFormat: "{elementName}" })).toEqual([]); + // …nor a casing change. + expect(pendingPrefills([edited], { ...CONFIG, variableCasing: "camelCase" })).toEqual([]); + }); + + it("toggling prefill off is inert; re-enabling with a NEW format regenerates", () => { + const node = makeNode({ id: "1:1" }); + const [applied] = pendingPrefills([node], CONFIG); + // Auto key: key still equals the marker we generated. + const keyed = makeNode({ id: "1:1", key: applied?.key, prefilledKey: applied?.key }); + expect(pendingPrefills([keyed], { prefillKeyFormat: false })).toEqual([]); + expect(pendingPrefills([keyed], { ...CONFIG, keyFormat: "{elementName}" })).toEqual([ + { id: "1:1", key: "Layer", ns: "" }, + ]); + }); + + it("finding 65 round-trip: prefill-off clears the key (main), re-enabling regenerates it", () => { + // While on, the key is generated + persisted. Turning prefill off clears + // the persisted key on the main thread (clearPrefilledKeys). Re-enabling + // with the same format regenerates it — no session-memory reset needed, + // because an empty key always regenerates. + const node = makeNode({ id: "1:1" }); + expect(pendingPrefills([node], CONFIG)[0]?.key).toBe("Sign in"); // generated + settled + const cleared = makeNode({ id: "1:1", key: "" }); // main thread cleared it on prefill-off + expect(pendingPrefills([cleared], CONFIG)).toEqual([{ id: "1:1", key: "Sign in", ns: "" }]); + }); + + it("waits for parent names before regenerating an auto key from a parent-based format", () => { + // Auto key (equals the marker) so a format change is allowed to reflow it. + const node = makeNode({ id: "1:1", key: "old", prefilledKey: "old" }); + pendingPrefills([node], CONFIG); // decided under {elementText} + const parentConfig = { ...CONFIG, keyFormat: "{frame}.{elementName}" }; + // Stale snapshot (scan under the old format resolved no ancestors) — + // generating now would persist a partial key. Stay undecided instead. + expect(pendingPrefills([node], parentConfig)).toEqual([]); + // Re-scan delivered the frame → full key generated. + const withParents = makeNode({ id: "1:1", key: "old", prefilledKey: "old", frame: "Login" }); + expect(pendingPrefills([withParents], parentConfig)).toEqual([ + { id: "1:1", key: "Login.Layer", ns: "" }, + ]); + }); +}); diff --git a/src/ui/lib/logic/__tests__/progress.test.ts b/src/ui/lib/logic/__tests__/progress.test.ts new file mode 100644 index 00000000..5b63df19 --- /dev/null +++ b/src/ui/lib/logic/__tests__/progress.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { computeProgressPercent, formatProgressCount } from "$ui/lib/logic/progress"; + +describe("computeProgressPercent", () => { + it("returns null (indeterminate) when total is null", () => { + expect(computeProgressPercent(0, null)).toBeNull(); + expect(computeProgressPercent(50, null)).toBeNull(); + }); + + it("returns null (indeterminate) when total is 0 or negative", () => { + expect(computeProgressPercent(0, 0)).toBeNull(); + expect(computeProgressPercent(10, -5)).toBeNull(); + }); + + it("computes a normal ratio, rounded to the nearest percent", () => { + expect(computeProgressPercent(50, 200)).toBe(25); + expect(computeProgressPercent(1, 3)).toBe(33); // 33.33.. -> 33 + expect(computeProgressPercent(2, 3)).toBe(67); // 66.66.. -> 67 + }); + + it("caps loaded exceeding total at 100", () => { + expect(computeProgressPercent(150, 100)).toBe(100); + }); + + it("caps negative loaded at 0", () => { + expect(computeProgressPercent(-10, 100)).toBe(0); + }); + + it("full/empty edges", () => { + expect(computeProgressPercent(0, 100)).toBe(0); + expect(computeProgressPercent(100, 100)).toBe(100); + }); +}); + +describe("formatProgressCount", () => { + it("counts when the total is known", () => { + expect(formatProgressCount(0, 1)).toBe("0 / 1"); + expect(formatProgressCount(3, 10)).toBe("3 / 10"); + }); + + it("returns null when the total is unknown, so the caller omits the counter", () => { + // Reported live: a single-translation upload showed "0 / …" — one request + // has no sub-progress, so there is no honest denominator to print. + expect(formatProgressCount(0, null)).toBeNull(); + expect(formatProgressCount(5, 0)).toBeNull(); + expect(formatProgressCount(0, -1)).toBeNull(); + }); + + it("clamps a stale loaded value instead of printing 7 / 5", () => { + expect(formatProgressCount(7, 5)).toBe("5 / 5"); + expect(formatProgressCount(-2, 5)).toBe("0 / 5"); + }); +}); diff --git a/src/ui/lib/logic/__tests__/pullDiff.test.ts b/src/ui/lib/logic/__tests__/pullDiff.test.ts new file mode 100644 index 00000000..e621b892 --- /dev/null +++ b/src/ui/lib/logic/__tests__/pullDiff.test.ts @@ -0,0 +1,617 @@ +import type { NodeInfo } from "$shared/types"; +import type { PulledKey } from "$ui/lib/api/pull"; +import { + buildApplyUpdates, + formatNodeText, + pullDiff, + skippedRenderMessage, +} from "$ui/lib/logic/pullDiff"; +import { describe, expect, it } from "vitest"; + +function makeNode(overrides: Partial = {}): NodeInfo { + return { + id: overrides.id ?? "node-1", + name: overrides.name ?? "Node", + characters: overrides.characters ?? "", + translation: overrides.translation ?? "", + isPlural: overrides.isPlural ?? false, + key: overrides.key ?? "", + ns: overrides.ns, + connected: overrides.connected ?? true, + visible: overrides.visible, + paramsValues: overrides.paramsValues, + pluralParamValue: overrides.pluralParamValue, + }; +} + +function makeRemoteKey(overrides: Partial = {}): PulledKey { + return { + keyName: overrides.keyName ?? "", + keyNamespace: overrides.keyNamespace, + isPlural: overrides.isPlural ?? false, + translations: overrides.translations ?? {}, + }; +} + +describe("pullDiff", () => { + it("classifies a node as changed when remote text differs", () => { + const node = makeNode({ + key: "greeting", + translation: "Hello", + connected: true, + }); + const remote = makeRemoteKey({ + keyName: "greeting", + translations: { en: { text: "Hi" } }, + }); + const diff = pullDiff([node], [remote], "en"); + + expect(diff.changedNodes).toHaveLength(1); + expect(diff.changedNodes[0]?.node).toBe(node); + expect(diff.changedNodes[0]?.newText).toBe("Hi"); + expect(diff.changedNodes[0]?.isPlural).toBe(false); + expect(diff.missingKeys).toEqual([]); + expect(diff.unchangedNodes).toEqual([]); + }); + + it("classifies a node as missing when remote has no matching key", () => { + const node = makeNode({ + key: "ghost", + translation: "Hello", + connected: true, + }); + const diff = pullDiff([node], [], "en"); + + expect(diff.missingKeys).toHaveLength(1); + expect(diff.missingKeys[0]).toBe(node); + expect(diff.changedNodes).toEqual([]); + expect(diff.unchangedNodes).toEqual([]); + }); + + it("classifies a node as unchanged when remote text matches", () => { + const node = makeNode({ + key: "greeting", + translation: "Hello", + connected: true, + }); + const remote = makeRemoteKey({ + keyName: "greeting", + translations: { en: { text: "Hello" } }, + }); + const diff = pullDiff([node], [remote], "en"); + + expect(diff.unchangedNodes).toHaveLength(1); + expect(diff.unchangedNodes[0]).toBe(node); + expect(diff.changedNodes).toEqual([]); + expect(diff.missingKeys).toEqual([]); + }); + + it("does NOT re-download a formatted string whose canvas is the tag-stripped render (Zuzka's perpetual '2 strings')", () => { + // After a successful download, the canvas holds the PLAIN text ("Tučný + // text" in a bold font) while `translation` holds the raw markup. The + // drift check must compare against what apply would WRITE, not the raw + // remote text — otherwise every formatted string re-downloads forever. + const node = makeNode({ + key: "Advaced-BoldText", + translation: "Tučný text", + characters: "Tučný text", + connected: true, + }); + const remote = makeRemoteKey({ + keyName: "Advaced-BoldText", + translations: { cs: { text: "Tučný text" } }, + }); + const diff = pullDiff([node], [remote], "cs"); + + expect(diff.changedNodes).toEqual([]); + expect(diff.unchangedNodes).toEqual([node]); + }); + + it("still re-applies a formatted string whose canvas GENUINELY drifted", () => { + const node = makeNode({ + key: "Advaced-BoldText", + translation: "Tučný text", + characters: "někdo to přepsal", + connected: true, + }); + const remote = makeRemoteKey({ + keyName: "Advaced-BoldText", + translations: { cs: { text: "Tučný text" } }, + }); + const diff = pullDiff([node], [remote], "cs"); + + expect(diff.changedNodes).toHaveLength(1); + expect(diff.changedNodes[0]?.newText).toBe("Tučný text"); + }); + + it("treats
    as the newline it becomes on canvas, not as drift", () => { + const node = makeNode({ + key: "two-lines", + translation: "první
    druhý", + characters: "první\ndruhý", + connected: true, + }); + const remote = makeRemoteKey({ + keyName: "two-lines", + translations: { cs: { text: "první
    druhý" } }, + }); + const diff = pullDiff([node], [remote], "cs"); + + expect(diff.changedNodes).toEqual([]); + expect(diff.unchangedNodes).toEqual([node]); + }); + + it("treats ICU quote-escaping ('' -> ') as rendered, not as drift", () => { + const node = makeNode({ + key: "apostrophe", + translation: "It''s done", + characters: "It's done", + connected: true, + }); + const remote = makeRemoteKey({ + keyName: "apostrophe", + translations: { en: { text: "It''s done" } }, + }); + const diff = pullDiff([node], [remote], "en"); + + expect(diff.changedNodes).toEqual([]); + expect(diff.unchangedNodes).toEqual([node]); + }); + + it("never flags drift when the remote ICU cannot be rendered", () => { + const node = makeNode({ + key: "broken", + translation: "{unclosed", + characters: "whatever is on canvas", + connected: true, + }); + const remote = makeRemoteKey({ + keyName: "broken", + translations: { en: { text: "{unclosed" } }, + }); + const diff = pullDiff([node], [remote], "en"); + + expect(diff.changedNodes).toEqual([]); + expect(diff.unchangedNodes).toEqual([node]); + }); + + it("classifies an empty remote translation as missing (no destructive overwrite)", () => { + const node = makeNode({ + key: "greeting", + translation: "Hello", + connected: true, + }); + const remote = makeRemoteKey({ + keyName: "greeting", + translations: { en: { text: "" } }, + }); + const diff = pullDiff([node], [remote], "en"); + + // Matches legacy plugin behaviour: a key without a translation for the + // selected language is surfaced under "missing" so the user can see they + // still need to translate it. We never overwrite a local string with "". + expect(diff.missingKeys).toHaveLength(1); + expect(diff.missingKeys[0]).toBe(node); + expect(diff.changedNodes).toEqual([]); + expect(diff.unchangedNodes).toEqual([]); + }); + + it("ignores nodes that are not connected", () => { + const node = makeNode({ + key: "greeting", + translation: "Hello", + connected: false, + }); + const diff = pullDiff([node], [], "en"); + + expect(diff.changedNodes).toEqual([]); + expect(diff.missingKeys).toEqual([]); + expect(diff.unchangedNodes).toEqual([]); + }); + + it("flags plural mismatch as changed even when text matches", () => { + const node = makeNode({ + key: "items", + translation: "1 item", + isPlural: false, + connected: true, + }); + const remote = makeRemoteKey({ + keyName: "items", + isPlural: true, + translations: { en: { text: "1 item" } }, + }); + const diff = pullDiff([node], [remote], "en"); + + expect(diff.changedNodes).toHaveLength(1); + expect(diff.changedNodes[0]?.isPlural).toBe(true); + }); + + it("disambiguates by namespace", () => { + const nodeA = makeNode({ + id: "a", + key: "greeting", + ns: "ui", + translation: "Hi (UI)", + connected: true, + }); + const nodeB = makeNode({ + id: "b", + key: "greeting", + ns: "marketing", + translation: "Hi (mkt)", + connected: true, + }); + const remoteUi = makeRemoteKey({ + keyName: "greeting", + keyNamespace: "ui", + translations: { en: { text: "Hi (UI)" } }, + }); + const remoteMkt = makeRemoteKey({ + keyName: "greeting", + keyNamespace: "marketing", + translations: { en: { text: "Hello (mkt)" } }, + }); + const diff = pullDiff([nodeA, nodeB], [remoteUi, remoteMkt], "en"); + + expect(diff.unchangedNodes).toContain(nodeA); + expect(diff.changedNodes).toHaveLength(1); + expect(diff.changedNodes[0]?.node).toBe(nodeB); + expect(diff.changedNodes[0]?.newText).toBe("Hello (mkt)"); + }); + + it("ignores a stale node ns when namespaces are disabled (matches push)", () => { + // The write pipeline drops the stored ns on ns-disabled projects, so the + // key lives in the default namespace — the diff lookup must follow, or + // the node lands in missingKeys and its update is silently skipped. + const node = makeNode({ + key: "greeting", + ns: "stale-ns", + translation: "Hello", + connected: true, + }); + const remote = makeRemoteKey({ + keyName: "greeting", + translations: { en: { text: "Hi" } }, + }); + const diff = pullDiff([node], [remote], "en", false); + + expect(diff.missingKeys).toEqual([]); + expect(diff.changedNodes).toHaveLength(1); + expect(diff.changedNodes[0]?.newText).toBe("Hi"); + }); + + it("still matches by exact ns when namespaces are enabled", () => { + const node = makeNode({ + key: "greeting", + ns: "web", + translation: "Hello", + connected: true, + }); + // Remote key sits in the default namespace — with namespaces ON this must + // NOT match the node's "web" ns. + const remote = makeRemoteKey({ + keyName: "greeting", + translations: { en: { text: "Hi" } }, + }); + const diff = pullDiff([node], [remote], "en", true); + + expect(diff.missingKeys).toEqual([node]); + expect(diff.changedNodes).toEqual([]); + }); +}); + +describe("formatNodeText", () => { + it("formats a plural using the named sample from paramsValues (this UI's edit path)", () => { + const node = makeNode({ + key: "items", + paramsValues: { count: "3" }, // count edited as the plural var's sample + isPlural: true, + }); + const out = formatNodeText(node, "{count, plural, one {1 item} other {# items}}", "en"); + expect(out.text).toBe("3 items"); + expect(out.error).toBeUndefined(); + }); + + it("uses a numeric pluralParamValue as the sample COUNT (original-file compat)", () => { + // Files written by the published plugin store the count in pluralParamValue. + const node = makeNode({ + key: "items", + pluralParamValue: "10", + isPlural: true, + }); + const out = formatNodeText(node, "{count, plural, one {1 item} other {# items}}", "en"); + expect(out.error).toBeUndefined(); + expect(out.text).toBe("10 items"); + }); + + it("defaults the plural count to 1 when nothing is stored", () => { + const node = makeNode({ key: "items", isPlural: true }); + const out = formatNodeText(node, "{count, plural, one {# item} other {# items}}", "en"); + expect(out.error).toBeUndefined(); + expect(out.text).toBe("1 item"); + }); + + it("ignores a non-numeric pluralParamValue left over as a name (no 'value items')", () => { + const node = makeNode({ key: "items", pluralParamValue: "value", isPlural: true }); + const out = formatNodeText(node, "{count, plural, one {1 item} other {# items}}", "en"); + expect(out.error).toBeUndefined(); + expect(out.text).toBe("1 item"); + expect(out.text).not.toContain("value"); + }); + + it("formats with explicit paramsValues taking precedence", () => { + const node = makeNode({ + key: "greet", + paramsValues: { name: "Alice" }, + }); + const out = formatNodeText(node, "Hello, {name}!", "en"); + expect(out.text).toBe("Hello, Alice!"); + expect(out.error).toBeUndefined(); + }); + + it("seeds a missing named param with its own name (never literal braces)", () => { + const node = makeNode({ key: "greet" }); // no sample stored + const out = formatNodeText(node, "Hello, {name}!", "en"); + expect(out.text).toBe("Hello, name!"); + expect(out.text).not.toContain("{"); + expect(out.error).toBeUndefined(); + }); + + it("does not overwrite an existing `count` in paramsValues", () => { + // An explicit sample value in paramsValues wins over the seeded default. + const node = makeNode({ + key: "items", + pluralParamValue: "count", + paramsValues: { count: "7" }, + isPlural: true, + }); + const out = formatNodeText(node, "{count, plural, one {1 item} other {# items}}", "en"); + expect(out.text).toBe("7 items"); + expect(out.error).toBeUndefined(); + }); + + it("keeps the node's canvas text (not raw ICU) and reports the Error on malformed ICU", () => { + const node = makeNode({ + key: "broken", + characters: "current canvas text", + paramsValues: { name: "Alice" }, + }); + const out = formatNodeText(node, "Hello {name", "en"); + expect(out.text).toBe("current canvas text"); + expect(out.text).not.toContain("{name"); + expect(out.error).toBeInstanceOf(Error); + }); + + it("returns the raw text untouched when there are no params at all", () => { + const node = makeNode({ key: "k" }); + const out = formatNodeText(node, "Just a string", "en"); + expect(out.text).toBe("Just a string"); + expect(out.error).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Task 59: a `keyNames`-filtered remote payload must diff identically to the +// unfiltered (full-project) one, for the exact same local nodes. `pullDiff` +// itself doesn't know or care how `remoteKeys` was fetched — this pins that +// invariant so the filtering change in `pull.ts` can never silently alter +// what counts as changed/unchanged/missing. +// --------------------------------------------------------------------------- + +describe("pullDiff — filtered vs. full remote payload equivalence", () => { + it("produces the identical diff whether remoteKeys is the full project or filtered to just the local nodes' keys", () => { + const changed = makeNode({ id: "a", key: "greeting", translation: "Hello" }); + const unchanged = makeNode({ id: "b", key: "farewell", translation: "Bye" }); + const missing = makeNode({ id: "c", key: "deleted-key", translation: "Gone" }); + const localNodes = [changed, unchanged, missing]; + + // The "full project" payload includes keys belonging to OTHER nodes not + // in this selection at all (simulating everything else in the project). + const fullRemote: PulledKey[] = [ + makeRemoteKey({ keyName: "greeting", translations: { en: { text: "Hi" } } }), + makeRemoteKey({ keyName: "farewell", translations: { en: { text: "Bye" } } }), + makeRemoteKey({ keyName: "unrelated-key", translations: { en: { text: "Noise" } } }), + ]; + // The filtered payload contains only what `keyNames` (built from + // localNodes) would have asked the server for — i.e. never + // "unrelated-key", and (like the real API) never a payload entry for the + // deleted key either. + const filteredRemote: PulledKey[] = fullRemote.filter((k) => k.keyName !== "unrelated-key"); + + const fullDiff = pullDiff(localNodes, fullRemote, "en"); + const filteredDiff = pullDiff(localNodes, filteredRemote, "en"); + + expect(filteredDiff).toEqual(fullDiff); + expect(filteredDiff.missingKeys.map((n) => n.id)).toEqual(["c"]); + expect(filteredDiff.changedNodes.map((c) => c.node.id)).toEqual(["a"]); + expect(filteredDiff.unchangedNodes.map((n) => n.id)).toEqual(["b"]); + }); +}); + +describe("buildApplyUpdates", () => { + // A node whose ICU cannot render. `formatNodeText` deliberately falls back to + // the node's CURRENT canvas text so raw ICU never lands on the design — which + // is exactly why the update must not be sent: it would rewrite the text + // already there while recording `translation` as applied, leaving the node + // permanently "up to date" and unable to retry. + const BROKEN_ICU = "Hello {name"; + + it("keeps a renderable node in the updates", () => { + const node = makeNode({ id: "n1", key: "greet", paramsValues: { name: "Alice" } }); + const { updates, skipped } = buildApplyUpdates( + [{ node, newText: "Hello, {name}!", isPlural: false }], + "en", + ); + expect(skipped).toEqual([]); + expect(updates).toEqual([ + { id: "n1", text: "Hello, Alice!", translation: "Hello, {name}!", isPlural: false }, + ]); + }); + + it("holds a failed render OUT of the updates instead of persisting it", () => { + const node = makeNode({ + id: "n2", + key: "broken", + characters: "current canvas text", + paramsValues: { name: "Alice" }, + }); + const { updates, skipped } = buildApplyUpdates( + [{ node, newText: BROKEN_ICU, isPlural: false }], + "en", + ); + expect(updates).toEqual([]); + expect(skipped).toHaveLength(1); + expect(skipped[0]?.node.id).toBe("n2"); + expect(skipped[0]?.error).toBeInstanceOf(Error); + }); + + it("applies the good nodes of a mixed batch and reports only the bad ones", () => { + const ok = makeNode({ id: "ok", key: "greet", paramsValues: { name: "Alice" } }); + const bad = makeNode({ id: "bad", key: "broken", characters: "untouched" }); + const { updates, skipped } = buildApplyUpdates( + [ + { node: ok, newText: "Hello, {name}!", isPlural: false }, + { node: bad, newText: BROKEN_ICU, isPlural: false }, + ], + "en", + ); + expect(updates.map((u) => u.id)).toEqual(["ok"]); + expect(skipped.map((s) => s.node.id)).toEqual(["bad"]); + }); + + it("summarises the skipped nodes for the user", () => { + const skipped = ["a", "b", "c", "d"].map((k) => ({ + node: makeNode({ id: k, key: k }), + error: new Error("boom"), + })); + expect(skippedRenderMessage(skipped.slice(0, 1))).toBe( + "1 string could not be rendered and was left unchanged (a).", + ); + expect(skippedRenderMessage(skipped)).toBe( + "4 strings could not be rendered and were left unchanged (a, b, c, +1 more).", + ); + }); +}); + +describe("nodes left stale by the published plugin", () => { + // The published plugin stores the remote translation even when its own ICU + // render threw (`src/ui/views/Pull/Pull.tsx` — `catch` returns + // `formatted: n.characters`, and `setNodesDataMutation` then writes + // `translation: changedNode.translation` regardless). That leaves a node + // whose cached translation already MATCHES the remote while its canvas still + // shows the old text — so the first pullDiff branch sees nothing to do, and + // only the drift branch can rescue it. + // + // An embedded plural is exactly the shape that used to fail that render, so + // these nodes arrive stale on migration. `canvasDrifted` swallows a render + // error as "no drift" (re-applying on unknowns loops forever), which meant + // the node stayed stale on every future pull too. Seeding the plural count + // for embedded plurals is what lets the render — and so the rescue — work. + const EMBEDDED = "{count, plural, one {# day} other {# days}} left"; + + it("re-applies a stale node whose translation already matches the remote", () => { + const node = makeNode({ + key: "trial", + translation: EMBEDDED, + characters: "old stale text", + pluralParamValue: "10", + }); + const remote = makeRemoteKey({ + keyName: "trial", + translations: { en: { text: EMBEDDED } }, + }); + + const result = pullDiff([node], [remote], "en", false); + expect(result.changedNodes).toHaveLength(1); + expect(result.changedNodes[0]?.newText).toBe(EMBEDDED); + expect(result.unchangedNodes).toHaveLength(0); + }); + + it("leaves the node alone once its canvas shows the rendered text", () => { + // The other half of the same property: having rescued it, a second pull + // must report nothing — that regression ("Downloaded 2 strings" forever) + // is what the drift branch's render-through-the-apply-pipeline is for. + const node = makeNode({ + key: "trial", + translation: EMBEDDED, + characters: "10 days left", + pluralParamValue: "10", + }); + const remote = makeRemoteKey({ + keyName: "trial", + translations: { en: { text: EMBEDDED } }, + }); + + const result = pullDiff([node], [remote], "en", false); + expect(result.changedNodes).toHaveLength(0); + expect(result.unchangedNodes).toHaveLength(1); + }); +}); + +describe("the plural exclusion in the drift branch", () => { + const PLURAL = "{count, plural, one {# day} other {# days}}"; + + it("does NOT re-apply a migrated plural layer whose sample is unusable", () => { + // The published plugin stored the argument NAME in `pluralParamValue` + // instead of a count. `numericCount` rejects it, so the sample falls back + // to 1 — a layer showing "10 days" renders as "1 day" and looks drifted. + // Re-applying would overwrite the design with the wrong plural form, on + // every single pull. This exclusion is what stops that, so it must not be + // "relaxed" to rescue stale nodes. + const node = makeNode({ + key: "trial", + isPlural: true, + translation: PLURAL, + characters: "10 days", + pluralParamValue: "count", // the arg name, as the original wrote it + }); + const remote = makeRemoteKey({ + keyName: "trial", + isPlural: true, + translations: { en: { text: PLURAL } }, + }); + + const result = pullDiff([node], [remote], "en", false); + + expect(result.changedNodes).toHaveLength(0); + expect(result.unchangedNodes).toHaveLength(1); + }); + + it("does NOT re-apply a parametric layer with no stored params", () => { + // Same shape: the render seeds `{name}` with its own name, so the canvas + // never matches and the layer would be rewritten to "Hello, name!". + const node = makeNode({ + key: "greet", + translation: "Hello, {name}!", + characters: "Hello, Alice!", + paramsValues: { name: "Alice" }, + }); + const remote = makeRemoteKey({ + keyName: "greet", + translations: { en: { text: "Hello, {name}!" } }, + }); + + const result = pullDiff([node], [remote], "en", false); + + expect(result.changedNodes).toHaveLength(0); + }); + + it("still re-applies a plain layer that genuinely drifted", () => { + // The case the branch exists for stays working. + const node = makeNode({ + key: "title", + translation: "Welcome", + characters: "typed over", + }); + const remote = makeRemoteKey({ + keyName: "title", + translations: { en: { text: "Welcome" } }, + }); + + const result = pullDiff([node], [remote], "en", false); + + expect(result.changedNodes).toHaveLength(1); + expect(result.changedNodes[0]?.newText).toBe("Welcome"); + }); +}); diff --git a/src/ui/lib/logic/__tests__/pushDiff.test.ts b/src/ui/lib/logic/__tests__/pushDiff.test.ts new file mode 100644 index 00000000..0cc41142 --- /dev/null +++ b/src/ui/lib/logic/__tests__/pushDiff.test.ts @@ -0,0 +1,664 @@ +import type { NodeInfo } from "$shared/types"; +import { + buildRemoteMapFromKeys, + droppedConflictNodeIds, + pushDiff, + textOfNode, +} from "$ui/lib/logic/pushDiff"; +import type { RemoteTranslationMap } from "$ui/lib/logic/pushDiff"; +import { describe, expect, it } from "vitest"; +import IntlMessageFormat from "intl-messageformat"; + +function makeNode(overrides: Partial = {}): NodeInfo { + return { + id: overrides.id ?? "node-1", + name: overrides.name ?? "Node", + characters: overrides.characters ?? "", + translation: overrides.translation ?? "", + isPlural: overrides.isPlural ?? false, + key: overrides.key ?? "", + ns: overrides.ns, + connected: overrides.connected ?? true, + visible: overrides.visible, + paramsValues: overrides.paramsValues, + pluralParamValue: overrides.pluralParamValue, + }; +} + +describe("pushDiff", () => { + it("returns empty buckets for empty input", () => { + const diff = pushDiff([], {}, { hasNamespacesEnabled: false }); + expect(diff.newKeys).toEqual([]); + expect(diff.changedKeys).toEqual([]); + expect(diff.unchangedKeys).toEqual([]); + expect(diff.conflictingNodes).toEqual([]); + }); + + it("marks a node as unchanged when remote text matches", () => { + const node = makeNode({ + id: "n1", + key: "greeting", + translation: "Hello", + }); + const remote: RemoteTranslationMap = { + "": { + greeting: { + translation: "Hello", + keyIsPlural: false, + keyTags: [], + }, + }, + }; + const diff = pushDiff([node], remote, { hasNamespacesEnabled: false }); + expect(diff.unchangedKeys).toHaveLength(1); + expect(diff.unchangedKeys[0]).toBe(node); + expect(diff.newKeys).toEqual([]); + expect(diff.changedKeys).toEqual([]); + }); + + it("marks a node as changed and surfaces the remoteText when text differs", () => { + const node = makeNode({ + id: "n1", + key: "greeting", + translation: "Hi", + }); + const remote: RemoteTranslationMap = { + "": { + greeting: { + translation: "Hello", + keyIsPlural: false, + keyTags: [], + }, + }, + }; + const diff = pushDiff([node], remote, { hasNamespacesEnabled: false }); + expect(diff.changedKeys).toHaveLength(1); + expect(diff.changedKeys[0]?.node).toBe(node); + expect(diff.changedKeys[0]?.remoteText).toBe("Hello"); + expect(diff.unchangedKeys).toEqual([]); + }); + + it("marks an UNCONNECTED node as new when remote has no entry for the key", () => { + const node = makeNode({ + id: "n1", + key: "new-key", + translation: "Hello", + connected: false, + }); + const diff = pushDiff([node], {}, { hasNamespacesEnabled: false }); + expect(diff.newKeys).toHaveLength(1); + expect(diff.newKeys[0]).toBe(node); + expect(diff.missingKeys).toEqual([]); + }); + + it("marks a CONNECTED node as missing (not new) when remote has no entry — the key was deleted on the platform", () => { + const node = makeNode({ + id: "n1", + key: "deleted-key", + translation: "Hello", + connected: true, + }); + const diff = pushDiff([node], {}, { hasNamespacesEnabled: false }); + expect(diff.missingKeys).toHaveLength(1); + expect(diff.missingKeys[0]).toBe(node); + expect(diff.newKeys).toEqual([]); + }); + + it("reports two nodes with the same (key, ns) but different text as conflicting", () => { + const nodeA = makeNode({ + id: "a", + key: "shared", + ns: "ui", + translation: "Hello", + }); + const nodeB = makeNode({ + id: "b", + key: "shared", + ns: "ui", + translation: "Hi", + }); + const diff = pushDiff([nodeA, nodeB], {}, { hasNamespacesEnabled: true }); + + expect(diff.conflictingNodes).toHaveLength(1); + expect(diff.conflictingNodes[0]?.key).toBe("shared"); + expect(diff.conflictingNodes[0]?.ns).toBe("ui"); + expect(diff.conflictingNodes[0]?.nodes).toHaveLength(2); + expect(diff.conflictingNodes[0]?.nodes).toContain(nodeA); + expect(diff.conflictingNodes[0]?.nodes).toContain(nodeB); + }); + + it("does NOT report duplicate (key, ns) pairs when the text agrees", () => { + const nodeA = makeNode({ + id: "a", + key: "shared", + translation: "Hello", + }); + const nodeB = makeNode({ + id: "b", + key: "shared", + translation: "Hello", + }); + const diff = pushDiff([nodeA, nodeB], {}, { hasNamespacesEnabled: false }); + expect(diff.conflictingNodes).toEqual([]); + }); + + it("does NOT report plural VARIANTS as conflicting (same key + ICU, different rendered text)", () => { + // Two Figma layers legitimately share one plural key: identical ICU in + // `translation`, but each renders a different sample form ("1 woman" / + // "10 women") in `characters`. `textOfNode` uses the ICU for plural/advanced + // strings, so this must NOT trip the same-key conflict heuristic. + const icu = "{count, plural, one {# woman} other {# women}}"; + const nodeA = makeNode({ + id: "a", + key: "plural-W", + isPlural: true, + translation: icu, + characters: "1 woman", + }); + const nodeB = makeNode({ + id: "b", + key: "plural-W", + isPlural: true, + translation: icu, + characters: "10 women", + }); + const diff = pushDiff([nodeA, nodeB], {}, { hasNamespacesEnabled: false }); + expect(diff.conflictingNodes).toEqual([]); + }); + + it("does NOT report plural variants as conflicting even without a stored ICU translation", () => { + // The screenshot case: two plural layers on one key, no ICU pulled yet, so + // `translation` is empty and only the rendered `characters` differ. A plural + // render is derived, never authoritative, so this must not be a conflict. + const nodeA = makeNode({ + id: "a", + key: "plural-W", + isPlural: true, + translation: "", + characters: "1 woman", + }); + const nodeB = makeNode({ + id: "b", + key: "plural-W", + isPlural: true, + translation: "", + characters: "10 women", + }); + const diff = pushDiff([nodeA, nodeB], {}, { hasNamespacesEnabled: false }); + expect(diff.conflictingNodes).toEqual([]); + }); + + it("does NOT report plural variants as conflicting when each was pluralized independently (different PARTIAL ICUs)", () => { + // The reported bug: three layers on one plural key, each pluralized from its + // own single form → DIFFERENT partial ICUs. They are still the same plural, + // so the same-key conflict must not fire (was flagged because the full ICU + // differed between "one {…}" and "other {…}"). + const one = makeNode({ + id: "a", + key: "test.plural.apple", + isPlural: true, + connected: false, + translation: "{value, plural, one {# apple} other {}}", + characters: "1 apple", + }); + const other = makeNode({ + id: "b", + key: "test.plural.apple", + isPlural: true, + connected: false, + translation: "{value, plural, other {# apples}}", + characters: "2 apples", + }); + const other2 = makeNode({ + id: "c", + key: "test.plural.apple", + isPlural: true, + connected: false, + translation: "{value, plural, other {# apples}}", + characters: "167 apples", + }); + const diff = pushDiff([one, other, other2], {}, { hasNamespacesEnabled: false }); + expect(diff.conflictingNodes).toEqual([]); + }); + + it("MERGES plural forms across layers on one key into one complete ICU on push", () => { + // The reported case: three layers on test.plural.apple, each pluralized from + // its own sample form. Push must send the UNION of forms, not just the first + // layer's partial ICU. + const one = makeNode({ + id: "a", + key: "test.plural.apple", + isPlural: true, + connected: false, + translation: "{value, plural, one {# apple} other {}}", + characters: "1 apple", + }); + const other = makeNode({ + id: "b", + key: "test.plural.apple", + isPlural: true, + connected: false, + translation: "{value, plural, other {# apples}}", + characters: "2 apples", + }); + const other2 = makeNode({ + id: "c", + key: "test.plural.apple", + isPlural: true, + connected: false, + translation: "{value, plural, other {# apples}}", + characters: "167 apples", + }); + // New key (no remote) → the representative in newKeys carries the merged ICU. + const diff = pushDiff([one, other, other2], {}, { hasNamespacesEnabled: false }); + expect(diff.newKeys).toHaveLength(1); + expect(diff.newKeys[0]?.id).toBe("a"); // keeps the first layer's id + expect(diff.newKeys[0]?.translation).toBe( + "{value, plural, one {# apple} other {# apples}}", + ); + }); + + it("does NOT merge a single-layer plural key (nothing to combine)", () => { + const only = makeNode({ + id: "a", + key: "solo.apple", + isPlural: true, + connected: false, + translation: "{value, plural, one {# apple} other {# apples}}", + characters: "2 apples", + }); + const diff = pushDiff([only], {}, { hasNamespacesEnabled: false }); + expect(diff.newKeys[0]?.translation).toBe(only.translation); // untouched + }); + + it("does NOT merge across a NON-plural group (plain duplicates keep their own text)", () => { + // Two plain layers on one key are a conflict, never a merge — the plural + // union must not touch them. + const a = makeNode({ id: "a", key: "dup", connected: false, translation: "Hello", characters: "Hello" }); + const b = makeNode({ id: "b", key: "dup", connected: false, translation: "Hi", characters: "Hi" }); + const diff = pushDiff([a, b], {}, { hasNamespacesEnabled: false }); + // Conflicting → the representative is the untouched first node, no merged ICU. + expect(diff.conflictingNodes).toHaveLength(1); + const rep = diff.newKeys[0] ?? diff.changedKeys[0]?.node ?? diff.unchangedKeys[0]; + expect(rep?.translation).toBe("Hello"); + }); + + it("STILL reports a plain layer sharing a plural layer's key as a conflict", () => { + // A form-agnostic plural marker must not swallow a genuine mix: a plural and + // a plain string on ONE key really do conflict (only one can upload). + const plural = makeNode({ + id: "a", + key: "shared", + isPlural: true, + translation: "{value, plural, other {# apples}}", + characters: "2 apples", + }); + const plain = makeNode({ id: "b", key: "shared", translation: "banana", characters: "banana" }); + const diff = pushDiff([plural, plain], {}, { hasNamespacesEnabled: false }); + expect(diff.conflictingNodes).toHaveLength(1); + }); + + it("flags plural mismatch as changed even when text matches", () => { + const node = makeNode({ + id: "n1", + key: "items", + translation: "1 item", + isPlural: true, + }); + const remote: RemoteTranslationMap = { + "": { + items: { + translation: "1 item", + keyIsPlural: false, + keyTags: [], + }, + }, + }; + const diff = pushDiff([node], remote, { hasNamespacesEnabled: false }); + expect(diff.changedKeys).toHaveLength(1); + expect(diff.changedKeys[0]?.node).toBe(node); + }); + + it("flags missing configuredTags as changed", () => { + const node = makeNode({ + id: "n1", + key: "greeting", + translation: "Hello", + }); + const remote: RemoteTranslationMap = { + "": { + greeting: { + translation: "Hello", + keyIsPlural: false, + keyTags: ["existing"], + }, + }, + }; + const diff = pushDiff([node], remote, { + hasNamespacesEnabled: false, + configuredTags: ["needs-this-tag"], + }); + expect(diff.changedKeys).toHaveLength(1); + }); + + it("does NOT flag missing tags when no tags are configured", () => { + // The contract the Push view relies on: with tag sync off it passes an + // EMPTY list, and an otherwise-identical key must then come back + // unchanged. Reported live — leaving tags configured while turning the + // toggle off made every push report changes, because the diff kept + // demanding tags the push would never apply. + const node = makeNode({ id: "n1", key: "greeting", translation: "Hello" }); + const remote: RemoteTranslationMap = { + "": { + greeting: { translation: "Hello", keyIsPlural: false, keyTags: [] }, + }, + }; + + for (const configuredTags of [[], undefined]) { + const diff = pushDiff([node], remote, { + hasNamespacesEnabled: false, + configuredTags, + }); + expect(diff.changedKeys).toHaveLength(0); + expect(diff.unchangedKeys).toHaveLength(1); + } + }); + + it("ignores nodes without a key", () => { + const node = makeNode({ id: "n1", key: "", translation: "Hello" }); + const diff = pushDiff([node], {}, { hasNamespacesEnabled: false }); + expect(diff.newKeys).toEqual([]); + expect(diff.changedKeys).toEqual([]); + expect(diff.unchangedKeys).toEqual([]); + }); + + it("falls back to `characters` when `translation` is empty", () => { + const node = makeNode({ + id: "n1", + key: "greeting", + translation: "", + characters: "Hello", + }); + const remote: RemoteTranslationMap = { + "": { + greeting: { + translation: "Hello", + keyIsPlural: false, + keyTags: [], + }, + }, + }; + const diff = pushDiff([node], remote, { hasNamespacesEnabled: false }); + expect(diff.unchangedKeys).toHaveLength(1); + }); +}); + +describe("droppedConflictNodeIds", () => { + it("returns every conflict-group loser (all but the first) so they skip connect-back AND screenshots", () => { + // "Ahoy" / "Ahoy2" share one key with different text → conflict. Only the + // first uploads/connects; the loser must not be captured or boxed on the + // key's screenshot. + const win = makeNode({ id: "ahoy", key: "greet", translation: "Ahoy", characters: "Ahoy" }); + const lose = makeNode({ id: "ahoy2", key: "greet", translation: "Ahoy2", characters: "Ahoy2" }); + const diff = pushDiff([win, lose], {}, { hasNamespacesEnabled: false }); + expect(diff.conflictingNodes).toHaveLength(1); + expect([...droppedConflictNodeIds(diff)]).toEqual(["ahoy2"]); + }); + + it("is empty when there are no conflicts", () => { + const diff = pushDiff( + [makeNode({ id: "x", key: "k", translation: "hi", connected: false })], + {}, + { hasNamespacesEnabled: false }, + ); + expect(droppedConflictNodeIds(diff).size).toBe(0); + }); +}); + +describe("buildRemoteMapFromKeys", () => { + it("returns an empty object when keys is undefined", () => { + expect(buildRemoteMapFromKeys(undefined, "en")).toEqual({}); + }); + + it("indexes by namespace ('' for default) and key", () => { + const map = buildRemoteMapFromKeys( + [ + { + keyName: "greeting", + translations: { en: { text: "Hello" } }, + }, + { + keyName: "greeting", + keyNamespace: "ui", + keyIsPlural: true, + keyTags: [{ name: "foo" }], + translations: { en: { text: "Hi" } }, + }, + ], + "en", + ); + + expect(map[""]?.greeting?.translation).toBe("Hello"); + expect(map[""]?.greeting?.keyIsPlural).toBe(false); + expect(map.ui?.greeting?.translation).toBe("Hi"); + expect(map.ui?.greeting?.keyIsPlural).toBe(true); + expect(map.ui?.greeting?.keyTags).toEqual(["foo"]); + }); + + it("returns undefined translation when no language entry is present", () => { + const map = buildRemoteMapFromKeys([{ keyName: "k", translations: {} }], "en"); + expect(map[""]?.k?.translation).toBeUndefined(); + }); +}); + +describe("textOfNode — authoritative text (shared with String details)", () => { + it("prefers the live canvas `characters` for a PLAIN string", () => { + // The reported bug: a connected string whose canvas text was overwritten + // (e.g. a duplicated node) must resolve to the NEW canvas text, not the + // stale stored translation it inherited. String details loads this too, so + // the editor shows exactly what Push will upload. + const node = makeNode({ + key: "test.myNameIsZuzka", + translation: "My name is Zuzka", + characters: "2 apples", + }); + expect(textOfNode(node)).toBe("2 apples"); + }); + + it("prefers the stored ICU `translation` for an ADVANCED string", () => { + // Plural: `characters` is only the rendered form ("2 apples"), the ICU in + // `translation` is authoritative and must not be clobbered by the render. + const node = makeNode({ + isPlural: true, + translation: "{count, plural, one {# apple} other {# apples}}", + characters: "2 apples", + }); + expect(textOfNode(node)).toBe("{count, plural, one {# apple} other {# apples}}"); + }); + + it("prefers the canvas edit for a plain string holding ICU-escaped braces", () => { + // The consequence of the brace-pair misclassification: `Use '{'braces'}'` + // is PLAIN text (the braces are escaped literals, not an argument), so a + // canvas edit must win here exactly as it does for any other plain string. + // While it read as advanced, the stale stored value won and the edit was + // silently dropped from the push. + const node = makeNode({ + translation: "Use '{'braces'}' here", + characters: "Use {braces} there", + }); + expect(textOfNode(node)).toBe("Use {braces} there"); + }); + + it("falls back across the missing side for both kinds", () => { + // Plain with no canvas text → the stored translation. + expect(textOfNode(makeNode({ translation: "only stored", characters: "" }))).toBe( + "only stored", + ); + // Advanced (markup) with no stored translation → the canvas render. + expect(textOfNode(makeNode({ translation: "", characters: "bold" }))).toBe( + "bold", + ); + }); +}); + +describe("two layers of one plural key that disagree WITHIN a category", () => { + // The headline trade-off in this PR's own description, and the one thing in + // the plural merge path with no test: the original plugin raised a conflict + // on any plural-form difference, which drowned the legitimate case (each + // layer pluralised from its own sample carries only the form its count + // selects). Suppressing that was a product decision — but it means a REAL + // disagreement inside one category now resolves first-wins, silently. + // + // Pinning it matters because `mergePluralForms` regenerates the whole ICU, + // which is exactly where the escaping bug in 7bc393e lived. + const FIRST = "{count, plural, one {# apple} other {# apples}}"; + const SECOND = "{count, plural, one {# pear} other {# pears}}"; + + function twoLayers() { + return [ + // Unconnected: with an empty remote map, connected nodes read as "the + // key was deleted on the platform" and land in `missingKeys` instead. + makeNode({ id: "a", key: "fruit", isPlural: true, translation: FIRST, connected: false }), + makeNode({ id: "b", key: "fruit", isPlural: true, translation: SECOND, connected: false }), + ]; + } + + it("does NOT report them as conflicting", () => { + const diff = pushDiff(twoLayers(), {}, { hasNamespacesEnabled: false }); + expect(diff.conflictingNodes).toHaveLength(0); + }); + + it("uploads the FIRST layer's body for every category they both define", () => { + // First-wins, per category. The second layer's text is dropped without a + // word — that is the accepted behaviour, not an oversight, so assert it + // rather than leaving it to be rediscovered. + const diff = pushDiff(twoLayers(), {}, { hasNamespacesEnabled: false }); + + expect(diff.newKeys).toHaveLength(1); + const uploaded = diff.newKeys[0]?.translation ?? ""; + expect(uploaded).toContain("# apple"); + expect(uploaded).toContain("# apples"); + expect(uploaded).not.toContain("pear"); + }); + + it("still unions categories the layers do NOT share", () => { + // The case the merge exists for: each layer pluralised from its own sample + // holds only the form its count selected, and the push must carry both. + const one = makeNode({ + id: "a", + key: "fruit", + isPlural: true, + connected: false, + translation: "{count, plural, one {# apple}}", + }); + const other = makeNode({ + id: "b", + key: "fruit", + isPlural: true, + connected: false, + translation: "{count, plural, other {# apples}}", + }); + + const diff = pushDiff([one, other], {}, { hasNamespacesEnabled: false }); + const uploaded = diff.newKeys[0]?.translation ?? ""; + + expect(uploaded).toContain("# apple}"); + expect(uploaded).toContain("# apples"); + }); + + it("produces ICU that still parses after the merge", () => { + // The merge regenerates the whole string through `tolgeeFormatGenerateIcu` + // — the path that used to corrupt nested arguments and double-escape. + const withArg = makeNode({ + id: "a", + key: "fruit", + isPlural: true, + connected: false, + translation: "{count, plural, one {# apple for {name}} other {# apples}}", + }); + const partial = makeNode({ + id: "b", + key: "fruit", + isPlural: true, + connected: false, + translation: "{count, plural, other {# pears}}", + }); + + const diff = pushDiff([withArg, partial], {}, { hasNamespacesEnabled: false }); + const uploaded = diff.newKeys[0]?.translation ?? ""; + + // The nested argument must survive the regeneration, not become literal. + expect(uploaded).toContain("{name}"); + expect(() => { + new IntlMessageFormat(uploaded, "en", undefined, { ignoreTag: true }).format({ + count: 1, + name: "Zuzka", + }); + }).not.toThrow(); + }); +}); + +describe("why a key is reported as changed", () => { + // Without a reason on the entry, a key changed only by tags or by the plural + // flag renders as a diff whose two halves are the same string — which reads + // as a broken diff. That ambiguity was reported as "push always shows + // changes" with the cause guessed wrong, so the reason is part of the data. + const node = () => makeNode({ id: "n1", key: "greeting", translation: "Hello" }); + + function remote(over: Partial<{ translation: string; keyIsPlural: boolean; keyTags: string[] }> = {}) { + return { + "": { + greeting: { + translation: over.translation ?? "Hello", + keyIsPlural: over.keyIsPlural ?? false, + keyTags: over.keyTags ?? [], + }, + }, + } as RemoteTranslationMap; + } + + it("reports `text` when the translation itself differs", () => { + const diff = pushDiff([node()], remote({ translation: "Bonjour" }), { + hasNamespacesEnabled: false, + }); + expect(diff.changedKeys[0]?.reason).toBe("text"); + }); + + it("reports `tags` when only a configured tag is missing", () => { + const diff = pushDiff([node()], remote(), { + hasNamespacesEnabled: false, + configuredTags: ["needs-this"], + }); + expect(diff.changedKeys).toHaveLength(1); + expect(diff.changedKeys[0]?.reason).toBe("tags"); + // The text is identical on both sides — which is exactly why the row can't + // render a strike-through diff for this case. + expect(diff.changedKeys[0]?.remoteText).toBe(textOfNode(node())); + }); + + it("reports `plural` when only the plural flag differs", () => { + const diff = pushDiff([node()], remote({ keyIsPlural: true }), { + hasNamespacesEnabled: false, + }); + expect(diff.changedKeys[0]?.reason).toBe("plural"); + }); + + it("prefers `text` when the text differs AND a tag is missing", () => { + // Text is the informative one — a strike-through diff shows real content, + // whereas the tag note would hide it. + const diff = pushDiff([node()], remote({ translation: "Bonjour" }), { + hasNamespacesEnabled: false, + configuredTags: ["needs-this"], + }); + expect(diff.changedKeys[0]?.reason).toBe("text"); + }); + + it("reports nothing changed when text, plural and tags all match", () => { + const diff = pushDiff([node()], remote({ keyTags: ["already"] }), { + hasNamespacesEnabled: false, + configuredTags: ["already"], + }); + expect(diff.changedKeys).toHaveLength(0); + expect(diff.unchangedKeys).toHaveLength(1); + }); +}); diff --git a/src/ui/lib/logic/__tests__/pushFlow.test.ts b/src/ui/lib/logic/__tests__/pushFlow.test.ts new file mode 100644 index 00000000..c69783cf --- /dev/null +++ b/src/ui/lib/logic/__tests__/pushFlow.test.ts @@ -0,0 +1,517 @@ +import type { FrameScreenshot, NodeInfo } from "$shared/types"; +import type { TolgeeClient } from "$ui/lib/api/client"; +import type { SimpleImportConflictResult } from "$ui/lib/api/push"; +import { + buildConnectBackUpdates, + buildPayload, + buildRelatedKeys, + canonicalKey, + defaultResolutions, + resolutionKey, +} from "$ui/lib/logic/pushFlow"; +import type { PushContext } from "$ui/lib/logic/pushFlow"; +import type { PushDiff } from "$ui/lib/logic/pushDiff"; +import { describe, expect, it } from "vitest"; +import { nsKeyIndex } from "$ui/lib/logic/namespaces"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeNode(overrides: Partial = {}): NodeInfo { + return { + id: overrides.id ?? "node-1", + name: overrides.name ?? "Node", + characters: overrides.characters ?? "", + translation: overrides.translation ?? "", + isPlural: overrides.isPlural ?? false, + key: overrides.key ?? "some-key", + ns: overrides.ns, + connected: overrides.connected ?? true, + visible: overrides.visible, + paramsValues: overrides.paramsValues, + pluralParamValue: overrides.pluralParamValue, + }; +} + +function makeConflict( + overrides: Partial = {}, +): SimpleImportConflictResult { + return { + keyName: overrides.keyName ?? "some-key", + keyNamespace: overrides.keyNamespace, + language: overrides.language ?? "en", + isOverridable: overrides.isOverridable ?? true, + }; +} + +const ctx: PushContext = { + client: {} as TolgeeClient, + apiUrl: "https://app.tolgee.io", + apiKey: "test", + language: "en", + hasNamespacesEnabled: false, +}; + +function makeScreenshot(keys: Array<{ key: string; ns?: string }>): FrameScreenshot { + return { + image: new Uint8Array(), + info: {} as FrameScreenshot["info"], + keys: keys.map((k, i) => ({ + ...makeNode({ id: `k${i}`, key: k.key, ns: k.ns }), + x: 0, + y: 0, + width: 1, + height: 1, + })) as FrameScreenshot["keys"], + }; +} + +// --------------------------------------------------------------------------- +// canonicalKey +// --------------------------------------------------------------------------- + +describe("canonicalKey", () => { + it("joins ns and key when ns is provided (namespaces enabled)", () => { + const node = makeNode({ key: "greeting", ns: "common" }); + expect(canonicalKey(node, true)).toBe(nsKeyIndex("common", "greeting")); + }); + + it("joins an empty ns when none is provided", () => { + const node = makeNode({ key: "greeting", ns: undefined }); + expect(canonicalKey(node, true)).toBe(nsKeyIndex(undefined, "greeting")); + }); + + it("ignores a stale ns when namespaces are DISABLED (matches the server's default-ns row)", () => { + // A node can carry a stale, invisible ns even on a ns-disabled project. + const node = makeNode({ key: "greeting", ns: "web" }); + expect(canonicalKey(node, false)).toBe(nsKeyIndex(undefined, "greeting")); + // …but with the feature ON the ns is honoured. + expect(canonicalKey(node, true)).toBe(nsKeyIndex("web", "greeting")); + }); +}); + +// --------------------------------------------------------------------------- +// resolutionKey +// --------------------------------------------------------------------------- + +describe("resolutionKey", () => { + it("joins namespace and keyName when a namespace is provided (namespaces enabled)", () => { + expect(resolutionKey("greeting", "common", true)).toBe(nsKeyIndex("common", "greeting")); + }); + + it("joins an empty namespace when none is provided", () => { + expect(resolutionKey("greeting", undefined, true)).toBe(nsKeyIndex(undefined, "greeting")); + }); + + it("ignores a stale namespace when namespaces are DISABLED", () => { + // The server reports conflicts for the default ns; a local node's stale ns + // must normalise to the same key so its resolution is found. + expect(resolutionKey("greeting", "web", false)).toBe(nsKeyIndex(undefined, "greeting")); + expect(resolutionKey("greeting", "web", true)).toBe(nsKeyIndex("web", "greeting")); + }); +}); + +// --------------------------------------------------------------------------- +// defaultResolutions +// --------------------------------------------------------------------------- + +describe("defaultResolutions", () => { + it("returns OVERRIDE for isOverridable:true conflicts", () => { + const conflicts = [makeConflict({ keyName: "k1", isOverridable: true })]; + const result = defaultResolutions(conflicts, true); + expect(result[nsKeyIndex(undefined, "k1")]).toBe("OVERRIDE"); + }); + + it("returns KEEP for isOverridable:false conflicts", () => { + const conflicts = [makeConflict({ keyName: "k1", isOverridable: false })]; + const result = defaultResolutions(conflicts, true); + expect(result[nsKeyIndex(undefined, "k1")]).toBe("KEEP"); + }); + + it("returns empty object for empty list", () => { + expect(defaultResolutions([], true)).toEqual({}); + }); + + it("uses resolutionKey for map keys", () => { + const conflicts = [makeConflict({ keyName: "btn", keyNamespace: "ui", isOverridable: true })]; + const result = defaultResolutions(conflicts, true); + expect(Object.keys(result)).toContain(nsKeyIndex("ui", "btn")); + }); + + it("handles mixed overridable and non-overridable conflicts", () => { + const conflicts = [ + makeConflict({ keyName: "k1", isOverridable: true }), + makeConflict({ keyName: "k2", isOverridable: false }), + ]; + const result = defaultResolutions(conflicts, true); + expect(result[nsKeyIndex(undefined, "k1")]).toBe("OVERRIDE"); + expect(result[nsKeyIndex(undefined, "k2")]).toBe("KEEP"); + }); + + it("REGRESSION: ns-disabled — a default-ns conflict resolves for a node with a stale ns", () => { + // The server reports the conflict under the DEFAULT namespace… + const resolutions = defaultResolutions( + [makeConflict({ keyName: "greeting", keyNamespace: undefined, isOverridable: true })], + false, + ); + // …and a local node that still carries a stale "web" ns must find it. + expect(resolutions[resolutionKey("greeting", "web", false)]).toBe("OVERRIDE"); + }); +}); + +// --------------------------------------------------------------------------- +// buildPayload +// --------------------------------------------------------------------------- + +describe("buildPayload", () => { + const emptyMap = new Map(); + const noScreenshots: FrameScreenshot[] = []; + + it("sets node.key as name and node.ns as namespace when hasNamespacesEnabled is true", () => { + const node = makeNode({ key: "title", ns: "home" }); + const [item] = buildPayload({ + ctx: { ...ctx, hasNamespacesEnabled: true }, + nodes: [node], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + }); + expect(item?.name).toBe("title"); + expect(item?.namespace).toBe("home"); + }); + + it("omits namespace when hasNamespacesEnabled is false", () => { + const node = makeNode({ key: "title", ns: "home" }); + const [item] = buildPayload({ + ctx: { ...ctx, hasNamespacesEnabled: false }, + nodes: [node], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + }); + expect(item?.namespace).toBeUndefined(); + }); + + it("omits namespace when hasNamespacesEnabled is true but ns is falsy", () => { + const node = makeNode({ key: "title", ns: undefined }); + const [item] = buildPayload({ + ctx: { ...ctx, hasNamespacesEnabled: true }, + nodes: [node], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + }); + expect(item?.namespace).toBeUndefined(); + }); + + it("includes translation text in translations[language]", () => { + const node = makeNode({ key: "greeting", translation: "Hello" }); + const [item] = buildPayload({ + ctx, + nodes: [node], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + }); + expect(item?.translations).toEqual({ + en: { text: "Hello", resolution: "OVERRIDE" }, + }); + }); + + it("sets translations to {} (empty) when resolutionFor returns KEEP", () => { + const node = makeNode({ key: "greeting", translation: "Hello" }); + const [item] = buildPayload({ + ctx, + nodes: [node], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + resolutionFor: () => "KEEP", + }); + expect(item?.translations).toEqual({}); + }); + + it("sends translations: {} for an UNCHANGED node (never re-overrides it)", () => { + const changed = makeNode({ id: "a", key: "changed", translation: "New" }); + const unchanged = makeNode({ id: "b", key: "same", translation: "Same" }); + const items = buildPayload({ + ctx, + nodes: [changed, unchanged], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + unchangedNodeIds: new Set(["b"]), + }); + // changed node still pushes its text with OVERRIDE... + expect(items[0]?.translations).toEqual({ + en: { text: "New", resolution: "OVERRIDE" }, + }); + // ...the unchanged one carries no translation at all. + expect(items[1]?.translations).toEqual({}); + expect(items[1]?.name).toBe("same"); + }); + + it("falls back to node.characters when translation is empty", () => { + const node = makeNode({ key: "label", translation: "", characters: "Fallback" }); + const [item] = buildPayload({ + ctx, + nodes: [node], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + }); + expect(item?.translations?.en?.text).toBe("Fallback"); + }); + + it("uses empty string when both translation and characters are empty", () => { + const node = makeNode({ key: "label", translation: "", characters: "" }); + const [item] = buildPayload({ + ctx, + nodes: [node], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + }); + expect(item?.translations?.en?.text).toBe(""); + }); + + it("returns empty array for empty nodes list", () => { + const result = buildPayload({ + ctx, + nodes: [], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + }); + expect(result).toEqual([]); + }); + + it("screenshots array is empty when uploadedImageIdByScreenshot has no matching entry", () => { + const node = makeNode({ key: "title" }); + const [item] = buildPayload({ + ctx, + nodes: [node], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + }); + expect(item?.screenshots).toEqual([]); + }); + + it("maps screenshots that reference the node's key and have an uploaded image id", () => { + const node = makeNode({ key: "title", ns: undefined }); + const screenshot: FrameScreenshot = { + image: new Uint8Array([1, 2, 3]), + info: { id: "frame-1", name: "Frame", width: 800, height: 600 }, + keys: [ + { + ...node, + x: 10, + y: 20, + width: 100, + height: 50, + }, + ], + }; + const uploadedMap = new Map([[screenshot, 42]]); + + const [item] = buildPayload({ + ctx, + nodes: [node], + screenshots: [screenshot], + uploadedImageIdByScreenshot: uploadedMap, + }); + + expect(item?.screenshots).toHaveLength(1); + expect(item?.screenshots?.[0]?.uploadedImageId).toBe(42); + expect(item?.screenshots?.[0]?.positions).toEqual([{ x: 10, y: 20, width: 100, height: 50 }]); + }); + + it("excludes screenshots whose key does not match the node", () => { + const node = makeNode({ key: "title" }); + const otherNode = makeNode({ key: "other-key" }); + const screenshot: FrameScreenshot = { + image: new Uint8Array([1]), + info: { id: "frame-2", name: "Frame", width: 400, height: 300 }, + keys: [{ ...otherNode, x: 0, y: 0, width: 50, height: 20 }], + }; + const uploadedMap = new Map([[screenshot, 99]]); + + const [item] = buildPayload({ + ctx, + nodes: [node], + screenshots: [screenshot], + uploadedImageIdByScreenshot: uploadedMap, + }); + + expect(item?.screenshots).toEqual([]); + }); + + it("processes multiple nodes independently", () => { + const nodeA = makeNode({ key: "a", translation: "Alpha" }); + const nodeB = makeNode({ key: "b", translation: "Beta" }); + const result = buildPayload({ + ctx, + nodes: [nodeA, nodeB], + screenshots: noScreenshots, + uploadedImageIdByScreenshot: emptyMap, + }); + expect(result).toHaveLength(2); + expect(result[0]?.name).toBe("a"); + expect(result[1]?.name).toBe("b"); + }); +}); + +describe("buildRelatedKeys", () => { + it("lists a screenshot's keys in order, dropping empty keys", () => { + const s = makeScreenshot([{ key: "a" }, { key: "" }, { key: "b" }]); + const rel = buildRelatedKeys(ctx, s); + expect(rel.map((k) => k.keyName)).toEqual(["a", "b"]); + }); + + it("includes namespace only when namespaces are enabled", () => { + const s = makeScreenshot([{ key: "a", ns: "common" }]); + expect(buildRelatedKeys({ ...ctx, hasNamespacesEnabled: true }, s)[0]?.namespace).toBe( + "common", + ); + expect(buildRelatedKeys(ctx, s)[0]?.namespace).toBeUndefined(); + }); + + it("carries the branch on each related key when set", () => { + const s = makeScreenshot([{ key: "a" }]); + expect(buildRelatedKeys({ ...ctx, branch: "feat" }, s)[0]?.branch).toBe("feat"); + expect(buildRelatedKeys(ctx, s)[0]?.branch).toBeUndefined(); + }); + + it("caps at 100 keys", () => { + const s = makeScreenshot(Array.from({ length: 150 }, (_, i) => ({ key: `k${i}` }))); + expect(buildRelatedKeys(ctx, s)).toHaveLength(100); + }); +}); + +// --------------------------------------------------------------------------- +// buildConnectBackUpdates +// --------------------------------------------------------------------------- + +function makeDiff(overrides: Partial = {}): PushDiff { + return { + newKeys: [], + changedKeys: [], + unchangedKeys: [], + missingKeys: [], + conflictingNodes: [], + ...overrides, + }; +} + +describe("buildConnectBackUpdates", () => { + it("connects every snapshot node, preferring the canonical translation", () => { + const a = makeNode({ id: "a", key: "title", characters: "Hi" }); + const b = makeNode({ id: "b", key: "title", characters: "Hi" }); + const canonical = new Map([ + [nsKeyIndex(undefined, "title"), { translation: "Hi canonical", isPlural: true }], + ]); + + const updates = buildConnectBackUpdates( + makeDiff({ newKeys: [a] }), + [a, b], + canonical, + true, + ); + + // Both nodes sharing the pushed key get connected (bulk-assign case). + expect(updates).toEqual([ + { id: "a", info: { connected: true, translation: "Hi canonical", isPlural: true } }, + { id: "b", info: { connected: true, translation: "Hi canonical", isPlural: true } }, + ]); + }); + + it("falls back to the node's own translation, then characters", () => { + const withTranslation = makeNode({ id: "a", key: "x", translation: "Saved" }); + const withoutAny = makeNode({ id: "b", key: "y", translation: "", characters: "Typed" }); + + const updates = buildConnectBackUpdates( + makeDiff(), + [withTranslation, withoutAny], + null, + true, + ); + + expect(updates[0]?.info.translation).toBe("Saved"); + // `??` semantics: an empty-string translation is kept as-is (matches the + // pre-extraction inline code — characters is only for nullish). + expect(updates[1]?.info.translation).toBe(""); + }); + + it("excludes dropped conflict-group members and missing keys", () => { + const kept = makeNode({ id: "kept", key: "dup" }); + const dropped = makeNode({ id: "dropped", key: "dup" }); + const missing = makeNode({ id: "missing", key: "gone" }); + const ok = makeNode({ id: "ok", key: "fine" }); + + const updates = buildConnectBackUpdates( + makeDiff({ + conflictingNodes: [{ key: "dup", nodes: [kept, dropped] }], + missingKeys: [missing], + }), + [kept, dropped, missing, ok], + null, + true, + ); + + expect(updates.map((u) => u.id)).toEqual(["kept", "ok"]); + }); + + it("REGRESSION: reads only the snapshot, never the current selection", () => { + // The push started over snapshot A… + const pushedNode = makeNode({ id: "pushed", key: "title", translation: "Hello" }); + const snapshotDiff = makeDiff({ newKeys: [pushedNode] }); + const snapshotSelection = [pushedNode]; + + // …but by the time the awaits resolved, the user selected something else + // entirely (this is what the reactive `diff`/`connectedNodes` would show). + const strayNode = makeNode({ + id: "stray", + key: "other", + connected: false, + characters: "Never pushed", + }); + const currentSelection = [strayNode]; + + const updates = buildConnectBackUpdates(snapshotDiff, snapshotSelection, null, true); + + // Only the pushed node gets connected — the stray selection must not + // appear (previously it got `connected: true` with a bogus baseline). + expect(updates.map((u) => u.id)).toEqual(["pushed"]); + expect(currentSelection.every((n) => !updates.some((u) => u.id === n.id))).toBe(true); + }); + + it("REGRESSION: ns-disabled — a node with a stale ns still gets the canonical translation", () => { + // Server (namespaces disabled) returns the key under the DEFAULT namespace… + const canonical = new Map([ + [nsKeyIndex(undefined, "greeting"), { translation: "Canonical", isPlural: false }], + ]); + // …while the local node still carries a stale, invisible "web" ns. + const node = makeNode({ id: "n", key: "greeting", ns: "web", translation: "Old" }); + + const updates = buildConnectBackUpdates( + makeDiff({ newKeys: [node] }), + [node], + canonical, + false, + ); + + // Before the fix the lookup ("web|greeting") missed the map ("|greeting") + // and fell back to the node's own stale "Old" → phantom diff next push. + expect(updates[0]?.info.translation).toBe("Canonical"); + }); + + it("with namespaces ENABLED, ns is honoured — no cross-namespace match", () => { + // A default-ns canonical row must NOT feed a node living in a real ns. + const canonical = new Map([ + [nsKeyIndex(undefined, "greeting"), { translation: "Default ns", isPlural: false }], + ]); + const node = makeNode({ id: "n", key: "greeting", ns: "web", translation: "Own" }); + + const updates = buildConnectBackUpdates( + makeDiff({ newKeys: [node] }), + [node], + canonical, + true, + ); + + // "web|greeting" ≠ "|greeting" → falls back to the node's own translation. + expect(updates[0]?.info.translation).toBe("Own"); + }); +}); diff --git a/src/ui/lib/logic/__tests__/queryResult.test.ts b/src/ui/lib/logic/__tests__/queryResult.test.ts new file mode 100644 index 00000000..7c15352e --- /dev/null +++ b/src/ui/lib/logic/__tests__/queryResult.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it, vi } from "vitest"; + +import { settleQuery } from "$ui/lib/logic/queryResult"; + +describe("settleQuery", () => { + it("wraps a resolved value as { ok: true }", async () => { + const out = await settleQuery(async () => 42, { toMessage: () => "nope" }); + expect(out).toEqual({ ok: true, value: 42 }); + }); + + it("converts a thrown error into { ok: false } with the mapped message", async () => { + const out = await settleQuery( + async () => { + throw new Error("500"); + }, + { retries: 0, toMessage: (e) => `msg:${(e as Error).message}` }, + ); + expect(out).toEqual({ ok: false, error: "msg:500" }); + }); + + it("never rejects — a failing run resolves rather than throwing", async () => { + // Regression for finding 62: the whole point is that queryFn resolves so + // the view can read the error off `.data` instead of a (broken) `.error`. + await expect( + settleQuery( + async () => { + throw new Error("network"); + }, + { retries: 0, toMessage: () => "handled" }, + ), + ).resolves.toEqual({ ok: false, error: "handled" }); + }); + + it("retries once by default, then reports the last failure", async () => { + const run = vi.fn(async () => { + throw new Error("boom"); + }); + const out = await settleQuery(run, { toMessage: () => "failed" }); + expect(run).toHaveBeenCalledTimes(2); // first attempt + one retry + expect(out).toEqual({ ok: false, error: "failed" }); + }); + + it("succeeds on the retry when the first attempt was a transient blip", async () => { + let calls = 0; + const out = await settleQuery( + async () => { + calls++; + if (calls === 1) throw new Error("blip"); + return "recovered"; + }, + { toMessage: () => "failed" }, + ); + expect(calls).toBe(2); + expect(out).toEqual({ ok: true, value: "recovered" }); + }); + + it("re-throws (does NOT swallow) when the signal is aborted — a cancelled query must not cache an error", async () => { + const controller = new AbortController(); + controller.abort(); + await expect( + settleQuery( + async () => { + throw new Error("aborted fetch"); + }, + { signal: controller.signal, toMessage: () => "handled" }, + ), + ).rejects.toThrow("aborted fetch"); + }); + + it("does not retry once the signal aborts mid-flight", async () => { + const controller = new AbortController(); + let calls = 0; + await expect( + settleQuery( + async () => { + calls++; + controller.abort(); + throw new Error("late abort"); + }, + { signal: controller.signal, retries: 3, toMessage: () => "handled" }, + ), + ).rejects.toThrow("late abort"); + expect(calls).toBe(1); // aborted → re-thrown immediately, no further attempts + }); +}); diff --git a/src/ui/lib/logic/__tests__/sanitizeIcuPreview.test.ts b/src/ui/lib/logic/__tests__/sanitizeIcuPreview.test.ts new file mode 100644 index 00000000..cd41395d --- /dev/null +++ b/src/ui/lib/logic/__tests__/sanitizeIcuPreview.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import { sanitizeIcuPreview } from "$ui/lib/logic/sanitizeIcuPreview"; + +describe("sanitizeIcuPreview", () => { + it("neutralizes an XSS payload", () => { + const out = sanitizeIcuPreview(""); + expect(out).not.toContain("bold
    as a real tag", () => { + expect(sanitizeIcuPreview("bold")).toBe("bold"); + }); + + it("keeps ... as a real tag", () => { + expect(sanitizeIcuPreview("italic")).toBe("italic"); + }); + + it("keeps ... as a real tag", () => { + expect(sanitizeIcuPreview("emphasis")).toBe("emphasis"); + }); + + it("keeps ... as a real tag", () => { + expect(sanitizeIcuPreview("underline")).toBe("underline"); + }); + + it("keeps ... as a real tag", () => { + expect(sanitizeIcuPreview("strong")).toBe("strong"); + }); + + it("keeps
    as a real self-closing tag", () => { + expect(sanitizeIcuPreview("line1
    line2")).toBe("line1
    line2"); + }); + + it("keeps
    as a real self-closing tag", () => { + expect(sanitizeIcuPreview("line1
    line2")).toBe("line1
    line2"); + }); + + it("strips the attribute off a whitelisted tag rather than letting it through live", () => { + const out = sanitizeIcuPreview('bold'); + // The attribute must never appear as a live attribute — it's fine if it + // shows up as escaped, inert text, but `onclick=` must not survive as a + // real DOM attribute assignment. + expect(out).not.toContain('onclick="alert(1)"'); + expect(out).not.toMatch(/]*onclick/); + }); + + it("renders plain literal < and > that don't form a recognized tag as escaped text", () => { + const out = sanitizeIcuPreview("if a < b then > c"); + expect(out).toBe("if a < b then > c"); + expect(out).not.toContain(" { + const out = sanitizeIcuPreview("bold and italic"); + expect(out).toBe("bold and italic"); + }); + + it("neutralizes an unknown tag like "); + expect(out).not.toContain(""); + }); + + it("neutralizes an unknown tag like ", () => { + const out = sanitizeIcuPreview('link'); + expect(out).not.toContain(""); + }); +}); diff --git a/src/ui/lib/logic/__tests__/saveQueue.test.ts b/src/ui/lib/logic/__tests__/saveQueue.test.ts new file mode 100644 index 00000000..352f6748 --- /dev/null +++ b/src/ui/lib/logic/__tests__/saveQueue.test.ts @@ -0,0 +1,92 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { UiToMain } from "$shared/messages"; + +const sent: UiToMain[] = []; + +vi.mock("$ui/lib/bus", () => ({ + send: (msg: UiToMain) => { + sent.push(msg); + }, + nextCorrelationId: () => "test-correlation", +})); + +import { cancelNodeSave, flushNodeSaves, queueNodeSave } from "$ui/lib/logic/saveQueue"; + +type SetNodesData = Extract; + +beforeEach(() => { + vi.useFakeTimers(); + sent.length = 0; +}); + +afterEach(() => { + // Drain anything a test left queued so cases stay independent. + flushNodeSaves(); + sent.length = 0; + vi.useRealTimers(); +}); + +describe("queueNodeSave", () => { + it("coalesces saves for many nodes into one message", () => { + queueNodeSave("1:1", { key: "a", connected: false }); + queueNodeSave("1:2", { key: "b", connected: false }); + queueNodeSave("1:3", { key: "c", connected: false }); + + expect(sent).toHaveLength(0); + vi.advanceTimersByTime(300); + + expect(sent).toHaveLength(1); + const msg = sent[0] as SetNodesData; + expect(msg.type).toBe("set-nodes-data"); + expect(msg.nodes.map((n) => n.id)).toEqual(["1:1", "1:2", "1:3"]); + }); + + it("merges repeated saves for the same node, last value winning", () => { + queueNodeSave("1:1", { key: "draft" }); + queueNodeSave("1:1", { key: "final", ns: "app" }); + vi.advanceTimersByTime(300); + + const msg = sent[0] as SetNodesData; + expect(msg.nodes).toEqual([{ id: "1:1", info: { key: "final", ns: "app" } }]); + }); + + it("restarts the debounce on every enqueue", () => { + queueNodeSave("1:1", { key: "a" }); + vi.advanceTimersByTime(200); + queueNodeSave("1:2", { key: "b" }); + vi.advanceTimersByTime(200); + expect(sent).toHaveLength(0); + + vi.advanceTimersByTime(100); + expect(sent).toHaveLength(1); + expect((sent[0] as SetNodesData).nodes).toHaveLength(2); + }); + + it("cancelNodeSave drops a queued intermediate edit", () => { + queueNodeSave("1:1", { key: "typo" }); + cancelNodeSave("1:1"); + vi.advanceTimersByTime(300); + + expect(sent).toHaveLength(0); + }); + + it("cancelling one node keeps the others queued", () => { + queueNodeSave("1:1", { key: "typo" }); + queueNodeSave("1:2", { key: "keep" }); + cancelNodeSave("1:1"); + vi.advanceTimersByTime(300); + + expect(sent).toHaveLength(1); + expect((sent[0] as SetNodesData).nodes).toEqual([{ id: "1:2", info: { key: "keep" } }]); + }); + + it("flushNodeSaves sends immediately and empties the queue", () => { + queueNodeSave("1:1", { key: "a" }); + flushNodeSaves(); + + expect(sent).toHaveLength(1); + vi.advanceTimersByTime(300); + expect(sent).toHaveLength(1); // nothing left to fire + }); +}); diff --git a/src/ui/lib/logic/__tests__/virtualWindow.test.ts b/src/ui/lib/logic/__tests__/virtualWindow.test.ts new file mode 100644 index 00000000..24c0bfa1 --- /dev/null +++ b/src/ui/lib/logic/__tests__/virtualWindow.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { computeVirtualWindow } from "../virtualWindow"; + +describe("computeVirtualWindow", () => { + it("returns the full range unclipped when everything fits the viewport", () => { + const w = computeVirtualWindow(10, 0, 1000, 60, 6); + expect(w).toEqual({ start: 0, end: 10, padTop: 0, padBottom: 0 }); + }); + + it("clips to a window around the scroll position, with overscan", () => { + // 1000 rows, 60px each, scrolled to row 100, 600px tall viewport. + const w = computeVirtualWindow(1000, 100 * 60, 600, 60, 6); + // floor(6000/60) - 6 = 94 + expect(w.start).toBe(94); + // ceil((6000+600)/60) + 6 = 116 + expect(w.end).toBe(116); + expect(w.padTop).toBe(94 * 60); + expect(w.padBottom).toBe((1000 - 116) * 60); + }); + + it("never lets start go negative near the top", () => { + const w = computeVirtualWindow(1000, 0, 600, 60, 6); + expect(w.start).toBe(0); + expect(w.padTop).toBe(0); + }); + + it("never lets end exceed the item count near the bottom", () => { + const w = computeVirtualWindow(100, 100 * 60, 600, 60, 6); + expect(w.end).toBe(100); + expect(w.padBottom).toBe(0); + }); + + it("clamps start below the last index for a very large scrollTop (stale scroll position)", () => { + const w = computeVirtualWindow(10, 999999, 600, 60, 6); + expect(w.start).toBeLessThanOrEqual(9); + expect(w.end).toBe(10); + }); + + it("returns an empty window for zero items", () => { + expect(computeVirtualWindow(0, 0, 600, 60, 6)).toEqual({ + start: 0, + end: 0, + padTop: 0, + padBottom: 0, + }); + }); + + it("falls back to the unclipped range when rowHeight isn't known yet (0)", () => { + const w = computeVirtualWindow(500, 0, 600, 0, 6); + expect(w).toEqual({ start: 0, end: 500, padTop: 0, padBottom: 0 }); + }); +}); diff --git a/src/ui/lib/logic/asyncPool.ts b/src/ui/lib/logic/asyncPool.ts new file mode 100644 index 00000000..4cd7afae --- /dev/null +++ b/src/ui/lib/logic/asyncPool.ts @@ -0,0 +1,23 @@ +/** + * Runs `worker` over `items` with at most `concurrency` calls in flight at + * once — a fixed-size worker pool, not `Promise.all` (unbounded) or a + * sequential loop (one at a time). Items are pulled off `items` in order, + * but workers finish independently, so callers needing per-item state + * (progress counters, cancellation checks) must do that inside `worker` + * itself rather than relying on completion order. + */ +export async function runWithConcurrency( + items: readonly T[], + concurrency: number, + worker: (item: T) => Promise, +): Promise { + let cursor = 0; + async function runNext(): Promise { + while (cursor < items.length) { + const item = items[cursor++] as T; + await worker(item); + } + } + const workerCount = Math.max(1, Math.min(concurrency, items.length)); + await Promise.all(Array.from({ length: workerCount }, runNext)); +} diff --git a/src/ui/lib/logic/authBootstrap.ts b/src/ui/lib/logic/authBootstrap.ts new file mode 100644 index 00000000..30922df5 --- /dev/null +++ b/src/ui/lib/logic/authBootstrap.ts @@ -0,0 +1,46 @@ +/** + * Decides what the silent startup auth-validation should do with its + * result, split out of `App.svelte` so it's testable without a component + * harness (this repo has none — see tasks 3/7/11). + * + * The distinction that matters: `auth.invalid_api_key` (401/403) means the + * credentials themselves are wrong. Every other failure (a network blip, + * a 5xx, a malformed payload) says nothing about whether the credentials + * are good — it must never log out a session that was already working, and + * the fingerprint must NOT be remembered so the next `config-changed` / + * `page-changed` event retries instead of getting stuck showing "sign in" + * with a perfectly valid key on file. + */ + +export type AuthValidationOutcome = + | { ok: true } + | { ok: false; error: string }; + +export type AuthBootstrapDecision = { + /** Whether to call `auth.clear()`. */ + clearAuth: boolean; + /** + * Whether to remember this fingerprint as validated (skip re-validating + * it next time). `false` means the next bootstrap call for the same + * credentials will try again. + */ + rememberFingerprint: boolean; +}; + +export function decideAuthBootstrap( + result: AuthValidationOutcome, + wasAuthenticated: boolean, +): AuthBootstrapDecision { + if (result.ok) { + return { clearAuth: false, rememberFingerprint: true }; + } + if (result.error === "auth.invalid_api_key") { + // The key really is wrong — clear (if there was anything to clear) so + // the UI honestly reflects it, and remember the fingerprint: retrying + // immediately would just get the same 401 again. + return { clearAuth: wasAuthenticated, rememberFingerprint: true }; + } + // Soft failure (network_error, request_failed, missing_project_id, or any + // future error code): not evidence the credentials are wrong. + return { clearAuth: false, rememberFingerprint: false }; +} diff --git a/src/ui/lib/logic/copyApply.ts b/src/ui/lib/logic/copyApply.ts new file mode 100644 index 00000000..d42040c8 --- /dev/null +++ b/src/ui/lib/logic/copyApply.ts @@ -0,0 +1,219 @@ +import { renderIcuForNode } from "$shared/interpolate"; +import type { UiToMain } from "$shared/messages"; +import type { NodeInfo } from "$shared/types"; +import { nextCorrelationId, on, send } from "$ui/lib/bus"; +import { createIdleTimeout } from "$ui/lib/busRequest"; +import { nsKeyIndex } from "$ui/lib/logic/namespaces"; + +/** + * UI half of the language-copy flow. The main thread only CLONES the page and + * returns its connected nodes (`create-copy-result.pages`) — it can't render + * ICU itself, Figma's main-thread sandbox has no `Intl` (plural rules), so + * every `{param}`/plural render silently failed there and copies came out + * half in the source language. This module renders each node exactly like + * the Download flow and writes the results back through the ordinary + * `apply-translations` request, which also persists the raw `translation` + * into plugin data — making a fresh copy identical to clone + Download all + * (an immediate Download afterwards reports "No changes found"). + */ + +export type CopyTranslations = Record; + +type ApplyUpdate = Extract["updates"][number]; + +/** + * The text to write for each connected node of a fresh language copy: the + * remote translation for its key (falling back to the node's own persisted + * `translation` on a miss, so the page never ends up empty), rendered through + * THIS node's own plural/param samples — same pipeline as the regular + * Download. `isPlural` comes from the remote key when available (a per-KEY + * Tolgee property) since it's more trustworthy than the cloned node's own + * possibly-stale flag. + * + * Nodes with nothing to write (no remote match and no persisted translation) + * or an unrenderable ICU are skipped — the clone keeps its source-language + * text rather than getting raw ICU dumped onto the canvas. + */ +export function buildCopyUpdates( + nodes: NodeInfo[], + translationsForLang: CopyTranslations, + language: string, +): ApplyUpdate[] { + const updates: ApplyUpdate[] = []; + for (const node of nodes) { + if (!node.connected || !node.key) continue; + const remote = translationsForLang[nsKeyIndex(node.ns, node.key)]; + const rawText = remote?.text ?? node.translation; + if (!rawText) continue; + const isPlural = remote?.isPlural ?? node.isPlural; + const out = renderIcuForNode(rawText, { ...node, isPlural }, language); + if (out.error) continue; + updates.push({ id: node.id, text: out.text, translation: rawText, isPlural }); + } + return updates; +} + +// Same idle-timeout rationale as CopyView's own apply watchdog: progress +// messages keep resetting the clock, so only a genuinely silent write trips +// it — large pages legitimately take a while between pings. +const APPLY_TIMEOUT_MS = 5 * 60_000; + +/** + * Send one `apply-translations` batch and resolve with its result. Mirrors + * `requestPageConnectedNodes`' promise-wrapper shape (correlation id pairing, + * idle watchdog touched by progress). `onProgress` forwards the write's + * done/total (only emitted by the main thread for batches > 100 nodes). + */ +export function applyTranslationsRequest( + updates: ApplyUpdate[], + onProgress?: (done: number, total: number) => void, +): Promise<{ ok: boolean; errors: string[] }> { + return new Promise((resolve) => { + const correlationId = nextCorrelationId(); + const cleanup = (): void => { + offResult(); + offProgress(); + watchdog.clear(); + }; + const watchdog = createIdleTimeout(APPLY_TIMEOUT_MS, () => { + cleanup(); + resolve({ ok: false, errors: ["Timed out waiting for the translations to apply."] }); + }); + const offResult = on("apply-translations-result", (msg) => { + if (msg.correlationId !== correlationId) return; + cleanup(); + resolve({ ok: msg.ok, errors: msg.errors }); + }); + const offProgress = on("apply-translations-progress", (msg) => { + if (msg.correlationId !== correlationId) return; + watchdog.touch(); + onProgress?.(msg.done, msg.total); + }); + send({ type: "apply-translations", correlationId, updates }); + }); +} + +/** + * Render + apply the translations onto every freshly cloned page, one + * `apply-translations` batch per page. Pages whose nodes all resolve to + * "nothing to write" are skipped without a round-trip. Stops at the first + * failing page and reports its first error — matching how the copy result + * itself is surfaced (one error message, not a per-node report). + */ +export async function applyCopyPages( + pages: Array<{ pageId: string; language: string; nodes: NodeInfo[] }>, + translationsByLang: Record, + onProgress?: (done: number, total: number) => void, +): Promise<{ ok: boolean; error?: string }> { + for (const page of pages) { + const updates = buildCopyUpdates( + page.nodes, + translationsByLang[page.language] ?? {}, + page.language, + ); + if (updates.length === 0) continue; + const result = await applyTranslationsRequest(updates, onProgress); + if (!result.ok) { + return { ok: false, error: result.errors[0] ?? "Failed to write the copy's translations." }; + } + } + return { ok: true }; +} + +const RECREATE_IDLE_TIMEOUT_MS = 5 * 60_000; + +/** + * Correlation id of the recreate currently in flight, or `null`. + * + * MODULE-scoped for the same reason the job itself is: recreating unmounts the + * view that started it, so a "busy" flag held in component state dies with the + * instance. A second Recreate could then start while the first was still + * applying — and since recreating DELETES the existing copy page, the second + * run would remove the fresh page the first one was midway through writing. + */ +let inFlightRecreate: string | null = null; + +/** Whether a recreate is still running — including across the unmount of the + * view that started it. Callers must check this BEFORE sending `create-copy`; + * by the time the job is registered, the main thread is already deleting. */ +export function isCopyRecreateInFlight(): boolean { + return inFlightRecreate !== null; +} + +/** + * MODULE-SCOPED continuation for "Recreate copy": waits for this + * `create-copy-result`, then renders + applies `translations` onto the fresh + * clone and toasts "Copy recreated.". + * + * Why not in CopyView: recreating deletes the CURRENT page first (main switches + * to the source page as fallback), and the source page isn't a copy — so + * App.svelte routes away and CopyView UNMOUNTS mid-flight, tearing down any + * `$effect`-registered result handler along with the held translations. The + * clone then completed with nobody left to apply the fetched language onto it, + * and the "recreated" page silently kept the source-language text. This job + * lives outside the component lifecycle, so it survives that unmount/remount. + * (Plain "Create copy" never hit this: it runs from the source page, which is + * not removed, so CreateCopy stays mounted.) + * + * `onProgress`/`onDone` are best-effort UI hooks — after the unmount they + * update a dead instance's state, which is harmless; the canvas work and the + * toast don't depend on them. + */ +export function finishCopyRecreate(opts: { + correlationId: string; + /** Per-language map to render onto the clone; null for a keys copy. */ + translations: Record | null; + onProgress?: (current: number, total: number) => void; + onDone?: (result: { ok: boolean; error?: string }) => void; +}): void { + inFlightRecreate = opts.correlationId; + const cleanup = (): void => { + offResult(); + offProgress(); + watchdog.clear(); + // Only clear if this job is still the current one — a later recreate that + // somehow started must not be un-marked by an older job settling. + if (inFlightRecreate === opts.correlationId) inFlightRecreate = null; + }; + // Idle timeout, not wall-clock: create-copy-progress pings keep resetting it, + // so only a genuinely silent recreate trips it (same shape as the apply + // watchdogs above). + const watchdog = createIdleTimeout(RECREATE_IDLE_TIMEOUT_MS, () => { + cleanup(); + opts.onDone?.({ ok: false, error: "Timed out waiting for the copy to be recreated." }); + }); + const offProgress = on("create-copy-progress", (msg) => { + if (msg.correlationId !== opts.correlationId) return; + watchdog.touch(); + opts.onProgress?.(msg.current, msg.total); + }); + const offResult = on("create-copy-result", (msg) => { + if (msg.correlationId !== opts.correlationId) return; + cleanup(); + void (async () => { + if (!msg.ok) { + opts.onDone?.({ ok: false, error: msg.error ?? "Failed to recreate the copy." }); + return; + } + const pages = msg.pages ?? []; + if (pages.length > 0 && opts.translations) { + const applied = await applyCopyPages(pages, opts.translations, opts.onProgress); + if (!applied.ok) { + opts.onDone?.({ ok: false, error: applied.error ?? "Failed to recreate the copy." }); + return; + } + } + send({ type: "notify", text: "Copy recreated." }); + opts.onDone?.({ ok: true }); + })().catch((err: unknown) => { + // Fire-and-forget, so a throw in here would otherwise surface only as an + // unhandled rejection — leaving the view stuck on its "recreating" state + // with no message. + console.error("[tolgee:ui] copy recreate failed", err); + opts.onDone?.({ + ok: false, + error: err instanceof Error ? err.message : "Failed to recreate the copy.", + }); + }); + }); +} diff --git a/src/ui/lib/logic/copyLanguage.ts b/src/ui/lib/logic/copyLanguage.ts new file mode 100644 index 00000000..8385638f --- /dev/null +++ b/src/ui/lib/logic/copyLanguage.ts @@ -0,0 +1,68 @@ +import type { TolgeeConfig } from "$shared/types"; + +// A BCP-47-ish language tag shape: `cs`, `en`, `pt-BR`, `zh-Hans`, `en-US`. +// Only consulted while the project's language list hasn't loaded — see +// `copyLanguageFromPageName`. A "keys" copy (named "…- keys") and a source name +// ending in "…- API" don't match, so they're never misread as a language. +const LANG_TAG_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/; + +/** + * A page copy's language, read from its NAME suffix — e.g. "Home - cs" → "cs". + * + * Both this plugin and the ORIGINAL name a language copy `${sourceName} - ${lang}` + * (and a keys copy `${sourceName} - keys`), so the suffix is a backward-compatible + * signal that survives even when the stored/merged config language gets repointed + * to the main/default. The last " - " segment is always the copy's language (any + * " - " inside the source name stays to its left). + * + * The suffix is accepted when it is a KNOWN project tag. Only while that list + * is still empty does the shape check stand in for it — which is the common + * case on a copy page, where the list is usually still in flight when + * Recreate/Download runs, and where requiring a loaded list meant a legitimate + * "cs" got rejected and the copy fell back to the main language. + * + * Requiring membership ONCE the list has loaded is what stops an unrelated + * rename from being read as a language: "Home - cs" renamed to "Home - wip" + * matches the tag SHAPE, and treating it as a language silently overrides the + * immutable `copyLanguage` marker and breaks Download for that page. + */ +export function copyLanguageFromPageName( + pageName: string | undefined, + knownTags: ReadonlySet, +): string | undefined { + if (!pageName) return undefined; + const i = pageName.lastIndexOf(" - "); + if (i < 0) return undefined; + const suffix = pageName.slice(i + 3).trim(); + if (!suffix) return undefined; + const accepted = knownTags.size > 0 ? knownTags.has(suffix) : LANG_TAG_RE.test(suffix); + return accepted ? suffix : undefined; +} + +/** + * The language a page copy actually holds — resolved most-reliable first, so + * Recreate/Download always target the language the copy was made in: + * + * 1. the page-name suffix — set at creation by BOTH this plugin and the + * original, and independent of the (repointable, sometimes poisoned) + * config; also self-healing — a correct name overrides a stale marker; + * 2. `copyLanguage` — the immutable marker this plugin writes on new copies + * (covers a copy whose page was renamed away from the "…- lang" pattern); + * 3. `config.language` — last resort. It's the selectable Push/Pull language, + * which shares the page scope and can be repointed to the main/default — + * exactly why (1) and (2) take precedence. + * + * Returns undefined for a "keys" copy (no language). + */ +export function resolveCopyLanguage( + config: Partial | null | undefined, + pageName: string | undefined, + knownTags: ReadonlySet, +): string | undefined { + return ( + copyLanguageFromPageName(pageName, knownTags) || + config?.copyLanguage || + config?.language || + undefined + ); +} diff --git a/src/ui/lib/logic/deeplink.ts b/src/ui/lib/logic/deeplink.ts new file mode 100644 index 00000000..4fb76069 --- /dev/null +++ b/src/ui/lib/logic/deeplink.ts @@ -0,0 +1,48 @@ +import type { NodeInfo, TolgeeConfig } from "$shared/types"; + +/** + * Strip any trailing slash from `apiUrl` so we can append paths safely without + * producing `//`. `URL` would normalize this for us, but constructing a `URL` + * throws on invalid inputs — and the inspect panel must render gracefully + * even when the connection has never been configured. + */ +function trimTrailingSlash(value: string): string { + return value.replace(/\/+$/, ""); +} + +/** + * Build a deep link to the Tolgee web UI translations view, pre-filtered to + * the selected node's key (and namespace, when present). + * + * When the document has been connected and a `projectId` was persisted, the + * link targets that project directly (`/projects/:id/translations`). + * Otherwise it falls back to the generic project-selector translations view, + * which still lets the user search by key inside the app once a project is + * picked. Returns `null` when the config is too incomplete to build a link + * (missing `apiUrl`) or the node has no key to filter on. + */ +export function buildKeyDeepLink( + config: Partial | null, + node: NodeInfo, +): string | null { + if (!config?.apiUrl || !node.key) return null; + const base = trimTrailingSlash(config.apiUrl); + const path = config.projectId + ? `/projects/${config.projectId}/translations` + : "/projects/translations"; + const params = new URLSearchParams(); + params.set("search", node.key); + if (node.ns) params.set("filterNamespace", node.ns); + return `${base}${path}?${params.toString()}`; +} + +/** + * Build a deep link to the project dashboard, used by the footer "Open + * project" action. Falls back to the project list when no `projectId` is + * available so the user can still navigate into the right place. + */ +export function buildProjectDashboardLink(config: Partial | null): string | null { + if (!config?.apiUrl) return null; + const base = trimTrailingSlash(config.apiUrl); + return config.projectId ? `${base}/projects/${config.projectId}` : `${base}/projects`; +} diff --git a/src/ui/lib/logic/icuParams.ts b/src/ui/lib/logic/icuParams.ts new file mode 100644 index 00000000..eb418629 --- /dev/null +++ b/src/ui/lib/logic/icuParams.ts @@ -0,0 +1,38 @@ +import type { NodeInfo } from "$shared/types"; + +/** + * Named ICU parameter names in a string, e.g. `"Hi {name}"` → `["name"]`. + * + * Skips the `#` plural placeholder, positional/numeric args, and ICU STRUCTURE + * selectors (`{count, plural, …}` / `{x, select, …}`) — those aren't a value + * the user fills, and for plurals the "Plural" badge already conveys them. + */ +export function extractParamNames(text: string): string[] { + const re = /\{(\w+)((?:,[^}]*)?)\}/g; + const names: string[] = []; + for (const match of text.matchAll(re)) { + const name = match[1]; + const tail = match[2] ?? ""; + if (!name || name === "#" || /^\d+$/.test(name)) continue; + if (/^,\s*(plural|select|selectordinal)\b/.test(tail)) continue; + names.push(name); + } + return names; +} + +// Inline formatting tags Tolgee supports (mirrors the editor's allow-list), so +// stray `a < b` text isn't mistaken for markup. +const MARKUP_RE = /<\/?(?:b|strong|i|em|u|br)\b[^>]*>/i; + +/** + * Whether a node's translation is "rich" — it carries fillable ICU parameters + * (`{name}`) OR inline markup (``, ``, …). Such strings aren't plain text + * and shouldn't be edited raw, so the list flags them with a "Format" badge + * (alongside the separate "Plural" one). Reads the stored ICU `translation`; + * `characters` (the rendered canvas text) has values/formatting applied, so it + * doesn't carry the source `{…}` / tags. + */ +export function hasRichFormat(node: NodeInfo): boolean { + const src = node.translation ?? ""; + return extractParamNames(src).length > 0 || MARKUP_RE.test(src); +} diff --git a/src/ui/lib/logic/manualChange.ts b/src/ui/lib/logic/manualChange.ts new file mode 100644 index 00000000..df5d755f --- /dev/null +++ b/src/ui/lib/logic/manualChange.ts @@ -0,0 +1,49 @@ +import type { NodeInfo } from "$shared/types"; +import { translationDiffersFromNodeCached } from "$shared/interpolate"; +import { hasIcuArgument } from "$shared/tolgeeFormat"; + +/** + * Detection for the "manual changes detected" conflict — an ADVANCED connected + * string (plural / params / markup) whose stored translation no longer matches + * the Figma canvas, i.e. it was edited directly in Figma and its formatting was + * lost. Purely read-only UI logic; never touches main thread / pushDiff. + * + * Deliberately ADVANCED-only: a plain connected string edited in Figma is just a + * normal "changed" push (the old plugin treats it that way), so flagging it only + * adds friction. Plain edits are NOT flagged. + */ + +// Inline HTML markup we render (b/i/u/strong/em) — formatting a plain Figma +// edit would drop. The ICU side is `hasIcuArgument`, NOT a brace-pair regex: +// Tolgee stores a plain translation containing literal braces as ICU-escaped +// text (`Use '{'braces'}'`), which has no argument in it at all. +const INLINE_MARKUP = /<\/?(?:b|i|u|strong|em)\b/i; + +/** + * Does this string rely on advanced ICU / formatting features (plural, params, + * or inline markup)? + * + * Getting this wrong in the permissive direction is not cosmetic: `textOfNode` + * hands ADVANCED strings their stored `translation` and PLAIN strings their + * live canvas `characters`. So a plain string misread as advanced has its + * designer edit outranked by the stale stored value — the Push diff then sees + * no change and drops the edit without a word. + */ +export function isAdvancedString(node: NodeInfo): boolean { + if (node.isPlural) return true; + if (node.paramsValues && Object.keys(node.paramsValues).length > 0) return true; + const translation = node.translation ?? ""; + return INLINE_MARKUP.test(translation) || hasIcuArgument(translation); +} + +export function hasManualChange(node: NodeInfo, language: string): boolean { + // Any ADVANCED string (plural / params / markup) whose canvas text diverges + // from its rendered stored translation — connected or NOT, matching the + // published plugin, which shows the warning + "Revert" regardless of link + // state (`NodeRow` / `StringDetails` gate purely on `translationDiffersFromNode`). + // That render/compare is a no-op for "simple" strings, so plain edits aren't + // flagged (they're a normal "changed" push), and a string with no stored + // translation has nothing to diverge from. + if (!(node.translation ?? "")) return false; + return translationDiffersFromNodeCached(node, language); +} diff --git a/src/ui/lib/logic/namespaces.ts b/src/ui/lib/logic/namespaces.ts new file mode 100644 index 00000000..683c51de --- /dev/null +++ b/src/ui/lib/logic/namespaces.ts @@ -0,0 +1,37 @@ +export { nsKeyIndex } from "$shared/keyIndex"; + +import type { NodeInfo } from "$shared/types"; + +/** + * The namespace names to offer in a namespace picker. + * + * Merges the project's SERVER namespaces (from the API) with any used LOCALLY + * on Figma nodes — including ones just created in the plugin that don't exist in + * Tolgee yet (a new namespace is only created once a key using it is pushed) — + * plus the configured default. Deduped and sorted. + */ +export function collectNamespaceNames( + serverNamespaces: { name: string }[], + nodes: NodeInfo[], + defaultNs?: string, +): string[] { + const set = new Set(); + for (const n of serverNamespaces) if (n.name) set.add(n.name); + for (const node of nodes) if (node.ns) set.add(node.ns); + if (defaultNs) set.add(defaultNs); + return [...set].sort((a, b) => a.localeCompare(b)); +} + +/** + * A key's display label, with its namespace prefixed — unless the project + * has namespaces disabled, in which case the prefix is always hidden (it + * would just be confusing UI noise for a concept the project doesn't use). + * Display-only: never affects what's stored/pushed. + */ +export function namespacedKeyLabel( + ns: string | undefined, + key: string, + namespacesEnabled: boolean, +): string { + return ns && namespacesEnabled ? `${ns}.${key}` : key; +} diff --git a/src/ui/lib/logic/prefillKey.ts b/src/ui/lib/logic/prefillKey.ts new file mode 100644 index 00000000..7bd9f8f3 --- /dev/null +++ b/src/ui/lib/logic/prefillKey.ts @@ -0,0 +1,160 @@ +import { formatKey, type KeyFormatContext, keyFormatUsesParents } from "$shared/keyFormat"; +import type { NodeInfo, TolgeeConfig } from "$shared/types"; + +/** + * Builds the `formatKey` context from a node's fields. + * + * Single source of truth for the placeholder inputs, so BOTH the in-row prefill + * and the "Generate key names" bulk action feed `formatKey` identical values — + * including the parent placeholders ({component}/{frame}/{artboard}/{section}/ + * {group}), which are resolved main-side and left undefined when the node has + * no such ancestor (or the format doesn't use one). + */ +export function keyContextForNode(node: NodeInfo): KeyFormatContext { + return { + elementName: node.name, + elementText: node.characters, + component: node.component, + instance: node.instance, + frame: node.frame, + artboard: node.artboard, + section: node.section, + group: node.group, + }; +} + +/** + * The format-generated key SUGGESTION for a node, given the current config. + * + * Drives the in-row prefill (NodeListItem persists this for empty unconnected + * keys). The "Generate key names" bulk action shares the same underlying + * `formatKey` primitive but with a user-editable template, so both stay + * consistent through that one helper. + * + * Returns "" when prefill is off or no template is configured — callers treat + * an empty result as "nothing to generate". + */ +function prefillKeyFor( + node: NodeInfo, + config: Partial | null | undefined, +): string { + if (!config?.prefillKeyFormat || !config.keyFormat) return ""; + return formatKey(config.keyFormat, keyContextForNode(node), config.variableCasing); +} + +/** + * Per-node prefill decisions for this plugin session: node id → the format + * signature (key format + casing) the decision was made under. + * + * Module-level on purpose: the decision used to live in each `NodeListItem` + * instance, but the virtualized list re-mounts rows on scroll, so a per-row + * flag "forgot" its decision and re-ran the prefill on every remount. Session + * scope keeps the decision stable across scrolls/re-scans. + * + * It only ever protects a NON-empty key from being rewritten under the same + * format (a manual edit, or the value we already generated). An EMPTY key is + * never "settled shut": clearing a key means "give me a fresh auto value", so + * it regenerates on the next pass — matching the published plugin, whose + * `usePrefilledKey` fills any keyless unconnected node (`enabled: !nodeKey`). + * + * The signature makes format changes work per node instead of as a global + * one-shot: a node decided under format A is simply "undecided" once the + * format is B, so it regenerates whenever it next shows up — even if the + * first pass after the change ran on a stale snapshot (parent placeholder + * names arrive only with the re-scan that follows a format save). + */ +const settled = new Map(); + +/** + * Compute the prefill batch for a selection. Decision per unconnected node: + * - first seen this session (not in `settled`): fill only an EMPTY key — + * keys persisted earlier (manual or generated by a previous session) are + * never overwritten just by opening the plugin; + * - MANUALLY edited (non-empty key that differs from the `prefilledKey` + * marker we last generated): never regenerated, not even on a format/casing + * change — the hand-edited key is the user's. Same rule `clearPrefilledKeys` + * uses, so "turn prefill off" and "change the style" agree; + * - decided under the CURRENT format AND still has a NON-empty key: leave + * alone. An empty key here fell through: the user cleared it, so it + * regenerates; + * - an UNTOUCHED auto key (key === marker) decided under an OLDER format: + * regenerate into the new format — changing the style is meant to reflow the + * keys you never touched. (The published plugin keyed its suggestion query + * on the format so a format change reflowed keys; we scope that to auto keys + * via the marker and also trigger on casing, which production missed.) + * A node whose suggestion comes out empty stays undecided and is retried on + * the next pass (typically once the re-scan delivers its parent names). + * Pure decision logic — the caller persists the result. + */ +/** Any parent placeholder name resolved on this snapshot? The scan fills + * these only when the SAVED format uses them, so right after a format save + * the current snapshot may predate that scan — generating from it would + * persist a partial key (`formatKey` drops empty placeholders). */ +function hasParentContext(n: NodeInfo): boolean { + return ( + n.component !== undefined || + n.instance !== undefined || + n.frame !== undefined || + n.artboard !== undefined || + n.section !== undefined || + n.group !== undefined + ); +} + +export function pendingPrefills( + nodes: NodeInfo[], + config: Partial | null | undefined, +): { id: string; key: string; ns: string }[] { + if (!config?.prefillKeyFormat || !config.keyFormat) return []; + const signature = `${config.keyFormat} ${config.variableCasing ?? ""}`; + const needsParents = keyFormatUsesParents(config.keyFormat); + + const out: { id: string; key: string; ns: string }[] = []; + for (const n of nodes) { + const decidedUnder = settled.get(n.id); + // Already decided under this format AND still has a key → leave it (protects + // a manual edit / the value we generated). But a now-EMPTY key falls through + // to regenerate: clearing means "give me a fresh auto value" (upstream fills + // any keyless unconnected node). + if (decidedUnder === signature && (n.key ?? "") !== "") continue; + if (n.connected) { + settled.set(n.id, signature); + continue; + } + // A manually-edited key — non-empty and no longer equal to the auto value we + // last generated (`prefilledKey`) — is never regenerated, not even across a + // format/casing change. Mirrors `clearPrefilledKeys`, which also keeps it: a + // hand-typed or hand-edited key is the user's, so a style change rewrites + // only UNTOUCHED auto keys (key === marker) and empty keys. A key that never + // carried a marker (undefined) counts as manual too — we can't prove we + // generated it, so we don't clobber it. + if ((n.key ?? "") !== "" && (n.key ?? "") !== (n.prefilledKey ?? "")) { + settled.set(n.id, signature); + continue; + } + // Not seen this session + already has a key → keep it (boot must never + // rewrite persisted keys). An UNTOUCHED auto key (key === marker) decided + // under an older format falls through below and regenerates. + if (decidedUnder === undefined && (n.key ?? "") !== "") { + settled.set(n.id, signature); + continue; + } + // Parent-based format, but this snapshot has no resolved ancestors — + // stay undecided until the follow-up scan delivers them. (A text node + // with genuinely no eligible ancestor never prefills under such a + // format; acceptable for the page-root edge case.) + if (needsParents && !hasParentContext(n)) continue; + const key = prefillKeyFor(n, config); + if (!key) continue; // no suggestion (yet) — stay undecided and retry + settled.set(n.id, signature); + if (key !== (n.key ?? "")) { + out.push({ id: n.id, key, ns: n.ns ?? config.namespace ?? "" }); + } + } + return out; +} + +/** Test hook — the session-scoped decision memory is module state. */ +export function resetPrefillSettled(): void { + settled.clear(); +} diff --git a/src/ui/lib/logic/progress.ts b/src/ui/lib/logic/progress.ts new file mode 100644 index 00000000..3e626660 --- /dev/null +++ b/src/ui/lib/logic/progress.ts @@ -0,0 +1,34 @@ +/** + * Pure percentage/indeterminate logic for `progressBar.svelte`, pulled out so + * it can be unit-tested without any Svelte component-render infrastructure + * (this repo deliberately has none — see tasks 3/7/11/13). + * + * `total === null` (or `<= 0`) means the size isn't known yet — the caller + * should render the indeterminate sliding animation instead of a fill. + * Otherwise the percentage is rounded and clamped to `[0, 100]` so a stale + * `loaded > total` (or a negative `loaded`) never overflows/underflows the + * bar. + */ +export function computeProgressPercent( + loaded: number, + total: number | null, +): number | null { + if (total === null || total <= 0) return null; + return Math.max(0, Math.min(100, Math.round((loaded / total) * 100))); +} + +/** + * The `3 / 10` counter shown beside a progress label, or `null` when there is + * nothing truthful to count. + * + * An indeterminate stage (`total === null`) has no denominator — a single + * upload request can't be subdivided, so it genuinely doesn't know. Rendering + * `0 / …` there reads as a glitch (reported live: "the progress jumps from + * 0/1 to 0/...") and, worse, invites the reader to believe nothing has + * happened. The sliding bar already says "working"; the label should say what + * is being worked on instead of pretending to count. + */ +export function formatProgressCount(loaded: number, total: number | null): string | null { + if (total === null || total <= 0) return null; + return `${Math.max(0, Math.min(loaded, total))} / ${total}`; +} diff --git a/src/ui/lib/logic/pullDiff.ts b/src/ui/lib/logic/pullDiff.ts new file mode 100644 index 00000000..02a11b4f --- /dev/null +++ b/src/ui/lib/logic/pullDiff.ts @@ -0,0 +1,222 @@ +import type { NodeInfo } from "$shared/types"; +import type { PulledKey } from "$ui/lib/api/pull"; +import { renderIcuForNode } from "$shared/interpolate"; +import { plainCanvasText } from "$shared/richText"; +import { nsKeyIndex } from "$ui/lib/logic/namespaces"; + +export type PullDiffResult = { + /** Nodes whose remote translation differs from the local `translation`. */ + changedNodes: Array<{ node: NodeInfo; newText: string; isPlural: boolean }>; + /** Connected nodes whose key is not present in the remote payload. */ + missingKeys: NodeInfo[]; + /** Connected nodes whose remote translation matches the local one. */ + unchangedNodes: NodeInfo[]; +}; + +/** + * Builds a lookup table of `ns|key` -> remote key. We index by namespace + + * keyName because Tolgee allows the same key name across namespaces. + * + * Empty namespace and `undefined` namespace both map to `""` so lookup is + * consistent regardless of whether the server omits the field. + */ +function indexRemote(remoteKeys: PulledKey[]): Map { + const map = new Map(); + for (const k of remoteKeys) { + const ns = k.keyNamespace ?? ""; + map.set(nsKeyIndex(ns, k.keyName), k); + } + return map; +} + +/** + * Compute the pull diff between local connected nodes and the remote + * translations payload, for a single `language`. + * + * This deliberately compares raw text only — no ICU formatting is applied + * here. Callers that need the final TextNode characters should run + * `formatNodeText` per changed node afterwards (and surface the formatting + * error to the user if any). + * + * Nodes that aren't connected to a key are ignored — they have nothing to + * pull. Connected nodes without a remote key land in `missingKeys`. + */ +/** + * Whether the canvas characters differ from what applying `remoteText` would + * ACTUALLY write. The comparison must run the remote text through the same + * pipeline the apply path uses — ICU render (quote unescaping, param + * seeding), then the shared `plainCanvasText` markup strip (`` becomes a + * font range, not characters; `
    ` becomes "\n") — because comparing the + * canvas against the RAW remote text made every formatted string look + * permanently drifted and re-download as "changed" on every single pull. + * + * A render failure counts as "no drift": we can't know what the canvas + * should look like, and re-applying on unknowns would loop forever too. + */ +function canvasDrifted(node: NodeInfo, remoteText: string, language: string): boolean { + const out = renderIcuForNode(remoteText, node, language); + if (out.error) return false; + return node.characters !== plainCanvasText(out.text); +} + +export function pullDiff( + localNodes: NodeInfo[], + remoteKeys: PulledKey[], + language: string, + namespacesEnabled = true, +): PullDiffResult { + const remote = indexRemote(remoteKeys); + + const changedNodes: PullDiffResult["changedNodes"] = []; + const missingKeys: NodeInfo[] = []; + const unchangedNodes: NodeInfo[] = []; + + for (const node of localNodes) { + if (!node.connected || !node.key) continue; + + // With namespaces disabled on the project the write pipeline ignores the + // node's stored `ns` (keys live in the default namespace), so the lookup + // must too — a stale invisible ns would otherwise land the node in + // `missingKeys` and silently skip its update. Mirrors push + the + // stale-link check (`effectiveNs`). + const ns = namespacesEnabled ? (node.ns ?? "") : ""; + const remoteKey = remote.get(nsKeyIndex(ns, node.key)); + + if (!remoteKey) { + missingKeys.push(node); + continue; + } + + const remoteText = remoteKey.translations[language]?.text ?? ""; + const remoteIsPlural = remoteKey.isPlural; + + // Treat an empty remote translation the same way the legacy plugin did: + // surface it as "missing" so the user sees they need to translate the + // key in Tolgee. Overwriting a local string with `""` would be + // destructive, so we don't fall into the changedNodes bucket either. + if (!remoteText) { + missingKeys.push(node); + continue; + } + + if (remoteText !== node.translation || remoteIsPlural !== Boolean(node.isPlural)) { + changedNodes.push({ + node, + newText: remoteText, + isPlural: remoteIsPlural, + }); + } else if ( + // The cached translation matches the remote, but the rendered canvas + // characters have drifted (e.g. someone typed over the layer manually). + // For non-plural / non-parametric keys we can safely re-apply the + // remote text so the canvas matches the source of truth again. + // + // The plural/params exclusion is LOAD-BEARING, not caution — do not + // relax it to "rescue" stale nodes. Drift is measured by rendering the + // remote text with the node's own sample, and a node migrated from the + // published plugin has no usable sample: that plugin stored the argument + // NAME in `pluralParamValue` (its own bug, see interpolate.ts), which + // `numericCount` rejects, so the sample falls back to 1. A migrated + // layer reading "10 days" therefore renders as "1 day", looks drifted, + // and re-applying would overwrite the design with the wrong plural form + // on every pull. Same for a parametric string with no stored params. + // + // Skip if `characters` is empty — that's not real drift, it just means + // the node hasn't been rendered yet (typical in tests / fresh syncs). + node.characters && + !remoteIsPlural && + !node.isPlural && + (!node.paramsValues || Object.keys(node.paramsValues).length === 0) && + canvasDrifted(node, remoteText, language) + ) { + changedNodes.push({ + node, + newText: remoteText, + isPlural: remoteIsPlural, + }); + } else { + unchangedNodes.push(node); + } + } + + return { changedNodes, missingKeys, unchangedNodes }; +} + +/** + * Renders the final string that will land in `TextNode.characters` for a + * single pulled translation, via the shared render core (`renderIcuForNode`). + * For a plural, the variable's sample COUNT comes from the node (so each layer + * keeps its own form — "1 woman" / "10 women"), the variable NAME from the ICU, + * and any other named param is seeded with its own name so nothing renders as a + * literal `{brace}`. + * + * On a formatting error, keeps the node's CURRENT canvas text (never dumps raw + * ICU onto the design) and returns the captured `Error` so the caller can warn + * — matching the original pull behaviour. + */ +export function formatNodeText( + node: NodeInfo, + remoteText: string, + language: string, +): { text: string; error?: Error } { + const out = renderIcuForNode(remoteText, node, language); + return out.error ? { text: node.characters, error: out.error } : out; +} + +/** One node's payload for the `apply-translations` message. */ +export type ApplyUpdate = { + id: string; + text: string; + translation: string; + isPlural: boolean; +}; + +/** + * Split a pull diff into the updates that can actually be written and the + * nodes whose ICU wouldn't render. + * + * A failed render must NOT travel on: `formatNodeText` falls back to the + * node's CURRENT canvas text (deliberately — raw ICU never gets dumped onto + * the design), so sending it would rewrite the text that is already there + * while persisting `translation` as the new remote value. The node then looks + * up to date FOREVER — the next diff finds the cached translation matching the + * remote, and `canvasDrifted` reports no drift because it trips over the very + * same render error — so the copy can never retry it, and the download claims + * a success it did not deliver. + * + * Callers are expected to surface `skipped`; silently dropping those nodes + * would just be the old bug with fewer writes. + */ +export function buildApplyUpdates( + changedNodes: PullDiffResult["changedNodes"], + language: string, +): { updates: ApplyUpdate[]; skipped: Array<{ node: NodeInfo; error: Error }> } { + const updates: ApplyUpdate[] = []; + const skipped: Array<{ node: NodeInfo; error: Error }> = []; + + for (const { node, newText, isPlural } of changedNodes) { + const { text, error } = formatNodeText(node, newText, language); + if (error) { + skipped.push({ node, error }); + continue; + } + updates.push({ id: node.id, text, translation: newText, isPlural }); + } + + return { updates, skipped }; +} + +/** User-facing summary of the nodes a render failure kept out of the write. */ +export function skippedRenderMessage( + skipped: Array<{ node: NodeInfo; error: Error }>, +): string { + const noun = skipped.length === 1 ? "string" : "strings"; + const names = skipped + .slice(0, 3) + .map((s) => s.node.key || s.node.name) + .join(", "); + const more = skipped.length > 3 ? `, +${skipped.length - 3} more` : ""; + return `${skipped.length} ${noun} could not be rendered and ${ + skipped.length === 1 ? "was" : "were" + } left unchanged (${names}${more}).`; +} diff --git a/src/ui/lib/logic/pushDiff.ts b/src/ui/lib/logic/pushDiff.ts new file mode 100644 index 00000000..4bfa7157 --- /dev/null +++ b/src/ui/lib/logic/pushDiff.ts @@ -0,0 +1,419 @@ +import type { NodeInfo } from "$shared/types"; +import IntlMessageFormat from "intl-messageformat"; +import { isAdvancedString } from "./manualChange"; +// Local mini-implementation of `getTolgeeFormat` from `@tginternal/editor`. +// The upstream package transitively pulls in `@codemirror/{state,view}` +// (~500 kB raw / ~125 kB gzip) — see `$shared/tolgeeFormat.ts` for the rationale. +import { getTolgeeFormat, tolgeeFormatGenerateIcu } from "$shared/tolgeeFormat"; +import { nsKeyIndex } from "$ui/lib/logic/namespaces"; + +/** + * Subset of `KeyWithTranslationsModel` we depend on for diffing. Kept narrow + * so callers can build it from any shape (search results, paged translations + * response, cached map, ...) without coupling to the generated schema. + */ +export type RemoteTranslation = { + /** Translation text as stored in Tolgee (may be ICU / plural source). */ + translation: string | undefined; + /** Whether the key is marked as plural on the server. */ + keyIsPlural: boolean; + /** Tag names attached to the key on the server. */ + keyTags: string[]; +}; + +/** + * Map of `namespace -> keyName -> remote translation`. The empty string is + * used as the namespace lookup key for the default namespace. + */ +export type RemoteTranslationMap = Record>; + +export type PushDiffOptions = { + /** Whether the project has namespaces feature enabled. */ + hasNamespacesEnabled: boolean; + /** Tags configured by the user — used to flag key as "changed" when missing. */ + configuredTags?: string[]; +}; + +/** + * WHY a key counts as changed. Reported so the UI can say so: a key changed + * only by tags or by the plural flag renders as a text diff whose two halves + * are IDENTICAL, which reads as a bug. That ambiguity is not hypothetical — + * it was reported as "push always shows changes" with the cause guessed wrong, + * and cost a round of manual testing to tell correct behaviour from a defect. + */ +export type ChangeReason = "text" | "plural" | "tags"; + +export type ChangedKey = { + node: NodeInfo; + /** Existing translation text in Tolgee. */ + remoteText: string; + /** What made this key changed — see `ChangeReason`. */ + reason: ChangeReason; +}; + +export type ConflictingNodes = { + key: string; + /** Undefined when namespaces are disabled or the key sits in default ns. */ + ns?: string; + /** All Figma nodes that share the same `(key, ns)` pair. */ + nodes: NodeInfo[]; +}; + +export type PushDiff = { + newKeys: NodeInfo[]; + changedKeys: ChangedKey[]; + unchangedKeys: NodeInfo[]; + /** + * Connected keys that no longer exist in Tolgee (deleted on the platform). + * Deliberately NOT pushed — silently re-creating a key the user removed is + * surprising; they should reconnect or delete the layer instead. + */ + missingKeys: NodeInfo[]; + /** + * Groups of Figma nodes that share `(key, ns)` but disagree on the + * translation text — the deduplication picks the first one, so the user + * should be warned. + */ + conflictingNodes: ConflictingNodes[]; +}; + +/** + * Compute a diff between Figma `nodes` and the latest `remote` translations. + * + * Pure function — no API calls, no side effects. The caller is responsible for + * collecting the remote map (typically via `GET /v2/projects/translations`) + * and the screenshot map (handled separately during push). + * + * The function deduplicates Figma nodes that share a `(key, ns)` pair. When + * the duplicates disagree on the displayed text, they are reported in + * `conflictingNodes` so the UI can warn the user before pushing. + */ +export function pushDiff( + nodes: NodeInfo[], + remote: RemoteTranslationMap, + options: PushDiffOptions, +): PushDiff { + const { hasNamespacesEnabled, configuredTags } = options; + + // Group nodes by `(key, ns)` so we can detect duplicate-but-different + // entries (a common source of confusion when several Figma layers reuse + // the same Tolgee key but were translated inconsistently in Figma). + const groups = new Map(); + for (const node of nodes) { + if (!node.key) continue; + const nsKey = hasNamespacesEnabled ? (node.ns ?? "") : ""; + const groupKey = nsKeyIndex(nsKey, node.key); + let list = groups.get(groupKey); + if (!list) { + list = []; + groups.set(groupKey, list); + } + list.push(node); + } + + const newKeys: NodeInfo[] = []; + const changedKeys: ChangedKey[] = []; + const unchangedKeys: NodeInfo[] = []; + const missingKeys: NodeInfo[] = []; + const conflictingNodes: ConflictingNodes[] = []; + + for (const [, group] of groups) { + const first = group[0]; + if (!first) continue; + + // Flag any group whose text differs across duplicates. + if (group.length > 1) { + const firstText = conflictText(first); + const hasMismatch = group.some((n) => conflictText(n) !== firstText); + if (hasMismatch) { + conflictingNodes.push({ + key: first.key, + ns: hasNamespacesEnabled ? first.ns : undefined, + nodes: group.slice(), + }); + } + } + + // PLURALS ONLY: when several layers hold one plural key, each may carry a + // different FORM (`one {# apple}` vs `other {# apples}`) because it was + // pluralized from its own sample. Assemble every form into ONE complete ICU + // so the push sends the whole plural, not just the first layer's partial + // one. Gated hard on the WHOLE group being plural, so plain / param / markup + // groups are untouched. The representative keeps `first`'s id — only its + // `translation` changes — so id-keyed bookkeeping (screenshots, unchanged + // set) is unaffected. + let representative = first; + if (group.length > 1 && group.every(isPluralLayer)) { + const merged = mergePluralForms(group); + if (merged) representative = { ...first, translation: merged }; + } + + const nsLookup = hasNamespacesEnabled ? (first.ns ?? "") : ""; + const remoteEntry = remote[nsLookup]?.[first.key]; + + if (!remoteEntry) { + // Absent remotely. If EVERY node on this key is connected, the key was + // deleted on the platform → skip it (don't re-create). If any node is + // unconnected, it's a genuinely new local key someone wants created, so + // don't drop it just because a stale connected node shares the key. + if (group.every((n) => n.connected)) missingKeys.push(representative); + else newKeys.push(representative); + continue; + } + + const reason = changeReason(representative, remoteEntry, configuredTags); + if (reason) { + changedKeys.push({ + node: representative, + remoteText: remoteEntry.translation ?? "", + reason, + }); + } else { + unchangedKeys.push(representative); + } + } + + return { newKeys, changedKeys, unchangedKeys, missingKeys, conflictingNodes }; +} + +/** + * Picks the text we consider authoritative for a Figma node, mirroring the + * legacy heuristic in `getPushChanges.ts`: + * + * - prefer the explicit Tolgee translation when present + * - otherwise fall back to the rendered `characters` value + */ +export function textOfNode(node: NodeInfo): string { + // PLAIN strings: the live `characters` ARE the value, and they reflect direct + // Figma-canvas edits that the stale stored `translation` would miss (so a + // connected string edited in Figma is correctly detected as changed + the new + // text is what gets pushed — matching the old plugin). ADVANCED strings: the + // stored `translation` is the authoritative ICU code (`characters` is only its + // rendered form, which must NOT be pushed as the value). + return isAdvancedString(node) + ? node.translation || node.characters || "" + : node.characters || node.translation || ""; +} + +/** + * Text used to decide whether two Figma layers that share one `(key, ns)` + * genuinely CONFLICT (and so only one could upload). + * + * For ADVANCED strings (plural / params / markup) the authoritative value is the + * shared Tolgee ICU in `translation` — the canvas `characters` is only a derived + * render, so two forms of one plural ("1 woman" / "10 women") must NOT count as + * a conflict. When the ICU is absent (e.g. not pulled yet) there is no + * distinguishing value, so such layers simply don't conflict. + * + * PLAIN strings conflict on their canvas `characters` — two different Figma + * texts mapped to one key ("brown 111" vs "brown") is a real conflict. + * + * Distinct from `textOfNode` (which keeps a `characters` fallback for detecting + * plain canvas edits); this one deliberately does NOT fall back for advanced + * strings. Shared by the Push diff and the Index warning banner so they agree. + */ +// A plural ICU, detected from the string itself (`{x, plural, …}`) — robust to a +// stale `isPlural` flag (see the plural render notes in interpolate.ts). +const PLURAL_ICU_RE = /,\s*plural\s*,/i; + +export function conflictText(node: NodeInfo): string { + // PLURAL: the forms (one / other / …) legitimately vary across layers of one + // key, and pluralizing each layer independently can even yield DIFFERENT + // partial ICUs ("1 apple" → `one {# apple}`, "2 apples" → `other {# apples}`). + // They are still the SAME plural on the SAME key, so they must NEVER count as a + // conflict — collapse every plural layer to one form-agnostic marker. (Was + // comparing the full ICU, which wrongly flagged those partial-ICU variants.) + if (node.isPlural || PLURAL_ICU_RE.test(node.translation ?? "")) { + return "\0plural"; + } + // Other ADVANCED (params / markup): the ICU is authoritative — two different + // param strings on one key DO conflict; no `characters` fallback (a render is + // derived). PLAIN: same as `textOfNode`. + return isAdvancedString(node) + ? (node.translation ?? "") + : node.characters || node.translation || ""; +} + +/** A layer participating in a plural key — same detection `conflictText` uses. */ +function isPluralLayer(node: NodeInfo): boolean { + return node.isPlural || PLURAL_ICU_RE.test(node.translation ?? ""); +} + +/** + * Ids of the same-key conflict LOSERS — every layer of a conflict group except + * the first, which is the one that actually uploads/connects. Push excludes + * these from BOTH connect-back and screenshot capture, so a layer that lost its + * key isn't marked connected, nor boxed on that key's uploaded screenshot. + */ +export function droppedConflictNodeIds(diff: PushDiff): Set { + return new Set(diff.conflictingNodes.flatMap((g) => g.nodes.slice(1).map((n) => n.id))); +} + +/** + * Merge the plural FORMS of several layers on one key into a single complete + * ICU. Each layer, pluralized from its own sample, may hold only the form its + * count selects (`one {# apple}` / `other {# apples}`); this unions them so the + * push carries every form instead of just the first layer's. The first NON-empty + * body per category wins (so the empty `other {}` placeholder partial generation + * appends never shadows a real `other`). Returns null when NO layer parses as a + * plural — the caller only reaches it for all-plural groups, but this keeps it + * inert for anything else. + */ +function mergePluralForms(nodes: NodeInfo[]): string | null { + const variants: Record = {}; + let parameter: string | undefined; + for (const n of nodes) { + const fmt = getTolgeeFormat(n.translation ?? "", true, false); + if (fmt.parameter === undefined) continue; // not a parseable plural ICU + parameter ??= fmt.parameter; + for (const [category, body] of Object.entries(fmt.variants)) { + if (body && variants[category] === undefined) variants[category] = body; + } + } + if (parameter === undefined || Object.keys(variants).length === 0) return null; + return tolgeeFormatGenerateIcu({ parameter, variants }, false); +} + +/** + * Why the Figma node differs from the remote translation in a way that + * warrants a push — text (after ICU normalization), plural flag flipped, or a + * configured `tagFiltered` tag missing. `null` when nothing differs. + * + * Returns the REASON rather than a boolean so the diff can be explained: the + * two non-text reasons produce a row whose before and after are the same + * string, and without a label there is no way to tell that from a broken diff. + */ +function changeReason( + node: NodeInfo, + remote: RemoteTranslation, + configuredTags: string[] | undefined, +): ChangeReason | null { + const text = textOfNode(node); + const remoteText = remote.translation ?? ""; + + // Use `getTolgeeFormat` so equivalent ICU representations (e.g. extra + // whitespace inside an ICU placeholder) don't show up as fake diffs. + let remoteNormalized: unknown; + let localNormalized: unknown; + try { + remoteNormalized = getTolgeeFormat(remoteText, remote.keyIsPlural, false); + localNormalized = getTolgeeFormat(text, node.isPlural, false); + } catch { + // If normalization throws (malformed ICU on either side) fall back to + // a plain string compare — better to report a spurious change than to + // silently drop a real one. + remoteNormalized = remoteText; + localNormalized = text; + } + + if (JSON.stringify(remoteNormalized) !== JSON.stringify(localNormalized)) { + // For plural keys, Tolgee normalises the ICU on its side and may extract + // a shared suffix out of the plural variants. Both forms render to the + // same output for every plural case, so a string-level mismatch isn't a + // real change. Fall back to rendering each case and comparing — only + // when both sides actually differ in the produced output we flag it. + if ((node.isPlural || remote.keyIsPlural) && pluralRendersAreEqual(text, remoteText)) { + // fall through to other checks (plural-flag, tags) below + } else { + return "text"; + } + } + + if (remote.keyIsPlural !== node.isPlural) return "plural"; + + if (configuredTags?.length) { + const remoteTags = new Set(remote.keyTags); + if (configuredTags.some((t) => !remoteTags.has(t))) { + return "tags"; + } + } + + return null; +} + +/** + * Convenience helper: turn a `KeysWithTranslationsPageModel` response into the + * shape `pushDiff` consumes. + * + * Accepts a loose shape so it can be fed either the typed openapi response or + * a plain object built in tests. Translation key is the requested language + * tag (the API only ever returns one entry per key when a single language is + * requested). + */ +// TODO: tighten when schema refined — this currently accepts the raw paged +// response shape and assumes a single language was requested. +export function buildRemoteMapFromKeys( + keys: + | Array<{ + keyName: string; + keyNamespace?: string; + keyIsPlural?: boolean; + keyTags?: Array<{ name: string }>; + translations?: Record; + }> + | undefined, + language: string, +): RemoteTranslationMap { + const out: RemoteTranslationMap = {}; + if (!keys) return out; + for (const k of keys) { + const ns = k.keyNamespace ?? ""; + if (!out[ns]) out[ns] = {}; + const bucket = out[ns]; + bucket[k.keyName] = { + translation: k.translations?.[language]?.text, + keyIsPlural: Boolean(k.keyIsPlural), + keyTags: (k.keyTags ?? []).map((t) => t.name), + }; + } + return out; +} + +/** + * Render two ICU strings against a fixed set of plural-trigger values and + * compare the outputs. Two ICU forms that produce the same rendered output + * for every probed value are functionally identical even if their source + * strings differ. Used to suppress spurious "changed" diffs when Tolgee + * re-emits a plural with a different (but equivalent) ICU shape — most + * commonly when Tolgee extracts a shared suffix out of every variant. + * + * Tries every parameter name the local string declares; if neither side + * declares a name we fall back to the conventional `count`. + */ +function pluralRendersAreEqual(local: string, remote: string): boolean { + if (local === remote) return true; + const params = new Set(); + extractParam(local, params); + extractParam(remote, params); + if (params.size === 0) params.add("count"); + + const probes = [0, 1, 2, 5, 11, 100]; + try { + for (const param of params) { + const lhs = new IntlMessageFormat(local, "en"); + const rhs = new IntlMessageFormat(remote, "en"); + for (const n of probes) { + if (lhs.format({ [param]: n }) !== rhs.format({ [param]: n })) { + return false; + } + } + } + return true; + } catch { + // Malformed ICU on either side — let the normal string compare decide. + return false; + } +} + +/** + * Extract the plural parameter name from an ICU source. Looks for the first + * `{name, plural, ...}` occurrence at any nesting level. + */ +function extractParam(input: string, out: Set): void { + const re = /\{\s*([A-Za-z_][\w]*)\s*,\s*plural\s*,/g; + let match = re.exec(input); + while (match !== null) { + if (match[1]) out.add(match[1]); + match = re.exec(input); + } +} diff --git a/src/ui/lib/logic/pushFlow.ts b/src/ui/lib/logic/pushFlow.ts new file mode 100644 index 00000000..eaba20db --- /dev/null +++ b/src/ui/lib/logic/pushFlow.ts @@ -0,0 +1,362 @@ +import type { FrameScreenshot, NodeInfo } from "$shared/types"; +import type { TolgeeClient } from "$ui/lib/api/client"; +import { effectiveNs } from "$ui/lib/api/keyExistence"; +import { fetchRemoteKeys } from "$ui/lib/api/keysByName"; +import { + type PushKeysResult, + type RelatedKeyDto, + type SimpleImportConflictResult, + type SingleStepImportResolvableItemRequest, + pushKeys, + storeBigMeta, +} from "$ui/lib/api/push"; +import type { components } from "$ui/lib/api/schema.generated"; +import { uploadScreenshot } from "$ui/lib/api/screenshots"; +import { applyTags } from "$ui/lib/api/tags"; +import { type PushDiff, droppedConflictNodeIds, textOfNode } from "./pushDiff"; +import { nsKeyIndex } from "$ui/lib/logic/namespaces"; + +/** + * How the user resolved a translation conflict. Lives here (not in the + * `.svelte` view) so plain TS modules can reference it without depending on + * Svelte's compiler magic for type re-exports. + */ +export type PushConflictResolution = "OVERRIDE" | "KEEP" | "FORCE_OVERRIDE"; + +type KeyScreenshotDto = components["schemas"]["KeyScreenshotDto"]; + +export type PushContext = { + client: TolgeeClient; + apiUrl: string; + apiKey: string; + language: string; + branch?: string; + hasNamespacesEnabled: boolean; +}; + +export type CanonicalKeyState = { + translation: string; + isPlural: boolean; +}; + +/** + * Map key for a (namespace, key) pair, normalised through `effectiveNs`. + * + * When the project has namespaces DISABLED the whole push pipeline drops a + * node's stored `ns` (see `buildPayload` / `pushDiff` / `keyExistence`), so the + * key lands in — and the server reports it back from — the DEFAULT namespace. + * A node can still carry a stale, invisible `ns` (the badge is hidden with the + * feature off). Without this normalisation such a node keys as `"web|greeting"` + * while the server's canonical row / conflict keys as `"|greeting"`, so the + * post-push connect-back and conflict resolution silently miss it — phantom + * "manual change" diffs and no-op OVERRIDEs. With the feature ON, `effectiveNs` + * is a passthrough, so behaviour there is unchanged. + */ +export function canonicalKey(n: NodeInfo, hasNamespacesEnabled: boolean): string { + return nsKeyIndex(effectiveNs(n.ns, hasNamespacesEnabled), n.key); +} + +export function resolutionKey( + keyName: string, + keyNamespace: string | undefined, + hasNamespacesEnabled: boolean, +): string { + return nsKeyIndex(effectiveNs(keyNamespace, hasNamespacesEnabled), keyName); +} + +/** + * After a successful push, pull the same keys back so we can persist the + * exact translation Tolgee owns plus the canonical plural flag. The next + * diff then compares like-for-like — without this, Tolgee's own canonical + * rewrites (e.g. shared suffix extraction across plural variants) show as + * phantom diffs forever. + */ +export async function fetchCanonicalAfterPush( + ctx: PushContext, + nodes: NodeInfo[], +): Promise> { + const result = new Map(); + const keysToFetch = new Set(nodes.map((n) => n.key).filter(Boolean)); + if (keysToFetch.size === 0) return result; + + const filterNamespace = ctx.hasNamespacesEnabled + ? Array.from(new Set(nodes.map((n) => n.ns ?? ""))) + : undefined; + + const fetched = await fetchRemoteKeys(ctx.client, { + filterKeyName: Array.from(keysToFetch), + filterNamespace, + language: ctx.language, + branch: ctx.branch, + }); + for (const k of fetched) { + const text = k.translations?.[ctx.language]?.text; + if (typeof text !== "string") continue; + // Key by the SAME normalised (ns, key) `buildConnectBackUpdates` looks up + // with — the server row's ns is already the effective one, but routing it + // through the shared helper keeps the two sides provably consistent. + result.set(resolutionKey(k.keyName, k.keyNamespace, ctx.hasNamespacesEnabled), { + translation: text, + isPlural: Boolean(k.keyIsPlural), + }); + } + return result; +} + +export type ConnectBackUpdate = { + id: string; + info: { connected: boolean; translation: string; isPlural: boolean }; +}; + +/** + * The `set-nodes-data` payload written after a successful push. Pure and fed + * ONLY from the snapshot captured when the user clicked Upload — never from + * live reactive state: the canvas selection can change while screenshots + * upload, and computing this from the then-current selection would mark + * never-pushed nodes as connected with a bogus translation baseline. + * + * Connects EVERY selected node that shares a pushed key — not just the + * per-key representative `pushDiff` kept. Without this, bulk-assigning one + * key to several identical strings would upload the key but leave all but + * the first node unconnected ("not all my keys uploaded"). Excluded: + * - dropped members of CONFLICTING groups (same key, different text) — only + * their first node was actually pushed; + * - missing keys (deleted on the platform) — intentionally NOT pushed, they + * need reconnecting/removing, so they must not be re-marked connected. + */ +export function buildConnectBackUpdates( + diff: PushDiff, + connectedNodes: NodeInfo[], + canonical: Map | null, + hasNamespacesEnabled: boolean, +): ConnectBackUpdate[] { + const droppedConflictIds = droppedConflictNodeIds(diff); + const missingIds = new Set(diff.missingKeys.map((n) => n.id)); + + return connectedNodes + .filter((n) => !droppedConflictIds.has(n.id) && !missingIds.has(n.id)) + .map((n) => { + const remote = canonical?.get(canonicalKey(n, hasNamespacesEnabled)); + return { + id: n.id, + info: { + connected: true, + translation: remote?.translation ?? n.translation ?? n.characters, + isPlural: remote?.isPlural ?? n.isPlural, + }, + }; + }); +} + +/** + * Build the `screenshots` array of the import payload for one local node. + * Each captured screenshot's `keys` may reference multiple Figma layers; we + * keep only those that match the (key, ns) we are pushing and record their + * positions. + */ +function mapScreenshotsForNode( + node: NodeInfo, + screenshots: FrameScreenshot[], + uploadedImageIdByScreenshot: Map, +): KeyScreenshotDto[] { + const out: KeyScreenshotDto[] = []; + for (const screenshot of screenshots) { + const positions = screenshot.keys + .filter((k) => k.key === node.key && (k.ns ?? "") === (node.ns ?? "")) + .map((k) => ({ x: k.x, y: k.y, width: k.width, height: k.height })); + if (positions.length === 0) continue; + const uploadedImageId = uploadedImageIdByScreenshot.get(screenshot); + if (uploadedImageId === undefined) continue; + out.push({ + text: textOfNode(node), + uploadedImageId, + positions, + }); + } + return out; +} + +export type BuildPayloadOptions = { + ctx: PushContext; + nodes: NodeInfo[]; + screenshots: FrameScreenshot[]; + uploadedImageIdByScreenshot: Map; + resolutionFor?: (key: string, ns: string | undefined) => PushConflictResolution | undefined; + /** Node ids whose translation is UNCHANGED vs the server. Their payload item + * carries screenshots but an empty `translations` — so the push never + * re-overrides an untouched (and possibly REVIEWED) translation. Matches the + * published plugin, which sends `translations: {}` for unchanged keys. */ + unchangedNodeIds?: Set; +}; + +export function buildPayload(opts: BuildPayloadOptions): SingleStepImportResolvableItemRequest[] { + const { ctx, nodes, screenshots, uploadedImageIdByScreenshot, resolutionFor } = opts; + + return nodes.map((node) => { + const resolution = resolutionFor?.(node.key, node.ns); + const text = textOfNode(node); + const screenshotsForNode = mapScreenshotsForNode( + node, + screenshots, + uploadedImageIdByScreenshot, + ); + + // `KEEP` (user chose to keep the server value on a conflict) OR an UNCHANGED + // key -> omit translations (only updates screenshots/tags), never touching + // the stored translation. + const isUnchanged = opts.unchangedNodeIds?.has(node.id) ?? false; + const translations = + resolution === "KEEP" || isUnchanged + ? {} + : { + [ctx.language]: { + text, + resolution: "OVERRIDE" as const, + }, + }; + + return { + name: node.key, + namespace: ctx.hasNamespacesEnabled ? node.ns || undefined : undefined, + screenshots: screenshotsForNode, + translations, + } satisfies SingleStepImportResolvableItemRequest; + }); +} + +export type ProgressEvent = { + current: number; + total: number; + message: string; +}; + +/** + * Upload a batch of frame screenshots sequentially. Sequential is the right + * choice here: each request is large (multipart PNG) and Tolgee throttles + * parallel uploads; the legacy plugin chained them too. + */ +export async function uploadScreenshots( + ctx: PushContext, + screenshots: FrameScreenshot[], + onProgress: (event: ProgressEvent) => void, +): Promise> { + const uploadedById = new Map(); + onProgress({ + current: 0, + total: screenshots.length, + message: "Uploading screenshots…", + }); + for (let i = 0; i < screenshots.length; i++) { + const screenshot = screenshots[i]; + if (!screenshot) continue; + const uploaded = await uploadScreenshot(ctx.apiUrl, ctx.apiKey, screenshot.image, { + location: `figma-${screenshot.info.id}`, + }); + uploadedById.set(screenshot, uploaded.id); + onProgress({ + current: i + 1, + total: screenshots.length, + message: "Uploading screenshots…", + }); + } + return uploadedById; +} + +export type SubmitPushOptions = { + ctx: PushContext; + nodes: NodeInfo[]; + screenshots: FrameScreenshot[]; + uploadedImageIdByScreenshot: Map; + resolutionMode: "RECOMMENDED" | "FORCE_OVERRIDE"; + resolutionFor?: BuildPayloadOptions["resolutionFor"]; + unchangedNodeIds?: Set; +}; + +export async function submitPush(opts: SubmitPushOptions): Promise { + const payload = buildPayload({ + ctx: opts.ctx, + nodes: opts.nodes, + screenshots: opts.screenshots, + uploadedImageIdByScreenshot: opts.uploadedImageIdByScreenshot, + resolutionFor: opts.resolutionFor, + unchangedNodeIds: opts.unchangedNodeIds, + }); + return pushKeys(opts.ctx.client, payload, { + branch: opts.ctx.branch || undefined, + resolutionMode: opts.resolutionMode, + errorOnUnresolvedConflict: false, + }); +} + +/** The `relatedKeysInOrder` for one screenshot: the keys it contains, in the + * order captured, capped at 100 (matching the original plugin). The branch is + * set per key on `RelatedKeyDto.branch` — the current big-meta API's mechanism, + * which replaced the old `?branch=` query param (see `storeBigMeta`). */ +export function buildRelatedKeys( + ctx: PushContext, + screenshot: FrameScreenshot, +): RelatedKeyDto[] { + return screenshot.keys + .filter((k) => k.key) + .map((k) => ({ + keyName: k.key, + namespace: ctx.hasNamespacesEnabled ? k.ns || undefined : undefined, + branch: ctx.branch || undefined, + })) + .slice(0, 100); +} + +/** + * Registers screenshot key-context ("related keys in order") with Tolgee via + * `POST /v2/projects/big-meta`, once per screenshot — feeding Tolgee's + * in-context translation suggestions, like the original plugin. Best-effort: + * the translations are already saved, so a failure here is swallowed and never + * fails the push. Runs the (lightweight, image-free) calls concurrently. + */ +export async function submitBigMeta( + ctx: PushContext, + screenshots: FrameScreenshot[], +): Promise { + await Promise.allSettled( + screenshots + .map((s) => buildRelatedKeys(ctx, s)) + .filter((keys) => keys.length > 0) + .map((keys) => storeBigMeta(ctx.client, keys)), + ); +} + +export type ApplyTagsOptions = { + ctx: PushContext; + tags: string[]; + nodes: NodeInfo[]; +}; + +export async function applyConfiguredTags(opts: ApplyTagsOptions): Promise { + if (opts.tags.length === 0) return; + await applyTags( + opts.ctx.client, + opts.tags, + opts.nodes.map((n) => ({ + name: n.key, + namespace: opts.ctx.hasNamespacesEnabled ? n.ns || undefined : undefined, + })), + opts.ctx.branch || undefined, + ); +} + +/** + * Build the default conflict-resolution map (override when allowed, keep + * otherwise). The caller mutates this map as the user picks alternatives. + */ +export function defaultResolutions( + list: SimpleImportConflictResult[], + hasNamespacesEnabled: boolean, +): Record { + const next: Record = {}; + for (const c of list) { + next[resolutionKey(c.keyName, c.keyNamespace, hasNamespacesEnabled)] = c.isOverridable + ? "OVERRIDE" + : "KEEP"; + } + return next; +} diff --git a/src/ui/lib/logic/queryResult.ts b/src/ui/lib/logic/queryResult.ts new file mode 100644 index 00000000..b5fb397a --- /dev/null +++ b/src/ui/lib/logic/queryResult.ts @@ -0,0 +1,64 @@ +/** + * A query outcome that carries its own failure INSTEAD of rejecting. + * + * The two data-loading queries whose failure the user must see — Push's diff + * and Pull's translations — cannot rely on TanStack's `.error`/terminal + * `.isPending` reactive fields: the `@tanstack/svelte-query` runes adapter we + * ship does not reliably propagate a query's terminal ERROR transition into + * Svelte's reactive graph, so a failed fetch left the loading spinner up + * forever (verified live: HTTP 500 and network throw both hung indefinitely). + * Its `.data` transition, by contrast, works — the happy path renders fine. + * + * So we funnel failures through `.data`: the `queryFn` never rejects, it + * resolves with `{ ok: false, error }`, and the view derives its error state + * from that value. This is independent of the adapter version, so a future + * dependency bump (or regression) can't silently resurrect the stuck spinner. + * + * Shape mirrors the existing `ValidateApiKeyResult` discriminated union in + * `api/auth.ts` for consistency. + */ +export type QueryOutcome = { ok: true; value: T } | { ok: false; error: string }; + +export type SettleOptions = { + /** + * The query's abort signal, when it has one. A CANCELLED query (query-key + * change, navigation away) must NOT surface as an `{ ok: false }` result — + * that would cache a bogus error under the old key. When the signal is + * aborted we re-throw so TanStack handles the cancellation normally. + */ + signal?: AbortSignal; + /** Turn a caught failure into the message shown to the user. */ + toMessage: (error: unknown) => string; + /** + * Extra attempts after the first on a GENUINE failure (not a cancellation), + * preserving the single automatic retry the query client used to provide via + * `retry: 1` — which no longer fires now that `queryFn` doesn't throw for API + * errors. Retries are immediate; a transient blip usually clears at once, and + * a persistent failure just costs one extra request before surfacing. + */ + retries?: number; +}; + +/** + * Run `run`, converting any genuine failure into an `{ ok: false }` outcome + * rather than rejecting — while re-throwing on cancellation so TanStack's + * abort handling is untouched. See `QueryOutcome` for why. + */ +export async function settleQuery( + run: () => Promise, + options: SettleOptions, +): Promise> { + const attempts = (options.retries ?? 1) + 1; + let lastError: unknown; + for (let attempt = 0; attempt < attempts; attempt++) { + try { + return { ok: true, value: await run() }; + } catch (error) { + // A cancelled query is not a failure to report — let TanStack see the + // throw and treat it as cancellation (no error cached, no retry). + if (options.signal?.aborted) throw error; + lastError = error; + } + } + return { ok: false, error: options.toMessage(lastError) }; +} diff --git a/src/ui/lib/logic/sanitizeIcuPreview.ts b/src/ui/lib/logic/sanitizeIcuPreview.ts new file mode 100644 index 00000000..35155c49 --- /dev/null +++ b/src/ui/lib/logic/sanitizeIcuPreview.ts @@ -0,0 +1,58 @@ +/** + * Sanitizes ICU preview HTML before it's rendered with `{@html ...}` in + * `IcuPreview.svelte`. + * + * `formatIcuMessage()` (see `$shared/icu.ts`) formats an ICU message with + * `ignoreTag: true`, which means any inline markup in the translation + * (``, ``, anything) survives into `result` as + * completely unescaped literal text — `intl-messageformat` never parses or + * strips it. That string comes straight from the connected Tolgee project + * (any translator with project access can write it), so rendering it with + * `{@html ...}` without sanitizing first is a stored-XSS hole: a malicious + * translation executes arbitrary script inside the plugin's UI iframe, which + * holds the live Tolgee API key and a postMessage channel that can mutate + * the Figma document. + * + * The fix: escape the ENTIRE string first, then selectively un-escape only + * the exact whitelisted formatting tags (bare, no attributes). This mirrors + * the same whitelist `IcuTextarea.svelte`'s tokenizer already highlights + * (`INLINE_TAG_RE` in `tolgeeFormat.ts`), so what the preview renders as a + * real tag lines up with what the textarea highlights as a tag pill. + * + * Any tag with an attribute (``), any non-whitelisted tag + * (` + +{#if !node} +
    No node selected.
    +{:else} +
    + + +
    + +
    + + {#if query.trim() && !loading} +

    + {#if totalKeys !== null} + {results.length} of {totalKeys} + {totalKeys === 1 ? "key" : "keys"} in Tolgee + {:else} + {results.length} {results.length === 1 ? "key" : "keys"} + {/if} +

    + {/if} +
    + + {#snippet resultRow(r: KeySearchResult)} + {@const connected = isConnectedKey(r)} +
  • +
    + +
    + + {r.translation ?? r.baseTranslation ?? r.name} + + {#if r.plural} + Plural + {/if} +
    +
    + + + {r.name} + + {#if auth.value.namespacesEnabled} + + ns:{r.namespace || ""} + {/if} +
    +
    + {#if connected} + + {:else} + + + {/if} +
  • + {/snippet} + + {#if results.length > 0} + +
    + {#if exactMatches.length > 0} +
    + Exact match +
    +
      + {#each exactMatches as r (r.id)}{@render resultRow(r)}{/each} +
    + {/if} + {#if otherMatches.length > 0} + {#if exactMatches.length > 0} +
    + Other suggestions +
    + {/if} +
      + {#each otherMatches as r (r.id)}{@render resultRow(r)}{/each} +
    + {/if} +
    + {:else if loading} + + {:else if query.trim()} + + + {:else} + + {/if} +
    +
    +{/if} diff --git a/src/ui/lib/routes/CopyView.svelte b/src/ui/lib/routes/CopyView.svelte new file mode 100644 index 00000000..084edccf --- /dev/null +++ b/src/ui/lib/routes/CopyView.svelte @@ -0,0 +1,735 @@ + + +
    + +
    +

    + {appState.value.pageName} (copy) + {#if !language} + + + + {/if} +

    + {#if branch} + + + + {#snippet child({ props })} + + + + {/snippet} + + + Copies follow the branch set in Settings. + + + + {/if} + {/snippet} + + +
    + {#if stage === "idle"} + + +
    + {#each [{ value: "keys", text: "Create page with key names" }, { value: "languages", text: "Create page per language" }] as option (option.value)} + + {/each} +
    +
    + + {#if mode === "languages"} + + + {#if !languagesLoaded} +

    + Loading languages… +

    + {:else if availableLanguages.length === 0} +

    + No languages available. +

    + {:else} +
    + {#each availableLanguages as lang (lang.id)} + toggleLang(lang.tag)} + /> + {/each} +
    + {/if} +
    + {/if} + {:else if stage === "fetching"} + + + + {:else if stage === "creating"} + + 0 ? progress.total : null} + label="Creating copy…" + /> + + {:else if stage === "error"} + {errorMsg ?? "Something went wrong."} + {/if} +
    + + +
    diff --git a/src/ui/lib/routes/Index.svelte b/src/ui/lib/routes/Index.svelte new file mode 100644 index 00000000..b342a413 --- /dev/null +++ b/src/ui/lib/routes/Index.svelte @@ -0,0 +1,1609 @@ + + + +
    + + {#snippet masterCheckbox()} + + {/snippet} + + + {#snippet radioItem( + active: boolean, + label: string, + count: number | null, + onSelect: () => void, + )} + + + {#if active} + + {/if} + + {label} + {#if count !== null} + {count} + {/if} + + {/snippet} + + +
    +
    + + {#if writeProgress !== null} + +
    + +
    + {/if} + + {#if branchMissing} + +
    + + Branch "{configuredBranch}" no longer exists. Please select a + different branch. + +
    + e.key === "Enter" && bulkSetKeyName()} + /> + + {:else if generateMode} + + (templateInput = v)} + onSubmit={bulkGenerateApply} + placeholder={"{elementName}"} + class="flex-1" + /> + + {:else if setNamespaceMode} + + (bulkNamespace = v)} + class="flex-1" + /> + + {:else if connectExactMode} + + +
    + +
    diff --git a/src/ui/lib/routes/Pull.svelte b/src/ui/lib/routes/Pull.svelte new file mode 100644 index 00000000..489b100e --- /dev/null +++ b/src/ui/lib/routes/Pull.svelte @@ -0,0 +1,478 @@ + + +
    + + +
    + {#if stage === "loading"} + {#if pageNodesQuery.isPending} + + {:else} + + {/if} + {:else if stage === "error"} +
    + {errorMessage ?? "Something went wrong."} +
    + + {:else if stage === "applying"} + + {:else if stage === "diff"} + {#if diff} + + + {#if diff.changedNodes.length === 0} +

    Everything is up to date.

    + {:else} +
    +
    + Changes to apply +
    +
      + {#each visibleChanged as change (change.node.id)} +
    • +
      + {formatKeyLabel(change.node.ns, change.node.key)} +
      +
      + {change.node.translation || change.node.characters || "—"} +
      +
      + {change.newText} +
      +
    • + {/each} +
    + {#if hiddenChangedCount > 0} +
    + + {hiddenChangedCount} more +
    + {/if} +
    + {/if} + + {#if diff.missingKeys.length > 0} +
    +
    + Missing keys (not in Tolgee) +
    +
      + {#each visibleMissing as node (node.id)} +
    • + {formatKeyLabel(node.ns, node.key)} +
    • + {/each} +
    + {#if hiddenMissingCount > 0} +
    + + {hiddenMissingCount} more +
    + {/if} +
    + {/if} + {/if} + {/if} +
    + + + + {#if stage === "diff" && diff && diff.changedNodes.length > 0} + + {:else if stage === "diff" && diff && diff.changedNodes.length === 0} + + {:else} + + {/if} + +
    diff --git a/src/ui/lib/routes/Push.svelte b/src/ui/lib/routes/Push.svelte new file mode 100644 index 00000000..9e5a6a2b --- /dev/null +++ b/src/ui/lib/routes/Push.svelte @@ -0,0 +1,887 @@ + + +
    + + +
    + {#if connectedNodes.length === 0} + + No connected strings to upload. + {:else if diffQuery.isPending} + + + + {:else if diffLoadError} + {diffLoadError} + {:else if stage === "error"} + {errorMessage ?? "An error occurred."} + {:else if stage === "done"} + {doneSummary} + {:else if stage === "uploading" || stage === "pushing"} +
    + +
    + +
    +
    + {:else if stage === "conflict"} + +
    + + + {conflicts.length} unresolved conflict(s) + +
    +

    + Pick a resolution for each conflict and re-submit. +

    +
    + {#each conflicts as conflict (conflict.keyName + (conflict.keyNamespace ?? "") + conflict.language)} + + {/each} +
    +
    + {:else if diff} + {#if diff.conflictingNodes.length > 0} + +
    + {diff.conflictingNodes.length} key(s) reuse the same name with different + text in Figma. +
    +

    + Only the first occurrence will be pushed for each. Update or + disconnect the duplicates to clear this warning. +

    +
      + {#each diff.conflictingNodes as group (group.key + (group.ns ?? ""))} +
    • + {group.key} + {#if hasNamespacesEnabled} + ns:{group.ns || ""} + {/if} + + ({group.nodes.length} nodes) + +
    • + {/each} +
    +
    + {/if} + + {#if diff.missingKeys.length > 0} + +
    +
    + {diff.missingKeys.length} connected key(s) no longer exist in Tolgee. +
    +

    + They were deleted on the platform, so they'll be skipped (not + re-created). Reconnect them to an existing key or remove the layer. +

    +
      + {#each diff.missingKeys as n (n.id)} +
    • + {n.key} + {#if hasNamespacesEnabled} + ns:{n.ns || ""} + {/if} +
    • + {/each} +
    +
    +
    + {/if} + + +
    + + 0 + ? () => scrollTo(newSection) + : undefined} + /> + 0 + ? () => scrollTo(changedSection) + : undefined} + /> + +
    +
    + + {#if hasAnyKeys} + +
    + (includeScreenshots = v)} + /> + {#if noTextChanges && includeScreenshots} +

    + No text changes — screenshots will still be uploaded for + {diff.unchangedKeys.length} + {diff.unchangedKeys.length === 1 ? "key" : "keys"}. +

    + {/if} +
    + {/if} + + {#if noTextChanges && !screenshotOnlyUpload} +

    + No changes to upload. +

    + {/if} + + {#if diff.newKeys.length > 0} +
    +
    + New ({diff.newKeys.length}) +
    +
      + {#each diff.newKeys as node (node.id)} +
    • +
      + {node.key} + {#if hasNamespacesEnabled} + ns:{node.ns || ""} + {/if} +
      +
      + {textOfNode(node)} +
      +
    • + {/each} +
    +
    + {/if} + + {#if diff.changedKeys.length > 0} +
    +
    + Changed ({diff.changedKeys.length}) +
    +
      + {#each diff.changedKeys as entry (entry.node.id)} +
    • +
      + + {entry.node.key} + + {#if hasNamespacesEnabled} + ns:{entry.node.ns || ""} + {/if} +
      + {#if entry.reason === "text"} +
      + {entry.remoteText} +
      +
      + {textOfNode(entry.node)} +
      + {:else} + +
      + {entry.reason === "tags" + ? "Text unchanged — the configured tag will be added." + : "Text unchanged — the plural setting will be updated."} +
      + {/if} +
    • + {/each} +
    +
    + {/if} + {/if} +
    + + + {#if stage === "conflict"} + + + {:else if stage === "done" || stage === "error"} + + {:else if stage === "idle" && diff} + + + {/if} + +
    diff --git a/src/ui/lib/routes/Settings.svelte b/src/ui/lib/routes/Settings.svelte new file mode 100644 index 00000000..abdfe50c --- /dev/null +++ b/src/ui/lib/routes/Settings.svelte @@ -0,0 +1,130 @@ + + +
    + + +
    + + + + Project + Strings and Keys + Upload options + +
    + +
    + +
    + + {#if auth.value.authenticated} + + + {/if} +
    +
    + +
    + + +
    +
    + + + +
    +
    + + + {#snippet leftContent()} + + + + + {#snippet child({ props })} + + {/snippet} + + + Tolgee for Figma · updated {APP_RELEASED} · open on Figma + + + + {/snippet} + +
    diff --git a/src/ui/lib/routes/StringDetails.svelte b/src/ui/lib/routes/StringDetails.svelte new file mode 100644 index 00000000..ca9d2452 --- /dev/null +++ b/src/ui/lib/routes/StringDetails.svelte @@ -0,0 +1,538 @@ + + +{#if !node} +
    No node selected.
    +{:else} +
    + + +
    + + {#if manualChange} + + +
    +

    + This string was edited in Figma and no longer matches its + formatting. Revert to restore it. +

    + +
    +
    + {:else if showAdvancedNotice} + + + Advanced text format detected. Edit the string via the plugin to + preserve formatting. + + {/if} + +
    + +
    + {#if node.ns && auth.value.namespacesEnabled} + + {node.ns}. + + {/if} + +
    +
    + + + { + isPlural = v; + // Toggling on: try to seed from a single >1 number in the text; + // otherwise fall back to the default preview value. + if (v && !makePluralFromNumber()) seedPluralValue(); + }} + disabled={node.connected} + > + {#snippet trailing()} + {#if node.connected} + + + {#snippet child({ props })} + + + + {/snippet} + + + You can't change this here. Change it in your Tolgee platform. + + + {/if} + {/snippet} + + +
    + + + + {#snippet child({ props })} + + + + {/snippet} + + +

    + You can use basic HTML tags such as + <strong>, <b>, + <em>, <i>, + <u>, <br> and also parameters + as {parameter} and # as plural + placeholder. +

    + +
    +
    +
    + + {#if isPlural} +
    + +
    + {:else} +
    + +
    + {/if} + + + {#if figmaParamCount > 0} +
    +
    + (paramsValues = v)} + /> +
    +
    + + +
    +
    + {:else} + + {/if} +
    +
    + + +
    +{/if} + + + { + if (!open) resolvePendingSwitch(true); + }} +> + + + Unsaved changes + +

    + You have unsaved changes for "{pendingSwitch?.label}". Save them? +

    +
    + + +
    +
    +
    diff --git a/src/ui/lib/stores/__tests__/app.svelte.test.ts b/src/ui/lib/stores/__tests__/app.svelte.test.ts new file mode 100644 index 00000000..aa369ed6 --- /dev/null +++ b/src/ui/lib/stores/__tests__/app.svelte.test.ts @@ -0,0 +1,100 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { NodeInfo } from "$shared/types"; +import { appState } from "$ui/lib/stores/app.svelte"; + +/** + * `appState` is a module-level singleton (mirrors how the real app imports + * it), so each test resets the one bit of state it exercises instead of + * re-creating the store. + */ +beforeEach(() => { + appState.clearWriteProgress(); + appState.setEditorType("figma"); + appState.navigate({ name: "index" }); +}); + +describe("appState write progress (nodes-set-progress / nodes-set-result glue)", () => { + it("starts out clear", () => { + expect(appState.value.writeProgress).toBeNull(); + }); + + it("setWriteProgress reflects an in-flight nodes-set-progress message", () => { + appState.setWriteProgress(50, 250); + expect(appState.value.writeProgress).toEqual({ done: 50, total: 250 }); + + // Later progress messages for the same write just replace the value. + appState.setWriteProgress(100, 250); + expect(appState.value.writeProgress).toEqual({ done: 100, total: 250 }); + }); + + it("clearWriteProgress resets to null once the matching nodes-set-result arrives", () => { + appState.setWriteProgress(250, 250); + expect(appState.value.writeProgress).not.toBeNull(); + + appState.clearWriteProgress(); + expect(appState.value.writeProgress).toBeNull(); + }); + + it("clearing when already null is a no-op (a small write with no progress messages still clears fine)", () => { + expect(appState.value.writeProgress).toBeNull(); + appState.clearWriteProgress(); + expect(appState.value.writeProgress).toBeNull(); + }); +}); + +describe("appState.navigate Dev-Mode gate (ROUTE_AVAILABILITY)", () => { + const node: NodeInfo = { + id: "1:1", + name: "Layer", + characters: "Hello", + translation: "Hello", + isPlural: false, + key: "greeting", + ns: undefined, + connected: true, + }; + + it("silently refuses a design-only route in dev, keeping the current route", () => { + appState.setEditorType("dev"); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + appState.navigate({ name: "stringDetails", node }); + expect(appState.value.route).toEqual({ name: "index" }); + + appState.navigate({ name: "createCopy" }); + expect(appState.value.route).toEqual({ name: "index" }); + + appState.navigate({ name: "pull", lang: "en" }); + expect(appState.value.route).toEqual({ name: "index" }); + + expect(warn).toHaveBeenCalledTimes(3); + warn.mockRestore(); + }); + + it("allows shared routes in dev (settings, push, connect, copyView)", () => { + appState.setEditorType("dev"); + + appState.navigate({ name: "settings" }); + expect(appState.value.route).toEqual({ name: "settings" }); + + appState.navigate({ name: "push" }); + expect(appState.value.route).toEqual({ name: "push" }); + + appState.navigate({ name: "connect", node }); + expect(appState.value.route.name).toBe("connect"); + + appState.navigate({ name: "copyView" }); + expect(appState.value.route).toEqual({ name: "copyView" }); + }); + + it("allows every route in the design editor", () => { + appState.navigate({ name: "stringDetails", node }); + expect(appState.value.route.name).toBe("stringDetails"); + + appState.navigate({ name: "createCopy" }); + expect(appState.value.route).toEqual({ name: "createCopy" }); + + appState.navigate({ name: "pull", lang: "en" }); + expect(appState.value.route.name).toBe("pull"); + }); +}); diff --git a/src/ui/lib/stores/app.svelte.ts b/src/ui/lib/stores/app.svelte.ts new file mode 100644 index 00000000..f68d7362 --- /dev/null +++ b/src/ui/lib/stores/app.svelte.ts @@ -0,0 +1,258 @@ +import type { NodeInfo, Route, TolgeeConfig } from "$shared/types"; +import { flushNodeSaves } from "$ui/lib/logic/saveQueue"; + +/** + * Which editor types may open each route — the navigation-side half of the + * Dev-Mode safety layer (the message-side half is `$shared/messagePolicy`). + * Exhaustive over `Route["name"]` so a NEW route can't ship unclassified: + * the compiler forces a decision here, mirroring `MESSAGE_IMPACT`. + * + * `design-only` routes exist to change the design (Pull rewrites canvas + * text, CreateCopy clones pages, StringDetails edits a string) — in Dev + * Mode `navigate()` refuses them as a SILENT no-op. Silent on purpose: + * clicking a row is a frequent reflex action in Dev Mode and a toast on + * every click would nag; the toast belongs to the message guard in + * `$main/bus.ts`, where being reached at all signals a real anomaly. + * + * `copyView` stays "all": it's the read-only viewer for copy pages, and + * both of its write actions (Download, Recreate) go through canvas-classed + * messages the bus guard already blocks. + */ +export const ROUTE_AVAILABILITY: Record = { + index: "all", + pageSetup: "all", + copyView: "all", + settings: "all", + push: "all", + connect: "all", + pull: "design-only", + stringDetails: "design-only", + createCopy: "design-only", +}; + +type AppState = { + config: Partial | null; + selectedNodes: NodeInfo[]; + /** + * `true` when the user has at least one node selected on the current + * Figma page. With no selection, `selectedNodes` is EMPTY (the main thread + * deliberately never falls back to a page-wide scan) — this flag exists so + * consumers can distinguish "nothing selected" from "selection contains no + * usable text nodes". + */ + hasUserSelection: boolean; + /** + * `true` when a selection scan on the main thread has been running longer + * than the spinner delay (see `setScanning`) and its nodes haven't arrived + * yet. Drives the Index loading overlay for genuinely slow scans only — + * quick scans (the overwhelming majority) never flip it, so ordinary canvas + * clicks don't flash the overlay. + */ + scanning: boolean; + route: Route; + editorType: "figma" | "dev"; + /** `figma.currentPage.name` — used directly in CopyView's header (the name + already carries the language, e.g. "Home - cs"), kept in sync on + `init`/`page-changed`. */ + pageName: string; + errorBanner: { message: string; severity: "error" | "warning" } | null; + /** + * Progress for ANY in-flight large `set-nodes-data` write — bulk actions in + * Index, auto-connect, and the save-queue's prefill/regen flush alike. + * Deliberately not paired to a single request's `correlationId` (see + * `nodes-set-progress` in `shared/messages.ts`): only one big write is ever + * meaningfully "in flight" from the user's point of view, and `null` means + * none is. Set on `nodes-set-progress`, cleared on `nodes-set-result`. + */ + writeProgress: { done: number; total: number } | null; +}; + +/** + * Reads the initial route from the E2E host page's URL state param. + * + * In E2E tests the host page and the plugin iframe share the same origin + * (localhost:4173), so `window.parent.location.search` is accessible and + * contains the ?state=… query the test fixtures encode. Reading it here — + * synchronously, before the first render — lets the app start in the correct + * view without a reactive navigate() call. + * + * In Figma the plugin iframe is sandboxed at a different origin; accessing + * window.parent.location throws a SecurityError that we swallow silently. + */ +function getInitialRoute(): Route { + try { + const params = new URLSearchParams(window.parent.location.search); + const raw = params.get("state"); + if (raw) { + const parsed = JSON.parse(raw) as { route?: string }; + if (parsed.route) return { name: parsed.route } as Route; + } + } catch { + // Cross-origin (Figma sandbox) or missing state — fall through. + } + return { name: "index" }; +} + +function createAppState() { + const state = $state>({ + config: null, + hasUserSelection: false, + scanning: false, + route: getInitialRoute(), + editorType: "figma", + pageName: "", + errorBanner: null, + writeProgress: null, + }); + // `$state.raw`: the nodes are immutable snapshots from the main thread and + // are only ever replaced wholesale (setSelection / patchNodes), so deep + // proxying hundreds of them — every field read of every row going through a + // proxy trap — is pure overhead. Reactivity comes from reassignment. + let selectedNodes = $state.raw([]); + + // Delayed scan spinner: most scans finish in well under this, and flashing + // the loading overlay for every canvas click reads as jank. Arm a timer on + // `selection-pending` and only flip `scanning` if the nodes haven't arrived + // by then — quick scans never show the overlay at all. + const SCAN_SPINNER_DELAY_MS = 200; + let scanSpinnerTimer: ReturnType | null = null; + + function cancelScanSpinner(): void { + if (scanSpinnerTimer) { + clearTimeout(scanSpinnerTimer); + scanSpinnerTimer = null; + } + } + + // Stable read view combining the reactive state with the raw node array, + // so consumers keep the `appState.value.selectedNodes` shape. + const view = { + get config() { + return state.config; + }, + get selectedNodes() { + return selectedNodes; + }, + get hasUserSelection() { + return state.hasUserSelection; + }, + get scanning() { + return state.scanning; + }, + get route() { + return state.route; + }, + get editorType() { + return state.editorType; + }, + get pageName() { + return state.pageName; + }, + get errorBanner() { + return state.errorBanner; + }, + get writeProgress() { + return state.writeProgress; + }, + }; + + return { + get value() { + return view; + }, + setConfig(c: Partial | null) { + state.config = c; + }, + setSelection(nodes: NodeInfo[], hasUserSelection: boolean) { + selectedNodes = nodes; + state.hasUserSelection = hasUserSelection; + cancelScanSpinner(); + state.scanning = false; + }, + /** + * One chunk of a streamed selection scan. The first batch REPLACES the + * list (and is the moment the user has real feedback, so it also clears + * the scan loader); later batches append. Streamed batches only exist + * for genuine selections, hence `hasUserSelection` flips on the first. + */ + appendSelection(nodes: NodeInfo[], first: boolean) { + selectedNodes = first ? nodes : [...selectedNodes, ...nodes]; + if (first) state.hasUserSelection = true; + cancelScanSpinner(); + state.scanning = false; + }, + /** + * Terminal marker of a streamed scan. A stream that delivered nothing + * (empty canvas click, or a selection whose texts are all ignored) + * must clear the previous list. + */ + finalizeSelection(hasUserSelection: boolean, total: number) { + if (total === 0) selectedNodes = []; + state.hasUserSelection = hasUserSelection; + cancelScanSpinner(); + state.scanning = false; + }, + /** + * Merge fresh post-write snapshots into the current selection by node id. + * Untouched rows keep their object identity, so the keyed `{#each}` list + * doesn't re-mount them. Nodes not currently in the selection are ignored + * (a Pull can write page-wide, beyond the selection). This replaces the + * old whole-selection re-scan the main thread used to do after writes. + */ + patchNodes(updated: NodeInfo[]) { + if (updated.length === 0) return; + const byId = new Map(updated.map((n) => [n.id, n])); + selectedNodes = selectedNodes.map((n) => byId.get(n.id) ?? n); + }, + setScanning() { + if (state.scanning) return; + if (scanSpinnerTimer) { + // A SECOND scan started before the first delivered — the user + // switched selection while we were still working. That's exactly + // when feedback matters: show the loader immediately instead of + // re-waiting out the delay on a visibly stale list. + clearTimeout(scanSpinnerTimer); + scanSpinnerTimer = null; + state.scanning = true; + return; + } + scanSpinnerTimer = setTimeout(() => { + scanSpinnerTimer = null; + state.scanning = true; + }, SCAN_SPINNER_DELAY_MS); + }, + navigate(route: Route) { + // Dev-Mode gate — covers EVERY entry point to a design-only route + // (row click, menus, future callers) in one place instead of chasing + // each button. Silent no-op by design; see ROUTE_AVAILABILITY's doc. + if (state.editorType === "dev" && ROUTE_AVAILABILITY[route.name] === "design-only") { + console.warn("[tolgee:ui] blocked design-only route in Dev Mode:", route.name); + return; + } + // Persist any debounced inline edits BEFORE the destination flow reads + // node state — Push must diff the key the user just typed, not the one + // still sitting in the queue. + flushNodeSaves(); + state.route = route; + }, + setEditorType(t: "figma" | "dev") { + state.editorType = t; + }, + setPageName(name: string) { + state.pageName = name; + }, + setError(banner: AppState["errorBanner"]) { + state.errorBanner = banner; + }, + /** A `nodes-set-progress` message arrived — some large write is in flight. */ + setWriteProgress(done: number, total: number) { + state.writeProgress = { done, total }; + }, + /** The in-flight write settled (`nodes-set-result`) — nothing to show. */ + clearWriteProgress() { + state.writeProgress = null; + }, + }; +} + +export const appState = createAppState(); diff --git a/src/ui/lib/stores/auth.svelte.ts b/src/ui/lib/stores/auth.svelte.ts new file mode 100644 index 00000000..e31d30f8 --- /dev/null +++ b/src/ui/lib/stores/auth.svelte.ts @@ -0,0 +1,117 @@ +import type { TolgeeClient } from "$ui/lib/api/client"; +import type { BranchInfo } from "$ui/lib/api/branches"; + +type LanguageInfo = { tag: string; name: string }; +type NamespaceInfo = { name: string }; + +type AuthState = { + client: TolgeeClient | null; + apiUrl: string; + apiKey: string; + projectId: number | null; + projectName: string | null; + scopes: string[]; + authenticated: boolean; + branchingEnabled: boolean; + namespacesEnabled: boolean; + languages: LanguageInfo[]; + /** The project's base (main) language tag, used to pre-fill "Current + * language" on first setup — matches the original plugin. "" when unknown. */ + baseLanguage: string; + namespaces: NamespaceInfo[]; + branches: BranchInfo[]; + /** Branch to pre-select when none is chosen (isDefault → "main" → first). */ + defaultBranch: string; + /** + * True only after `branches` reflects a SUCCESSFUL fetch. Distinguishes + * "list is empty because it hasn't loaded / the fetch failed" from "list + * really is what the server has" — the missing-branch warning must only + * trust the latter. + */ + branchesLoaded: boolean; +}; + +function createAuth() { + const state = $state({ + client: null, + apiUrl: "", + apiKey: "", + projectId: null, + projectName: null, + scopes: [], + authenticated: false, + branchingEnabled: false, + namespacesEnabled: false, + languages: [], + baseLanguage: "", + namespaces: [], + branches: [], + defaultBranch: "", + branchesLoaded: false, + }); + + return { + get value() { + return state; + }, + setAuth(opts: { + client: TolgeeClient; + apiUrl: string; + apiKey: string; + projectId: number; + scopes: string[]; + }) { + state.client = opts.client; + state.apiUrl = opts.apiUrl.replace(/\/$/, ""); + state.apiKey = opts.apiKey; + state.projectId = opts.projectId; + state.scopes = opts.scopes; + state.authenticated = true; + }, + setProjectFeatures(features: { + branchingEnabled: boolean; + namespacesEnabled: boolean; + projectName?: string; + }): void { + state.branchingEnabled = features.branchingEnabled; + state.namespacesEnabled = features.namespacesEnabled; + if (features.projectName !== undefined) { + state.projectName = features.projectName; + } + }, + setLanguages(langs: LanguageInfo[], baseLanguage = ""): void { + state.languages = langs; + state.baseLanguage = baseLanguage; + }, + setNamespaces(nss: NamespaceInfo[]): void { + state.namespaces = nss; + }, + setBranches(branches: BranchInfo[], defaultBranch = "", loaded = true): void { + state.branches = branches; + state.defaultBranch = defaultBranch; + state.branchesLoaded = loaded; + }, + clear() { + state.client = null; + state.apiUrl = ""; + state.apiKey = ""; + state.projectId = null; + state.projectName = null; + state.scopes = []; + state.authenticated = false; + state.branchingEnabled = false; + state.namespacesEnabled = false; + state.languages = []; + state.baseLanguage = ""; + state.namespaces = []; + state.branches = []; + state.defaultBranch = ""; + state.branchesLoaded = false; + }, + hasScope(scope: string): boolean { + return state.scopes.includes(scope); + }, + }; +} + +export const auth = createAuth(); diff --git a/src/ui/lib/stores/query.svelte.ts b/src/ui/lib/stores/query.svelte.ts new file mode 100644 index 00000000..227dc78a --- /dev/null +++ b/src/ui/lib/stores/query.svelte.ts @@ -0,0 +1,21 @@ +import { QueryClient } from "@tanstack/svelte-query"; + +/** + * Shared TanStack Query client. + * + * Defaults are tuned for a Figma plugin context: short-lived stale time so + * UI navigations re-fetch reasonably fresh data, retries kept low because + * the user-facing UI surfaces error states explicitly, and + * `refetchOnWindowFocus` disabled because focus events fire constantly + * while interacting with the Figma canvas. + */ +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30 * 1000, + gcTime: 5 * 60 * 1000, + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); diff --git a/src/ui/lib/utils.ts b/src/ui/lib/utils.ts new file mode 100644 index 00000000..13858afa --- /dev/null +++ b/src/ui/lib/utils.ts @@ -0,0 +1,17 @@ +import { type ClassValue, clsx } from "clsx"; + +/** + * Conditionally compose Tailwind class strings. + * + * We intentionally avoid `tailwind-merge` here: it ships ~146 kB / ~25 kB + * gzip of conflict-resolution rules that we don't meaningfully exercise + * inside this Figma plugin (we don't compose orthogonal utilities from + * untrusted callers — components own their base classes and consumers + * extend, not override). If a caller needs to override a base utility, + * Tailwind's source-order specificity rules take care of it; in the + * rare cases that breaks down, the caller can drop the base class with + * a conditional instead. + */ +export function cn(...inputs: ClassValue[]): string { + return clsx(inputs); +} diff --git a/src/ui/main.ts b/src/ui/main.ts new file mode 100644 index 00000000..6c4aeab2 --- /dev/null +++ b/src/ui/main.ts @@ -0,0 +1,7 @@ +import { mount } from "svelte"; +import App from "./App.svelte"; +import "./styles.css"; + +const target = document.getElementById("app"); +if (!target) throw new Error("#app missing"); +mount(App, { target }); diff --git a/src/ui/state/GlobalState.ts b/src/ui/state/GlobalState.ts deleted file mode 100644 index af180749..00000000 --- a/src/ui/state/GlobalState.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { emit, on } from "@/utilities"; -import { useEffect, useState } from "preact/hooks"; - -import { - ConfigChangeHandler, - NodeInfo, - ResetHandler, - ResizeHandler, - SetBranchHandler, - SetLanguageHandler, - SetupHandle, - TolgeeConfig, - WindowSize, -} from "@/types"; -import type { Route } from "../views/routes"; -import { createProvider } from "@/tools/createProvider"; -import { DEFAULT_SIZE } from "./sizes"; - -type Props = { - initialConfig: Partial | null; -}; - -export const globalState = { - actions: undefined as unknown as ReturnType, -}; - -export const [GlobalState, useGlobalActions, useGlobalState] = createProvider( - ({ initialConfig }: Props) => { - const [route, _setRoute] = useState(["index"]); - const routeKey = route[0]; - const [config, _setConfig] = useState(initialConfig); - const [globalError, setGlobalError] = useState( - undefined, - ); - const [sizeStack, setSizeStack] = useState([]); - - // Emit initial size on mount - useEffect(() => { - const initialSize = sizeStack[sizeStack.length - 1] ?? DEFAULT_SIZE; - emit("RESIZE", initialSize); - }, []); - - const [editedKeys, setEditedKeys] = useState>({}); - - useEffect(() => { - return on("CONFIG_CHANGE", (data) => { - _setConfig(data); - }); - }, []); - - const data = { - route, - routeKey, - selection: [] as NodeInfo[], - config, - globalError, - editedKeys, - sizeStack, - }; - - const actions = { - setRoute(...route: Route) { - setGlobalError(undefined); - _setRoute(route); - }, - setConfig(_config: Partial) { - _setConfig(_config); - emit("SETUP", _config); - }, - setLanguage(language: string) { - _setConfig({ ...config, language }); - emit("SET_LANGUAGE", language); - }, - setBranch(branch: string) { - _setConfig({ ...config, branch }); - emit("SET_BRANCH", branch); - }, - setEditedKey(id: string, key: string) { - setEditedKeys((keys) => ({ ...keys, [id]: key })); - }, - resetConfig() { - emit("RESET"); - }, - setSizeStack, - setGlobalError, - }; - - globalState.actions = actions; - - return [data, actions]; - }, -); -export { DEFAULT_SIZE }; diff --git a/src/ui/state/sizes.ts b/src/ui/state/sizes.ts deleted file mode 100644 index 27cbea1b..00000000 --- a/src/ui/state/sizes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const EXPANDED_SIZE = { width: 500, height: 800 }; -export const DEFAULT_SIZE = { width: 500, height: 400 }; -export const MINIMUM_SIZE = { width: 400, height: 160 }; diff --git a/src/ui/styles.css b/src/ui/styles.css index dd1c552b..4e9530cb 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -1,84 +1,226 @@ -[class^="_selectableItem_"] { - height: auto; - padding: var(--space-medium) var(--space-small) var(--space-medium) - var(--space-medium); - margin-bottom: var(--space-small); -} +@import "tailwindcss"; -[class^="_selectableItem_"]:not([class^="_disabled_"]) - [class^="_input_"]:checked - ~ [class^="_children_"] { - color: var(--figma-color-text-onbrand); -} -[class^="_selectableItem_"]:not([class^="_disabled_"]) - [class^="_input_"]:checked - ~ [class^="_box_"] { - background-color: #ec407a; -} +@custom-variant dark (&:where(.figma-dark, .figma-dark *)); + +/* Override Figma's default brand (blue) with Tolgee's primary pink so every + * `--figma-color-*-brand` token — and the `--color-*-brand` aliases mapped in + * `@theme` below — picks up the Tolgee identity automatically. */ :root { --figma-color-bg-brand: #ec407a; - --figma-color-text-brand: #ec407a; - --figma-color-text-primary: #1f2d40de; + --figma-color-bg-brand-hover: #d63872; + --figma-color-bg-brand-pressed: #c0306a; + --figma-color-bg-brand-tertiary: rgba(236, 64, 122, 0.12); --figma-color-bg-selected: #ec407a; - --figma-color-bg-brand-contrast: #ffffff; - --figma-color-icon-brand: white; - --figma-color-teal: rgb(0, 175, 154); - --figma-color-teal-bg: rgb(0, 175, 154, 0.08); - --figma-color-red: rgb(255, 0, 46); - --figma-color-red-bg: rgb(255, 0, 46, 0.08); - --figma-color-bg-grey: #e6e6e6; - --figma-color-text-grey: #000000; + --figma-color-bg-selected-tertiary: rgba(236, 64, 122, 0.12); + --figma-color-text-brand: #ec407a; + --figma-color-text-onbrand: #ffffff; + --figma-color-border-brand: #ec407a; + --figma-color-icon-brand: #ec407a; + --figma-color-icon-onbrand: #ffffff; } -@media (prefers-color-scheme: dark) { - :root { - --figma-color-teal: rgb(53, 196, 176); - --figma-color-teal-bg: rgb(53, 196, 176, 0.16); - --figma-color-red: rgb(255, 46, 83); - --figma-color-red-bg: rgb(255, 46, 83, 0.16); - --figma-color-text-primary: #ffffff; - } +/* Tolgee semantic palette — light values (dark overrides in `.figma-dark` + * below). Surfaced as Tailwind utilities (bg-primary, text-success, + * border-error-light, …) through the `@theme` block. Each token has a + * main / dark / light shade. */ +:root { + --sem-primary: #ec407a; + --sem-primary-dark: #a52c55; + --sem-primary-light: #ef6694; + --sem-secondary: #00af9a; + --sem-secondary-dark: #007a6b; + --sem-secondary-light: #33bfae; + --sem-success: #00b962; + --sem-success-dark: #008144; + --sem-success-light: #33c781; + --sem-error: #d60027; + --sem-error-dark: #850018; + --sem-error-light: #ff2e53; + --sem-info: #0288d1; + --sem-info-dark: #01579b; + --sem-info-light: #03a9f4; + --sem-warning: #f59e0b; + --sem-warning-dark: #b45309; + --sem-warning-light: #fbbf24; + + /* Foreground (text/icon) for messages: the dark shade in light mode so it + * stays readable on the light tint; the dark-mode override below flips it to + * the brighter main shade for contrast on dark. */ + --sem-error-fg: var(--sem-error-dark); + --sem-success-fg: var(--sem-success-dark); + --sem-info-fg: var(--sem-info-dark); + --sem-secondary-fg: var(--sem-secondary-dark); + --sem-warning-fg: var(--sem-warning-dark); + + /* Muted icon colour for large decorative / empty-state glyphs. Deliberately + * FULLY OPAQUE (not Figma's translucent --figma-color-icon-secondary): + * lucide icons have overlapping strokes, and a translucent colour + * double-paints the overlaps into visible seams. */ + --sem-icon-muted: #b3b3b3; + + /* Checked-checkbox fill: a light blue-grey — a touch more alive than plain + * grey, still neutral (not the brand pink, which is reserved for actions). */ + --sem-checkbox: #7c8b9e; } -a { - color: var(--figma-color-text-brand); +.figma-dark { + /* Render native UI (scrollbars, form controls) dark to match the theme — + * otherwise Figma shows a light scrollbar in the dark plugin. */ + color-scheme: dark; + --sem-primary: #f06695; + --sem-primary-dark: #a84768; + --sem-primary-light: #f384aa; + --sem-secondary: #99e5d6; + --sem-secondary-dark: #6ba095; + --sem-secondary-light: #adeade; + --sem-success: #33d589; + --sem-success-dark: #23955f; + --sem-success-light: #5bdda0; + --sem-error: #ff5c79; + --sem-error-dark: #ad001f; + --sem-error-light: #ff5c79; + --sem-info: #29b6f6; + --sem-info-dark: #0288d1; + --sem-info-light: #4fc3f7; + --sem-warning: #fbbf24; + --sem-warning-dark: #d97706; + --sem-warning-light: #fcd34d; + + /* On dark, the brighter main shade reads better on the dark tint. */ + --sem-error-fg: var(--sem-error); + --sem-success-fg: var(--sem-success); + --sem-info-fg: var(--sem-info); + --sem-secondary-fg: var(--sem-secondary); + --sem-warning-fg: var(--sem-warning); + + /* Opaque muted icon, tuned for the dark surface. */ + --sem-icon-muted: #808080; + + /* Light blue-grey on dark — a brightened take on the light-mode fill so the + * checked box reads as active (plain mid grey looked disabled), still neutral + * rather than the brand pink. The dark check reads well on it. */ + --sem-checkbox: #9aa8bc; + + /* Dedicated end-stop for the header gradient (Header/ViewHeader/Index/ + * Settings) — deliberately NOT the same token as --color-bg-secondary + * below, which is also used elsewhere (e.g. progressBar.svelte's track) + * and must stay untouched. Darker than --color-bg (rgb(44,44,44)), not + * lighter: an earlier lighter end-stop read like a highlight/glow at the + * header's bottom edge in dark mode, which looked out of place next to a + * border-b. Darker reads as a natural shadow instead. Plain hand-picked + * hex value (not computed), same 6/255 delta as before, just flipped. */ + --color-header-gradient-end: #262626; } -select { - border: 1px solid var(--figma-color-border); - overflow-x: hidden; - text-overflow: ellipsis; - border-radius: var(--border-radius-2); - cursor: text; - background: var(--figma-color-bg); - -webkit-appearance: none !important; - -moz-appearance: none !important; - background-repeat: no-repeat; - background-image: url('data:image/svg+xml;utf8,'); - background-position: 100%; - background-size: 20px; - width: 100%; - padding: 6px 8px; +@theme { + --color-bg: var(--figma-color-bg); + --color-bg-secondary: var(--figma-color-bg-secondary); + /* Light-mode default: identical to --color-bg-secondary so the light + * gradient is pixel-unchanged. Overridden in .figma-dark above. */ + --color-header-gradient-end: var(--figma-color-bg-secondary); + --color-bg-brand: var(--figma-color-bg-brand); + --color-bg-selected: var(--figma-color-bg-selected); + --color-text: var(--figma-color-text); + --color-text-secondary: var(--figma-color-text-secondary); + --color-text-brand: var(--figma-color-text-brand); + --color-text-onbrand: var(--figma-color-text-onbrand); + --color-border: var(--figma-color-border); + --color-border-brand: var(--figma-color-border-brand); + --color-icon: var(--figma-color-icon); + --color-icon-secondary: var(--figma-color-icon-secondary); + --color-icon-muted: var(--sem-icon-muted); + --color-checkbox: var(--sem-checkbox); + + /* Semantic palette (theme-aware via the --sem-* vars above). */ + --color-primary: var(--sem-primary); + --color-primary-dark: var(--sem-primary-dark); + --color-primary-light: var(--sem-primary-light); + --color-secondary: var(--sem-secondary); + --color-secondary-dark: var(--sem-secondary-dark); + --color-secondary-light: var(--sem-secondary-light); + --color-success: var(--sem-success); + --color-success-dark: var(--sem-success-dark); + --color-success-light: var(--sem-success-light); + --color-error: var(--sem-error); + --color-error-dark: var(--sem-error-dark); + --color-error-light: var(--sem-error-light); + --color-info: var(--sem-info); + --color-info-dark: var(--sem-info-dark); + --color-info-light: var(--sem-info-light); + --color-warning: var(--sem-warning); + --color-warning-dark: var(--sem-warning-dark); + --color-warning-light: var(--sem-warning-light); } + +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +body { + font-family: "Inter", system-ui, sans-serif; + font-size: 12px; + line-height: 1.4; + background: var(--color-bg); + color: var(--color-text); +} + +/* Theme-aware scrollbar: the OS/browser default renders a light bar in the dark + * plugin. Style it explicitly via theme vars (works regardless of whether the + * webview honours `color-scheme` for scrollbars). Firefox: `scrollbar-color`. */ +* { + scrollbar-color: var(--color-border) transparent; +} +::-webkit-scrollbar { + width: 10px; + height: 10px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--color-border); + border: 2px solid var(--color-bg); + border-radius: 6px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--color-icon-muted); +} + +/* Tailwind v4's preflight intentionally leaves buttons at the browser default + * cursor (an arrow). For a plugin UI we want interactive controls to feel + * clickable, so opt every actionable element back into `pointer`. */ button, -button > * { +[role="button"], +[role="link"], +[role="menuitem"], +[role="tab"], +[role="checkbox"], +[role="switch"], +summary, +select, +label[for] { cursor: pointer; } -@media (prefers-color-scheme: dark) { - select { - background-image: url('data:image/svg+xml;utf8,'); - } +/* Disabled controls should still indicate non-interactivity. */ +button:disabled, +[role="button"][aria-disabled="true"], +[aria-disabled="true"] { + cursor: not-allowed; } -select:disabled { - border: 1px solid var(--figma-color-border-disabled-strong); -} -.noSelection { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-weight: 500; - height: 100%; +/* Text inputs / editable surfaces get the I-beam. Tailwind v4 already does + * this for native inputs, but CodeMirror's contenteditable layer doesn't pick + * it up — make it explicit there too. */ +input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]):not( + [type="radio"] + ):not([type="range"]):not([type="file"]):not([type="color"]), +textarea, +[contenteditable="true"], +[contenteditable="plaintext-only"], +.cm-editor .cm-content { + cursor: text; } diff --git a/src/ui/styles.css.d.ts b/src/ui/styles.css.d.ts deleted file mode 100644 index d1358453..00000000 --- a/src/ui/styles.css.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare const styles: { - readonly "noSelection": string; -}; -export = styles; - diff --git a/src/ui/ui.tsx b/src/ui/ui.tsx deleted file mode 100644 index a0304ce4..00000000 --- a/src/ui/ui.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { render } from "@create-figma-plugin/ui"; -import { Plugin } from "./Plugin"; - -export default render(Plugin); diff --git a/src/ui/views/Connect/Connect.css b/src/ui/views/Connect/Connect.css deleted file mode 100644 index a7a1407b..00000000 --- a/src/ui/views/Connect/Connect.css +++ /dev/null @@ -1,28 +0,0 @@ -.page { - display: grid; - height: 100vh; - grid-template-rows: auto 1fr; -} - -.grid { - display: grid; -} - -.container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; -} - -.title { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - max-width: 100%; -} - -.results { - display: grid; - max-height: 220px; - overflow-y: auto; -} diff --git a/src/ui/views/Connect/Connect.css.d.ts b/src/ui/views/Connect/Connect.css.d.ts deleted file mode 100644 index 38284c26..00000000 --- a/src/ui/views/Connect/Connect.css.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare const styles: { - readonly "container": string; - readonly "grid": string; - readonly "page": string; - readonly "results": string; - readonly "title": string; -}; -export = styles; - diff --git a/src/ui/views/Connect/Connect.tsx b/src/ui/views/Connect/Connect.tsx deleted file mode 100644 index 46bacd3d..00000000 --- a/src/ui/views/Connect/Connect.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { Fragment, h } from "preact"; -import { useState } from "preact/hooks"; -import { useDebounce } from "use-debounce"; -import { - Button, - Container, - Divider, - Muted, - Text, - Textbox, - VerticalSpace, -} from "@create-figma-plugin/ui"; - -import { useGlobalActions, useGlobalState } from "@/ui/state/GlobalState"; -import { TopBar } from "@/ui/components/TopBar/TopBar"; -import { ActionsBottom } from "@/ui/components/ActionsBottom/ActionsBottom"; -import { useApiMutation, useApiQuery } from "@/ui/client/useQueryApi"; -import { FullPageLoading } from "@/ui/components/FullPageLoading/FullPageLoading"; -import { useSetNodesDataMutation } from "@/ui/hooks/useSetNodesDataMutation"; -import { RouteParam } from "../routes"; -import styles from "./Connect.css"; -import { SearchRow } from "./SearchRow"; - -type Props = RouteParam<"connect">; - -export const Connect = ({ node }: Props) => { - const { setRoute } = useGlobalActions(); - const branch = useGlobalState((c) => c.config?.branch); - - const language = useGlobalState((c) => c.config?.language); - - const [search, setSearch] = useState(node.key || node.characters); - - const [debouncedSearch] = useDebounce(search, 300); - - const translationsLoadable = useApiQuery({ - url: "/v2/projects/keys/search", - method: "get", - query: { - search: debouncedSearch, - size: 20, - languageTag: language, - }, - options: { - enabled: Boolean(debouncedSearch), - }, - }); - - // Fetches just the picked key (with plural metadata) instead of paginating - // through every translation in a namespace. - const keyTranslationLoadable = useApiMutation({ - url: "/v2/projects/translations", - method: "get", - }); - - const setNodesDataMutation = useSetNodesDataMutation(); - - const handleGoBack = () => { - setRoute("index"); - }; - - const handleConnect = async ( - keyId: number, - key: string, - ns: string | undefined, - translation: string | undefined, - ) => { - let resolvedTranslation = translation; - let isPlural: boolean | undefined; - let pluralParamValue: string | undefined; - - try { - const result = await keyTranslationLoadable.mutateAsync({ - query: { - filterKeyId: [keyId], - languages: language ? [language] : undefined, - size: 1, - branch: branch || undefined, - }, - }); - const tolgeeKey = result._embedded?.keys?.[0]; - if (tolgeeKey) { - isPlural = tolgeeKey.keyIsPlural; - pluralParamValue = tolgeeKey.keyPluralArgName; - resolvedTranslation = - (language && tolgeeKey.translations?.[language]?.text) || - resolvedTranslation; - } - } catch (e) { - console.error("Failed to load key metadata, connecting without it.", e); - } - - await setNodesDataMutation.mutateAsync({ - nodes: [ - { - ...node, - translation: resolvedTranslation ?? node.characters, - isPlural: isPlural ?? node.isPlural, - pluralParamValue: pluralParamValue ?? node.pluralParamValue, - key, - ns: ns || "", - connected: true, - }, - ], - }); - setRoute("index"); - }; - - const handleRemoveConnection = async () => { - await setNodesDataMutation.mutateAsync({ - nodes: [ - { - ...node, - key: "", - ns: undefined, - connected: false, - }, - ], - }); - setRoute("index"); - }; - - const isConnecting = - keyTranslationLoadable.isLoading || setNodesDataMutation.isLoading; - - return ( - - {(translationsLoadable.isFetching || isConnecting) && } - Connect to existing key
    } - /> - - - -
    -
    - - Search - - - setSearch(value)} - value={search} - variant="border" - /> -
    -
    -
    - - -
    - {debouncedSearch && - translationsLoadable.data?._embedded?.keys?.map((key) => ( - - handleConnect(key.id, key.name, key.namespace, key.translation) - } - /> - ))} -
    - - {node.connected && ( - - )} - - - - ); -}; diff --git a/src/ui/views/Connect/SearchRow.css b/src/ui/views/Connect/SearchRow.css deleted file mode 100644 index a5159013..00000000 --- a/src/ui/views/Connect/SearchRow.css +++ /dev/null @@ -1,30 +0,0 @@ -.container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2px 16px; - padding: 6px 0px; - margin: 0px 16px; - cursor: pointer; - height: 48px; -} - -.container > * { - padding: 0px 8px; - cursor: pointer; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - height: 17px; -} - -.container:hover { - background-color: var(--figma-color-bg-hover); -} - -.disabled { - color: var(--figma-color-text-disabled); -} - -.translation { - grid-column: 1 / -1; -} diff --git a/src/ui/views/Connect/SearchRow.css.d.ts b/src/ui/views/Connect/SearchRow.css.d.ts deleted file mode 100644 index 6e2c5011..00000000 --- a/src/ui/views/Connect/SearchRow.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare const styles: { - readonly "container": string; - readonly "disabled": string; - readonly "translation": string; -}; -export = styles; - diff --git a/src/ui/views/Connect/SearchRow.tsx b/src/ui/views/Connect/SearchRow.tsx deleted file mode 100644 index c29fa2f1..00000000 --- a/src/ui/views/Connect/SearchRow.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { h } from "preact"; -import clsx from "clsx"; - -import { components } from "@/ui/client/apiSchema.generated"; -import styles from "./SearchRow.css"; - -type KeySearchSearchResultModel = - components["schemas"]["KeySearchSearchResultModel"]; - -type Props = { - onClick: () => void; - data: KeySearchSearchResultModel; -}; - -export const SearchRow = ({ data, onClick }: Props) => { - return ( -
    -
    {data.translation}
    -
    {data.name}
    -
    - {data.namespace || ""} -
    -
    - ); -}; diff --git a/src/ui/views/CopyView/CopyView.tsx b/src/ui/views/CopyView/CopyView.tsx deleted file mode 100644 index fa35fbb4..00000000 --- a/src/ui/views/CopyView/CopyView.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Fragment, h } from "preact"; -import { - Button, - Container, - Divider, - Muted, - Text, - VerticalSpace, -} from "@create-figma-plugin/ui"; -import { useGlobalState } from "@/ui/state/GlobalState"; -import { FullPageLoading } from "@/ui/components/FullPageLoading/FullPageLoading"; - -import { NodeList } from "../../components/NodeList/NodeList"; -import { TopBar } from "../../components/TopBar/TopBar"; -import { useSelectedNodes } from "@/ui/hooks/useSelectedNodes"; -import { useConnectedMutation } from "@/ui/hooks/useConnectedMutation"; -import { useUpdateNodesMutation } from "@/ui/hooks/useUpdateNodesMutation"; -import { LocateNodeButton } from "@/ui/components/LocateNodeButton/LocateNodeButton"; -import { getPullChanges } from "@/tools/getPullChanges"; -import { useAllTranslations } from "@/ui/hooks/useAllTranslations"; -import { getTolgeeFormat } from "@tginternal/editor"; -import { createFormatIcu } from "../../../createFormatIcu"; - -export const CopyView = () => { - const selectionLoadable = useSelectedNodes(); - const language = useGlobalState((c) => c.config?.language); - const branch = useGlobalState((c) => c.config?.branch); - const connectedNodesLoadable = useConnectedMutation({ - ignoreSelection: false, - }); - - const selection = selectionLoadable.data?.items ?? []; - - const nothingSelected = !selectionLoadable.data?.somethingSelected; - - const updateNodesLoadable = useUpdateNodesMutation(); - const allTranslationsLoadable = useAllTranslations(); - - const handlePull = async () => { - const translations = await allTranslationsLoadable.getData({ - language: language ?? "", - branch: branch || undefined, - }); - - const connectedNodes = await connectedNodesLoadable.mutateAsync(undefined); - - const { changedNodes } = getPullChanges( - connectedNodes.items, - language!, - translations, - ); - - const formatter = createFormatIcu(); - - await updateNodesLoadable.mutateAsync({ - nodes: changedNodes.map((n) => { - const tolgeeValue = getTolgeeFormat(n.translation, n.isPlural, false); - const formatted = formatter.format({ - language: language ?? "en", - translation: n.translation, - params: { - ...n.paramsValues, - [tolgeeValue.parameter ?? ""]: n.pluralParamValue, - }, - }); - return { - ...n, - formatted, - }; - }), - }); - }; - - if ( - allTranslationsLoadable.isLoading || - connectedNodesLoadable.isLoading || - updateNodesLoadable.isLoading - ) { - return ; - } - - return ( - - - - Page copy - {language ? language : "keys"} - {language && ( - - )} - - } - /> - - - - - {nothingSelected ? ( - - - Select texts for translation -
    - (single texts or frames) -
    -
    - ) : ( - { - if (!node.connected) { - return Not connected; - } - return node.key || ""; - }} - actionCallback={(item) => } - /> - )} - - ); -}; diff --git a/src/ui/views/CreateCopy/CreateCopy.tsx b/src/ui/views/CreateCopy/CreateCopy.tsx deleted file mode 100644 index 61609df1..00000000 --- a/src/ui/views/CreateCopy/CreateCopy.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useApiQuery } from "@/ui/client/useQueryApi"; -import { ActionsBottom } from "@/ui/components/ActionsBottom/ActionsBottom"; -import { FullPageLoading } from "@/ui/components/FullPageLoading/FullPageLoading"; -import { TopBar } from "@/ui/components/TopBar/TopBar"; -import { useGlobalActions, useGlobalState } from "@/ui/state/GlobalState"; -import { getPullChanges } from "@/tools/getPullChanges"; -import { - VerticalSpace, - Text, - Muted, - Container, - Button, - Divider, - RadioButtons, - Checkbox, -} from "@create-figma-plugin/ui"; -import { Fragment, FunctionComponent, h } from "preact"; -import { useState } from "preact/hooks"; -import { useCopyPage } from "@/ui/hooks/useCopyPage"; -import { useConnectedNodes } from "@/ui/hooks/useConnectedNodes"; -import { useAllTranslations } from "@/ui/hooks/useAllTranslations"; - -type CopyType = "language" | "keys"; - -export const CreateCopy: FunctionComponent = () => { - const { setRoute } = useGlobalActions(); - const branch = useGlobalState((c) => c.config?.branch); - - const [copyType, setCopyType] = useState("keys"); - const [selectedLanguages, setSelectedLanguages] = useState([]); - - const connectedNodes = useConnectedNodes({ ignoreSelection: true }); - - const languagesLoadable = useApiQuery({ - url: "/v2/projects/languages", - method: "get", - options: { - cacheTime: 0, - staleTime: 0, - }, - }); - - const allTranslationsLoadable = useAllTranslations(); - - const copyPageMutation = useCopyPage(); - - const languages = languagesLoadable.data?._embedded?.languages; - - const handleSubmit = async () => { - if (copyType === "keys") { - await copyPageMutation.mutateAsync(undefined); - } else { - for (const language of selectedLanguages) { - const response = await allTranslationsLoadable.getData({ - language, - branch: branch || undefined, - }); - - const { changedNodes } = getPullChanges( - connectedNodes.data?.items || [], - language, - response, - ); - - await copyPageMutation.mutateAsync({ - language, - nodes: changedNodes, - }); - } - } - goToIndex(); - }; - - const goToIndex = () => { - setRoute("index"); - }; - - const handleToggleLanguage = (lang: string) => { - setSelectedLanguages((langs) => { - if (langs.includes(lang)) { - return langs.filter((l) => l !== lang); - } - return [...langs, lang]; - }); - }; - - const validated = Boolean(copyType === "keys" || selectedLanguages.length); - - if (connectedNodes.isLoading) { - return ; - } - - if (languagesLoadable.isLoading || copyPageMutation.isLoading) { - return ; - } - - return ( - - Create new page
    } onBack={goToIndex} /> - - - - setCopyType(e.currentTarget.value as CopyType)} - options={[ - { - value: "keys", - children: Create page with key names, - }, - { - value: "language", - children: Create page with languages, - }, - ]} - /> - - - - {copyType === "language" && ( - - - Languages - - - -
    - {languages?.map((language) => ( -
    - handleToggleLanguage(language.tag)} - > - {language.name} - - -
    - ))} -
    -
    - )} - - - - - -
    - - ); -}; diff --git a/src/ui/views/Index/Index.css b/src/ui/views/Index/Index.css deleted file mode 100644 index 7bae1ee2..00000000 --- a/src/ui/views/Index/Index.css +++ /dev/null @@ -1,53 +0,0 @@ -.languageContainer { - display: flex; - height: 100%; - margin-left: -3px; - min-width: 100px; - padding-right: 20px; -} - -.settingsButton { - padding: 5px 0px 5px 5px; - cursor: pointer; -} - -.connectButton { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - width: 20px; - height: 20px; -} - -.disabled { - color: var(--figma-color-text-disabled); -} - -.keyInput { - background: blue; -} - -.screenshot { - position: relative; -} - -.container { - display: grid; - grid-template-rows: auto 1fr; - height: 100%; -} - -.actionsContainer { - display: flex; - gap: 4px; -} - -.branchText { - font-size: 11px; - opacity: 0.6; - max-width: 80px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/src/ui/views/Index/Index.css.d.ts b/src/ui/views/Index/Index.css.d.ts deleted file mode 100644 index 45c5a673..00000000 --- a/src/ui/views/Index/Index.css.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare const styles: { - readonly "actionsContainer": string; - readonly "branchText": string; - readonly "connectButton": string; - readonly "container": string; - readonly "disabled": string; - readonly "keyInput": string; - readonly "languageContainer": string; - readonly "screenshot": string; - readonly "settingsButton": string; -}; -export = styles; - diff --git a/src/ui/views/Index/Index.tsx b/src/ui/views/Index/Index.tsx deleted file mode 100644 index 3d9a9fc4..00000000 --- a/src/ui/views/Index/Index.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import { Fragment, h } from "preact"; -import { - useCallback, - useEffect, - useState, - useMemo, - useRef, -} from "preact/hooks"; -import { - Banner, - Button, - Container, - Divider, - IconWarning32, - Text, - VerticalSpace, -} from "@create-figma-plugin/ui"; - -import { Settings } from "@/ui/icons/SvgIcons"; -import { useApiQuery } from "@/ui/client/useQueryApi"; -import { getConflictingNodes } from "@/tools/getConflictingNodes"; -import { FullPageLoading } from "@/ui/components/FullPageLoading/FullPageLoading"; -import { useGlobalActions, useGlobalState } from "@/ui/state/GlobalState"; -import { useSelectedNodes } from "@/ui/hooks/useSelectedNodes"; -import { useConnectedNodes } from "@/ui/hooks/useConnectedNodes"; - -import { NodeList } from "../../components/NodeList/NodeList"; -import { TopBar } from "../../components/TopBar/TopBar"; -import styles from "./Index.css"; -import { ListItem } from "./ListItem"; -import { useEditorMode } from "../../hooks/useEditorMode"; -import { useHasNamespacesEnabled } from "../../hooks/useHasNamespacesEnabled"; -import { useHasBranchingEnabled } from "../../hooks/useHasBranchingEnabled"; -import { BranchSelect } from "../../components/BranchSelect/BranchSelect"; - -export const Index = () => { - const selectionLoadable = useSelectedNodes(); - const selection = selectionLoadable.data?.items || []; - - // index page is not removed on certain routes - // refetch when we go back to it - const route = useGlobalState((c) => c.route); - - const [error, setError] = useState(); - - const languagesLoadable = useApiQuery({ - url: "/v2/projects/languages", - method: "get", - query: { - size: 1000, - }, - }); - - const namespacesLoadable = useApiQuery({ - url: "/v2/projects/used-namespaces", - method: "get", - }); - - const hasNamespacesEnabled = useHasNamespacesEnabled(); - const hasBranchingEnabled = useHasBranchingEnabled(); - const currentBranch = useGlobalState((c) => c.config?.branch); - const { setBranch } = useGlobalActions(); - - const apiKeyInfo = useApiQuery({ - url: "/v2/api-keys/current", - method: "get", - options: { cacheTime: 60000, staleTime: 60000 }, - }); - - const branchesLoadable = useApiQuery({ - url: "/v2/projects/{projectId}/branches", - method: "get", - path: { projectId: apiKeyInfo.data?.projectId ?? 0 }, - options: { - cacheTime: 0, - staleTime: 0, - enabled: hasBranchingEnabled && apiKeyInfo.data?.projectId !== undefined, - }, - }); - - const branches = branchesLoadable.data?._embedded?.branches ?? []; - - const isBranchMissing = - hasBranchingEnabled && - !branchesLoadable.isLoading && - branchesLoadable.data !== undefined && - !!currentBranch && - branches.length > 0 && - !branches.some((b) => b.name === currentBranch); - - const languages = languagesLoadable.data?._embedded?.languages; - - const language = - useGlobalState((c) => c.config?.language) || languages?.[0]?.tag || ""; - - const { setRoute } = useGlobalActions(); - const allNodes = useConnectedNodes({ ignoreSelection: true }); - const mountedRef = useRef(false); - - // index page is not removed on certain routes (e.g. Connect dialog). - // When returning to it, refetch selection + connected-nodes so changes - // made elsewhere are reflected. We deliberately do not refetch on every - // node-data write (see useSetNodesDataMutation) to avoid full-page tree - // walks while the user is typing a key. - useEffect(() => { - if (!mountedRef.current) { - mountedRef.current = true; - return; - } - if (route[0] === "index") { - selectionLoadable.refetch(); - // Skip the page-wide rescan when the mutation already patched the cache - // (useSetNodesDataMutation.onSuccess + staleTime:30s keeps it fresh). - if (allNodes.isStale) { - allNodes.refetch(); - } - } - }, [route]); - - // Combine API namespaces + all namespaces from nodes - const allAvailableNamespaces = useMemo(() => { - const apiNamespaces = - namespacesLoadable.data?._embedded?.namespaces?.map( - (n) => n.name || "", - ) || []; - const nodeNamespaces = Array.from( - new Set( - (allNodes.data?.items || []) - .map((node) => node.ns) - .filter((ns): ns is string => Boolean(ns)), - ), - ); - return Array.from(new Set([...apiNamespaces, ...nodeNamespaces])) - .filter(Boolean) - .sort((a, b) => { - // Sort alphabetically, but put empty string at the end - if (!a) return 1; - if (!b) return -1; - return a.localeCompare(b); - }); - }, [namespacesLoadable.data, allNodes.data]); - - const loadedNamespaces = useMemo( - () => allAvailableNamespaces.map((name) => ({ name })), - [allAvailableNamespaces], - ); - - // Refresh namespaces: refetch API and all nodes - const handleRefreshNamespaces = useCallback(async () => { - await Promise.all([namespacesLoadable.refetch(), allNodes.refetch()]); - }, []); - - const renderRow = useCallback( - (node: (typeof selection)[number]) => ( - - ), - [hasNamespacesEnabled, loadedNamespaces, handleRefreshNamespaces], - ); - - const nothingSelected = !selectionLoadable.data?.somethingSelected; - - const handleLanguageChange = (lang: string) => { - if (lang !== language) { - setRoute("pull", { lang }); - } - }; - - const defaultNamespace = useGlobalState((c) => c.config?.namespace); - - const handlePush = () => { - const subjectNodes = selection.map((node) => ({ - ...node, - ns: node.ns ?? defaultNamespace, - })); - const conflicts = getConflictingNodes(subjectNodes); - if (conflicts.length > 0) { - const keys = Array.from(new Set(conflicts.map((n) => n.key))); - setError( - `There are multiple different translations for single key (${keys.join( - ", ", - )})`, - ); - } else { - setRoute("push"); - } - }; - - const handlePull = () => { - setRoute("pull", { - lang: language, - }); - }; - - const handleCopy = () => { - setRoute("create_copy"); - }; - - useEffect(() => { - setError(undefined); - }, [selection]); - - const editorMode = useEditorMode(); - - if (languagesLoadable.isLoading || namespacesLoadable.isLoading) { - return ; - } - - return ( -
    - {selectionLoadable.isFetching && } -
    - - - {languages && editorMode.data !== "dev" && ( - - )} - - - - {editorMode.data !== "dev" && ( - - - - - - )} - - } - rightPart={ -
    - {hasBranchingEnabled && currentBranch && ( - - {currentBranch} - - )} -
    setRoute("settings")} - role="button" - > - -
    -
    - } - /> -
    - - - {isBranchMissing && ( - - - }> - Branch "{currentBranch}" no longer exists. Please - select a different branch. - - - { - await branchesLoadable.refetch(); - }} - /> - - - )} - {error && ( - - } - style={{ cursor: "pointer" }} - onClick={() => setError(undefined)} - > - {error} - - - - )} - -
    - - {nothingSelected || selection.length === 0 ? ( - - - Select texts for translation -
    - (single texts or frames) -
    -
    - ) : ( - - )} -
    - ); -}; diff --git a/src/ui/views/Index/KeyInput.tsx b/src/ui/views/Index/KeyInput.tsx deleted file mode 100644 index 729981b4..00000000 --- a/src/ui/views/Index/KeyInput.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Textbox } from "@create-figma-plugin/ui"; -import { h } from "preact"; -import { memo } from "preact/compat"; - -type Props = { - value: string; - onChange: (value: string) => void; -}; - -export const KeyInput = memo(({ value, onChange }: Props) => { - return ( - { - onChange(e.currentTarget.value); - }} - variant="border" - style={{ - fontSize: 12, - height: 23, - }} - /> - ); -}); diff --git a/src/ui/views/Index/ListItem.tsx b/src/ui/views/Index/ListItem.tsx deleted file mode 100644 index 1eae4f6e..00000000 --- a/src/ui/views/Index/ListItem.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { h } from "preact"; -import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; -import { useDebounce } from "use-debounce"; -import { NodeInfo } from "@/types"; -import { NodeRow } from "@/ui/components/NodeList/NodeRow"; -import { KeyInput } from "./KeyInput"; -import { useSetNodesDataMutation } from "@/ui/hooks/useSetNodesDataMutation"; -import { NamespaceSelect } from "@/ui/components/NamespaceSelect/NamespaceSelect"; -import styles from "./Index.css"; -import { useGlobalActions, useGlobalState } from "@/ui/state/GlobalState"; -import { components } from "@/ui/client/apiSchema.generated"; -import { InsertLink } from "@/ui/icons/SvgIcons"; -import { KeyOptionsButton } from "../../components/KeyOptionsButton/KeyOptionsButton"; -import { usePrefilledKey } from "../../hooks/usePrefilledKey"; - -type UsedNamespaceModel = components["schemas"]["UsedNamespaceModel"]; - -type Props = { - node: NodeInfo; - loadedNamespaces: UsedNamespaceModel[] | undefined; - hasNamespacesEnabled: boolean; - onRefreshNamespaces?: () => void; -}; - -export const ListItem = ({ - node, - loadedNamespaces, - hasNamespacesEnabled, - onRefreshNamespaces, -}: Props) => { - const nodeId = node?.id ?? ""; - const tolgeeConfig = useGlobalState((c) => c.config); - - const prefilledKey = usePrefilledKey( - nodeId, - tolgeeConfig?.keyFormat ?? "", - tolgeeConfig?.variableCasing, - tolgeeConfig?.prefillKeyFormat ?? false, - ); - - const [keyName, setKeyName] = useState((node.key || prefilledKey.key) ?? ""); - const [debouncedKeyName] = useDebounce(keyName, 300); - - const defaultNamespace = useGlobalState((c) => c.config?.namespace); - const [namespace, setNamespace] = useState(node.ns ?? defaultNamespace); - - const setNodesDataMutation = useSetNodesDataMutation(); - - const { setRoute } = useGlobalActions(); - - useEffect(() => { - if (prefilledKey.key && !node.connected) { - setKeyName(prefilledKey.key); - } - }, [prefilledKey.key]); - - useEffect(() => { - if (node.connected) { - setKeyName(node.key || ""); - setNamespace(node.ns ?? defaultNamespace); - } - }, [node.connected, node.key, node.ns, defaultNamespace]); - - // Single debounced mutation for both key and namespace changes - const [debouncedNamespace] = useDebounce(namespace, 300); - - useEffect(() => { - if (node.connected) return; - const keyChanged = debouncedKeyName !== (node.key || ""); - const nsChanged = debouncedNamespace !== (node.ns ?? defaultNamespace); - if (keyChanged || nsChanged) { - setNodesDataMutation.mutate({ - nodes: [{ ...node, key: debouncedKeyName, ns: debouncedNamespace }], - }); - } - }, [ - debouncedKeyName, - debouncedNamespace, - node.id, - node.connected, - defaultNamespace, - ]); - - const handleConnect = () => { - setRoute("connect", { node }); - }; - - const namespaces = useMemo( - () => - Array.from( - new Set([ - ...(loadedNamespaces?.map((ns) => ns.name || "") || []), - defaultNamespace || "", - ]), - ), - [loadedNamespaces, defaultNamespace], - ); - - const handleKeyChange = useCallback((value: string) => { - setKeyName(value); - }, []); - - const handleNsChange = useCallback((value: string) => { - setNamespace(value); - }, []); - - return ( - - ) - } - nsComponent={ - !node.connected && - hasNamespacesEnabled && ( - - ) - } - action={ -
    -
    - {node.connected ? ( - - ) : ( - - )} -
    - -
    - } - /> - ); -}; diff --git a/src/ui/views/PageSetup/PageSetup.tsx b/src/ui/views/PageSetup/PageSetup.tsx deleted file mode 100644 index 22be8fb7..00000000 --- a/src/ui/views/PageSetup/PageSetup.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useApiQuery } from "@/ui/client/useQueryApi"; -import { ActionsBottom } from "@/ui/components/ActionsBottom/ActionsBottom"; -import { FullPageLoading } from "@/ui/components/FullPageLoading/FullPageLoading"; -import { TopBar } from "@/ui/components/TopBar/TopBar"; -import { useGlobalActions, useGlobalState } from "@/ui/state/GlobalState"; -import { CurrentPageSettings } from "@/types"; -import { - VerticalSpace, - Text, - Muted, - Container, - Button, - Divider, -} from "@create-figma-plugin/ui"; -import { Fragment, FunctionComponent, h } from "preact"; -import { useState } from "preact/hooks"; - -export const PageSetup: FunctionComponent = () => { - const config = useGlobalState((c) => c.config) || {}; - const { setRoute, setConfig } = useGlobalActions(); - - const [settings, setSettings] = useState< - Partial | undefined - >({ language: "" }); - - const languagesLoadable = useApiQuery({ - url: "/v2/projects/languages", - method: "get", - query: { - size: 1000, - }, - options: { - cacheTime: 0, - staleTime: 0, - }, - }); - - const languages = languagesLoadable.data?._embedded?.languages; - - const handleSubmit = async () => { - setConfig({ ...config, ...settings }); - setRoute("index"); - }; - - const handleClear = () => { - setConfig({ pageCopy: false }); - }; - - const validated = Boolean(settings?.language); - - if (languagesLoadable.isLoading) { - return ; - } - - return ( - - - Page setup
    } - rightPart={ - - } - /> - - - - - - Language - - - - - - - - - - ); -}; diff --git a/src/ui/views/Pull/Pull.css b/src/ui/views/Pull/Pull.css deleted file mode 100644 index 046a9bf6..00000000 --- a/src/ui/views/Pull/Pull.css +++ /dev/null @@ -1,13 +0,0 @@ -.list { - margin: 10px -16px; -} - -.missing { - color: var(--figma-color-icon-danger); -} - -.sectionTitle { - margin-bottom: -5px; - margin-top: 20px; - font-size: 12px; -} diff --git a/src/ui/views/Pull/Pull.css.d.ts b/src/ui/views/Pull/Pull.css.d.ts deleted file mode 100644 index 8aecd3fb..00000000 --- a/src/ui/views/Pull/Pull.css.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare const styles: { - readonly "list": string; - readonly "missing": string; - readonly "sectionTitle": string; -}; -export = styles; - diff --git a/src/ui/views/Pull/Pull.tsx b/src/ui/views/Pull/Pull.tsx deleted file mode 100644 index b3f5ed7a..00000000 --- a/src/ui/views/Pull/Pull.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { Fragment, FunctionalComponent, h } from "preact"; -import { useEffect, useMemo, useState } from "preact/hooks"; -import clsx from "clsx"; -import { - Banner, - Button, - Container, - Divider, - IconWarning32, - VerticalSpace, -} from "@create-figma-plugin/ui"; - -import { ActionsBottom } from "@/ui/components/ActionsBottom/ActionsBottom"; -import { FullPageLoading } from "@/ui/components/FullPageLoading/FullPageLoading"; -import { useGlobalActions } from "@/ui/state/GlobalState"; -import { NodeList } from "@/ui/components/NodeList/NodeList"; -import { getPullChanges } from "@/tools/getPullChanges"; - -import { TopBar } from "../../components/TopBar/TopBar"; -import { RouteParam } from "../routes"; -import styles from "./Pull.css"; -import { useConnectedNodes } from "@/ui/hooks/useConnectedNodes"; -import { useUpdateNodesMutation } from "@/ui/hooks/useUpdateNodesMutation"; -import { useHighlightNodeMutation } from "@/ui/hooks/useHighlightNodeMutation"; -import { useSetNodesDataMutation } from "@/ui/hooks/useSetNodesDataMutation"; -import { useAllTranslations } from "@/ui/hooks/useAllTranslations"; -import { useHasNamespacesEnabled } from "@/ui/hooks/useHasNamespacesEnabled"; -import { getPlaceholders, getTolgeeFormat } from "@tginternal/editor"; -import { createFormatIcu } from "../../../createFormatIcu"; -import { NodeInfo } from "../../../types"; -import { useGlobalState } from "@/ui/state/GlobalState"; - -type Props = RouteParam<"pull">; - -export const Pull: FunctionalComponent = ({ lang }) => { - const selectedNodes = useConnectedNodes({ ignoreSelection: false }); - const { setRoute, setLanguage } = useGlobalActions(); - const branch = useGlobalState((c) => c.config?.branch); - - const updateNodeLoadable = useUpdateNodesMutation(); - const setNodesDataMutation = useSetNodesDataMutation(); - const allTranslationsLoadable = useAllTranslations(); - const hasNamespacesEnabled = useHasNamespacesEnabled(); - const [diffData, setDiffData] = useState>(); - - const [error, setError] = useState(); - - async function computeDiff() { - try { - const translations = await allTranslationsLoadable.getData({ - language: lang ?? "", - branch: branch || undefined, - }); - setDiffData( - getPullChanges( - selectedNodes.data?.items || [], - lang, - translations, - hasNamespacesEnabled, - ), - ); - setError(undefined); - } catch (e) { - if (e === "invalid_project_api_key") { - setError("Invalid project API key"); - } else if (e === "too_many_uploaded_images") { - setError( - "Too many uploaded images. Disable update screenshots in settings.", - ); - } else { - setError(`Cannot get translation data. ${e}`); - } - } - } - - useEffect(() => { - computeDiff(); - }, [selectedNodes.data, lang, branch, hasNamespacesEnabled]); - - const formatter = useMemo(() => createFormatIcu(), [lang]); - - const getFormattedForNode = (n: NodeInfo) => { - const tolgeeValue = getTolgeeFormat(n.translation, n.isPlural, false); - const paramValues = { ...(n.paramsValues ?? {}) }; - const placeholders = getPlaceholders(n.translation); - - for (const placeholder of placeholders ?? []) { - if (paramValues[placeholder.name] == null) { - paramValues[placeholder.name] = placeholder.name; - } - } - - try { - const formatted = formatter.format({ - language: lang ?? "en", - translation: n.translation, - params: { - ...paramValues, - [tolgeeValue.parameter ?? ""]: n.pluralParamValue ?? "1", - }, - }); - return { - ...n, - formatted, - } as NodeInfo & { formatted: string }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - return { - ...n, - formatted: n.characters, - } as NodeInfo & { formatted: string }; - } - }; - - const handleProcess = async () => { - if (diffData!.changedNodes.length !== 0) { - await updateNodeLoadable.mutateAsync({ - nodes: diffData!.changedNodes.map(getFormattedForNode), - }); - } - - await setNodesDataMutation.mutateAsync({ - nodes: - selectedNodes.data?.items - .filter((i) => i.connected) - .map((n) => { - const changedNode = diffData!.changedNodes.find( - (c) => c.key === n.key, - ); - if (!changedNode) { - return n; - } - const characters = getFormattedForNode(changedNode).formatted; - const changed = { - ...n, - characters, - isPlural: changedNode?.isPlural ?? n.isPlural, - pluralParamValue: - changedNode?.pluralParamValue ?? n.pluralParamValue, - translation: changedNode?.translation ?? n.translation, - paramsValues: changedNode?.paramsValues ?? n.paramsValues, - connected: true, - }; - return changed; - }) ?? [], - }); - setLanguage(lang); - setRoute("index"); - }; - - const handleGoBack = () => { - setRoute("index"); - }; - - const handleRepeat = () => { - computeDiff(); - }; - - const highlightNode = useHighlightNodeMutation(); - - const isLoading = - allTranslationsLoadable.isLoading || selectedNodes.isLoading; - - return ( - - Pull translations from Tolgee ({lang})
    } - /> - - - - {error ? ( - }>{error} - ) : isLoading || !diffData ? ( - - ) : allTranslationsLoadable.error ? ( - -
    - {allTranslationsLoadable.error || "Cannot get translation data."} -
    - - - -
    - ) : ( - -
    - {diffData.changedNodes.length === 0 - ? "Everything up to date" - : `This action will replace translations in ${diffData.changedNodes.length} text(s).`} -
    - {diffData.missingKeys.length > 0 && ( - -
    Missing keys:
    -
    - highlightNode.mutate({ id: item.id })} - compact - /> -
    -
    - )} - - {diffData.changedNodes.length === 0 ? ( - - ) : ( - - - - - )} - -
    - )} -
    - - ); -}; diff --git a/src/ui/views/Push/Changes.css b/src/ui/views/Push/Changes.css deleted file mode 100644 index 1bb726f5..00000000 --- a/src/ui/views/Push/Changes.css +++ /dev/null @@ -1,44 +0,0 @@ -.sectionTitle { - margin-bottom: -3px; - margin-top: 10px; - font-size: 14px; - font-weight: bold; -} - -.new { - color: var(--figma-color-text-success); -} - -.change { - color: var(--figma-color-text-warning); -} - -.container { - margin-top: -10px; - display: grid; - gap: 10px 10px; - grid-template-columns: auto 1fr; - align-items: center; -} - -.list { - margin: 10px -16px; -} - -.rowText { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; -} - -.rowKey { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - display: flex; - align-items: center; -} - -.spanAll { - grid-column: 1 / -1; -} diff --git a/src/ui/views/Push/Changes.css.d.ts b/src/ui/views/Push/Changes.css.d.ts deleted file mode 100644 index bdea3157..00000000 --- a/src/ui/views/Push/Changes.css.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare const styles: { - readonly "change": string; - readonly "container": string; - readonly "list": string; - readonly "new": string; - readonly "rowKey": string; - readonly "rowText": string; - readonly "sectionTitle": string; - readonly "spanAll": string; -}; -export = styles; - diff --git a/src/ui/views/Push/Changes.tsx b/src/ui/views/Push/Changes.tsx deleted file mode 100644 index 9c289951..00000000 --- a/src/ui/views/Push/Changes.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Fragment, h } from "preact"; -import clsx from "clsx"; - -import { KeyChanges } from "@/tools/getPushChanges"; -import { NodeList } from "@/ui/components/NodeList/NodeList"; - -import styles from "./Changes.css"; - -type Props = { - changes: KeyChanges; -}; - -export const Changes = ({ changes }: Props) => { - return ( - - {changes.newKeys.length > 0 && ( - -
    - New keys -
    -
    - ({ - id: k.key, - key: k.key, - ns: k.ns, - characters: k.newValue, - }))} - /> -
    -
    - )} - {changes.changedKeys.length > 0 && ( - -
    - Changed keys -
    -
    - ({ - id: k.key, - key: k.key, - ns: k.ns, - characters: k.newValue, - }))} - /> -
    -
    - )} -
    - ); -}; diff --git a/src/ui/views/Push/Push.tsx b/src/ui/views/Push/Push.tsx deleted file mode 100644 index 1875dfec..00000000 --- a/src/ui/views/Push/Push.tsx +++ /dev/null @@ -1,594 +0,0 @@ -import { Fragment, FunctionalComponent, h } from "preact"; -import { useEffect, useMemo, useState } from "preact/hooks"; -import { - Banner, - Button, - Checkbox, - Container, - Divider, - IconWarning32, - Text, - VerticalSpace, -} from "@create-figma-plugin/ui"; - -import { components } from "@/ui/client/apiSchema.generated"; -import { useApiMutation } from "@/ui/client/useQueryApi"; -import { ActionsBottom } from "@/ui/components/ActionsBottom/ActionsBottom"; -import { FullPageLoading } from "@/ui/components/FullPageLoading/FullPageLoading"; -import { useGlobalActions, useGlobalState } from "@/ui/state/GlobalState"; -import { - getPushChanges, - KeyChanges, - KeyChangeValue, -} from "@/tools/getPushChanges"; -import { TopBar } from "../../components/TopBar/TopBar"; -import { Changes } from "./Changes"; -import { FrameScreenshot, NodeInfo } from "@/types"; -import { compareNs } from "@/tools/compareNs"; -import { getScreenshotsEndpoint } from "@/main/endpoints/getScreenshots"; -import { useConnectedNodes } from "@/ui/hooks/useConnectedNodes"; -import { useSetNodesDataMutation } from "@/ui/hooks/useSetNodesDataMutation"; -import { useAllTranslations } from "@/ui/hooks/useAllTranslations"; -import { useHasNamespacesEnabled } from "../../hooks/useHasNamespacesEnabled"; - -type SingleStepImportResolvableItemRequest = - components["schemas"]["SingleStepImportResolvableItemRequest"]; -type KeyScreenshotDto = components["schemas"]["KeyScreenshotDto"]; -type SimpleImportConflictResult = - components["schemas"]["SimpleImportConflictResult"]; - -export const Push: FunctionalComponent = () => { - const language = useGlobalState((c) => c.config!.language!); - const branch = useGlobalState((c) => c.config?.branch); - const { setRoute } = useGlobalActions(); - const [error, setError] = useState(false); - const [errorMessage, setErrorMessage] = useState(); - const [success, setSuccess] = useState(false); - const [isPushing, setIsPushing] = useState(false); - const [_loadingStatus, setLoadingStatus] = useState(); - const [changes, setChanges] = useState(); - const [pushedKeysCount, setPushedKeysCount] = useState(0); - const [unresolvedConflicts, setUnresolvedConflicts] = useState< - SimpleImportConflictResult[] - >([]); - const selectedNodes = useConnectedNodes({ ignoreSelection: false }); - const tolgeeConfig = useGlobalState((c) => c.config); - const [uploadedScreenshotCount, setUploadedScreenshotCount] = useState(0); - - const nodes = selectedNodes.data?.items ?? []; - - const [uploadScreenshots, setUploadScreenshots] = useState(true); - - // Optimize deduplication from O(n²) to O(n) using Set - const deduplicatedNodes = useMemo(() => { - const seen = new Set(); - const deduplicatedNodes: NodeInfo[] = []; - for (const node of nodes) { - const key = `${node.key}:${node.ns || ""}`; - if (!seen.has(key)) { - seen.add(key); - deduplicatedNodes.push(node); - } - } - return deduplicatedNodes; - }, [nodes]); - - const allTranslationsLoadable = useAllTranslations(); - const hasNamespacesEnabled = useHasNamespacesEnabled(); - - // Create a stable key from nodes to detect changes (including namespace changes) - const nodesKey = useMemo( - () => nodes.map((n) => `${n.id}:${n.key}:${n.ns || ""}`).join("|"), - [nodes], - ); - - // Extract unique namespaces from nodes being pushed (performance optimization) - // Only load translations for namespaces actually used by the nodes - // This is computed inside the effect to avoid dependency issues - const requiredNamespacesKey = useMemo(() => { - if (!hasNamespacesEnabled) return ""; - const uniqueNamespaces = Array.from( - new Set( - deduplicatedNodes - .map((node) => node.ns) - .filter((ns): ns is string => Boolean(ns)), - ), - ); - return uniqueNamespaces.sort().join(","); - }, [deduplicatedNodes, hasNamespacesEnabled]); - - // Memoize screenshots key to avoid regenerating screenshots unnecessarily - const screenshotsKey = useMemo( - () => - tolgeeConfig?.updateScreenshots - ? nodes.map((n) => `${n.id}:${n.key}:${n.ns || ""}`).join("|") - : "", - [nodes, tolgeeConfig?.updateScreenshots], - ); - - // Extract stable primitive values from tolgeeConfig to avoid object reference issues - const tolgeeConfigTags = useMemo( - () => JSON.stringify([...(tolgeeConfig?.tags ?? [])].sort()), - [tolgeeConfig?.tags], - ); - const tolgeeConfigUpdateScreenshots = tolgeeConfig?.updateScreenshots ?? true; - const tolgeeConfigAddTags = tolgeeConfig?.addTags ?? false; - - useEffect(() => { - // Don't recompute if we're showing success screen - if (success) { - return; - } - - let cancelled = false; - - async function computeDiff() { - setLoadingStatus("Loading translations"); - try { - // Compute requiredNamespaces inside the effect to avoid dependency issues - // Include all namespaces from nodes, including empty string for default namespace - const requiredNamespaces = - hasNamespacesEnabled && deduplicatedNodes.length > 0 - ? Array.from( - new Set(deduplicatedNodes.map((node) => node.ns ?? "")), - ) - : undefined; - - const translations = await allTranslationsLoadable.getData({ - language, - namespaces: requiredNamespaces, - branch: branch || undefined, - }); - - // Check if cancelled before expensive screenshot operation - if (cancelled) return; - - const screenshots = tolgeeConfigUpdateScreenshots - ? await getScreenshotsEndpoint.call(nodes) - : []; - - // Check if cancelled before setting state - if (cancelled) return; - - // Reconstruct tolgeeConfig object for getPushChanges - const configForDiff = { - ...tolgeeConfig, - updateScreenshots: tolgeeConfigUpdateScreenshots, - addTags: tolgeeConfigAddTags, - }; - - setChanges( - getPushChanges( - deduplicatedNodes, - translations, - hasNamespacesEnabled, - screenshots, - configForDiff, - ), - ); - } catch (e) { - if (cancelled) return; - if (e === "invalid_project_api_key") { - setErrorMessage("Invalid API key"); - } else { - setErrorMessage(`Cannot get translation data. ${e}`); - } - } finally { - if (!cancelled) { - setLoadingStatus(undefined); - } - } - } - - computeDiff(); - - // Cleanup: cancel if dependencies change before async operations complete - return () => { - cancelled = true; - }; - }, [ - nodesKey, - screenshotsKey, - hasNamespacesEnabled, - language, - branch, - requiredNamespacesKey, - tolgeeConfigTags, - tolgeeConfigUpdateScreenshots, - tolgeeConfigAddTags, - success, // Include success to prevent recompute when showing success screen - // Note: allTranslationsLoadable.getData is stable via useCallback with refs - // We don't include allTranslationsLoadable itself to avoid infinite loops - ]); - - const totalScreenshotCount = useMemo(() => { - return changes?.screenshots.length || 0; - }, [changes]); - - const setNodesDataMutation = useSetNodesDataMutation(); - - const loadingStatus = - allTranslationsLoadable.isLoading || selectedNodes.isLoading - ? "Loading data and generating screenshots" - : _loadingStatus; - - const updateTranslations = useApiMutation({ - url: "/v2/projects/single-step-import-resolvable", - method: "post", - }); - - const addTagsToKeys = useApiMutation({ - url: "/v2/projects/tag-complex", - method: "put", - }); - - const uploadImage = useApiMutation({ - url: "/v2/image-upload", - method: "post", - }); - - const bigMeta = useApiMutation({ - url: "/v2/projects/big-meta", - method: "post", - }); - - const handleGoBack = () => { - setRoute("index"); - }; - - const connectNodes = async () => { - await setNodesDataMutation.mutateAsync({ - nodes: nodes.map((n) => ({ - ...n, - translation: - changes?.changedKeys.find( - (k) => k.key === n.key && compareNs(k.ns, n.ns), - )?.newValue ?? n.translation, - connected: true, - })), - }); - }; - - const handleConnectOnly = async () => { - await connectNodes(); - setRoute("index"); - }; - - const handleSubmit = async () => { - const keys: SingleStepImportResolvableItemRequest[] = []; - - if (!changes) { - return; - } - - setUploadedScreenshotCount(0); - setIsPushing(true); - - try { - const screenshotsMap = new Map(); - const requiredScreenshots = uploadScreenshots ? changes.screenshots : []; - - for (const [i, screenshot] of requiredScreenshots.entries()) { - setLoadingStatus( - `Uploading images (${i + 1}/${requiredScreenshots.length})`, - ); - const imageBlob = new Blob([screenshot.image.buffer as ArrayBuffer], { - type: "image/png", - }); - const location = `figma-${screenshot.info.id}`; - const infoBlob = new Blob([JSON.stringify({ location })], { - type: "application/json", - }); - const data = await uploadImage.mutateAsync({ - content: { - "multipart/form-data": { - image: imageBlob as any, - info: infoBlob as any, - }, - }, - }); - screenshotsMap.set(screenshot, data.id); - } - - const mapScreenshots = (item: KeyChangeValue): KeyScreenshotDto[] => { - const result: KeyScreenshotDto[] = []; - if (uploadScreenshots) { - item.screenshots.forEach((screenshot) => { - const relevantNodes = screenshot.keys.filter( - ({ key, ns }) => key === item.key && compareNs(ns, item.ns), - ); - - result.push({ - text: item.newValue, - uploadedImageId: screenshotsMap.get(screenshot)!, - positions: relevantNodes.map(({ x, y, width, height }) => { - return { - x, - y, - width, - height, - }; - }), - }); - }); - } - return result; - }; - - setLoadingStatus("Updating keys"); - - changes.unchangedKeys.forEach((item) => { - keys.push({ - name: item.key, - namespace: item.ns || undefined, - screenshots: mapScreenshots(item), - translations: {}, - }); - }); - - changes.changedKeys.forEach((item) => { - keys.push({ - name: item.key, - namespace: item.ns || undefined, - screenshots: mapScreenshots(item), - translations: { - [language]: { - text: item.newValue, - resolution: "OVERRIDE", - }, - }, - }); - }); - - changes.newKeys.forEach((item) => { - keys.push({ - name: item.key, - namespace: item.ns || undefined, - screenshots: mapScreenshots(item), - translations: { - [language]: { text: item.newValue, resolution: "OVERRIDE" }, - }, - }); - }); - - const updateResult = await updateTranslations.mutateAsync({ - content: { - "application/json": { - keys, - errorOnUnresolvedConflict: false, - overrideMode: "RECOMMENDED", - branch: branch || undefined, - }, - }, - }); - - const conflicts = updateResult?.unresolvedConflicts ?? []; - setUnresolvedConflicts(conflicts); - - try { - // Add tags to keys - if ( - (tolgeeConfig?.addTags ?? false) && - tolgeeConfig?.tags && - tolgeeConfig.tags.length > 0 - ) { - await addTagsToKeys.mutateAsync({ - query: { branch: branch || undefined }, - content: { - "application/json": { - tagFiltered: tolgeeConfig?.tags ?? [], - filterKeys: [ - ...changes.newKeys, - ...changes.unchangedKeys, - ...changes.changedKeys, - ].map((k) => ({ - name: k.key, - namespace: k.ns || undefined, - })), - }, - }, - }); - } - } catch (e) { - setErrorMessage( - `Error adding tags. ${e}. Translations were still updated.`, - ); - } - - if ((tolgeeConfig?.updateScreenshots ?? true) && uploadScreenshots) { - for (const screenshot of changes.screenshots.values()) { - try { - const relatedKeys = screenshot.keys - .map((data) => ({ - keyName: data.key, - namespace: data.ns || undefined, - })) - .slice(0, 100); - await bigMeta.mutateAsync({ - query: { branch: branch || undefined }, - content: { - "application/json": { - relatedKeysInOrder: relatedKeys, - }, - }, - }); - setUploadedScreenshotCount((c) => c + 1); - } catch (e) { - if (e === "too_many_uploaded_images") { - setErrorMessage( - "Too many uploaded images. Disable update screenshots in settings. Translations were still updated.", - ); - } else { - setErrorMessage( - `Error updating screenshots. ${e}. Translations were still updated.`, - ); - } - } - } - } - - // Store the count of keys that were pushed before clearing cache - const keysPushed = changes.newKeys.length + changes.changedKeys.length; - setPushedKeysCount(keysPushed); - - // Await so local Figma node data is updated (and the connected-nodes - // query is invalidated) before we leave the view. Otherwise navigating - // back to Push quickly can see stale node data within the 30s staleTime - // window and re-show the changes that were just pushed. - await connectNodes(); - - // Clear translations cache so newly pushed keys are recognized on next check - allTranslationsLoadable.clearCache(); - - setSuccess(true); - } catch (e) { - setError(true); - if (e === "invalid_project_api_key") { - setErrorMessage("Invalid project API key"); - } else if (e === "too_many_uploaded_images") { - setErrorMessage( - "Too many uploaded images. Disable update screenshots in settings.", - ); - } else if (e === "import_keys_error") { - setErrorMessage("Error pushing translations. Please try again."); - } else { - setErrorMessage(`Cannot push translations. ${e}`); - } - console.error(e); - } finally { - setIsPushing(false); - setLoadingStatus(undefined); - } - }; - - const handleRepeat = () => { - setError(false); - setSuccess(false); - setPushedKeysCount(0); - setUploadedScreenshotCount(0); - setErrorMessage(undefined); - setUnresolvedConflicts([]); - }; - - const isLoading = - allTranslationsLoadable.isLoading || - updateTranslations.isLoading || - uploadImage.isLoading; - - // Use pushedKeysCount if we're showing success (after push), otherwise use current changes - const changesSize = success - ? pushedKeysCount - : changes - ? changes.changedKeys.length + changes.newKeys.length - : 0; - - const noChanges = changesSize === 0; - - return ( - - Push translations to Tolgee ({language})
    } - /> - - - - {errorMessage && !error ? ( - }>{errorMessage} - ) : success ? ( - // Show success screen immediately, don't wait for recompute - -
    - Successfully updated{" "} - {Math.max(pushedKeysCount - unresolvedConflicts.length, 0)} key(s) - {uploadScreenshots - ? ` and uploaded ${uploadedScreenshotCount} screenshot(s).` - : "."} -
    - {unresolvedConflicts.length > 0 && ( - - - }> -
    - These translations could not be updated because they are - either disabled or already reviewed: -
    - -
      - {unresolvedConflicts.map((c, i) => ( -
    • - {c.keyNamespace ? `${c.keyNamespace}:` : ""} - {c.keyName} ({c.language}) -
    • - ))} -
    -
    -
    - )} - - - -
    - ) : isLoading || !changes || isPushing ? ( - - ) : error ? ( - -
    - }> - {errorMessage || - updateTranslations.error || - "An error has occurred during push."} - -
    - - - -
    - ) : ( - - {totalScreenshotCount !== 0 && ( - - - setUploadScreenshots(Boolean(e.currentTarget.checked)) - } - > - Upload {totalScreenshotCount} screenshot(s) - - - - )} - - {noChanges &&
    No changes necessary
    } - - - {noChanges && - (totalScreenshotCount === 0 || !uploadScreenshots) ? ( - - ) : ( - - )} - -
    - )} -
    - - ); -}; diff --git a/src/ui/views/Router.tsx b/src/ui/views/Router.tsx deleted file mode 100644 index ecc83fe6..00000000 --- a/src/ui/views/Router.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Fragment, h } from "preact"; -import { Banner, IconWarning32 } from "@create-figma-plugin/ui"; - -import { Route } from "./routes"; -import { Index } from "./Index/Index"; -import { Settings } from "./Settings/Settings"; -import { useGlobalActions, useGlobalState } from "../state/GlobalState"; -import { Push } from "./Push/Push"; -import { Pull } from "./Pull/Pull"; -import { Connect } from "./Connect/Connect"; -import { PageSetup } from "./PageSetup/PageSetup"; -import { CreateCopy } from "./CreateCopy/CreateCopy"; -import { CopyView } from "./CopyView/CopyView"; -import { Dialog } from "../components/Dialog/Dialog"; -import { StringDetails } from "./StringDetails/StringDetails"; -import { ResizeHandle } from "../components/ResizeHandle/ResizeHandle"; -import { useEditorMode } from "../hooks/useEditorMode"; - -const getDialogPage = ([routeKey, routeData]: Route) => { - switch (routeKey) { - case "connect": - return ; - - default: - return null; - } -}; - -type PageProps = { - route: Route; - setRoute: (...route: Route) => void; -}; - -const Page = ({ route: [routeKey, routeData], setRoute }: PageProps) => { - switch (routeKey) { - case "settings": - return ; - case "push": - return ; - case "pull": - return ; - case "create_copy": - return ; - case "string_details": - return ; - } - - const dialogPage = getDialogPage([routeKey, routeData] as Route); - - return ( - -
    - -
    - {dialogPage && ( - setRoute("index")}>{dialogPage} - )} -
    - ); -}; - -export const Router = () => { - const route = useGlobalState((c) => c.route); - const routeKey = useGlobalState((c) => c.routeKey); - const globalError = useGlobalState((c) => c.globalError); - const documentInfo = useGlobalState((c) => c.config?.documentInfo); - const pageInfo = useGlobalState((c) => c.config?.pageInfo); - const pageCopy = useGlobalState((c) => c.config?.pageCopy); - const pageStringDetails = useGlobalState((c) => c.config?.pageStringDetails); - const pageStringDetailsNodeInfo = useGlobalState((c) => c.config?.nodeInfo); - const { setRoute } = useGlobalActions(); - const editorMode = useEditorMode(); - - const forceSettings = !pageCopy && !documentInfo; - const errorOnTop = !forceSettings && routeKey !== "settings"; - - const handleResolveError = () => { - setRoute("settings"); - }; - - return ( - - {globalError && errorOnTop && ( - } - > - {globalError} - - )} - {pageCopy ? ( - - ) : pageStringDetails ? ( - - ) : forceSettings ? ( - - ) : !pageInfo ? ( - - ) : ( - - )} - {editorMode.data !== "dev" && } - - ); -}; diff --git a/src/ui/views/Settings/Expandable.tsx b/src/ui/views/Settings/Expandable.tsx deleted file mode 100644 index c006d9b1..00000000 --- a/src/ui/views/Settings/Expandable.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { FunctionComponent, h } from "preact"; -import { useState } from "preact/hooks"; -import { ChevronDown } from "../../icons/SvgIcons"; - -export const Expandable: FunctionComponent<{ - title: string; - dataCy?: string; - defaultOpen?: boolean; -}> = ({ title, defaultOpen = false, dataCy, children }) => { - const [open, setOpen] = useState(defaultOpen); - return ( -
    -
    setOpen((v) => !v)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - setOpen((v) => !v); - } - }} - tabIndex={0} - role="button" - aria-expanded={open} - aria-controls={`expandable-content-${title - .replace(/\s+/g, "-") - .toLowerCase()}`} - > - {title} -
    - {open && ( -
    - {children} -
    - )} -
    - ); -}; diff --git a/src/ui/views/Settings/ProjectSection.tsx b/src/ui/views/Settings/ProjectSection.tsx deleted file mode 100644 index 6cfdc7bb..00000000 --- a/src/ui/views/Settings/ProjectSection.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { h, FunctionComponent, Fragment } from "preact"; -import { - Text, - Muted, - VerticalSpace, - Textbox, - Button, - Bold, -} from "@create-figma-plugin/ui"; -import { ProjectSettings } from "./ProjectSettings"; -import styles from "./Settings.css"; -import { CheckCircle } from "@/ui/icons/SvgIcons"; -import { TolgeeConfig } from "@/types"; - -export interface ProjectSectionProps { - showHeadline: boolean; - tolgeeConfig: Partial & { apiUrl: string }; - setTolgeeConfig: (c: Partial & { apiUrl: string }) => void; - validated: boolean; - projectName: string | undefined; - projectId: number | undefined; - setValidated: (v: boolean) => void; - handleValidate: () => void; - error?: string; -} - -export const ProjectSection: FunctionComponent = ({ - showHeadline, - tolgeeConfig, - setTolgeeConfig, - validated, - projectName, - projectId, - setValidated, - handleValidate, -}) => ( - - {showHeadline && ( - - - Project - - - )} - - - Tolgee URL - - - { - setValidated(false); - setTolgeeConfig({ ...tolgeeConfig, apiUrl }); - }} - value={tolgeeConfig.apiUrl} - variant="border" - /> - - - Tolgee Project API key - - - { - setValidated(false); - setTolgeeConfig({ ...tolgeeConfig, apiKey }); - }} - value={tolgeeConfig.apiKey ?? ""} - variant="border" - /> - - {validated ? ( - - {" "} -
    - {projectName} - {" "} - was successfully connected - - ) : ( - - )} - {validated && ( - setTolgeeConfig({ ...tolgeeConfig, ...data })} - initialData={tolgeeConfig} - /> - )} - -); diff --git a/src/ui/views/Settings/ProjectSettings.css b/src/ui/views/Settings/ProjectSettings.css deleted file mode 100644 index 0b74e503..00000000 --- a/src/ui/views/Settings/ProjectSettings.css +++ /dev/null @@ -1,20 +0,0 @@ -.namespacesRow { - display: flex; - align-items: center; - gap: 30px; -} -.namespaceShowRow { - display: flex; - align-items: center; - gap: 8px; -} -.branchingRow { - display: flex; - align-items: center; - gap: 8px; -} -.branchingShowRow { - display: flex; - align-items: center; - gap: 8px; -} diff --git a/src/ui/views/Settings/ProjectSettings.css.d.ts b/src/ui/views/Settings/ProjectSettings.css.d.ts deleted file mode 100644 index 4892406c..00000000 --- a/src/ui/views/Settings/ProjectSettings.css.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare const styles: { - readonly "branchingRow": string; - readonly "branchingShowRow": string; - readonly "namespaceShowRow": string; - readonly "namespacesRow": string; -}; -export = styles; - diff --git a/src/ui/views/Settings/ProjectSettings.tsx b/src/ui/views/Settings/ProjectSettings.tsx deleted file mode 100644 index 362c0a14..00000000 --- a/src/ui/views/Settings/ProjectSettings.tsx +++ /dev/null @@ -1,391 +0,0 @@ -import { useApiQuery } from "@/ui/client/useQueryApi"; -import { FullPageLoading } from "@/ui/components/FullPageLoading/FullPageLoading"; -import { NamespaceSelect } from "@/ui/components/NamespaceSelect/NamespaceSelect"; -import { BranchSelect } from "@/ui/components/BranchSelect/BranchSelect"; -import { TolgeeConfig } from "@/types"; -import { VerticalSpace, Text, Muted } from "@create-figma-plugin/ui"; -import { h, Fragment, FunctionComponent } from "preact"; -import { useEffect, useMemo, useState } from "preact/hooks"; -import styles from "./ProjectSettings.css"; -import { InfoTooltip } from "../../components/InfoTooltip/InfoTooltip"; -import { useHasNamespacesEnabled } from "../../hooks/useHasNamespacesEnabled"; -import { useHasBranchingEnabled } from "../../hooks/useHasBranchingEnabled"; -import { getProjectIdFromApiKey } from "../../client/decodeApiKey"; -import { Branch } from "../../icons/SvgIcons"; - -type Props = { - apiKey: string; - apiUrl: string; - initialData?: Partial; - onChange: (data: Partial) => void; -}; - -const namespaceHelpText = ({ - apiUrl, - projectId, -}: { - apiUrl: string; - projectId: number; -}) => ( - -
    - Namespaces are enabled in Platform. -
    - Disable them{" "} - - here - {" "} - to no longer user namespaces. -
    - For more information about namespaces, -
    - check the{" "} - - docs of the platform - - . -
    -
    -); -const namespaceHelpTextSetUp = ({ - apiUrl, - projectId, -}: { - apiUrl: string; - projectId: number; -}) => ( - -
    - Namespaces are disabled in Platform. -
    - Enable them{" "} - - here - {" "} - to use namespaces. -
    -
    -); - -const branchingHelpTextSetUp = ({ - apiUrl, - projectId, -}: { - apiUrl: string; - projectId: number; -}) => ( - -
    - Branching is disabled in Platform. -
    - Enable it{" "} - - here - {" "} - to use branches. -
    -
    -); - -const branchingHelpText = () => ( - -
    - This project uses branching.
    - Upload keys only to permitted branches.
    - - Read more about branching - - . -
    -
    -); - -export const ProjectSettings: FunctionComponent = ({ - apiKey, - apiUrl, - onChange, - initialData, -}) => { - const [settings, setSettings] = useState | undefined>( - undefined, - ); - - const languagesLoadable = useApiQuery({ - url: "/v2/projects/languages", - method: "get", - query: { - size: 1000, - }, - options: { - cacheTime: 0, - staleTime: 0, - }, - clientOptions: { - config: { - apiKey, - apiUrl, - }, - }, - }); - - const namespacesLoadable = useApiQuery({ - url: "/v2/projects/used-namespaces", - method: "get", - options: { - cacheTime: 0, - staleTime: 0, - }, - clientOptions: { - config: { - apiKey, - apiUrl, - }, - }, - }); - - const hasNamespacesEnabled = useHasNamespacesEnabled(); - const hasBranchingEnabled = useHasBranchingEnabled(); - - const apiKeyInfo = useApiQuery({ - url: "/v2/api-keys/current", - method: "get", - options: { - enabled: hasBranchingEnabled, - cacheTime: 60000, - staleTime: 60000, - }, - clientOptions: { - config: { - apiKey, - apiUrl, - }, - }, - }); - - const branchesLoadable = useApiQuery({ - url: "/v2/projects/{projectId}/branches", - method: "get", - path: { - projectId: apiKeyInfo.data?.projectId ?? 0, - }, - options: { - cacheTime: 0, - staleTime: 0, - enabled: hasBranchingEnabled && apiKeyInfo.data?.projectId !== undefined, - }, - clientOptions: { - config: { - apiKey, - apiUrl, - }, - }, - }); - - const branches = useMemo( - () => branchesLoadable.data?._embedded?.branches ?? [], - [branchesLoadable.data], - ); - - const languages = languagesLoadable.data?._embedded?.languages; - const namespaces = useMemo(() => { - const ns = namespacesLoadable.data?._embedded?.namespaces?.map( - (n) => n.name || "", - ) ?? [""]; - - if (ns.length === 0) { - ns.push(""); - } - - if (settings?.namespace !== undefined && !ns.includes(settings.namespace)) { - ns.push(settings.namespace); - } - - // Sort namespaces: keep empty string (implicit default) first, then alphabetically - ns.sort((a, b) => { - if (a === "" || !a) return -1; - if (b === "" || !b) return 1; - return a.localeCompare(b); - }); - - return ns; - }, [namespacesLoadable.data, settings?.namespace]); - - const branchNames = useMemo(() => branches.map((b) => b.name), [branches]); - - useEffect(() => { - if (!namespacesLoadable.data || !languagesLoadable.data || settings) { - return; - } - if (hasBranchingEnabled && branchesLoadable.isLoading) { - return; - } - - const defaultBranch = branches.find((b) => b.isDefault)?.name; - const savedBranch = - initialData?.branch && branchNames.includes(initialData.branch) - ? initialData.branch - : undefined; - - setSettings({ - language: - initialData?.language || languages?.find((l) => l.base)?.tag || "", - namespace: initialData?.namespace ?? namespaces?.[0] ?? "", - branch: hasBranchingEnabled ? (savedBranch ?? defaultBranch) : undefined, - }); - }, [ - branchNames, - branches, - branchesLoadable.isLoading, - hasBranchingEnabled, - languages, - namespaces, - settings, - ]); - - useEffect(() => { - if (settings) { - onChange(settings); - } - }, [settings]); - - if (languagesLoadable.isLoading || namespacesLoadable.isLoading) { - return ; - } - - return ( - - - - Current language - - - - - {!hasNamespacesEnabled && ( - -
    - Namespaces are disabled - - {namespaceHelpTextSetUp({ - apiUrl, - projectId: getProjectIdFromApiKey(apiKey) ?? 0, - })} - -
    - -
    - )} - {hasNamespacesEnabled && ( - - -
    - - Default namespace - - - {namespaceHelpText({ - apiUrl, - projectId: getProjectIdFromApiKey(apiKey) ?? 0, - })} - -
    - -
    - - setSettings((settings) => ({ - ...settings!, - namespace, - })) - } - /> -
    -
    - )} - {!hasBranchingEnabled && ( - - -
    - - Branching is disabled - - - {branchingHelpTextSetUp({ - apiUrl, - projectId: getProjectIdFromApiKey(apiKey) ?? 0, - })} - -
    - -
    - )} - {hasBranchingEnabled && ( - - -
    - - - Branch - - {branchingHelpText()} -
    - -
    - - setSettings((settings) => ({ - ...settings!, - branch, - })) - } - onRefresh={async () => { - await branchesLoadable.refetch(); - }} - /> -
    -
    - )} -
    - ); -}; diff --git a/src/ui/views/Settings/PushSection.tsx b/src/ui/views/Settings/PushSection.tsx deleted file mode 100644 index 3bf9c8e1..00000000 --- a/src/ui/views/Settings/PushSection.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { h, FunctionComponent, Fragment } from "preact"; -import { useEffect, useState, useMemo } from "preact/hooks"; -import { Bold, Checkbox, Text, VerticalSpace } from "@create-figma-plugin/ui"; -import { TolgeeConfig } from "@/types"; -import { AutocompleteSelect } from "../../components/AutocompleteSelect/AutocompleteSelect"; -import { useAllTags } from "../../hooks/useAllTags"; - -export interface PushSectionProps { - showHeadline: boolean; - tolgeeConfig: Partial & { apiUrl: string }; - setTolgeeConfig: (c: Partial & { apiUrl: string }) => void; - onTagsChange?: (tags: string[]) => void; -} - -export const PushSection: FunctionComponent = ({ - showHeadline, - tolgeeConfig, - setTolgeeConfig, - onTagsChange, -}) => { - const [tags, setTags] = useState(tolgeeConfig.tags || []); - - const allTags = useAllTags(); - - // Get tag names as options - const tagOptions = useMemo(() => { - return allTags.tagsData - .map((t) => t.name) - .sort((a, b) => a.localeCompare(b)); - }, [allTags.tagsData]); - - useEffect(() => { - allTags.getData().catch((e) => { - console.error("Error loading tags", e); - }); - }, []); - - useEffect(() => { - setTolgeeConfig({ ...tolgeeConfig, tags }); - if (onTagsChange) onTagsChange(tags); - }, [tags]); - - return ( - -
    - {showHeadline && ( - - - Push - - - )} - - - setTolgeeConfig({ - ...tolgeeConfig, - updateScreenshots: e.currentTarget.checked, - }) - } - > - Update screenshots - - - { - setTolgeeConfig({ - ...tolgeeConfig, - addTags: e.currentTarget.checked, - }); - allTags.getData(); - }} - > - Add tags - - {(tolgeeConfig.addTags ?? false) && ( -
    - - Tags - - -
    - )} -
    -
    - ); -}; diff --git a/src/ui/views/Settings/Settings.css b/src/ui/views/Settings/Settings.css deleted file mode 100644 index 315f6eb2..00000000 --- a/src/ui/views/Settings/Settings.css +++ /dev/null @@ -1,95 +0,0 @@ -.success { - display: flex; - align-items: center; - gap: 4px; - color: var(--figma-color-bg-brand); -} - -.tagAutocompleteDropdown { - position: absolute; - left: 0; - right: 0; - background: var(--figma-color-bg); - border: 1px solid var(--figma-color-border); - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07); - z-index: 2; - max-height: 180px; - overflow-y: auto; -} - -.tagAutocompleteDropdown.down { - top: 110%; -} -.tagAutocompleteDropdown.up { - bottom: 110%; -} - -.tagAutocompleteDropdownHeader { - padding: 12px 12px 0 12px; - font-size: 10px; -} -.tagAutocompleteDropdownHeaderEmpty { - padding: 12px; - font-size: 10px; -} - -.tagAutocompleteDropdownItem { - padding: 8px 12px; - cursor: pointer; - font-weight: 500; - background: none; - border: none; - color: var(--figma-color-text); -} -.tagAutocompleteDropdownItem:hover { - background: var(--figma-color-bg-hover); -} - -.tagAutocompleteDropdownAdd { - padding: 12px 12px 12px 12px; - color: var(--figma-color-bg-brand); - font-weight: 500; - cursor: pointer; - border-top: 1px solid var(--figma-color-border); - display: flex; - gap: 4px; - align-items: center; - justify-content: space-between; -} -.tagAutocompleteDropdownAdd > * { - cursor: pointer; -} - -.tagAutocompleteDropdownAddHint { - font-style: italic; - color: var(--figma-color-bg-brand); - font-weight: 400; -} - -.keyFormat { - display: flex; - gap: 12px; - margin-top: 8px; - justify-content: start; -} - -.keyFormat > * { - flex: 1; -} -.headerRow { - display: flex; - align-items: center; - gap: 8px; -} -.textLayers { - display: flex; - justify-content: start; - align-items: center; - gap: 10px; - margin-top: 4px; -} - -.sectionTitle { - font-size: 14px; -} diff --git a/src/ui/views/Settings/Settings.css.d.ts b/src/ui/views/Settings/Settings.css.d.ts deleted file mode 100644 index a13e62bd..00000000 --- a/src/ui/views/Settings/Settings.css.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -declare const styles: { - readonly "down": string; - readonly "headerRow": string; - readonly "keyFormat": string; - readonly "sectionTitle": string; - readonly "success": string; - readonly "tagAutocompleteDropdown": string; - readonly "tagAutocompleteDropdownAdd": string; - readonly "tagAutocompleteDropdownAddHint": string; - readonly "tagAutocompleteDropdownHeader": string; - readonly "tagAutocompleteDropdownHeaderEmpty": string; - readonly "tagAutocompleteDropdownItem": string; - readonly "textLayers": string; - readonly "up": string; -}; -export = styles; - diff --git a/src/ui/views/Settings/Settings.tsx b/src/ui/views/Settings/Settings.tsx deleted file mode 100644 index ca90c455..00000000 --- a/src/ui/views/Settings/Settings.tsx +++ /dev/null @@ -1,332 +0,0 @@ -import { h, Fragment, FunctionComponent } from "preact"; -import { useState, useEffect, useCallback } from "preact/hooks"; -import { - Banner, - Button, - Container, - Divider, - IconWarning32, - LoadingIndicator, - Modal, - VerticalSpace, -} from "@create-figma-plugin/ui"; - -import { useGlobalActions, useGlobalState } from "@/ui/state/GlobalState"; -import { useApiMutation } from "@/ui/client/useQueryApi"; -import { ActionsBottom } from "@/ui/components/ActionsBottom/ActionsBottom"; -import { TopBar } from "../../components/TopBar/TopBar"; -import { useQueryClient } from "react-query"; -import { Expandable } from "./Expandable"; -import { ProjectSection } from "./ProjectSection"; -import { PushSection } from "./PushSection"; -import { StringsSection } from "./StringsSection"; - -const DEFAULT_TOLGEE_URL = "https://app.tolgee.io"; - -type Props = { - noNavigation?: boolean; -}; - -export const Settings: FunctionComponent = ({ noNavigation }) => { - const queryClient = useQueryClient(); - const config = useGlobalState((c) => c.config) || {}; - const [tolgeeConfig, setTolgeeConfig] = useState({ - apiUrl: DEFAULT_TOLGEE_URL, - ...config, - }); - const [setupStep, setSetupStep] = useState< - "project" | "strings" | "push" | null - >(noNavigation ? "project" : null); - - const { mutateAsync, isLoading } = useApiMutation({ - url: "/v2/api-keys/current", - method: "get", - clientOptions: { - config: { - apiKey: tolgeeConfig.apiKey, - apiUrl: tolgeeConfig.apiUrl, - apiTimeout: 15000, - }, - }, - }); - - const [validated, setValidated] = useState(false); - - const { setRoute, setConfig, resetConfig } = useGlobalActions(); - - const [error, setError] = useState(); - - useEffect(() => { - setError(undefined); - }, [tolgeeConfig]); - - const [projectName, setProjectName] = useState(); - const [projectId, setProjectId] = useState(); - const [forgetDialogOpen, setForgetDialogOpen] = useState(false); - - const validateTolgeeCredentials = async () => { - try { - setError(undefined); - const res = await mutateAsync({}); - if ( - res && - res.scopes?.includes("translations.view") && - res.scopes?.includes("translations.edit") - ) { - setProjectName(res.projectName); - setProjectId(res.projectId); - return true; - } - throw new Error( - "Missing token scopes. The token should have translations.view and translations.edit scopes.", - ); - } catch (e: any) { - if (e === "Forbidden") { - throw new Error("Invalid API key"); - } - throw e; - } - }; - - const handleValidate = async () => { - try { - await validateTolgeeCredentials(); - setValidated(true); - } catch (e: any) { - setError( - (e === "invalid_project_api_key" - ? "Invalid project API key" - : e.message) || e, - ); - } - }; - - useEffect(() => { - if (tolgeeConfig.apiKey && tolgeeConfig.apiUrl) { - handleValidate(); - } - }, []); - - const handleSubmit = async () => { - try { - setConfig(tolgeeConfig); - setRoute("index"); - } catch (e: any) { - setError( - (e === "invalid_project_api_key" - ? "Invalid project API key" - : e.message) || e, - ); - } - queryClient.clear(); - }; - - const handleGoBack = useCallback(() => { - setRoute("index"); - }, []); - - const handleForget = () => { - resetConfig(); - setValidated(false); - setTolgeeConfig({ - apiUrl: DEFAULT_TOLGEE_URL, - }); - setForgetDialogOpen(false); - }; - - function handleOpenButtonClick() { - setForgetDialogOpen(true); - } - - const handleNextStep = () => { - if (setupStep === "project") { - setSetupStep("strings"); - } else if (setupStep === "strings") { - setSetupStep("push"); - } else if (setupStep === "push") { - handleSubmit(); - } - }; - - const renderSection = ( - step: "project" | "strings" | "push" | null = setupStep, - isSetup = false, - ) => { - switch (step) { - case "project": - return ( - - ); - case "strings": - return ( - - ); - case "push": - return ( - - ); - default: - return null; - } - }; - - return ( - - {noNavigation ? ( - - Settings