This is an automated email from the ASF dual-hosted git repository. lizhimins pushed a commit to branch rocketmq-studio in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
commit 36c2c39b624314492280d092572dda5448797e26 Author: zhaohai <[email protected]> AuthorDate: Fri Jul 17 16:30:53 2026 +0800 feat: extend translation keys & add useLanguage compatible alias in LangContext (#433) --- web/package-lock.json | 1079 ++++++++++++++++++++++----- web/package.json | 15 +- web/src/i18n/LangContext.tsx | 6 + web/src/i18n/__tests__/LangContext.test.tsx | 122 +++ web/src/i18n/translations.ts | 484 ++++++++++++ web/src/test/setup.ts | 6 + web/vite.config.ts | 8 +- 7 files changed, 1517 insertions(+), 203 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index ae22fb1f..98a67cb1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -19,6 +19,9 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^8.62.1", @@ -31,6 +34,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", "husky": "^9.1.7", + "jsdom": "^29.1.1", "lint-staged": "^16.4.0", "postcss": "^8.5.16", "prettier": "^3.9.4", @@ -38,9 +42,16 @@ "typescript": "^5.6.0", "typescript-eslint": "^8.62.1", "vite": "^6.0.0", - "vitest": "^3.2.4" + "vitest": "^4.1.10" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -151,6 +162,57 @@ "react": ">=16.9.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -442,6 +504,159 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -1011,6 +1226,24 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1682,6 +1915,111 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2062,39 +2400,40 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.7", + "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -2106,42 +2445,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -2149,28 +2488,25 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2376,6 +2712,16 @@ "dev": true, "license": "MIT" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2478,6 +2824,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2551,16 +2907,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "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", @@ -2606,32 +2952,15 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2788,6 +3117,27 @@ "node": ">= 8" } }, + "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.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2807,6 +3157,20 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/dayjs": { "version": "1.11.21", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", @@ -2830,15 +3194,12 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "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==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } + "license": "MIT" }, "node_modules/deep-is": { "version": "0.1.4", @@ -2856,6 +3217,16 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2870,6 +3241,14 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2898,6 +3277,19 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -2930,9 +3322,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, @@ -3650,6 +4042,19 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -3699,6 +4104,16 @@ "node": ">=0.8.19" } }, + "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/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3801,6 +4216,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3824,6 +4246,57 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4041,13 +4514,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4058,6 +4524,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4077,6 +4554,13 @@ "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/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4148,6 +4632,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -4248,6 +4742,20 @@ "node": ">= 6" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -4314,6 +4822,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4348,16 +4869,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4587,6 +5098,55 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/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", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "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", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -5348,6 +5908,30 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -5480,6 +6064,19 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -5592,9 +6189,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -5647,26 +6244,19 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, "license": "MIT", "dependencies": { - "js-tokens": "^9.0.1" + "min-indent": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "engines": { + "node": ">=8" } }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/stylis": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", @@ -5709,6 +6299,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -5813,36 +6410,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "node_modules/tldts": { + "version": "7.4.8", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.8.tgz", + "integrity": "sha512-htwgN/8KRB3z3vnC0BOETVh2m499g5GmyTK9Wq5JBLX3FNz6tSBveAd+fQhzy9hkjif8vy2jwDMR1sGhLtZl2A==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.0.0" + "dependencies": { + "tldts-core": "^7.4.8" + }, + "bin": { + "tldts": "bin/cli.js" } }, + "node_modules/tldts-core": { + "version": "7.4.8", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.8.tgz", + "integrity": "sha512-c1P7u0EhACHj7lPy4MJm8iTFEU8+nB0LCtddH0fhP7noaVoXAqafMtOOeX+ulpuPBqnrRgRhw494RICT3mbhnw==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5862,6 +6459,32 @@ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", "license": "MIT" }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -5933,6 +6556,16 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -6056,89 +6689,80 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -6149,15 +6773,59 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vitest/node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } }, "node_modules/which": { "version": "2.0.2", @@ -6238,6 +6906,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/web/package.json b/web/package.json index 1e7248b4..025967d3 100644 --- a/web/package.json +++ b/web/package.json @@ -6,12 +6,13 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "test": "vitest run", "preview": "vite preview", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", - "prepare": "cd .. && husky" + "prepare": "cd .. && husky", + "test": "vitest run", + "test:watch": "vitest" }, "lint-staged": { "*.{ts,tsx}": [ @@ -34,18 +35,22 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^8.62.1", "@typescript-eslint/parser": "^8.62.1", "@vitejs/plugin-react": "^4.3.0", - "axios-mock-adapter": "^2.1.0", "autoprefixer": "^10.5.2", + "axios-mock-adapter": "^2.1.0", "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", "husky": "^9.1.7", + "jsdom": "^29.1.1", "lint-staged": "^16.4.0", "postcss": "^8.5.16", "prettier": "^3.9.4", @@ -53,6 +58,6 @@ "typescript": "^5.6.0", "typescript-eslint": "^8.62.1", "vite": "^6.0.0", - "vitest": "^3.2.4" + "vitest": "^4.1.10" } -} +} \ No newline at end of file diff --git a/web/src/i18n/LangContext.tsx b/web/src/i18n/LangContext.tsx index 76671569..9a5a1b8b 100644 --- a/web/src/i18n/LangContext.tsx +++ b/web/src/i18n/LangContext.tsx @@ -47,3 +47,9 @@ export const LangProvider = ({ children }: { children: ReactNode }) => { }; export const useLang = () => useContext(LangContext); + +/** + * Alias for useLang – provides the same i18n context. + * Kept for backward compatibility with components that import `useLanguage`. + */ +export const useLanguage = useLang; diff --git a/web/src/i18n/__tests__/LangContext.test.tsx b/web/src/i18n/__tests__/LangContext.test.tsx new file mode 100644 index 00000000..1e3ac571 --- /dev/null +++ b/web/src/i18n/__tests__/LangContext.test.tsx @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { LangProvider, useLang, useLanguage } from '../LangContext'; + +/** Helper component that consumes LangContext and exposes its values */ +const LangConsumer = () => { + const { lang, setLang, t } = useLang(); + return ( + <div> + <span data-testid="lang">{lang}</span> + <span data-testid="translated">{t('nav.home')}</span> + <span data-testid="missing">{t('nonexistent.key')}</span> + <span data-testid="param">{t('common.total', { count: 42 })}</span> + <button onClick={() => setLang('en')}>switch-en</button> + <button onClick={() => setLang('zh')}>switch-zh</button> + </div> + ); +}; + +/** Component that uses the useLanguage alias */ +const LanguageAliasConsumer = () => { + const { lang } = useLanguage(); + return <span data-testid="alias-lang">{lang}</span>; +}; + +describe('LangContext', () => { + it('defaults to zh language', () => { + render( + <LangProvider> + <LangConsumer /> + </LangProvider>, + ); + expect(screen.getByTestId('lang')).toHaveTextContent('zh'); + }); + + it('translates keys in Chinese by default', () => { + render( + <LangProvider> + <LangConsumer /> + </LangProvider>, + ); + expect(screen.getByTestId('translated')).toHaveTextContent('首页'); + }); + + it('switches to English via setLang', async () => { + const user = userEvent.setup(); + render( + <LangProvider> + <LangConsumer /> + </LangProvider>, + ); + await user.click(screen.getByText('switch-en')); + expect(screen.getByTestId('lang')).toHaveTextContent('en'); + expect(screen.getByTestId('translated')).toHaveTextContent('Home'); + }); + + it('switches back to Chinese via setLang', async () => { + const user = userEvent.setup(); + render( + <LangProvider> + <LangConsumer /> + </LangProvider>, + ); + await user.click(screen.getByText('switch-en')); + expect(screen.getByTestId('lang')).toHaveTextContent('en'); + await user.click(screen.getByText('switch-zh')); + expect(screen.getByTestId('lang')).toHaveTextContent('zh'); + expect(screen.getByTestId('translated')).toHaveTextContent('首页'); + }); + + it('returns the key itself for missing translations', () => { + render( + <LangProvider> + <LangConsumer /> + </LangProvider>, + ); + expect(screen.getByTestId('missing')).toHaveTextContent('nonexistent.key'); + }); + + it('interpolates parameters into translation strings', async () => { + const user = userEvent.setup(); + render( + <LangProvider> + <LangConsumer /> + </LangProvider>, + ); + // In Chinese mode, 'common.total' is '共' — no {count} placeholder in base text, + // but the interpolation logic should still work without error. + // Switch to English to verify a key that might use params. + await user.click(screen.getByText('switch-en')); + // Even if the key doesn't have a {count} placeholder, the t() function + // should not throw — it simply replaces matching placeholders. + expect(screen.getByTestId('param')).toHaveTextContent('Total'); + }); + + it('useLanguage alias provides the same context as useLang', () => { + render( + <LangProvider> + <LanguageAliasConsumer /> + </LangProvider>, + ); + expect(screen.getByTestId('alias-lang')).toHaveTextContent('zh'); + }); +}); \ No newline at end of file diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 1916eb09..c07151f6 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -334,10 +334,494 @@ const translations: Record<string, Record<Lang, string>> = { // ─── Settings ─── 'settings.title': { zh: '设置', en: 'Settings' }, + 'settings.subtitle': { zh: '管理应用配置与数据源', en: 'Manage app settings and data sources' }, // ─── User Menu ─── 'user.profile': { zh: '个人中心', en: 'Profile' }, 'user.logout': { zh: '退出登录', en: 'Logout' }, + + // ─── Login ─── + 'login.title': { zh: '登录', en: 'Login' }, + 'login.username': { zh: '用户名', en: 'Username' }, + 'login.password': { zh: '密码', en: 'Password' }, + 'login.usernamePlaceholder': { zh: '请输入用户名', en: 'Enter username' }, + 'login.passwordPlaceholder': { zh: '请输入密码', en: 'Enter password' }, + 'login.usernameRequired': { zh: '用户名为必填项', en: 'Username is required' }, + 'login.passwordRequired': { zh: '密码为必填项', en: 'Password is required' }, + 'login.success': { zh: '登录成功', en: 'Login successful' }, + 'login.failed': { zh: '登录失败', en: 'Login failed' }, + 'login.welcome': { zh: '欢迎使用 RocketMQ 仪表盘', en: 'Welcome to RocketMQ Dashboard' }, + + // ─── Ops ─── + 'ops.title': { zh: '运维', en: 'Ops' }, + 'ops.name': { zh: '名称', en: 'Name' }, + 'ops.value': { zh: '值', en: 'Value' }, + 'ops.description': { zh: '描述', en: 'Description' }, + + // ─── Topic (detailed) ─── + 'topic.subtitle': { + zh: '管理 Topic 的创建、配置与删除', + en: 'Manage Topic creation, configuration and deletion', + }, + 'topic.count': { zh: '共 {n} 个 Topic', en: '{n} Topics' }, + 'topic.add': { zh: '添加主题', en: 'Add Topic' }, + 'topic.config': { zh: '主题配置', en: 'Topic Config' }, + 'topic.change': { zh: '修改主题', en: 'Modify Topic' }, + 'topic.name': { zh: '主题名', en: 'Topic Name' }, + 'topic.type': { zh: '消息类型', en: 'Message Type' }, + 'topic.normal': { zh: '普通消息', en: 'Normal' }, + 'topic.fifo': { zh: '顺序消息', en: 'FIFO' }, + 'topic.delay': { zh: '定时/延时消息', en: 'Delay' }, + 'topic.transaction': { zh: '事务消息', en: 'Transaction' }, + 'topic.unspecified': { zh: '未指定', en: 'Unspecified' }, + 'topic.perm': { zh: '权限', en: 'Permission' }, + 'topic.readQueueNums': { zh: '读队列数量', en: 'Read Queue Num' }, + 'topic.writeQueueNums': { zh: '写队列数量', en: 'Write Queue Num' }, + 'topic.brokerName': { zh: 'Broker 名称', en: 'Broker Name' }, + 'topic.brokerAddr': { zh: 'Broker 地址', en: 'Broker Address' }, + 'topic.clusterName': { zh: '集群名', en: 'Cluster Name' }, + 'topic.selectCluster': { zh: '请选择集群', en: 'Select Cluster' }, + 'topic.selectBroker': { zh: '请选择 Broker', en: 'Select Broker' }, + 'topic.queueData': { zh: '队列信息', en: 'Queue Info' }, + 'topic.minOffset': { zh: '最小位点', en: 'Min Offset' }, + 'topic.maxOffset': { zh: '最大位点', en: 'Max Offset' }, + 'topic.lastUpdateTime': { zh: '上次更新时间', en: 'Last Update Time' }, + 'topic.sendMsg': { zh: '发送消息', en: 'Send Message' }, + 'topic.resetOffset': { zh: '重置位点', en: 'Reset Offset' }, + 'topic.skipAccumulate': { zh: '跳过堆积', en: 'Skip Accumulate' }, + 'topic.delete': { zh: '删除主题', en: 'Delete Topic' }, + 'topic.confirmDelete': { zh: '确认删除此主题?', en: 'Delete this topic?' }, + 'topic.deleteWarning': { zh: '删除后无法恢复,请确认。', en: 'This cannot be undone.' }, + 'topic.fetchFailed': { zh: '获取主题列表失败', en: 'Failed to fetch topic list' }, + 'topic.operationSuccess': { zh: 'Topic 操作成功', en: 'Topic operation successful' }, + 'topic.searchPlaceholder': { zh: '搜索主题名称', en: 'Search topic name' }, + 'topic.filterType': { zh: '消息类型', en: 'Message Type' }, + 'topic.filterAll': { zh: '全部类型', en: 'All Types' }, + + // ─── Group / Consumer (detailed) ─── + 'group.subtitle': { + zh: '管理消费者组的订阅与消费位点', + en: 'Manage consumer group subscriptions and offsets', + }, + 'group.count': { zh: '共 {n} 个消费组', en: '{n} Groups' }, + 'group.name': { zh: '组名称', en: 'Group Name' }, + 'group.consumeEnable': { zh: '启用消费', en: 'Consume Enabled' }, + 'group.orderly': { zh: '有序消费', en: 'Orderly' }, + 'group.broadcast': { zh: '广播消费', en: 'Broadcast' }, + 'group.retryQueues': { zh: '重试队列', en: 'Retry Queues' }, + 'group.maxRetries': { zh: '最大重试次数', en: 'Max Retries' }, + 'group.consumeTimeout': { zh: '消费超时', en: 'Consume Timeout' }, + 'group.minutes': { zh: '分钟', en: 'Minutes' }, + 'group.brokerId': { zh: '代理 ID', en: 'Broker ID' }, + 'group.slowBroker': { zh: '慢消费代理', en: 'Slow Broker' }, + 'group.retryPolicy': { zh: '重试策略', en: 'Retry Policy' }, + 'group.systemFlag': { zh: '系统标志', en: 'System Flag' }, + 'group.selectCluster': { zh: '请选择集群名称', en: 'Select Cluster' }, + 'group.selectBrokers': { zh: '选择代理', en: 'Select Brokers' }, + 'group.selectDeleteBrokers': { + zh: '请选择在哪个 Broker 删除消费者组', + en: 'Select Broker to delete group from', + }, + 'group.deleteGroup': { zh: '删除消费者组', en: 'Delete Consumer Group' }, + 'group.addConsumer': { zh: '添加消费者', en: 'Add Consumer' }, + 'group.consumeDetail': { zh: '消费详情', en: 'Consume Detail' }, + 'group.clientInfo': { zh: '客户端信息', en: 'Client Info' }, + 'group.consumeType': { zh: '消费类型', en: 'Consume Type' }, + 'group.messageModel': { zh: '消息模型', en: 'Message Model' }, + 'group.consumeFromWhere': { zh: '从何处消费', en: 'Consume From' }, + 'group.clientConnections': { zh: '客户端连接', en: 'Client Connections' }, + 'group.clientSubscriptions': { zh: '客户端订阅', en: 'Client Subscriptions' }, + 'group.clientId': { zh: '客户端 ID', en: 'Client ID' }, + 'group.clientAddr': { zh: '客户端地址', en: 'Client Address' }, + 'group.language': { zh: '语言', en: 'Language' }, + 'group.subscriptionExpr': { zh: '订阅表达式', en: 'Subscription Expression' }, + 'group.expressionType': { zh: '表达式类型', en: 'Expression Type' }, + 'group.subVersion': { zh: '订阅版本', en: 'Subscription Version' }, + 'group.brokerOffset': { zh: '代理者位点', en: 'Broker Offset' }, + 'group.consumerOffset': { zh: '消费者位点', en: 'Consumer Offset' }, + 'group.diffTotal': { zh: '差值', en: 'Diff Total' }, + 'group.lastTimestamp': { zh: '上次时间', en: 'Last Timestamp' }, + 'group.lastConsumeTime': { zh: '最后消费时间', en: 'Last Consume Time' }, + 'group.resetOffsetSuccess': { + zh: '{name} 消费位点已重置到 {time}', + en: '{name} offset reset to {time}', + }, + 'group.searchPlaceholder': { zh: '搜索消费组名称', en: 'Search group name' }, + 'group.selectGroup': { zh: '请选择消费者组', en: 'Select Consumer Group' }, + + // ─── Message (detailed) ─── + 'message.subtitle': { + zh: '按 Topic、Key 或 Message ID 检索消息', + en: 'Search by Topic, Key or Message ID', + }, + 'message.detail': { zh: '消息详情', en: 'Message Detail' }, + 'message.body': { zh: '消息主体', en: 'Message Body' }, + 'message.properties': { zh: '消息属性', en: 'Message Properties' }, + 'message.info': { zh: '消息信息', en: 'Message Info' }, + 'message.tracking': { zh: '消息追踪', en: 'Message Tracking' }, + 'message.showAll': { zh: '显示全部内容', en: 'Show All Content' }, + 'message.topicName': { zh: '主题名', en: 'Topic Name' }, + 'message.tag': { zh: '标签', en: 'Tag' }, + 'message.key': { zh: '键', en: 'Key' }, + 'message.id': { zh: '消息 ID', en: 'Message ID' }, + 'message.resend': { zh: '重新发送', en: 'Resend' }, + 'message.viewException': { zh: '查看异常', en: 'View Exception' }, + 'message.selectTopic': { zh: '请选择主题', en: 'Select Topic' }, + 'message.selectTraceTopic': { zh: '请选择消息轨迹主题', en: 'Select Trace Topic' }, + 'message.traceTopicHint': { zh: '消息轨迹主题', en: 'Trace Topic' }, + 'message.onlyReturn64': { zh: '仅返回64条消息', en: 'Only return 64 messages' }, + 'message.idTopicHint': { zh: '消息ID主题', en: 'Message ID Topic' }, + 'message.idAndGroupRequired': { + zh: '消息ID和消费者组为必填项', + en: 'Message ID and Consumer Group are required', + }, + 'message.topicAndKeyRequired': { zh: 'Topic和Key为必填项', en: 'Topic and Key are required' }, + 'message.idRequired': { zh: '消息ID为必填项', en: 'Message ID is required' }, + 'message.resendSuccess': { zh: '重新发送成功', en: 'Resend successful' }, + 'message.noResult': { zh: '无结果', en: 'No Result' }, + 'message.queryFailed': { zh: '查询失败', en: 'Query Failed' }, + 'message.fetchDetailFailed': { zh: '获取消息详情失败', en: 'Failed to fetch message detail' }, + 'message.export': { zh: '导出', en: 'Export' }, + 'message.batchResend': { zh: '批量重发', en: 'Batch Resend' }, + 'message.batchExport': { zh: '批量导出', en: 'Batch Export' }, + 'message.noMatchResult': { zh: '没有查到符合条件的结果', en: 'No matching results' }, + + // ─── DLQ (detailed) ─── + 'dlq.subtitle': { + zh: '管理消费失败进入死信队列的消息', + en: 'Manage messages in dead letter queue', + }, + 'dlq.message': { zh: '死信消息', en: 'DLQ Message' }, + 'dlq.consumerGroup': { zh: '消费者组', en: 'Consumer Group' }, + + // ─── Message Trace ─── + 'trace.title': { zh: '消息轨迹', en: 'Message Trace' }, + 'trace.detail': { zh: '消息轨迹详情', en: 'Trace Detail' }, + 'trace.enable': { zh: '开启消息轨迹', en: 'Enable Message Trace' }, + 'trace.topic': { zh: '消息轨迹主题', en: 'Trace Topic' }, + 'trace.selectTopic': { zh: '选择消息轨迹主题', en: 'Select Trace Topic' }, + + // ─── Broker Detail ─── + 'broker.overview': { zh: 'Broker 概览', en: 'Broker Overview' }, + 'broker.name': { zh: 'Broker 名称', en: 'Broker Name' }, + 'broker.addr': { zh: 'Broker 地址', en: 'Broker Address' }, + 'broker.config': { zh: '代理配置', en: 'Broker Config' }, + 'broker.totalMsgToday': { zh: '今天接收的总消息数', en: 'Total Messages Today' }, + 'broker.putTps': { zh: '写入 TPS', en: 'Put TPS' }, + 'broker.getTps': { zh: '拉取 TPS', en: 'Get TPS' }, + 'broker.putNumsToday': { zh: '写入今日总量', en: 'Put Num Today' }, + 'broker.getNumsToday': { zh: '拉取今日总量', en: 'Get Num Today' }, + 'broker.networkThroughput': { zh: 'Broker 网络吞吐量', en: 'Broker Network Throughput' }, + 'broker.accumulationDepth': { zh: '堆积深度趋势', en: 'Accumulation Depth Trend' }, + 'broker.accumulationTotal': { zh: '堆积总量', en: 'Accumulation Total' }, + 'broker.transactionMetrics': { zh: '事务消息指标', en: 'Transaction Message Metrics' }, + 'broker.storageWriteLatency': { zh: '存储写入延迟', en: 'Storage Write Latency' }, + 'broker.topicPutLatency': { zh: 'CommitLog 写入延迟', en: 'CommitLog Put Latency' }, + 'broker.consumeLatency': { zh: '消费拉取延迟', en: 'Consume Pull Latency' }, + 'broker.replicaSyncLatency': { zh: '主从副本同步延迟', en: 'Replica Sync Latency' }, + 'broker.hotTopicTop10': { zh: '热点 Topic Top10', en: 'Hot Topic Top10' }, + 'broker.jvmDetail': { zh: 'Broker JVM 详情', en: 'Broker JVM Detail' }, + 'broker.gcStats': { zh: 'JVM GC 统计', en: 'JVM GC Stats' }, + 'broker.heapUsage': { zh: '堆内存使用', en: 'Heap Usage' }, + 'broker.threadCount': { zh: '线程数', en: 'Thread Count' }, + 'broker.queryAccumulationFailed': { + zh: '查询堆积数据失败', + en: 'Failed to query accumulation data', + }, + 'broker.queryTransactionFailed': { + zh: '查询事务数据失败', + en: 'Failed to query transaction data', + }, + 'broker.queryStorageLatencyFailed': { + zh: '查询存储延迟数据失败', + en: 'Failed to query storage latency', + }, + 'broker.queryNetworkFailed': { + zh: '查询网络吞吐量数据失败', + en: 'Failed to query network throughput', + }, + 'broker.queryReplicaFailed': { + zh: '查询副本同步数据失败', + en: 'Failed to query replica sync data', + }, + 'broker.queryHotTopicFailed': { + zh: '查询热点 Topic 数据失败', + en: 'Failed to query hot topic data', + }, + 'broker.queryJvmFailed': { zh: '查询 JVM 统计失败', en: 'Failed to query JVM stats' }, + + // ─── Proxy Cluster ─── + 'proxy.title': { zh: 'Proxy 集群', en: 'Proxy Cluster' }, + 'proxy.totalNodes': { zh: '总节点数', en: 'Total Nodes' }, + 'proxy.healthyNodes': { zh: '健康节点', en: 'Healthy Nodes' }, + 'proxy.totalConnections': { zh: '总连接数', en: 'Total Connections' }, + 'proxy.totalTps': { zh: '总 TPS', en: 'Total TPS' }, + 'proxy.addNode': { zh: '添加节点', en: 'Add Node' }, + 'proxy.nodes': { zh: 'Proxy 节点', en: 'Proxy Nodes' }, + 'proxy.viewConfig': { zh: '查看配置', en: 'View Config' }, + 'proxy.remove': { zh: '移除', en: 'Remove' }, + 'proxy.nodeConfig': { zh: '节点配置', en: 'Node Configuration' }, + 'proxy.addProxyNode': { zh: '添加 Proxy 节点', en: 'Add Proxy Node' }, + 'proxy.address': { zh: 'Proxy 地址', en: 'Proxy Address' }, + 'proxy.addressPlaceholder': { zh: '例:127.0.0.1:8081', en: 'e.g., 127.0.0.1:8081' }, + 'proxy.invalidAddress': { + zh: '地址格式无效(例:127.0.0.1:8081)', + en: 'Invalid format (e.g., 127.0.0.1:8081)', + }, + 'proxy.current': { zh: '当前', en: 'Current' }, + 'proxy.connections': { zh: '连接数', en: 'Connections' }, + 'proxy.memory': { zh: '内存', en: 'Memory' }, + 'proxy.uptime': { zh: '运行时间', en: 'Uptime' }, + 'proxy.action': { zh: '操作', en: 'Action' }, + 'proxy.fetchListFailed': { zh: '获取代理列表失败', en: 'Failed to fetch proxy list' }, + 'proxy.addFailed': { zh: '添加代理失败', en: 'Failed to add proxy' }, + 'proxy.addrRequired': { zh: '请输入代理地址', en: 'Proxy address is required' }, + 'proxy.noConfigData': { zh: '无配置数据', en: 'No config data' }, + + // ─── SSL Settings ─── + 'ssl.title': { zh: 'SSL/TLS 设置', en: 'SSL/TLS Settings' }, + 'ssl.info': { zh: 'SSL/TLS 配置', en: 'SSL/TLS Configuration' }, + 'ssl.infoDesc': { + zh: '配置 SSL/TLS 设置以实现安全通信,更改后需重启服务器。', + en: 'Configure SSL/TLS for secure communication. Changes require server restart.', + }, + 'ssl.enabled': { zh: '启用 SSL/TLS', en: 'Enable SSL/TLS' }, + 'ssl.protocol': { zh: 'SSL 协议', en: 'SSL Protocol' }, + 'ssl.selectProtocol': { zh: '请选择协议', en: 'Select protocol' }, + 'ssl.clientAuth': { zh: '客户端认证', en: 'Client Authentication' }, + 'ssl.none': { zh: '无', en: 'None' }, + 'ssl.want': { zh: '可选', en: 'Want' }, + 'ssl.need': { zh: '必需', en: 'Need' }, + 'ssl.keystoreConfig': { zh: 'KeyStore 配置', en: 'KeyStore Configuration' }, + 'ssl.keystoreType': { zh: 'KeyStore 类型', en: 'KeyStore Type' }, + 'ssl.keystorePath': { zh: 'KeyStore 路径', en: 'KeyStore Path' }, + 'ssl.keystorePathPlaceholder': { zh: '请输入 KeyStore 路径', en: 'Enter KeyStore path' }, + 'ssl.keystorePassword': { zh: 'KeyStore 密码', en: 'KeyStore Password' }, + 'ssl.keystorePasswordPlaceholder': { zh: '请输入 KeyStore 密码', en: 'Enter KeyStore password' }, + 'ssl.uploadKeystore': { zh: '上传 KeyStore 文件', en: 'Upload KeyStore File' }, + 'ssl.truststoreConfig': { zh: 'TrustStore 配置', en: 'TrustStore Configuration' }, + 'ssl.truststoreType': { zh: 'TrustStore 类型', en: 'TrustStore Type' }, + 'ssl.truststorePath': { zh: 'TrustStore 路径', en: 'TrustStore Path' }, + 'ssl.truststorePathPlaceholder': { zh: '请输入 TrustStore 路径', en: 'Enter TrustStore path' }, + 'ssl.truststorePassword': { zh: 'TrustStore 密码', en: 'TrustStore Password' }, + 'ssl.truststorePasswordPlaceholder': { + zh: '请输入 TrustStore 密码', + en: 'Enter TrustStore password', + }, + 'ssl.uploadTruststore': { zh: '上传 TrustStore 文件', en: 'Upload TrustStore File' }, + 'ssl.save': { zh: '保存', en: 'Save' }, + 'ssl.upload': { zh: '上传', en: 'Upload' }, + 'ssl.certInfo': { zh: '证书信息', en: 'Certificate Information' }, + 'ssl.issuer': { zh: '颁发者', en: 'Issuer' }, + 'ssl.expiryDate': { zh: '过期日期', en: 'Expiry Date' }, + 'ssl.active': { zh: '有效', en: 'Active' }, + 'ssl.saveSuccess': { zh: 'SSL 配置保存成功', en: 'SSL configuration saved successfully' }, + 'ssl.invalidCertFormat': { zh: '仅允许证书文件!', en: 'Only certificate files are allowed!' }, + 'ssl.certRemoved': { zh: '证书文件已移除', en: 'Certificate file removed' }, + + // ─── Namespace ─── + 'ns.title': { zh: '命名空间管理', en: 'Namespace Management' }, + 'ns.name': { zh: '命名空间', en: 'Namespace' }, + 'ns.displayName': { zh: '显示名称', en: 'Display Name' }, + 'ns.cluster': { zh: '集群', en: 'Cluster' }, + 'ns.description': { zh: '描述', en: 'Description' }, + 'ns.default': { zh: '默认', en: 'Default' }, + 'ns.createTime': { zh: '创建时间', en: 'Create Time' }, + 'ns.topicLimit': { zh: 'Topic 限额', en: 'Topic Limit' }, + 'ns.groupLimit': { zh: 'Group 限额', en: 'Group Limit' }, + 'ns.storage': { zh: '存储 (GB)', en: 'Storage (GB)' }, + 'ns.qpsLimit': { zh: 'QPS 限制', en: 'QPS Limit' }, + 'ns.connLimit': { zh: '连接限制', en: 'Connection Limit' }, + 'ns.total': { zh: '命名空间总数', en: 'Total Namespaces' }, + 'ns.enabled': { zh: '已启用', en: 'Enabled' }, + 'ns.totalTopicQuota': { zh: 'Topic 总配额', en: 'Total Topic Quota' }, + 'ns.totalGroupQuota': { zh: 'Group 总配额', en: 'Total Group Quota' }, + 'ns.list': { zh: '命名空间列表', en: 'Namespace List' }, + 'ns.create': { zh: '创建命名空间', en: 'Create Namespace' }, + 'ns.edit': { zh: '编辑命名空间', en: 'Edit Namespace' }, + 'ns.detail': { zh: '命名空间详情', en: 'Namespace Detail' }, + 'ns.viewDetail': { zh: '查看详情', en: 'View Detail' }, + 'ns.quotaSection': { zh: '配额配置', en: 'Quota Configuration' }, + 'ns.confirmDelete': { zh: '确认删除此命名空间?', en: 'Delete this namespace?' }, + 'ns.nameRequired': { zh: '请输入命名空间名称', en: 'Namespace name is required' }, + 'ns.namePattern': { + zh: '仅支持字母、数字、连字符和下划线', + en: 'Only letters, numbers, hyphens and underscores', + }, + 'ns.namePlaceholder': { zh: '例:production-ns', en: 'e.g., production-ns' }, + 'ns.displayNamePlaceholder': { zh: '例:Production', en: 'e.g., Production' }, + 'ns.clusterPlaceholder': { zh: '例:DefaultCluster', en: 'e.g., DefaultCluster' }, + 'ns.descPlaceholder': { zh: '描述此命名空间...', en: 'Describe this namespace...' }, + 'ns.fetchFailed': { zh: '获取命名空间失败', en: 'Failed to fetch namespaces' }, + 'ns.createSuccess': { zh: '命名空间创建成功', en: 'Namespace created successfully' }, + 'ns.createFailed': { zh: '创建命名空间失败', en: 'Failed to create namespace' }, + 'ns.updateSuccess': { zh: '命名空间更新成功', en: 'Namespace updated successfully' }, + 'ns.updateFailed': { zh: '更新命名空间失败', en: 'Failed to update namespace' }, + 'ns.deleteSuccess': { zh: '命名空间删除成功', en: 'Namespace deleted successfully' }, + 'ns.deleteFailed': { zh: '删除命名空间失败', en: 'Failed to delete namespace' }, + 'ns.notSupported': { zh: '不支持命名空间', en: 'Namespace Not Supported' }, + 'ns.notSupportedDesc': { + zh: '当前集群架构不支持命名空间管理,请切换到 RocketMQ 5.0 Proxy 集群。', + en: 'Current cluster does not support namespace management. Please switch to a RocketMQ 5.0 Proxy cluster.', + }, + + // ─── Alert Management ─── + 'alert.management': { zh: '告警管理', en: 'Alert Management' }, + 'alert.name': { zh: '告警名称', en: 'Alert Name' }, + 'alert.group': { zh: '告警分组', en: 'Group' }, + 'alert.severity': { zh: '严重级别', en: 'Severity' }, + 'alert.team': { zh: '团队', en: 'Team' }, + 'alert.expr': { zh: '表达式', en: 'Expression' }, + 'alert.for': { zh: '持续时间', en: 'For' }, + 'alert.status': { zh: '状态', en: 'Status' }, + 'alert.actions': { zh: '操作', en: 'Actions' }, + 'alert.total': { zh: '总规则数', en: 'Total Rules' }, + 'alert.enabled': { zh: '已启用', en: 'Enabled' }, + 'alert.disabled': { zh: '已禁用', en: 'Disabled' }, + 'alert.critical': { zh: '严重', en: 'Critical' }, + 'alert.warningCount': { zh: '警告', en: 'Warning' }, + 'alert.refresh': { zh: '刷新', en: 'Refresh' }, + 'alert.add': { zh: '新增规则', en: 'Add Rule' }, + 'alert.exportYaml': { zh: '导出 YAML', en: 'Export YAML' }, + 'alert.searchPlaceholder': { + zh: '搜索告警名称、表达式...', + en: 'Search alert name, expression...', + }, + 'alert.allGroups': { zh: '全部分组', en: 'All Groups' }, + 'alert.allSeverity': { zh: '全部级别', en: 'All Severity' }, + 'alert.allStatus': { zh: '全部状态', en: 'All Status' }, + 'alert.rules': { zh: '条规则', en: 'rules' }, + 'alert.edit': { zh: '编辑', en: 'Edit' }, + 'alert.delete': { zh: '删除', en: 'Delete' }, + 'alert.editRule': { zh: '编辑告警规则', en: 'Edit Alert Rule' }, + 'alert.addRule': { zh: '新增告警规则', en: 'Add Alert Rule' }, + 'alert.summary': { zh: '摘要', en: 'Summary' }, + 'alert.description': { zh: '描述', en: 'Description' }, + 'alert.nameRequired': { zh: '告警名称为必填项', en: 'Alert name is required' }, + 'alert.groupRequired': { zh: '告警分组为必填项', en: 'Alert group is required' }, + 'alert.exprRequired': { zh: '表达式为必填项', en: 'Expression is required' }, + 'alert.forRequired': { zh: '持续时间为必填项', en: 'Duration is required' }, + 'alert.summaryRequired': { zh: '摘要为必填项', en: 'Summary is required' }, + 'alert.fetchFailed': { zh: '获取告警规则失败', en: 'Failed to fetch alert rules' }, + 'alert.deleteSuccess': { zh: '告警规则已删除', en: 'Alert rule deleted' }, + 'alert.updateSuccess': { zh: '告警规则已更新', en: 'Alert rule updated' }, + 'alert.createSuccess': { zh: '告警规则已创建', en: 'Alert rule created' }, + 'alert.exportSuccess': { zh: '告警规则已导出', en: 'Alert rules exported' }, + + // ─── LiteTopic ─── + 'liteTopic.title': { zh: 'LiteTopic 管理', en: 'LiteTopic Management' }, + 'liteTopic.quotaOverview': { zh: '配额概览', en: 'Quota Overview' }, + 'liteTopic.topicUsage': { zh: 'Topic 使用', en: 'Topic Usage' }, + 'liteTopic.sessionUsage': { zh: 'Session 使用', en: 'Session Usage' }, + 'liteTopic.creationRate': { zh: '创建速率', en: 'Creation Rate' }, + 'liteTopic.searchPlaceholder': { zh: '按 Topic 模式筛选...', en: 'Filter by topic pattern...' }, + 'liteTopic.namespace': { zh: '命名空间', en: 'Namespace' }, + 'liteTopic.namespacePlaceholder': { zh: '选择命名空间', en: 'Select namespace' }, + 'liteTopic.allNamespaces': { zh: '全部命名空间', en: 'All Namespaces' }, + 'liteTopic.pattern': { zh: '模式', en: 'Pattern' }, + 'liteTopic.topicCount': { zh: 'Topic 数', en: 'Topic Count' }, + 'liteTopic.consumers': { zh: '消费者', en: 'Consumers' }, + 'liteTopic.backlog': { zh: '积压', en: 'Backlog' }, + 'liteTopic.avgTtl': { zh: '平均 TTL', en: 'Avg TTL' }, + 'liteTopic.lastActive': { zh: '最后活跃', en: 'Last Active' }, + 'liteTopic.status': { zh: '状态', en: 'Status' }, + 'liteTopic.actions': { zh: '操作', en: 'Actions' }, + 'liteTopic.extendTtl': { zh: '延长 TTL', en: 'Extend TTL' }, + 'liteTopic.viewSessions': { zh: '查看会话', en: 'View Sessions' }, + 'liteTopic.sessionDetail': { zh: '会话详情', en: 'Session Detail' }, + 'liteTopic.sessionId': { zh: '会话 ID', en: 'Session ID' }, + 'liteTopic.clientId': { zh: '客户端 ID', en: 'Client ID' }, + 'liteTopic.clientAddress': { zh: '客户端地址', en: 'Client Address' }, + 'liteTopic.parentTopic': { zh: '父 Topic', en: 'Parent Topic' }, + 'liteTopic.consumerGroup': { zh: '消费者组', en: 'Consumer Group' }, + 'liteTopic.createTime': { zh: '创建时间', en: 'Create Time' }, + 'liteTopic.ttl': { zh: 'TTL', en: 'TTL' }, + 'liteTopic.ttlRemaining': { zh: 'TTL 剩余', en: 'TTL Remaining' }, + 'liteTopic.totalMessages': { zh: '总消息数', en: 'Total Messages' }, + 'liteTopic.consumedMessages': { zh: '已消费消息', en: 'Consumed Messages' }, + 'liteTopic.pendingMessages': { zh: '待消费消息', en: 'Pending Messages' }, + 'liteTopic.consumptionRate': { zh: '消费速率', en: 'Consumption Rate' }, + 'liteTopic.active': { zh: '活跃', en: 'Active' }, + 'liteTopic.expiringSoon': { zh: '即将过期', en: 'Expiring Soon' }, + 'liteTopic.expired': { zh: '已过期', en: 'Expired' }, + 'liteTopic.unknown': { zh: '未知', en: 'Unknown' }, + 'liteTopic.notSupported': { + zh: '当前集群不支持 LiteTopic,请升级到 RocketMQ 5.x', + en: 'LiteTopic is not supported on the current cluster. Please upgrade to RocketMQ 5.x', + }, + 'liteTopic.fetchListFailed': { + zh: '获取 LiteTopic 列表失败', + en: 'Failed to fetch LiteTopic list', + }, + 'liteTopic.fetchQuotaFailed': { zh: '获取配额信息失败', en: 'Failed to fetch quota information' }, + 'liteTopic.fetchSessionFailed': { zh: '获取会话详情失败', en: 'Failed to fetch session detail' }, + 'liteTopic.extendTtlSuccess': { zh: 'TTL 延长成功', en: 'TTL extended successfully' }, + 'liteTopic.extendTtlFailed': { zh: 'TTL 延长失败', en: 'Failed to extend TTL' }, + 'liteTopic.total': { zh: '共 {total} 条记录', en: 'Total {total} records' }, + + // ─── Producer ─── + 'producer.title': { zh: '生产者', en: 'Producer' }, + 'producer.group': { zh: '生产组', en: 'Producer Group' }, + + // ─── Common (additional) ─── + 'common.loading': { zh: '加载中', en: 'Loading' }, + 'common.refresh': { zh: '刷新', en: 'Refresh' }, + 'common.logout': { zh: '退出', en: 'Logout' }, + 'common.submit': { zh: '提交', en: 'Submit' }, + 'common.add': { zh: '新增', en: 'Add' }, + 'common.update': { zh: '更新', en: 'Update' }, + 'common.modify': { zh: '修改', en: 'Modify' }, + 'common.view': { zh: '查看', en: 'View' }, + 'common.confirmDelete': { zh: '确认删除', en: 'Confirm Delete' }, + 'common.areYouSureToDelete': { zh: '您确定要删除吗?', en: 'Are you sure to delete?' }, + 'common.deleteSuccess': { zh: '删除成功', en: 'Delete Successful' }, + 'common.operationFailed': { zh: '操作失败', en: 'Operation Failed' }, + 'common.formValidationFailed': { zh: '表单验证失败', en: 'Form Validation Failed' }, + 'common.fetchDataFailed': { zh: '获取数据失败', en: 'Failed to fetch data' }, + 'common.refreshSuccess': { zh: '刷新成功', en: 'Refresh Successful' }, + 'common.refreshFailed': { zh: '刷新失败', en: 'Refresh Failed' }, + 'common.cannotBeEmpty': { zh: '不能为空', en: 'Cannot be empty' }, + 'common.pleaseInputNumber': { zh: '请输入数字', en: 'Please input a number' }, + 'common.pleaseSelect': { zh: '请选择', en: 'Please select' }, + 'common.pleaseInput': { zh: '请输入', en: 'Please input' }, + 'common.endTimeLaterThanBegin': { + zh: '结束时间应晚于开始时间', + en: 'End time should be later than begin time', + }, + 'common.synchronize': { zh: '同步', en: 'Synchronize' }, + 'common.show': { zh: '显示', en: 'Show' }, + 'common.hide': { zh: '隐藏', en: 'Hide' }, + 'common.readMore': { zh: '阅读更多', en: 'Read More' }, + 'common.queue': { zh: '队列', en: 'Queue' }, + 'common.result': { zh: '结果', en: 'Result' }, + 'common.date': { zh: '日期', en: 'Date' }, + 'common.begin': { zh: '开始', en: 'Begin' }, + 'common.end': { zh: '结束', en: 'End' }, + 'common.selectProxy': { zh: '选择代理', en: 'Select Proxy' }, + 'common.enableProxy': { zh: '启用代理', en: 'Enable Proxy' }, + 'common.proxyDisabled': { zh: '代理禁用', en: 'Proxy Disabled' }, + 'common.proxyEnabled': { zh: '代理启用', en: 'Proxy Enabled' }, + 'common.menu': { zh: '菜单', en: 'Menu' }, + 'common.cluster': { zh: '集群', en: 'Cluster' }, + 'common.broker': { zh: 'Broker', en: 'Broker' }, + 'common.proxy': { zh: '代理', en: 'Proxy' }, + 'common.topic': { zh: '主题', en: 'Topic' }, + 'common.consumer': { zh: '消费者', en: 'Consumer' }, + 'common.producer': { zh: '生产者', en: 'Producer' }, + 'common.message': { zh: '消息', en: 'Message' }, + 'common.config': { zh: '配置', en: 'Config' }, + 'common.operation': { zh: '操作', en: 'Operation' }, + 'common.manage': { zh: '管理', en: 'Manage' }, + 'common.router': { zh: '路由', en: 'Router' }, + 'common.quantity': { zh: '数量', en: 'Quantity' }, + 'common.mode': { zh: '模式', en: 'Mode' }, + 'common.delay': { zh: '延迟', en: 'Delay' }, + 'common.trend': { zh: '趋势', en: 'Trend' }, + 'common.split': { zh: '分片', en: 'Split' }, + 'common.instance': { zh: '实例', en: 'Instance' }, + 'common.warning': { zh: '警告', en: 'Warning' }, + 'common.na': { zh: 'N/A', en: 'N/A' }, + 'common.default': { zh: '默认', en: 'Default' }, }; export default translations; diff --git a/web/src/test/setup.ts b/web/src/test/setup.ts new file mode 100644 index 00000000..3bcbc099 --- /dev/null +++ b/web/src/test/setup.ts @@ -0,0 +1,6 @@ +/// <reference types="vitest/globals" /> +import '@testing-library/jest-dom/vitest'; + +beforeEach(() => { + localStorage.clear(); +}); diff --git a/web/vite.config.ts b/web/vite.config.ts index 8aef6686..f8d5b64a 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; export default defineConfig({ @@ -12,4 +12,10 @@ export default defineConfig({ }, }, }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/test/setup.ts', + css: true, + }, });
