diff --git a/.github/workflows/i18n-check.yml b/.github/workflows/i18n-check.yml new file mode 100644 index 0000000..9359823 --- /dev/null +++ b/.github/workflows/i18n-check.yml @@ -0,0 +1,32 @@ +name: I18n Completeness Check + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: i18n-check-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + i18n-check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Validate i18n completeness + run: npm run check:i18n diff --git a/package-lock.json b/package-lock.json index e3eb2b8..857936f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,9 @@ "lucide-vue-next": "^1.0.0", "pinia": "^3.0.4", "vue": "^3.5.31", - "vue-router": "^5.0.4" + "vue-i18n": "^11.3.2", + "vue-router": "^5.0.4", + "vue-sonner": "^2.0.9" }, "devDependencies": { "@tailwindcss/vite": "^4.2.2", @@ -34,6 +36,7 @@ "npm-run-all2": "^8.0.4", "oxlint": "~1.57.0", "prettier": "3.8.1", + "sharp": "^0.34.5", "tailwindcss": "^4.2.2", "typescript": "~6.0.0", "vite": "^7.3.2", @@ -643,6 +646,17 @@ "node": ">=20.19.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -1236,6 +1250,557 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@intlify/core-base": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.2.tgz", + "integrity": "sha512-cgsUaV/dyD6aS49UPgerIblrWeXAZHNaDWqm4LujOGC7IafSyhghGXEiSVvuDYaDPiQTP+tSFSTM1HIu7Yp1nA==", + "license": "MIT", + "dependencies": { + "@intlify/devtools-types": "11.3.2", + "@intlify/message-compiler": "11.3.2", + "@intlify/shared": "11.3.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/devtools-types": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.2.tgz", + "integrity": "sha512-q96G2ZZw0FNoXzejbjIf9dbfgz1xyYBZu6ZT4b5TE/55j8d1O9X5jv0k+U+L3fVe7uebPcqRQFD0ffm30i5mJA==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.3.2", + "@intlify/shared": "11.3.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.2.tgz", + "integrity": "sha512-d/awyHUkNSaGPxBxT/qlUpfRizxHX9dt55CnW03xx5p1KmMyfYHKupCnvzINX+Na8JR8LAR7y32lPKjoeQGmzA==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "11.3.2", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.2.tgz", + "integrity": "sha512-x66fjdH6i+lNYPae5URSQGTjBL68Av6hi09jvC5Ci96iTkwfqrPhCj46aylQZmgMaG89rOZCIKqS7ApC8ZDVjg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "dev": true, @@ -5462,6 +6027,51 @@ "node": ">=10" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "dev": true, @@ -5793,6 +6403,14 @@ "typescript": ">=4.8.4" } }, + "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==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -6324,6 +6942,33 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/vue-i18n": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.2.tgz", + "integrity": "sha512-gmFrvM+iuf2AH4ygligw/pC7PRJ63AdRNE68E0GPlQ83Mzfyck6g6cRQC3KzkYXr+ZidR91wq+5YBmAMpkgE1A==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.3.2", + "@intlify/devtools-types": "11.3.2", + "@intlify/shared": "11.3.2", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-i18n/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/vue-router": { "version": "5.0.4", "license": "MIT", @@ -6402,6 +7047,28 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vue-sonner": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/vue-sonner/-/vue-sonner-2.0.9.tgz", + "integrity": "sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==", + "license": "MIT", + "peerDependencies": { + "@nuxt/kit": "^4.0.3", + "@nuxt/schema": "^4.0.3", + "nuxt": "^4.0.3" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@nuxt/schema": { + "optional": true + }, + "nuxt": { + "optional": true + } + } + }, "node_modules/vue-tsc": { "version": "3.2.6", "dev": true, diff --git a/package.json b/package.json index f3fb18a..6153d09 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,9 @@ "type": "module", "scripts": { "dev": "vite", - "build": "run-p type-check \"build-only {@}\" --", + "build": "vue-tsc --build && vite build", "preview": "vite preview", - "test:unit": "vitest", - "build-only": "vite build", - "type-check": "vue-tsc --build", - "lint": "run-s lint:*", - "lint:oxlint": "oxlint . --fix", - "lint:eslint": "eslint . --fix --cache", - "format": "prettier --write --experimental-cli src/" + "check:i18n": "node scripts/check-i18n.mjs" }, "dependencies": { "@headlessui/vue": "^1.7.23", @@ -21,7 +15,9 @@ "lucide-vue-next": "^1.0.0", "pinia": "^3.0.4", "vue": "^3.5.31", - "vue-router": "^5.0.4" + "vue-i18n": "^11.3.2", + "vue-router": "^5.0.4", + "vue-sonner": "^2.0.9" }, "devDependencies": { "@tailwindcss/vite": "^4.2.2", @@ -42,6 +38,7 @@ "npm-run-all2": "^8.0.4", "oxlint": "~1.57.0", "prettier": "3.8.1", + "sharp": "^0.34.5", "tailwindcss": "^4.2.2", "typescript": "~6.0.0", "vite": "^7.3.2", diff --git a/scripts/check-i18n.mjs b/scripts/check-i18n.mjs new file mode 100644 index 0000000..78fb804 --- /dev/null +++ b/scripts/check-i18n.mjs @@ -0,0 +1,96 @@ +import { resolve } from 'node:path' + +import { createJiti } from 'jiti' + +const jiti = createJiti(import.meta.url) + +const LOCALE_PAIRS = [ + { + name: 'text', + zhPath: resolve('src/locales/text/zh-CN.ts'), + enPath: resolve('src/locales/text/en-US.ts'), + }, + { + name: 'questions', + zhPath: resolve('src/locales/questions/zh-CN.ts'), + enPath: resolve('src/locales/questions/en-US.ts'), + }, +] + +const getValueType = (value) => { + if (Array.isArray(value)) return 'array' + if (value === null) return 'null' + return typeof value +} + +const collectShape = (value, path, shape) => { + const currentPath = path || '' + const type = getValueType(value) + shape.set(currentPath, type) + + if (Array.isArray(value)) { + shape.set(`${currentPath}.__len`, String(value.length)) + value.forEach((item, index) => { + collectShape(item, `${currentPath}[${index}]`, shape) + }) + return + } + + if (type === 'object') { + Object.entries(value).forEach(([key, child]) => { + const childPath = path ? `${path}.${key}` : key + collectShape(child, childPath, shape) + }) + } +} + +const compareShapes = (leftShape, rightShape, leftLabel, rightLabel) => { + const errors = [] + + for (const [path, leftType] of leftShape.entries()) { + if (!rightShape.has(path)) { + errors.push(`[missing] ${rightLabel} 缺少键: ${path}`) + continue + } + const rightType = rightShape.get(path) + if (leftType !== rightType) { + errors.push(`[type] ${path} 类型不一致: ${leftLabel}=${leftType}, ${rightLabel}=${rightType}`) + } + } + + return errors +} + +const run = () => { + const allErrors = [] + + for (const pair of LOCALE_PAIRS) { + const zhModule = jiti(pair.zhPath) + const enModule = jiti(pair.enPath) + const zh = zhModule.default ?? zhModule + const en = enModule.default ?? enModule + + const zhShape = new Map() + const enShape = new Map() + collectShape(zh, '', zhShape) + collectShape(en, '', enShape) + + const zhToEnErrors = compareShapes(zhShape, enShape, 'zh-CN', 'en-US') + const enToZhErrors = compareShapes(enShape, zhShape, 'en-US', 'zh-CN') + + if (zhToEnErrors.length > 0 || enToZhErrors.length > 0) { + allErrors.push(`\n[${pair.name}]`) + allErrors.push(...zhToEnErrors, ...enToZhErrors) + } + } + + if (allErrors.length > 0) { + console.error('i18n 完备性校验失败:') + console.error(allErrors.join('\n')) + process.exit(1) + } + + console.log('i18n 完备性校验通过(zh-CN <-> en-US)') +} + +run() diff --git a/src/App.vue b/src/App.vue index 7c2aa3f..1f962ec 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,3 +1,8 @@ + + diff --git a/src/assets/pets/enfj.webp b/src/assets/pets/enfj.webp new file mode 100644 index 0000000..cab6ea3 Binary files /dev/null and b/src/assets/pets/enfj.webp differ diff --git a/src/assets/pets/enfp.webp b/src/assets/pets/enfp.webp new file mode 100644 index 0000000..a6b719d Binary files /dev/null and b/src/assets/pets/enfp.webp differ diff --git a/src/assets/pets/entj.webp b/src/assets/pets/entj.webp new file mode 100644 index 0000000..840d450 Binary files /dev/null and b/src/assets/pets/entj.webp differ diff --git a/src/assets/pets/entp.webp b/src/assets/pets/entp.webp new file mode 100644 index 0000000..5470b02 Binary files /dev/null and b/src/assets/pets/entp.webp differ diff --git a/src/assets/pets/esfj.webp b/src/assets/pets/esfj.webp new file mode 100644 index 0000000..0aee177 Binary files /dev/null and b/src/assets/pets/esfj.webp differ diff --git a/src/assets/pets/esfp.webp b/src/assets/pets/esfp.webp new file mode 100644 index 0000000..0965d67 Binary files /dev/null and b/src/assets/pets/esfp.webp differ diff --git a/src/assets/pets/estj.webp b/src/assets/pets/estj.webp new file mode 100644 index 0000000..f391f60 Binary files /dev/null and b/src/assets/pets/estj.webp differ diff --git a/src/assets/pets/estp.webp b/src/assets/pets/estp.webp new file mode 100644 index 0000000..673b77a Binary files /dev/null and b/src/assets/pets/estp.webp differ diff --git a/src/assets/pets/infj.webp b/src/assets/pets/infj.webp new file mode 100644 index 0000000..81da6cd Binary files /dev/null and b/src/assets/pets/infj.webp differ diff --git a/src/assets/pets/infp.webp b/src/assets/pets/infp.webp new file mode 100644 index 0000000..abeb828 Binary files /dev/null and b/src/assets/pets/infp.webp differ diff --git a/src/assets/pets/intj.webp b/src/assets/pets/intj.webp new file mode 100644 index 0000000..e340a9e Binary files /dev/null and b/src/assets/pets/intj.webp differ diff --git a/src/assets/pets/intp.webp b/src/assets/pets/intp.webp new file mode 100644 index 0000000..1d7a63b Binary files /dev/null and b/src/assets/pets/intp.webp differ diff --git a/src/assets/pets/isfj.webp b/src/assets/pets/isfj.webp new file mode 100644 index 0000000..c090fad Binary files /dev/null and b/src/assets/pets/isfj.webp differ diff --git a/src/assets/pets/isfp.webp b/src/assets/pets/isfp.webp new file mode 100644 index 0000000..fd1f8c3 Binary files /dev/null and b/src/assets/pets/isfp.webp differ diff --git a/src/assets/pets/istj.webp b/src/assets/pets/istj.webp new file mode 100644 index 0000000..868e4d4 Binary files /dev/null and b/src/assets/pets/istj.webp differ diff --git a/src/assets/pets/istp.webp b/src/assets/pets/istp.webp new file mode 100644 index 0000000..9dd2f74 Binary files /dev/null and b/src/assets/pets/istp.webp differ diff --git a/src/components/AppToaster.vue b/src/components/AppToaster.vue new file mode 100644 index 0000000..7191295 --- /dev/null +++ b/src/components/AppToaster.vue @@ -0,0 +1,8 @@ + + + diff --git a/src/data/pets.ts b/src/data/pets.ts index 050e36a..248e169 100644 --- a/src/data/pets.ts +++ b/src/data/pets.ts @@ -1,186 +1,122 @@ import type { RocoPet } from '@/types' -const getWikiPortraitUrl = (petName: string): string => - `https://wiki.biligame.com/rocom/index.php?title=Special:FilePath/${encodeURIComponent(`页面_宠物_立绘_${petName}_1.png`)}` +import imageAbu from '@/assets/pets/esfp.webp' +import imageDimo from '@/assets/pets/enfj.webp' +import imageEnchanterCat from '@/assets/pets/infp.webp' +import imageEvilDing from '@/assets/pets/entp.webp' +import imageFireGod from '@/assets/pets/entj.webp' +import imageFrostDoll from '@/assets/pets/isfp.webp' +import imageGranBall from '@/assets/pets/esfj.webp' +import imageGuardDog from '@/assets/pets/isfj.webp' +import imageHolyWater from '@/assets/pets/infj.webp' +import imageKula from '@/assets/pets/istp.webp' +import imageLuoYin from '@/assets/pets/istj.webp' +import imageMechaCube from '@/assets/pets/intp.webp' +import imagePalsas from '@/assets/pets/intj.webp' +import imageRoyalGryphon from '@/assets/pets/estj.webp' +import imageSonicDog from '@/assets/pets/estp.webp' +import imageSpringFlower from '@/assets/pets/enfp.webp' -export const pets: RocoPet[] = [ - { - id: 'pet-intj', - name: '迪莫', - mbti: 'INTJ', - title: '永远的伙伴', - stats: { hp: 82, atk: 88, def: 92, spAtk: 110, spDef: 96, speed: 85 }, - description: '图鉴编号 NO001,光系代表精灵,适合作为沉稳策略型人格映射。', - wikiUrl: 'https://wiki.biligame.com/rocom/迪莫', - habitat: '叽叽喳喳台地', - imageUrl: getWikiPortraitUrl('迪莫'), - }, - { - id: 'pet-intp', - name: '菊花梨', - mbti: 'INTP', - title: '图鉴研究者', - stats: { hp: 76, atk: 70, def: 78, spAtk: 116, spDef: 101, speed: 99 }, - description: '来自精灵图鉴的真实词条,用于表现探索中的分析与推演倾向。', - wikiUrl: 'https://wiki.biligame.com/rocom/菊花梨', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('菊花梨'), - }, - { - id: 'pet-entj', - name: '立方人', - mbti: 'ENTJ', - title: '几何指挥官', - stats: { hp: 95, atk: 112, def: 85, spAtk: 96, spDef: 82, speed: 90 }, - description: '图鉴中的经典精灵形象,映射强组织与高执行的队伍领袖风格。', - wikiUrl: 'https://wiki.biligame.com/rocom/立方人', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('立方人'), - }, - { - id: 'pet-entp', - name: '龙鱼', - mbti: 'ENTP', - title: '灵感破局者', - stats: { hp: 80, atk: 92, def: 74, spAtk: 108, spDef: 76, speed: 118 }, - description: '精灵图鉴词条中高人气宠物,适配灵活应变和创意驱动人格。', - wikiUrl: 'https://wiki.biligame.com/rocom/龙鱼', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('龙鱼'), - }, - { - id: 'pet-infj', - name: '多灵主', - mbti: 'INFJ', - title: '灵魂共鸣', - stats: { hp: 92, atk: 72, def: 88, spAtk: 99, spDef: 114, speed: 79 }, - description: '图鉴收录精灵,多重人格融合感强,适配洞察与共情型人格。', - wikiUrl: 'https://wiki.biligame.com/rocom/多灵主', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('多灵主'), - }, - { - id: 'pet-infp', - name: '小皮球', - mbti: 'INFP', - title: '柔韧旅者', - stats: { hp: 84, atk: 64, def: 77, spAtk: 106, spDef: 112, speed: 87 }, - description: '来自精灵图鉴的宠物名称,呈现温和而富有想象力的探索方式。', - wikiUrl: 'https://wiki.biligame.com/rocom/小皮球', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('小皮球'), - }, - { - id: 'pet-enfj', - name: '健猫教练', - mbti: 'ENFJ', - title: '团队教练', - stats: { hp: 90, atk: 86, def: 83, spAtk: 104, spDef: 100, speed: 98 }, - description: '图鉴中具备鲜明协作氛围的精灵名,映射高同理与引导能力。', - wikiUrl: 'https://wiki.biligame.com/rocom/健猫教练', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('健猫教练'), - }, - { - id: 'pet-enfp', - name: '酷拉', - mbti: 'ENFP', - title: '节奏点燃者', - stats: { hp: 78, atk: 84, def: 70, spAtk: 96, spDef: 74, speed: 122 }, - description: '来自图鉴词条,风格张扬灵活,适配高外向与高创造型人格。', - wikiUrl: 'https://wiki.biligame.com/rocom/酷拉', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('酷拉'), - }, - { - id: 'pet-istj', - name: '鳗尾兽', - mbti: 'ISTJ', - title: '秩序守线', - stats: { hp: 102, atk: 95, def: 118, spAtk: 62, spDef: 101, speed: 54 }, - description: '图鉴精灵名,用于表现流程优先、稳定推进的执行人格。', - wikiUrl: 'https://wiki.biligame.com/rocom/鳗尾兽', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('鳗尾兽'), - }, - { - id: 'pet-isfj', - name: '闪电鳗鱼', - mbti: 'ISFJ', - title: '守护支援', - stats: { hp: 96, atk: 68, def: 104, spAtk: 82, spDef: 112, speed: 66 }, - description: '引用精灵图鉴中的命名,强调稳定、谨慎与持续辅助能力。', - wikiUrl: 'https://wiki.biligame.com/rocom/闪电鳗鱼', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('闪电鳗鱼'), - }, - { - id: 'pet-estj', - name: '彩蝶鲨', - mbti: 'ESTJ', - title: '阵地统筹', - stats: { hp: 108, atk: 116, def: 109, spAtk: 55, spDef: 88, speed: 60 }, - description: '图鉴真实精灵名,映射重目标和高纪律性的决策风格。', - wikiUrl: 'https://wiki.biligame.com/rocom/彩蝶鲨', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('彩蝶鲨'), - }, - { - id: 'pet-esfj', - name: '卷毛鸭', - mbti: 'ESFJ', - title: '协同应援', - stats: { hp: 88, atk: 72, def: 84, spAtk: 94, spDef: 102, speed: 92 }, - description: '取自精灵图鉴的角色,用于表达高社交和团队情绪连接。', - wikiUrl: 'https://wiki.biligame.com/rocom/卷毛鸭', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('卷毛鸭'), - }, - { - id: 'pet-istp', - name: '熔岩布丁', - mbti: 'ISTP', - title: '冷静操作手', - stats: { hp: 83, atk: 120, def: 86, spAtk: 68, spDef: 79, speed: 111 }, - description: '图鉴中常见的实战派精灵名,适配快速判断和极简执行。', - wikiUrl: 'https://wiki.biligame.com/rocom/熔岩布丁', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('熔岩布丁'), - }, - { - id: 'pet-isfp', - name: '怒目怂猫', - mbti: 'ISFP', - title: '感性漫游', - stats: { hp: 86, atk: 89, def: 75, spAtk: 91, spDef: 84, speed: 108 }, - description: '来自精灵图鉴,映射自由探索与审美驱动的行动偏好。', - wikiUrl: 'https://wiki.biligame.com/rocom/怒目怂猫', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('怒目怂猫'), - }, - { - id: 'pet-estp', - name: '海豹船长', - mbti: 'ESTP', - title: '前线破局', - stats: { hp: 91, atk: 124, def: 78, spAtk: 72, spDef: 73, speed: 119 }, - description: '精灵图鉴命名中的高机动角色,适配强行动和临场突破风格。', - wikiUrl: 'https://wiki.biligame.com/rocom/海豹船长', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('海豹船长'), - }, - { - id: 'pet-esfp', - name: '绅士鸡', - mbti: 'ESFP', - title: '舞台焦点', - stats: { hp: 82, atk: 98, def: 71, spAtk: 102, spDef: 77, speed: 124 }, - description: '来自精灵图鉴的代表形象,强调表现力和氛围带动能力。', - wikiUrl: 'https://wiki.biligame.com/rocom/绅士鸡', - habitat: '图鉴记录区域', - imageUrl: getWikiPortraitUrl('绅士鸡'), - }, -] +const PET_NAME = { + 'pets.INTJ.name': '帕尔萨斯', + 'pets.INTP.name': '机械方方', + 'pets.ENTJ.name': '烈火战神', + 'pets.ENTP.name': '恶魔叮', + 'pets.INFJ.name': '圣水守护', + 'pets.INFP.name': '魔力猫', + 'pets.ENFJ.name': '迪莫', + 'pets.ENFP.name': '蹦蹦花', + 'pets.ISTJ.name': '罗隐', + 'pets.ISFJ.name': '护主犬', + 'pets.ESTJ.name': '皇家狮鹫', + 'pets.ESFJ.name': '格兰球', + 'pets.ISTP.name': '酷拉', + 'pets.ISFP.name': '雪影娃娃', + 'pets.ESTP.name': '音速犬', + 'pets.ESFP.name': '阿布', +} as const + +const MBTI_LIST = [ + 'INTJ', + 'INTP', + 'ENTJ', + 'ENTP', + 'INFJ', + 'INFP', + 'ENFJ', + 'ENFP', + 'ISTJ', + 'ISFJ', + 'ESTJ', + 'ESFJ', + 'ISTP', + 'ISFP', + 'ESTP', + 'ESFP', +] as const + +type MbtiType = (typeof MBTI_LIST)[number] +type PetNameKey = keyof typeof PET_NAME + +const MBTI_TO_NAME_KEY: Record = { + INTJ: 'pets.INTJ.name', + INTP: 'pets.INTP.name', + ENTJ: 'pets.ENTJ.name', + ENTP: 'pets.ENTP.name', + INFJ: 'pets.INFJ.name', + INFP: 'pets.INFP.name', + ENFJ: 'pets.ENFJ.name', + ENFP: 'pets.ENFP.name', + ISTJ: 'pets.ISTJ.name', + ISFJ: 'pets.ISFJ.name', + ESTJ: 'pets.ESTJ.name', + ESFJ: 'pets.ESFJ.name', + ISTP: 'pets.ISTP.name', + ISFP: 'pets.ISFP.name', + ESTP: 'pets.ESTP.name', + ESFP: 'pets.ESFP.name', +} + +const PET_IMAGE_URL: Record = { + 'pets.INTJ.name': imagePalsas, + 'pets.INTP.name': imageMechaCube, + 'pets.ENTJ.name': imageFireGod, + 'pets.ENTP.name': imageEvilDing, + 'pets.INFJ.name': imageHolyWater, + 'pets.INFP.name': imageEnchanterCat, + 'pets.ENFJ.name': imageDimo, + 'pets.ENFP.name': imageSpringFlower, + 'pets.ISTJ.name': imageLuoYin, + 'pets.ISFJ.name': imageGuardDog, + 'pets.ESTJ.name': imageRoyalGryphon, + 'pets.ESFJ.name': imageGranBall, + 'pets.ISTP.name': imageKula, + 'pets.ISFP.name': imageFrostDoll, + 'pets.ESTP.name': imageSonicDog, + 'pets.ESFP.name': imageAbu, +} + +const getWikiNameByNameKey = (nameKey: PetNameKey): string => PET_NAME[nameKey] + +const getWikiPageUrlByNameKey = (nameKey: PetNameKey): string => + `https://wiki.biligame.com/rocom/${encodeURIComponent(getWikiNameByNameKey(nameKey))}` + +const getLocalPortraitUrlByNameKey = (nameKey: PetNameKey): string => PET_IMAGE_URL[nameKey] + +export const pets: RocoPet[] = MBTI_LIST.map((mbti) => { + const nameKey = MBTI_TO_NAME_KEY[mbti] + + return { + id: `pet-${mbti.toLowerCase()}`, + nameKey, + mbti, + titleKey: `pets.${mbti}.title`, + descriptionKey: `pets.${mbti}.description`, + habitatKey: `pets.${mbti}.habitat`, + wikiUrl: getWikiPageUrlByNameKey(nameKey), + imageUrl: getLocalPortraitUrlByNameKey(nameKey), + } +}) export const petByMbti = pets.reduce>((accumulator, pet) => { accumulator[pet.mbti] = pet diff --git a/src/data/questions.ts b/src/data/questions.ts index 35675e6..f66b5f7 100644 --- a/src/data/questions.ts +++ b/src/data/questions.ts @@ -3,98 +3,182 @@ import type { Question } from '@/types' export const questions: Question[] = [ { id: 1, - text: '你第一次进入轻风山地时,更想先做什么?', + textKey: 'questions.q1.text', options: [ - { text: '和路过训练师聊天套情报', weights: { E: 2, N: 1 } }, - { text: '先观察地形和可采集资源', weights: { I: 2, S: 1 } }, + { textKey: 'questions.q1.options.a', weights: { E: 2 } }, + { textKey: 'questions.q1.options.b', weights: { E: 1 } }, + { textKey: 'questions.q1.options.c', weights: { I: 1 } }, + { textKey: 'questions.q1.options.d', weights: { I: 2 } }, ], }, { id: 2, - text: '遇到遗迹机关谜题,你会怎么处理?', + textKey: 'questions.q2.text', options: [ - { text: '先按直觉尝试激活组合', weights: { N: 2, P: 1 } }, - { text: '记录线索并按顺序推演', weights: { S: 1, J: 2 } }, + { textKey: 'questions.q2.options.a', weights: { E: 2 } }, + { textKey: 'questions.q2.options.b', weights: { E: 1 } }, + { textKey: 'questions.q2.options.c', weights: { I: 1 } }, + { textKey: 'questions.q2.options.d', weights: { I: 2 } }, ], }, { id: 3, - text: '队友想临时改路线去抓稀有宠物,你会?', + textKey: 'questions.q3.text', options: [ - { text: '支持,临场变化更有惊喜', weights: { P: 2, E: 1 } }, - { text: '先看是否影响主线目标', weights: { J: 2, T: 1 } }, + { textKey: 'questions.q3.options.a', weights: { E: 2 } }, + { textKey: 'questions.q3.options.b', weights: { E: 1 } }, + { textKey: 'questions.q3.options.c', weights: { I: 1 } }, + { textKey: 'questions.q3.options.d', weights: { I: 2 } }, ], }, { id: 4, - text: '在营地分配补给时你更看重?', + textKey: 'questions.q4.text', options: [ - { text: '每个人都能安心继续探索', weights: { F: 2, I: 1 } }, - { text: '按战斗贡献优先分配效率最高', weights: { T: 2, J: 1 } }, + { textKey: 'questions.q4.options.a', weights: { E: 2 } }, + { textKey: 'questions.q4.options.b', weights: { E: 1 } }, + { textKey: 'questions.q4.options.c', weights: { I: 1 } }, + { textKey: 'questions.q4.options.d', weights: { I: 2 } }, ], }, { id: 5, - text: '面对未知天气区域,你会?', + textKey: 'questions.q5.text', options: [ - { text: '凭经验快速试探边界', weights: { S: 1, P: 2 } }, - { text: '先推测气候规律再行动', weights: { N: 2, J: 1 } }, + { textKey: 'questions.q5.options.a', weights: { E: 2 } }, + { textKey: 'questions.q5.options.b', weights: { E: 1 } }, + { textKey: 'questions.q5.options.c', weights: { I: 1 } }, + { textKey: 'questions.q5.options.d', weights: { I: 2 } }, ], }, { id: 6, - text: '你更喜欢哪种探索节奏?', + textKey: 'questions.q6.text', options: [ - { text: '高频互动、一路触发事件', weights: { E: 2, P: 1 } }, - { text: '沉浸探索、稳步推进地图', weights: { I: 2, J: 1 } }, + { textKey: 'questions.q6.options.a', weights: { S: 2 } }, + { textKey: 'questions.q6.options.b', weights: { S: 1 } }, + { textKey: 'questions.q6.options.c', weights: { N: 1 } }, + { textKey: 'questions.q6.options.d', weights: { N: 2 } }, ], }, { id: 7, - text: '发现疑似隐藏剧情 NPC 时,你倾向?', + textKey: 'questions.q7.text', options: [ - { text: '先感受角色动机再选择对话', weights: { F: 2, N: 1 } }, - { text: '先确认奖励与任务收益', weights: { T: 2, S: 1 } }, + { textKey: 'questions.q7.options.a', weights: { S: 2 } }, + { textKey: 'questions.q7.options.b', weights: { S: 1 } }, + { textKey: 'questions.q7.options.c', weights: { N: 1 } }, + { textKey: 'questions.q7.options.d', weights: { N: 2 } }, ], }, { id: 8, - text: '打 Boss 前准备环节你会?', + textKey: 'questions.q8.text', options: [ - { text: '预设站位和技能循环', weights: { J: 2, T: 1 } }, - { text: '保留弹性,视局势临时应变', weights: { P: 2, N: 1 } }, + { textKey: 'questions.q8.options.a', weights: { S: 2 } }, + { textKey: 'questions.q8.options.b', weights: { S: 1 } }, + { textKey: 'questions.q8.options.c', weights: { N: 1 } }, + { textKey: 'questions.q8.options.d', weights: { N: 2 } }, ], }, { id: 9, - text: '当队伍出现分歧时你更可能?', + textKey: 'questions.q9.text', options: [ - { text: '主动发言推动大家达成共识', weights: { E: 2, F: 1 } }, - { text: '先独立思考后给关键建议', weights: { I: 2, T: 1 } }, + { textKey: 'questions.q9.options.a', weights: { S: 2 } }, + { textKey: 'questions.q9.options.b', weights: { S: 1 } }, + { textKey: 'questions.q9.options.c', weights: { N: 1 } }, + { textKey: 'questions.q9.options.d', weights: { N: 2 } }, ], }, { id: 10, - text: '在精灵培养上你更偏好?', + textKey: 'questions.q10.text', options: [ - { text: '依据数值和克制关系精细养成', weights: { S: 2, T: 1 } }, - { text: '根据主题与故事构建队伍', weights: { N: 2, F: 1 } }, + { textKey: 'questions.q10.options.a', weights: { T: 2 } }, + { textKey: 'questions.q10.options.b', weights: { T: 1 } }, + { textKey: 'questions.q10.options.c', weights: { F: 1 } }, + { textKey: 'questions.q10.options.d', weights: { F: 2 } }, ], }, { id: 11, - text: '地图开荒时你习惯?', + textKey: 'questions.q11.text', options: [ - { text: '先清主干线,再补支线', weights: { J: 2, S: 1 } }, - { text: '哪里有趣就先去哪里', weights: { P: 2, E: 1 } }, + { textKey: 'questions.q11.options.a', weights: { T: 2 } }, + { textKey: 'questions.q11.options.b', weights: { T: 1 } }, + { textKey: 'questions.q11.options.c', weights: { F: 1 } }, + { textKey: 'questions.q11.options.d', weights: { F: 2 } }, ], }, { id: 12, - text: '结算时你更在意哪件事?', + textKey: 'questions.q12.text', options: [ - { text: '团队是否玩得开心、彼此认可', weights: { F: 2, E: 1 } }, - { text: '策略是否有效、目标是否达成', weights: { T: 2, I: 1 } }, + { textKey: 'questions.q12.options.a', weights: { T: 2 } }, + { textKey: 'questions.q12.options.b', weights: { T: 1 } }, + { textKey: 'questions.q12.options.c', weights: { F: 1 } }, + { textKey: 'questions.q12.options.d', weights: { F: 2 } }, + ], + }, + { + id: 13, + textKey: 'questions.q13.text', + options: [ + { textKey: 'questions.q13.options.a', weights: { T: 2 } }, + { textKey: 'questions.q13.options.b', weights: { T: 1 } }, + { textKey: 'questions.q13.options.c', weights: { F: 1 } }, + { textKey: 'questions.q13.options.d', weights: { F: 2 } }, + ], + }, + { + id: 14, + textKey: 'questions.q14.text', + options: [ + { textKey: 'questions.q14.options.a', weights: { J: 2 } }, + { textKey: 'questions.q14.options.b', weights: { J: 1 } }, + { textKey: 'questions.q14.options.c', weights: { P: 1 } }, + { textKey: 'questions.q14.options.d', weights: { P: 2 } }, + ], + }, + { + id: 15, + textKey: 'questions.q15.text', + options: [ + { textKey: 'questions.q15.options.a', weights: { J: 2 } }, + { textKey: 'questions.q15.options.b', weights: { J: 1 } }, + { textKey: 'questions.q15.options.c', weights: { P: 1 } }, + { textKey: 'questions.q15.options.d', weights: { P: 2 } }, + ], + }, + { + id: 16, + textKey: 'questions.q16.text', + options: [ + { textKey: 'questions.q16.options.a', weights: { J: 2 } }, + { textKey: 'questions.q16.options.b', weights: { J: 1 } }, + { textKey: 'questions.q16.options.c', weights: { P: 1 } }, + { textKey: 'questions.q16.options.d', weights: { P: 2 } }, + ], + }, + { + id: 17, + textKey: 'questions.q17.text', + options: [ + { textKey: 'questions.q17.options.a', weights: { J: 2 } }, + { textKey: 'questions.q17.options.b', weights: { J: 1 } }, + { textKey: 'questions.q17.options.c', weights: { P: 1 } }, + { textKey: 'questions.q17.options.d', weights: { P: 2 } }, + ], + }, + { + id: 18, + textKey: 'questions.q18.text', + options: [ + { textKey: 'questions.q18.options.a', weights: { J: 2 } }, + { textKey: 'questions.q18.options.b', weights: { J: 1 } }, + { textKey: 'questions.q18.options.c', weights: { P: 1 } }, + { textKey: 'questions.q18.options.d', weights: { P: 2 } }, ], }, ] diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..d86df57 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,16 @@ +import { createI18n } from 'vue-i18n' + +import enUSQuestions from '@/locales/questions/en-US' +import zhCNQuestions from '@/locales/questions/zh-CN' +import enUSText from '@/locales/text/en-US' +import zhCNText from '@/locales/text/zh-CN' + +export const i18n = createI18n({ + legacy: false, + locale: navigator.language.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en-US', + fallbackLocale: 'zh-CN', + messages: { + 'zh-CN': { ...zhCNText, ...zhCNQuestions }, + 'en-US': { ...enUSText, ...enUSQuestions }, + }, +}) diff --git a/src/locales/questions/en-US.ts b/src/locales/questions/en-US.ts new file mode 100644 index 0000000..9522519 --- /dev/null +++ b/src/locales/questions/en-US.ts @@ -0,0 +1,168 @@ +const enUSQuestions = { + questions: { + q1: { + text: 'On your first day at the Magic Academy, you arrive at a lively plaza. You will:', + options: { + a: 'Greet people enthusiastically and proactively look for adventure partners.', + b: 'Join the crowd and naturally jump into interesting conversations.', + c: 'Avoid the crowded center and explore nearby buildings and shops alone first.', + d: 'Feel drained by the crowd and quickly find a quiet corner to recharge.', + }, + }, + q2: { + text: 'You are incredibly lucky and catch an extremely rare spirit in the wild. You tend to:', + options: { + a: 'Run back to town and show your new partner in a busy area immediately.', + b: 'Contact close friends with a magic communicator and share the joy right away.', + c: 'Keep it as a secret trump card and study its traits in private.', + d: 'Stay in a quiet camp and spend the whole day bonding with it alone.', + }, + }, + q3: { + text: 'The academy is holding its annual grand festival. Your state is:', + options: { + a: 'Excitedly moving through every booth and joining all games and battles.', + b: 'Enjoying the lively vibe with a few familiar friends.', + c: 'Sitting in a side rest area, quietly watching the celebration with a drink.', + d: 'Feeling the festival is too noisy, so you find a quiet rooftop to watch fireworks alone.', + }, + }, + q4: { + text: 'When your expedition meets an unfamiliar mage team in the wild, you will:', + options: { + a: 'Approach openly and exchange danger intel and supply info.', + b: 'Smile and wave politely, then respond warmly if they start talking.', + c: 'Stay alert, nod politely, and pass by quickly.', + d: 'Hide early or detour to avoid any unnecessary interaction.', + }, + }, + q5: { + text: 'What is your usual training atmosphere with your spirit like?', + options: { + a: 'Full of laughter and challenges with many people, like a big magic meetup.', + b: 'Train with one or two skilled partners and exchange practical insights.', + c: 'Find an open space with only you and your spirit, and ignore passersby.', + d: 'Train in a fully hidden mountain or cave, immersed in your own world.', + }, + }, + q6: { + text: 'When learning a brand-new advanced spell, you usually:', + options: { + a: 'Follow the spellbook steps strictly and make every movement precise.', + b: 'Repeat the standard pattern until it becomes reliable muscle memory.', + c: 'After understanding the principle, explore its origin and links to other spells.', + d: 'After grasping the basics, immediately try to improve or reinvent it.', + }, + }, + q7: { + text: 'Exploring a foggy unknown forest, you rely more on:', + options: { + a: 'A detailed parchment map, compass, and physical marks left by predecessors.', + b: 'Careful observation of footprints, broken branches, and subtle sounds.', + c: 'Your first instinct: go where the path feels right.', + d: 'A strange glimmer or mysterious premonition that pulls you off the main route.', + }, + }, + q8: { + text: 'When choosing a long-term partner spirit, you value most:', + options: { + a: 'Its base stats, growth values, and currently learned practical spells.', + b: 'Physical traits (sharp claws, hard shell) and direct combat value.', + c: 'Its unique aura and appearance that fits your personal aesthetics.', + d: 'Its future potential, ancient lore, and a sense of destined soul resonance.', + }, + }, + q9: { + text: 'When you find a damaged magic scroll in ruins, your first focus is:', + options: { + a: 'Which formulas can be used in battle now or sold for a high price.', + b: 'Its material and preservation status to judge immediate practical value.', + c: 'The adventure story of the mage who once wrote it.', + d: 'Its role as a key to lost continental history and ancient mysteries.', + }, + }, + q10: { + text: 'Your spirit is injured and defeated in an important battle. Your first reaction is:', + options: { + a: 'Calmly review the battle and analyze timing and matchup mistakes.', + b: 'Take it to treatment immediately and restore stamina efficiently for the next battle.', + c: 'Feel a little guilty and comfort it with its favorite food.', + d: 'Hold it tightly right away, feeling deep pain and self-blame.', + }, + }, + q11: { + text: 'The academy is forming an elite team against dangerous creatures. Who should lead?', + options: { + a: 'The strongest and most experienced mage with the highest success rate.', + b: 'A very calm, clear-minded person who allocates resources rationally.', + c: 'A friendly person who cares for emotions and keeps morale light.', + d: 'A highly empathetic person who unites everyone and leaves no one behind.', + }, + }, + q12: { + text: 'You find a lost, badly injured “danger-type” spirit in the wild. You will:', + options: { + a: 'Control it and call academy guards immediately due to potential risk.', + b: 'Keep a safe distance and leave some healing potion when safe.', + c: 'See it as a pitiful life and try to soothe it with gentle magic.', + d: 'Approach despite risk and heal it with everything you have.', + }, + }, + q13: { + text: 'A teammate’s low-level mistake puts your team in danger. You will:', + options: { + a: 'Point out the exact mistake seriously and reinforce expedition rules.', + b: 'Take command fast, patch the strategy, then review rationally after.', + c: 'Confirm everyone is safe first, then comfort the teammate gently.', + d: 'Take part of the blame yourself to protect your teammate’s dignity.', + }, + }, + q14: { + text: 'Before exploring a new high-difficulty map on the weekend, you will:', + options: { + a: 'Plan an exact route and prepare antidotes, rations, and counter tools.', + b: 'Check map intel roughly and bring enough basic recovery items.', + c: 'Bring only essentials and gather resources along the way.', + d: 'Head out almost empty, enjoying improvisation in unknown environments.', + }, + }, + q15: { + text: 'Facing many academy daily quests (herbs, letters, etc.), you usually:', + options: { + a: 'Use a daily schedule and clear all quests early with full checklist satisfaction.', + b: 'At least finish the highest-reward quests before resting each day.', + c: 'Do them casually if you happen to pass quest targets.', + d: 'Often forget quests after being distracted by scenery or rare spirits.', + }, + }, + q16: { + text: 'How are items arranged in your spatial magic backpack?', + options: { + a: 'Strictly sorted by type and tier; you can find anything instantly.', + b: 'Roughly categorized (combat on one side, daily items on another).', + c: 'Usually messy; only organize with one-click sort when it is totally full.', + d: 'Always chaotic; you rely on mysterious muscle memory to find items.', + }, + }, + q17: { + text: 'When making a spirit training plan, you tend to:', + options: { + a: 'Create a strict timetable and diet plan, then execute it every day.', + b: 'Set a clear phase goal (for example, this week target) and advance steadily.', + c: 'Skip fixed plans and observe natural growth through real battles.', + d: 'Arrange everything freely by mood and weather of the day.', + }, + }, + q18: { + text: 'Facing a sudden ambush by a powerful creature, your reaction is:', + options: { + a: 'Recall and execute the rehearsed defense plan and standard formation perfectly.', + b: 'Assess power gap quickly and decide to fight with standard tactics or retreat.', + c: 'Scan surroundings for cliffs, rocks, and terrain advantages.', + d: 'Rely on extreme on-the-spot reflexes and improvise miracles in chaos.', + }, + }, + }, +} + +export default enUSQuestions diff --git a/src/locales/questions/zh-CN.ts b/src/locales/questions/zh-CN.ts new file mode 100644 index 0000000..e7cd464 --- /dev/null +++ b/src/locales/questions/zh-CN.ts @@ -0,0 +1,168 @@ +const zhCNQuestions = { + questions: { + q1: { + text: '魔法学院新生报到第一天,来到热闹的广场,你会?', + options: { + a: '热情地向周围人打招呼,主动寻觅志同道合的探险伙伴。', + b: '走到人群中看热闹,如果有有趣的谈话就顺理成章地加入。', + c: '避开拥挤的中心区域,独自先去熟悉周围的建筑和商铺。', + d: '觉得人太多有些消耗精力,迅速穿过广场寻找安静的角落休息。', + }, + }, + q2: { + text: '你在野外极其幸运地捕捉到了一只极为罕见的精灵,你更倾向于?', + options: { + a: '立刻跑回镇上,在人群密集的区域向大家展示你的新搭档。', + b: '马上用魔法通讯装置联络好朋友,分享这份喜悦。', + c: '将其作为秘密底牌,暂时不告诉别人,暗中研究它的特性。', + d: '独自留在静谧的营地里,花一整天时间安静地与它培养感情。', + }, + }, + q3: { + text: '魔法学院正在举办一年一度的盛大庆典,你的状态是?', + options: { + a: '兴致勃勃地穿梭在各个摊位,参与所有的狂欢与对战游戏。', + b: '和几个熟悉的朋友结伴游玩,享受热闹的氛围。', + c: '坐在边缘地带的休息区,喝着饮料安静地看着大家狂欢。', + d: '觉得庆典过于吵闹,找个高处无人打扰的屋顶独自看烟火。', + }, + }, + q4: { + text: '在野外探险遇到其他素不相识的魔法师队伍,你会?', + options: { + a: '大方地迎上前去,主动交流前方的危险情报和物资储备。', + b: '给予友善的微笑并挥手致意,如果对方搭话就热情回应。', + c: '保持基本的警惕,礼貌地点头示意后快速交错而过。', + d: '提前隐蔽起来或主动绕路,完全不想产生任何不必要的交集。', + }, + }, + q5: { + text: '你的精灵训练场通常是怎样的氛围?', + options: { + a: '充满欢声笑语,大家互相挑战,是一场大型的魔法交流会。', + b: '邀请一两把好手共同练习,互相探讨魔法心得。', + c: '找一块空地,只有你和你的精灵,偶尔有路人经过也不在意。', + d: '绝对隐秘的深山或洞穴,完全沉浸在自己的训练世界里。', + }, + }, + q6: { + text: '学习一门全新的高阶魔法时,你通常?', + options: { + a: '严格按照魔法书上的图解步骤,力求施法动作分毫不差。', + b: '反复练习标准范式,直到形成绝对可靠的肌肉记忆。', + c: '听懂原理后,总想探究这门魔法的起源以及与其他魔法的联系。', + d: '刚学个大概,就忍不住尝试加入自己的理解去改良或创造新效果。', + }, + }, + q7: { + text: '在一片迷雾笼罩的未知森林中探险,你更依赖?', + options: { + a: '手中详细的羊皮纸地图、指南针和前人留下的实体标记。', + b: '仔细观察地上的脚印、折断的树枝和周围细微的声音。', + c: '相信自己的第一直觉,觉得哪条路“感觉”对就走哪条。', + d: '被远处一阵奇异的微光或某种莫名的预感吸引,直接偏离主路。', + }, + }, + q8: { + text: '挑选长期陪伴你的搭档精灵时,你最看重?', + options: { + a: '它的基础属性面板、成长数值和目前已经掌握的实用魔法。', + b: '它外表的生理特征(如锋利的爪子或坚硬的甲壳)带来的实战价值。', + c: '它身上散发的独特气质,以及符合你个人审美的外观。', + d: '它未来的无限潜能、背后的古老传说,以及你们灵魂共鸣的宿命感。', + }, + }, + q9: { + text: '面对在遗迹中发现的一本残破魔法卷轴,你首先关注的是?', + options: { + a: '上面具体记载了哪些能立刻用于实战或者卖出高价的配方。', + b: '检查卷轴的材质和保存状态,评估它在当下的实际用途。', + c: '想象当初写下这卷轴的魔法师经历了怎样的冒险故事。', + d: '认为这是解开整个大陆失落历史和远古谜团的一把核心钥匙。', + }, + }, + q10: { + text: '你的精灵在重要的对战中受伤战败,你的第一反应是?', + options: { + a: '冷静复盘战局,分析属性克制问题和魔法释放的时机失误。', + b: '立刻带它去治疗点,用最高效的方式恢复它的体力以便再战。', + c: '感到有些内疚,用它最喜欢的食物来安抚它失落的情绪。', + d: '立刻上前紧紧抱住它,为它受到的伤害感到十分心疼和自责。', + }, + }, + q11: { + text: '学院需要组建一支对抗危险生物的精英小队,你会推荐谁当队长?', + options: { + a: '魔法实力最强、实战经验最丰富、能绝对保证任务成功率的人。', + b: '极其冷静、头脑清晰,能合理分配每一份资源和战利品的人。', + c: '性格随和、能照顾到大家情绪,让队伍氛围保持轻松的人。', + d: '极具同理心、能把大家团结在一起,不抛弃任何一个队员的人。', + }, + }, + q12: { + text: '在野外发现了一只迷路且受重伤的“危险系别”精灵,你会?', + options: { + a: '考虑到它潜在的破坏力,将其控制住并立刻呼叫学院守卫。', + b: '保持安全距离,在不危及自身的前提下留下一点恢复药剂。', + c: '觉得它此刻只是个可怜的生命,尝试用温和的魔法安抚它。', + d: '不顾危险直接靠近它并全力为它疗伤,坚信没有精灵是天生邪恶的。', + }, + }, + q13: { + text: '当你的探险队友因为低级失误导致你们陷入险境时,你会?', + options: { + a: '严肃指出失误的具体原因,强调探险规则,确保下次绝不再犯。', + b: '迅速接管指挥权,改变战术以弥补漏洞,事后再进行理性复盘。', + c: '先确定大家是否安全,然后温和地安慰队友不要过于自责。', + d: '主动把责任揽到自己身上(比如“是我没提醒你”),保护队友的自尊。', + }, + }, + q14: { + text: '周末前往新的高难度地图探索前,你会?', + options: { + a: '提前规划好精确路线,准备好对应环境的解药、干粮和克制道具。', + b: '大致查阅一下地图情报,确保带够了基础的恢复品就出发。', + c: '只带上常用的几样东西,决定在探索过程中边走边收集资源。', + d: '背包空空就跑过去,觉得利用野外未知环境即兴发挥才是探险的乐趣。', + }, + }, + q15: { + text: '面对一堆需要完成的魔法学院日常委托(采药、送信等),你通常?', + options: { + a: '每天按时间表列出清单,早早地全部做完,享受全部打钩的快感。', + b: '至少保证在每天休息前,把收益最高的几个委托稳定完成。', + c: '走到哪里算哪里,如果路上刚好碰到任务目标就顺手做一下。', + d: '经常因为被路边好看的风景或稀有精灵吸引,而彻底忘掉委托。', + }, + }, + q16: { + text: '在你的空间魔法背包里,物品的摆放是怎样的?', + options: { + a: '药水、装备、材料严格按照种类、等级分门别类,闭着眼都能找到。', + b: '大致有个分类(战斗用的放一边,生活用的放另一边)。', + c: '平时乱塞,只有当背包彻底满了装不下时,才会按一下“一键整理”。', + d: '永远是一团乱麻,全凭神奇的肌肉记忆在千百件杂物中翻找需要的道具。', + }, + }, + q17: { + text: '制定精灵的培育计划时,你倾向于?', + options: { + a: '制定极度严格的训练时间表和饮食计划,每天雷打不动地执行。', + b: '设定一个清晰的阶段性目标(例如“本周达到一定水平”),稳步推进。', + c: '没有固定计划,带它去实战中转转,看看它自然成长的方向。', + d: '完全看自己和精灵当天的心情、天气状态,极其随性地安排一天。', + }, + }, + q18: { + text: '面对突如其来的强大生物伏击,你的反应是?', + options: { + a: '迅速回忆并完美执行之前反复演练过的防御预案和标准阵型。', + b: '快速评估敌我实力差距,果断决定是按常规战术迎敌还是撤退。', + c: '视线立刻扫向四周,寻找悬崖、落石等可以利用的地形环境。', + d: '依靠极其变态的临场反应,随手抓起能用的东西或魔法,在混乱中创造奇迹。', + }, + }, + }, +} + +export default zhCNQuestions diff --git a/src/locales/text/en-US.ts b/src/locales/text/en-US.ts new file mode 100644 index 0000000..89c6598 --- /dev/null +++ b/src/locales/text/en-US.ts @@ -0,0 +1,130 @@ +const enUSText = { + quiz: { + badgeTitle: 'Roco Personality Mirror', + progressLabel: 'Question {current} / {total}', + }, + result: { + completed: 'Assessment Complete', + restart: 'Restart Quiz', + portraitSource: 'Artwork source: Roco Kingdom World WIKI bestiary entry', + personality: 'Nature', + habitat: 'Habitat', + viewWiki: 'View Wiki', + shareToMoments: 'Share to Moments (copy link)', + githubStar: 'Like it? Give us a Star on GitHub', + copySuccess: 'Share text and link copied', + copyFailed: 'Copy failed, please copy the URL manually', + entertainmentNotice: 'Questions and personality insights are AI-generated. Results are for entertainment only.', + shareText: 'I got {mbti} in Roco Personality Mirror, and my spirit partner is {petName}! Come and try on:', + personalities: ['Adamant', 'Bold', 'Calm', 'Jolly', 'Relaxed', 'Careful', 'Brave', 'Gentle'], + }, + fallbackPet: { + name: 'Starsea Sentinel', + title: 'Default Personality Mirror', + description: 'Fallback result data for exceptional cases only.', + habitat: 'Starsea Nexus', + }, + pets: { + INTJ: { + name: 'Palsas', + title: 'Architect', + description: + "The absolute core of William Castle and Count Dracula's trusted aide. Cold, mysterious, highly rational, and independent, with excellent strategic vision and control.", + habitat: 'Chatter Plateau', + }, + INTP: { + name: 'Mecha Cubic', + title: 'Logician', + description: 'Full of tech spirit. Obsessed with underlying mechanisms, rational and highly creative.', + habitat: 'Dex Archive Zone', + }, + ENTJ: { + name: 'Blazing War Deity', + title: 'Commander', + description: 'A burning war god. Born leader with power, confidence, and decisive execution.', + habitat: 'Dex Archive Zone', + }, + ENTP: { + name: 'Devil Ding', + title: 'Debater', + description: + "A classic early demon-type companion and a signature sidekick for Enzo and the man in black. Curious, mischievous, and unconventional, always breaking norms with clever tricks.", + habitat: 'Dex Archive Zone', + }, + INFJ: { + name: 'Sacred Water Guardian', + title: 'Advocate', + description: 'Incarnation of pure water. Deep inner world, steadfast belief, and silent healing.', + habitat: 'Dex Archive Zone', + }, + INFP: { + name: 'Magic Cat', + title: 'Mediator', + description: 'Beloved by the forest. Gentle, natural, and dreamy with a rich inner world.', + habitat: 'Dex Archive Zone', + }, + ENFJ: { + name: 'Dimo', + title: 'Protagonist', + description: 'A light-element icon. Sunny and inspiring, the emotional pillar of a team.', + habitat: 'Dex Archive Zone', + }, + ENFP: { + name: 'Bouncy Bloom', + title: 'Campaigner', + description: 'Always bouncing. Curious, energetic, and full of personal charm.', + habitat: 'Dex Archive Zone', + }, + ISTJ: { + name: 'Luo Yin', + title: 'Logistician', + description: 'Solid as a rock. Reliable and practical, always defending order.', + habitat: 'Dex Archive Zone', + }, + ISFJ: { + name: 'Guardian Hound', + title: 'Defender', + description: 'Absolutely loyal. Warm, caring, and quietly dependable.', + habitat: 'Dex Archive Zone', + }, + ESTJ: { + name: 'Royal Gryphon', + title: 'Executive', + description: 'Majestic royal mount. Highly disciplined and tradition-driven with strong execution.', + habitat: 'Dex Archive Zone', + }, + ESFJ: { + name: 'Gran Ball', + title: 'Consul', + description: 'A national-level support unit. Warm-hearted and dedicated to helping teammates.', + habitat: 'Dex Archive Zone', + }, + ISTP: { + name: 'Kula', + title: 'Virtuoso', + description: 'A lone electric mage. Cool-headed, agile, practical, and calm in crisis.', + habitat: 'Dex Archive Zone', + }, + ISFP: { + name: 'Snowshade Doll', + title: 'Adventurer', + description: 'A spirit of ice and snow. Cool outside, warm inside, with artistic freedom.', + habitat: 'Dex Archive Zone', + }, + ESTP: { + name: 'Sonic Hound', + title: 'Entrepreneur', + description: 'Fast as lightning. Action-driven, challenge-seeking, and battle-focused.', + habitat: 'Dex Archive Zone', + }, + ESFP: { + name: 'Abu', + title: 'Entertainer', + description: + 'Energetic, food-loving, emotionally expressive, and naturally performative. It acts on instinct in adventures and quickly becomes the joyful center of any crowd.', + habitat: 'Dex Archive Zone', + }, + }, +} + +export default enUSText diff --git a/src/locales/text/zh-CN.ts b/src/locales/text/zh-CN.ts new file mode 100644 index 0000000..b81a84a --- /dev/null +++ b/src/locales/text/zh-CN.ts @@ -0,0 +1,130 @@ +const zhCNText = { + quiz: { + badgeTitle: '洛克世界人格镜像', + progressLabel: '第 {current} / {total} 题', + }, + result: { + completed: '鉴定完成', + restart: '重新测试', + portraitSource: '立绘来源:洛克王国世界 WIKI 精灵图鉴词条', + personality: '性格', + habitat: '栖息地', + viewWiki: '查看 Wiki 资料', + shareToMoments: '分享到朋友圈(复制链接)', + githubStar: '觉得不错?来 GitHub 点个 Star', + copySuccess: '已复制分享文案与链接', + copyFailed: '复制失败,请手动复制地址栏链接', + entertainmentNotice: '题目与人格分析皆由AI生成,测试结果仅供娱乐。', + shareText: '我在洛克世界人格镜像测出了 {mbti},本命精灵是 {petName}!你也快来试试吧:', + personalities: ['固执', '大胆', '冷静', '开朗', '悠闲', '慎重', '勇敢', '温和'], + }, + fallbackPet: { + name: '星海守望兽', + title: '默认人格镜像', + description: '默认结果数据,仅用于异常兜底展示。', + habitat: '星海中枢', + }, + pets: { + INTJ: { + name: '帕尔萨斯', + title: '建筑师', + description: + '威廉古堡的绝对核心,德古拉伯爵的得力助手。冷酷神秘且高度理性,独立而富有战略眼光,拥有极强的掌控力。', + habitat: '叽叽喳喳台地', + }, + INTP: { + name: '机械方方', + title: '逻辑学家', + description: '充满科技感。热衷于探索事物运行的底层逻辑和机制,理性、专注且充满创造性。', + habitat: '图鉴记录区域', + }, + ENTJ: { + name: '烈火战神', + title: '指挥官', + description: '燃烧的战神。天生的领袖,充满力量、自信与决断力,能够带领团队冲锋陷阵。', + habitat: '图鉴记录区域', + }, + ENTP: { + name: '恶魔叮', + title: '辩论家', + description: + '早期经典恶魔系宠物,恩佐与黑衣人的招牌跟班。天生好奇又调皮,不按套路出牌,常用小聪明打破常规。', + habitat: '图鉴记录区域', + }, + INFJ: { + name: '圣水守护', + title: '提倡者', + description: '纯净之水的化身。内心深邃,拥有坚定的信仰,默默治愈和守护着整个水之源头。', + habitat: '图鉴记录区域', + }, + INFP: { + name: '魔力猫', + title: '调停者', + description: '森林的宠儿。亲近自然,温柔随性,不喜争斗,内心有着自己丰富且梦幻的小世界。', + habitat: '图鉴记录区域', + }, + ENFJ: { + name: '迪莫', + title: '主人公', + description: '光系的代表。充满阳光与感染力,总是用正能量指引同伴,是团队中的精神支柱。', + habitat: '图鉴记录区域', + }, + ENFP: { + name: '蹦蹦花', + title: '竞选者', + description: '永远在跳跃。活力四射,对世界上的一切充满好奇,热情洋溢且极具个人魅力。', + habitat: '图鉴记录区域', + }, + ISTJ: { + name: '罗隐', + title: '物流师', + description: '坚如磐石。极其可靠、务实,遵循规则与秩序,是无论面对什么冲击都不退缩的坚实后盾。', + habitat: '图鉴记录区域', + }, + ISFJ: { + name: '护主犬', + title: '守卫者', + description: '绝对忠诚。默默付出,总是优先考虑主人的安危,温暖、体贴且值得信赖。', + habitat: '图鉴记录区域', + }, + ESTJ: { + name: '皇家狮鹫', + title: '总经理', + description: '威风凛凛的皇家坐骑。极度自律,尊重传统,执行力极强,维护着天空的秩序。', + habitat: '图鉴记录区域', + }, + ESFJ: { + name: '格兰球', + title: '执政官', + description: '国民级辅助。热心肠,总是积极地为大家提供催眠、寄生等战术支援,极具奉献精神。', + habitat: '图鉴记录区域', + }, + ISTP: { + name: '酷拉', + title: '鉴赏家', + description: '孤傲的电系法师。外表高冷,行动敏捷,崇尚实用主义,能在危机中迅速做出冷静反应。', + habitat: '图鉴记录区域', + }, + ISFP: { + name: '雪影娃娃', + title: '探险家', + description: '冰雪中的精灵。外冷内热,有着独特的审美和艺术气质,喜欢按照自己的节奏自由生活。', + habitat: '图鉴记录区域', + }, + ESTP: { + name: '音速犬', + title: '企业家', + description: '风驰电掣。绝对的行动派,追求速度、激情与挑战,喜欢在实战中解决问题。', + habitat: '图鉴记录区域', + }, + ESFP: { + name: '阿布', + title: '表演者', + description: + '精力充沛、贪吃且情绪外露,极具表现力。总能在冒险中凭直觉行动,是宠物群体里的开心果和焦点。', + habitat: '图鉴记录区域', + }, + }, +} + +export default zhCNText diff --git a/src/main.ts b/src/main.ts index 8999d8a..29ab4db 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,12 +2,14 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' +import { i18n } from './i18n' import router from './router' import './style.css' const app = createApp(App) app.use(createPinia()) +app.use(i18n) app.use(router) app.mount('#app') diff --git a/src/types/index.ts b/src/types/index.ts index da33687..a2ce4e6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,21 +11,20 @@ export interface PetStats { export interface RocoPet { id: string - name: string + nameKey: string mbti: string - title: string - stats: PetStats - description: string + titleKey: string + descriptionKey: string + habitatKey: string wikiUrl: string - habitat: string imageUrl: string } export interface Question { id: number - text: string + textKey: string options: { - text: string + textKey: string weights: Partial> }[] } diff --git a/src/views/Quiz.vue b/src/views/Quiz.vue index 7157cf0..5c5d0b9 100644 --- a/src/views/Quiz.vue +++ b/src/views/Quiz.vue @@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue' import { useRouter } from 'vue-router' import { useTransition } from '@vueuse/core' import { Sparkles } from 'lucide-vue-next' +import { useI18n } from 'vue-i18n' import { useQuizStore } from '@/stores/quiz' @@ -12,6 +13,7 @@ defineOptions({ const quizStore = useQuizStore() const router = useRouter() +const { t } = useI18n() const direction = ref<'next' | 'prev'>('next') const progressSource = computed(() => quizStore.progress) @@ -38,19 +40,19 @@ const transitionName = computed(() =>