diff --git a/bun.lock b/bun.lock index 37b8b5f..3b8c3e4 100644 --- a/bun.lock +++ b/bun.lock @@ -7,8 +7,12 @@ "dependencies": { "dompurify": "^3.3.1", "emoji-datasource": "^16.0.0", + "i18next": "^25.8.1", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^16.5.4", "wrangler": "3.90.0", }, "devDependencies": { @@ -16,6 +20,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", + "i18next-parser": "^9.3.0", "typescript": "^5.5.4", "vite": "^5.4.0", "vite-plugin-svgr": "^4.5.0", @@ -55,6 +60,8 @@ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], @@ -81,54 +88,64 @@ "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], + "@gulpjs/to-absolute-glob": ["@gulpjs/to-absolute-glob@4.0.0", "", { "dependencies": { "is-negated-glob": "^1.0.0" } }, "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -139,6 +156,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], @@ -223,6 +242,8 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="], + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], @@ -233,6 +254,8 @@ "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], + "@types/symlink-or-copy": ["@types/symlink-or-copy@1.2.2", "", {}, "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], @@ -241,16 +264,46 @@ "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "as-table": ["as-table@1.0.55", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="], + "b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], + "bl": ["bl@5.1.0", "", { "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "broccoli-node-api": ["broccoli-node-api@1.7.0", "", {}, "sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw=="], + + "broccoli-node-info": ["broccoli-node-info@2.2.0", "", {}, "sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg=="], + + "broccoli-output-wrapper": ["broccoli-output-wrapper@3.2.5", "", { "dependencies": { "fs-extra": "^8.1.0", "heimdalljs-logger": "^0.1.10", "symlink-or-copy": "^1.2.0" } }, "sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw=="], + + "broccoli-plugin": ["broccoli-plugin@4.0.7", "", { "dependencies": { "broccoli-node-api": "^1.7.0", "broccoli-output-wrapper": "^3.2.5", "fs-merger": "^3.2.1", "promise-map-series": "^0.3.0", "quick-temp": "^0.1.8", "rimraf": "^3.0.2", "symlink-or-copy": "^1.3.1" } }, "sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg=="], + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], @@ -259,14 +312,40 @@ "capnp-ts": ["capnp-ts@0.7.0", "", { "dependencies": { "debug": "^4.3.1", "tslib": "^2.2.0" } }, "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g=="], + "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], + + "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="], + + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], + "cross-fetch": ["cross-fetch@4.0.0", "", { "dependencies": { "node-fetch": "^2.6.12" } }, "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="], @@ -277,19 +356,37 @@ "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + "dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="], + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], "emoji-datasource": ["emoji-datasource@16.0.0", "", {}, "sha512-/qHKqK5Nr3+8zhgO6kHmF43Fm5C8HNn0AaFRIpgw8HF3+uF0Vfc8jgLI1ZQS5ba1vBzksS8NBCjHejwLb2D/Sg=="], + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], + + "ensure-posix-path": ["ensure-posix-path@1.1.1", "", {}, "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "eol": ["eol@0.9.1", "", {}, "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg=="], + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -297,8 +394,26 @@ "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], + "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + + "fs-merger": ["fs-merger@3.2.1", "", { "dependencies": { "broccoli-node-api": "^1.7.0", "broccoli-node-info": "^2.1.0", "fs-extra": "^8.0.1", "fs-tree-diff": "^2.0.1", "walk-sync": "^2.2.0" } }, "sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug=="], + + "fs-mkdirp-stream": ["fs-mkdirp-stream@2.0.1", "", { "dependencies": { "graceful-fs": "^4.2.8", "streamx": "^2.12.0" } }, "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw=="], + + "fs-tree-diff": ["fs-tree-diff@2.0.1", "", { "dependencies": { "@types/symlink-or-copy": "^1.2.0", "heimdalljs-logger": "^0.1.7", "object-assign": "^4.1.0", "path-posix": "^1.0.0", "symlink-or-copy": "^1.1.8" } }, "sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -307,18 +422,70 @@ "get-source": ["get-source@2.0.12", "", { "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" } }, "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w=="], + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "glob-stream": ["glob-stream@8.0.3", "", { "dependencies": { "@gulpjs/to-absolute-glob": "^4.0.0", "anymatch": "^3.1.3", "fastq": "^1.13.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "is-negated-glob": "^1.0.0", "normalize-path": "^3.0.0", "streamx": "^2.12.5" } }, "sha512-fqZVj22LtFJkHODT+M4N1RJQ3TjnnQhfE9GwZI8qXscYarnhpip70poMldRnP8ipQ/w0B621kOhfc53/J9bd/A=="], + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "gulp-sort": ["gulp-sort@2.0.0", "", { "dependencies": { "through2": "^2.0.1" } }, "sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "heimdalljs": ["heimdalljs@0.2.6", "", { "dependencies": { "rsvp": "~3.2.1" } }, "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA=="], + + "heimdalljs-logger": ["heimdalljs-logger@0.1.10", "", { "dependencies": { "debug": "^2.2.0", "heimdalljs": "^0.2.6" } }, "sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g=="], + + "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], + + "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + + "i18next": ["i18next@25.8.1", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-nFFxhwcRNggIrkv2hx/xMYVMG7Z8iMUA4ZuH4tgcbZiI0bK1jn3kSDIXNWuQDt1xVAu7mb7Qn82TpH7ZAk/okA=="], + + "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="], + + "i18next-http-backend": ["i18next-http-backend@3.0.2", "", { "dependencies": { "cross-fetch": "4.0.0" } }, "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g=="], + + "i18next-parser": ["i18next-parser@9.3.0", "", { "dependencies": { "@babel/runtime": "^7.25.0", "broccoli-plugin": "^4.0.7", "cheerio": "^1.0.0", "colors": "^1.4.0", "commander": "^12.1.0", "eol": "^0.9.1", "esbuild": "^0.25.0", "fs-extra": "^11.2.0", "gulp-sort": "^2.0.0", "i18next": "^23.5.1 || ^24.2.0", "js-yaml": "^4.1.0", "lilconfig": "^3.1.3", "rsvp": "^4.8.5", "sort-keys": "^5.0.0", "typescript": "^5.0.4", "vinyl": "^3.0.0", "vinyl-fs": "^4.0.0" }, "bin": { "i18next": "bin/cli.js" } }, "sha512-VaQqk/6nLzTFx1MDiCZFtzZXKKyBV6Dv0cJMFM/hOt4/BWHWRgYafzYfVQRUzotwUwjqeNCprWnutzD/YAGczg=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-negated-glob": ["is-negated-glob@1.0.0", "", {}, "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-valid-glob": ["is-valid-glob@1.0.0", "", {}, "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "itty-time": ["itty-time@1.0.6", "", {}, "sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -329,6 +496,12 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "lead": ["lead@4.0.0", "", {}, "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -339,10 +512,18 @@ "magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], + "matcher-collection": ["matcher-collection@2.0.1", "", { "dependencies": { "@types/minimatch": "^3.0.3", "minimatch": "^3.0.2" } }, "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ=="], + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], "miniflare": ["miniflare@3.20241106.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "^8.8.0", "acorn-walk": "^8.2.0", "capnp-ts": "^0.7.0", "exit-hook": "^2.2.1", "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", "workerd": "1.20241106.1", "ws": "^8.18.0", "youch": "^3.2.2", "zod": "^3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-dM3RBlJE8rUFxnqlPCaFCq0E7qQqEQvKbYX7W/APGCK+rLcyLmEBzC4GQR/niXdNM/oV6gdg9AA50ghnn2ALuw=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "mktemp": ["mktemp@2.0.2", "", {}, "sha512-Q9wJ/xhzeD9Wua1MwDN2v3ah3HENsUVSlzzL9Qw149cL9hHZkXtQGl3Eq36BbdLV+/qUwaP1WtJQ+H/+Oxso8g=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], @@ -351,18 +532,46 @@ "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "now-and-later": ["now-and-later@3.0.0", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], + + "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-posix": ["path-posix@1.0.0", "", {}, "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], @@ -377,20 +586,40 @@ "printable-characters": ["printable-characters@1.0.42", "", {}, "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ=="], + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "promise-map-series": ["promise-map-series@0.3.0", "", {}, "sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA=="], + + "quick-temp": ["quick-temp@0.1.9", "", { "dependencies": { "mktemp": "^2.0.1", "rimraf": "^5.0.10", "underscore.string": "~3.3.6" } }, "sha512-yI0h7tIhKVObn03kD+Ln9JFi4OljD28lfaOsTdfpTR0xzrhGOod+q66CjGafUqYX2juUfT9oHIGrTBBo22mkRA=="], + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + "react-i18next": ["react-i18next@16.5.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g=="], + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "remove-trailing-separator": ["remove-trailing-separator@1.1.0", "", {}, "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw=="], + + "replace-ext": ["replace-ext@2.0.0", "", {}, "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-options": ["resolve-options@2.0.0", "", { "dependencies": { "value-or-function": "^4.0.0" } }, "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A=="], + "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], "rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="], @@ -399,52 +628,134 @@ "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], + "rsvp": ["rsvp@4.8.5", "", {}, "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA=="], + + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "selfsigned": ["selfsigned@2.4.1", "", { "dependencies": { "@types/node-forge": "^1.3.0", "node-forge": "^1" } }, "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + "sort-keys": ["sort-keys@5.1.0", "", { "dependencies": { "is-plain-obj": "^4.0.0" } }, "sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="], + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="], "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="], + "stream-composer": ["stream-composer@1.0.2", "", { "dependencies": { "streamx": "^2.13.2" } }, "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w=="], + + "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], + + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "svg-parser": ["svg-parser@2.0.4", "", {}, "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="], + "symlink-or-copy": ["symlink-or-copy@1.3.1", "", {}, "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA=="], + + "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], + + "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], + + "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], + + "to-through": ["to-through@3.0.0", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], - "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "underscore.string": ["underscore.string@3.3.6", "", { "dependencies": { "sprintf-js": "^1.1.1", "util-deprecate": "^1.0.2" } }, "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ=="], + + "undici": ["undici@7.20.0", "", {}, "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unenv": ["unenv-nightly@2.0.0-20241111-080453-894aa31", "", { "dependencies": { "defu": "^6.1.4", "ohash": "^1.1.4", "pathe": "^1.1.2", "ufo": "^1.5.4" } }, "sha512-0W39QQOQ9VE8kVVUpGwEG+pZcsCXk5wqNG6rDPE6Gr+fiA69LR0qERM61hW5KCOkC1/ArCFrfCGjwHyyv/bI0Q=="], + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "value-or-function": ["value-or-function@4.0.0", "", {}, "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg=="], + + "vinyl": ["vinyl@3.0.1", "", { "dependencies": { "clone": "^2.1.2", "remove-trailing-separator": "^1.1.0", "replace-ext": "^2.0.0", "teex": "^1.0.1" } }, "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA=="], + + "vinyl-contents": ["vinyl-contents@2.0.0", "", { "dependencies": { "bl": "^5.0.0", "vinyl": "^3.0.0" } }, "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q=="], + + "vinyl-fs": ["vinyl-fs@4.0.2", "", { "dependencies": { "fs-mkdirp-stream": "^2.0.1", "glob-stream": "^8.0.3", "graceful-fs": "^4.2.11", "iconv-lite": "^0.6.3", "is-valid-glob": "^1.0.0", "lead": "^4.0.0", "normalize-path": "3.0.0", "resolve-options": "^2.0.0", "stream-composer": "^1.0.2", "streamx": "^2.14.0", "to-through": "^3.0.0", "value-or-function": "^4.0.0", "vinyl": "^3.0.1", "vinyl-sourcemap": "^2.0.0" } }, "sha512-XRFwBLLTl8lRAOYiBqxY279wY46tVxLaRhSwo3GzKEuLz1giffsOquWWboD/haGf5lx+JyTigCFfe7DWHoARIA=="], + + "vinyl-sourcemap": ["vinyl-sourcemap@2.0.0", "", { "dependencies": { "convert-source-map": "^2.0.0", "graceful-fs": "^4.2.10", "now-and-later": "^3.0.0", "streamx": "^2.12.5", "vinyl": "^3.0.0", "vinyl-contents": "^2.0.0" } }, "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q=="], + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], "vite-plugin-svgr": ["vite-plugin-svgr@4.5.0", "", { "dependencies": { "@rollup/pluginutils": "^5.2.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, "peerDependencies": { "vite": ">=2.6.0" } }, "sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA=="], + "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + + "walk-sync": ["walk-sync@2.2.0", "", { "dependencies": { "@types/minimatch": "^3.0.3", "ensure-posix-path": "^1.1.0", "matcher-collection": "^2.0.0", "minimatch": "^3.0.4" } }, "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "workerd": ["workerd@1.20241106.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20241106.1", "@cloudflare/workerd-darwin-arm64": "1.20241106.1", "@cloudflare/workerd-linux-64": "1.20241106.1", "@cloudflare/workerd-linux-arm64": "1.20241106.1", "@cloudflare/workerd-windows-64": "1.20241106.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-1GdKl0kDw8rrirr/ThcK66Kbl4/jd4h8uHx5g7YHBrnenY5SX1UPuop2cnCzYUxlg55kPjzIqqYslz1muRFgFw=="], "wrangler": ["wrangler@3.90.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@cloudflare/workers-shared": "0.8.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", "chokidar": "^4.0.1", "date-fns": "^4.1.0", "esbuild": "0.17.19", "itty-time": "^1.0.6", "miniflare": "3.20241106.1", "nanoid": "^3.3.3", "path-to-regexp": "^6.3.0", "resolve": "^1.22.8", "resolve.exports": "^2.0.2", "selfsigned": "^2.0.1", "source-map": "^0.6.1", "unenv": "npm:unenv-nightly@2.0.0-20241111-080453-894aa31", "workerd": "1.20241106.1", "xxhash-wasm": "^1.0.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20241106.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-E/6E9ORAl987+3kP8wDiE3L1lj9r4vQ32/dl5toIxIkSMssmPRQVdxqwgMxbxJrytbFNo8Eo6swgjd4y4nUaLg=="], + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -455,12 +766,110 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "broccoli-output-wrapper/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "fs-merger/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "heimdalljs/rsvp": ["rsvp@3.2.1", "", {}, "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg=="], + + "heimdalljs-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "i18next-parser/i18next": ["i18next@24.2.3", "", { "dependencies": { "@babel/runtime": "^7.26.10" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A=="], + + "miniflare/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "quick-temp/rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "wrangler/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "broccoli-output-wrapper/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "broccoli-output-wrapper/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "fs-merger/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "fs-merger/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "heimdalljs-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "quick-temp/rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="], "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="], @@ -504,5 +913,13 @@ "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="], "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "quick-temp/rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "quick-temp/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], } } diff --git a/i18next-parser.config.cjs b/i18next-parser.config.cjs new file mode 100644 index 0000000..e677c42 --- /dev/null +++ b/i18next-parser.config.cjs @@ -0,0 +1,12 @@ +module.exports = { + locales: ["ko", "en"], + input: ["src/**/*.{ts,tsx}"], + output: "public/locales/$LOCALE/$NAMESPACE.json", + defaultNamespace: "common", + defaultValue: "", + keySeparator: ".", + namespaceSeparator: ":", + createOldCatalogs: false, + keepRemoved: true, + sort: true +}; diff --git a/package.json b/package.json index 31a135d..59eb9c7 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,18 @@ "dev:preview": "bun run build && bunx wrangler pages dev dist", "preview": "vite preview", "test": "bun test", - "test:watch": "bun test --watch" + "test:watch": "bun test --watch", + "i18n:extract": "bunx i18next --config i18next-parser.config.cjs" }, "dependencies": { "dompurify": "^3.3.1", "emoji-datasource": "^16.0.0", + "i18next": "^25.8.1", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^16.5.4", "wrangler": "3.90.0" }, "devDependencies": { @@ -23,6 +28,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.1", + "i18next-parser": "^9.3.0", "typescript": "^5.5.4", "vite": "^5.4.0", "vite-plugin-svgr": "^4.5.0" diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000..7f801c2 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,559 @@ +{ + "actions": { + "add": "Add", + "cancel": "Cancel", + "close": "Close", + "reload": "Reload", + "remove": "Remove", + "reply": "Reply", + "selectHint": "Select (Enter)", + "send": "Send" + }, + "accountAdd": { + "closeAria": "Close add account", + "closeLabel": "Close add account", + "connectWithOAuth": "Connect with OAuth", + "connecting": "Connecting...", + "oauthRedirectHint": "You'll be redirected back after OAuth approval.", + "openAria": "Add account", + "openLabel": "Add account", + "serverAddress": "Server address", + "serverPlaceholder": "mastodon.social" + }, + "accountLabel": { + "unknown": "Unknown" + }, + "accountSelector": { + "empty": "No accounts registered.", + "placeholder": "Select an account.", + "shortcutHint": "Select account (Ctrl+Shift+A)", + "shortcutHintCompact": "Select account (A)" + }, + "app": { + "logoAlt": "Deck logo", + "logoHomeAria": "Deck home", + "mobileBlocker": { + "aria": "Mobile notice", + "description": "The multi-column interface is limited on mobile. Please use desktop or tablet.", + "title": "This app isn't available on mobile 🙇‍♂️" + }, + "sidebarDescription": "Switch between multiple accounts and monitor timelines in real time.", + "sourceCode": "Source code", + "tagline": "Open-source fediverse web client" + }, + "colorScheme": { + "dark": "Dark", + "light": "Light", + "system": "System" + }, + "compose": { + "aria": "Compose", + "attachments": { + "addAria": "Add image", + "addHint": "Add image (Ctrl+Shift+I)", + "previewAria": "Image preview", + "selectedImageAlt": "Selected image", + "selectedImageOriginalAlt": "Original selected image" + }, + "cw": { + "aria": "Content warning", + "placeholder": "Enter content warning", + "toggleAria": "Toggle content warning", + "toggleHint": "Content warning (Ctrl+Shift+W)" + }, + "closeAria": "Close compose", + "emojiPanel": { + "aria": "Emoji palette", + "emojiAria": "Emoji {{label}}", + "empty": "No emojis available.", + "loading": "Loading emojis...", + "noSearchResults": "No search results.", + "openAria": "Open emoji palette", + "openHint": "Open emoji palette (Ctrl+Shift+E)", + "searchAria": "Search emojis", + "searchPlaceholder": "Search emojis", + "searchResults": "Search results", + "selectAccount": "Please select an account.", + "standardDividerAria": "Standard emoji divider", + "standardTitle": "Standard emoji" + }, + "emojiSuggestions": { + "aria": "Emoji suggestions", + "itemAria": "Emoji {{label}}" + }, + "inputHint": "Compose (N / Ctrl+Shift+N)", + "openAria": "Open compose", + "placeholder": "What's on your mind?", + "replyingTo": "Replying to: {{summary}}", + "submitAria": "Post", + "submitHint": "Send (Ctrl+Enter)", + "submitting": "Posting...", + "title": "Compose", + "visibility": { + "direct": "DM", + "hint": "Visibility (Ctrl+Shift+O)", + "private": "Followers", + "public": "Public", + "publicWarning": "Posting publicly. Please be cautious with sensitive content.", + "unlisted": "Unlisted" + } + }, + "confirm": { + "clearLocalStorage": "Clear all local data? Accounts and settings will be reset.", + "removeAccount": "Remove this account?" + }, + "emojiCategory": { + "activities": "Activities", + "animals": "Animals & Nature", + "flags": "Flags", + "food": "Food & Drink", + "objects": "Objects", + "other": "Other", + "people": "People & Body", + "recent": "Recent", + "smileys": "Smileys & Emotion", + "symbols": "Symbols", + "travel": "Travel & Places" + }, + "emoji": { + "alt": "{{label}} emoji" + }, + "favourite": { + "addLabel": "Favorite", + "added": "Added to favorites.", + "error": "Failed to process favorite.", + "loading": "Loading...", + "removeLabel": "Unfavorite", + "removed": "Removed from favorites." + }, + "bookmark": { + "label": "Bookmark", + "removeLabel": "Remove bookmark" + }, + "like": { + "add": "Like", + "remove": "Unlike" + }, + "boost": { + "add": "Boost", + "remove": "Undo boost" + }, + "media": { + "attachment": "Attachment", + "floatingCloseAria": "Close floating player", + "imageAttachment": "Attached image", + "nextImage": "Next image", + "openAttachment": "Open attachment", + "prevImage": "Previous image", + "selectedImageOriginalAlt": "Original selected image", + "videoAttachment": "Attached video", + "viewImage": "View image", + "viewImageWithDescription": "View image: {{description}}" + }, + "errors": { + "accountAlreadyRegistered": "This account is already registered.", + "accountReauthNotFound": "Couldn't find the account to reauthenticate.", + "accountRequired": "Please select an account.", + "appContextRequired": "AppContext provider is required.", + "appRegistrationLoadFailed": "Couldn't load app registration details.", + "api": { + "accountVerifyFailed": "Account verification failed.", + "bookmarksLoadFailed": "Couldn't load bookmarks.", + "conversationLoadFailed": "Couldn't load the conversation.", + "createdNoteNotFound": "Couldn't find the created note.", + "customEmojisLoadFailed": "Failed to load emojis.", + "followFailed": "Failed to follow.", + "instanceInfoLoadFailed": "Couldn't load instance information.", + "mediaUploadFailed": "Failed to upload media.", + "mediaUploadNotFound": "Couldn't find uploaded media info.", + "muteFailed": "Failed to mute.", + "noteStateLoadFailed": "Couldn't load post state.", + "notificationsLoadFailed": "Couldn't load notifications.", + "profileLoadFailed": "Couldn't load profile information.", + "relationshipLoadFailed": "Couldn't load relationship information.", + "renoteNotFound": "Couldn't find the renote to undo.", + "replyLoadFailed": "Couldn't load replies.", + "requestFailed": "Request failed.", + "statusInfoLoadFailed": "Couldn't load post details.", + "statusesLoadFailed": "Couldn't load posts.", + "translationFailed": "Translation failed.", + "translationResultMissing": "Couldn't read translation results.", + "unblockFailed": "Failed to unblock.", + "unfollowFailed": "Failed to unfollow.", + "unmuteFailed": "Failed to unmute.", + "blockFailed": "Failed to block." + }, + "bookmarkFailed": "Failed to process bookmark.", + "boostFailed": "Failed to process boost.", + "characterLimitExceeded": "Character limit ({{limit}} chars) exceeded.", + "composeFailed": "Failed to publish the post.", + "emojisLoadFailed": "Failed to load emojis.", + "followFailed": "Failed to follow.", + "followRequestCancelFailed": "Failed to cancel follow request.", + "likeFailed": "Failed to process like.", + "blockFailed": "Failed to block.", + "muteFailed": "Failed to mute.", + "unblockFailed": "Failed to unblock.", + "unfollowFailed": "Failed to unfollow.", + "unmuteFailed": "Failed to unmute.", + "oauth": { + "appRegisterFailed": "App registration failed.", + "appRegisterInvalid": "App registration info is invalid.", + "failed": "OAuth processing failed.", + "invalidState": "OAuth state is invalid. Please try again.", + "mastodonInfoRequired": "Mastodon OAuth info is required.", + "misskeyInfoRequired": "Misskey OAuth info is required.", + "misskeyMissingSession": "Couldn't receive Misskey session info. Please try again.", + "misskeySessionMissing": "Couldn't find Misskey session info.", + "misskeySessionMismatch": "Misskey session info doesn't match. Please try again.", + "misskeyTokenInvalid": "Misskey token response is invalid.", + "misskeyTokenMissing": "Couldn't receive Misskey token.", + "missingCode": "Couldn't receive the OAuth code. Please try again." + }, + "oauthConnectFailed": "Failed to connect via OAuth.", + "reactionAlreadyExists": "You've already reacted. Remove it first.", + "reactionFailed": "Failed to process reaction.", + "reactionMisskeyOnly": "Reactions are only available on Misskey accounts.", + "serverAddressRequired": "Please enter a server address.", + "statusesLoadFailed": "Couldn't load posts.", + "statusDeleteFailed": "Failed to delete the post.", + "relationshipLoadFailed": "Couldn't load relationship information.", + "profileLoadFailed": "Couldn't load profile information.", + "threadLoadFailed": "Couldn't load the thread.", + "timelineLoadFailed": "Couldn't load the timeline.", + "timelineLoadMoreFailed": "Couldn't load more posts." + }, + "infoModal": { + "closeAria": "Close {{title}}" + }, + "infoPages": { + "backToTimeline": "Back to timeline", + "license": "License", + "oss": "Open-source list", + "shortcuts": "Shortcuts", + "terms": "Terms of service" + }, + "language": { + "english": "English", + "korean": "Korean" + }, + "mention": { + "viewProfile": "View profile: {{name}}" + }, + "menu": { + "closeAria": "Close menu", + "openAria": "Open menu", + "title": "Menu" + }, + "notifications": { + "actorAction": " {{action}}", + "badgeCount": "{{count}}", + "badgeOver": "99+", + "close": "Close notifications", + "empty": "No notifications to display.", + "loading": "Loading notifications...", + "open": "Open notifications", + "openHint": "Open notifications (G)", + "openWithCount": "Open notifications (new {{badge}})", + "title": "Notifications", + "toast": "New notification received.", + "toastAction": "Go to the column with notifications", + "toastActionAria": "Go to the column where notifications arrived" + }, + "oauth": { + "authenticating": "Authenticating with OAuth..." + }, + "pomodoro": { + "controls": { + "reset": "Reset", + "resetHint": "Reset (X)", + "start": "Start", + "stop": "Stop", + "toggleHint": "Start/Stop (S)" + }, + "focusSession": { + "description": "Pomodoro timer is running.
Timeline stays hidden until focus ends.", + "title": "🎯 Focus session in progress" + }, + "nextSession": "Switch to next session", + "progress": { + "completed": "Session {{session}} ({{label}} complete)", + "pending": "Session {{session}} (pending)" + }, + "session": { + "break": "Break", + "focus": "Focus", + "longBreak": "Long break" + }, + "todos": { + "addAria": "Add todo", + "aria": "Pomodoro todos", + "completeAria": "Complete todo: {{text}}", + "inputAria": "Pomodoro todo input", + "inputHint": "Add task (F) · ↑ to list · ESC blur", + "listHint": "Up/Down move · Space complete · D delete · → timeline · ESC clear", + "placeholder": "Add a task", + "removeAria": "Remove todo: {{text}}" + } + }, + "profile": { + "aria": "User profile", + "block": "Block", + "closeAria": "Close profile", + "follow": "Follow", + "followAction": "Follow", + "followAriaRequested": "Follow request pending", + "followRequest": "Request to follow", + "followRequestSend": "Send follow request", + "following": "Following", + "imageAlt": "{{name}} profile image", + "loading": "Loading profile...", + "menuOpenAria": "Open profile menu", + "mute": "Mute", + "openOrigin": "View on origin site", + "postsLoading": "Loading posts...", + "postsTitle": "Posts", + "requested": "Requested", + "title": "Profile", + "unblock": "Unblock", + "unfollow": "Unfollow", + "unfollowConfirmAria": "Confirm unfollow", + "unfollowConfirmMessage": "Unfollow this account?", + "unmute": "Unmute", + "unknownUser": "Unknown user", + "viewAria": "View profile: {{name}}" + }, + "reactions": { + "add": "Reaction", + "addAria": "Add reaction", + "panelAria": "Select reaction", + "receivedAria": "Received reactions", + "countTitle": "{{label}} {{count}}", + "toggleAria": "{{label}} reaction {{action}}" + }, + "sectionMenu": { + "addLeft": "Add section to the left", + "addRight": "Add section to the right", + "moveLeft": "Move left", + "moveRight": "Move right", + "openAria": "Open section menu", + "openHint": "Open section menu (M)", + "openOrigin": "View on origin server", + "refresh": "Refresh", + "remove": "Remove section", + "settings": "Section settings" + }, + "sectionSettings": { + "customEmojis": { + "description": "Show custom emojis in names and content.", + "title": "Show custom emojis" + }, + "profileImages": { + "description": "Show profile images only in this section.", + "title": "Show profile images" + }, + "reactions": { + "description": "Show reactions from servers that support them.", + "title": "Show reactions" + }, + "sectionSize": { + "aria": "Set section width", + "description": "Adjust the column width for this section.", + "large": "Large", + "medium": "Medium", + "small": "Small", + "title": "Section width" + }, + "title": "Section settings" + }, + "settings": { + "account": { + "description": "Select an account to reauthenticate or remove.", + "reauth": "Reauthenticate", + "reauthAria": "Reauthenticate account", + "reauthing": "Reauthenticating...", + "removeAria": "Remove account", + "title": "Account management" + }, + "colorScheme": { + "aria": "Select color mode", + "description": "Follow system settings or lock light/dark mode.", + "title": "Color mode" + }, + "language": { + "aria": "Select language", + "description": "Choose the display language.", + "title": "Language" + }, + "pomodoro": { + "break": "Break", + "description": "Show the Pomodoro timer in the sidebar.", + "focus": "Focus", + "longBreak": "Long break", + "timerDescription": "Set focus, break, and long break minutes.", + "timerTitle": "Pomodoro durations", + "title": "Pomodoro timer" + }, + "storage": { + "clear": "Clear all", + "clearAria": "Clear local storage", + "description": "Delete all local data including accounts and settings.", + "title": "Clear local storage" + }, + "theme": { + "aria": "Select theme", + "description": "Choose from Default, Christmas, Sky Pink, Monochrome, and Matcha Core.", + "title": "Theme" + }, + "open": "Open settings", + "title": "Settings" + }, + "shortcuts": { + "compose": { + "attachMedia": "Attach media", + "blur": "Blur compose input", + "focus": "Focus compose input", + "focusWhileActive": "Focus compose input (while active)", + "note": "Applies within the compose area.", + "openAccountSelector": "Open account selector", + "openVisibility": "Open visibility selector", + "submit": "Post", + "title": "Compose", + "toggleContentWarning": "Toggle content warning", + "toggleEmojiPanel": "Toggle emoji panel" + }, + "emojiPanel": { + "close": "Close emoji picker", + "navigate": "Navigate emojis/categories", + "note": "Works only when the emoji picker is open.", + "select": "Insert selected emoji / react", + "title": "Emoji panel / reactions", + "toggleCategory": "Collapse/expand categories" + }, + "imageViewer": { + "close": "Close image viewer", + "navigate": "Navigate images", + "title": "Image viewer" + }, + "pomodoro": { + "focusTask": "Focus add-task input", + "reset": "Reset Pomodoro timer", + "title": "Pomodoro timer", + "toggle": "Start/stop Pomodoro timer" + }, + "selected": { + "boost": "Boost", + "closeMenu": "Close open menu", + "likeOrReaction": "Like (Mastodon) / ❤️ reaction (Misskey)", + "navigateMenu": "Navigate open menu", + "note": "Works only when a post is selected.", + "openAccount": "Open account selector", + "openColumnMenu": "Open column menu", + "openMedia": "Open attached media", + "openNotifications": "Open notifications", + "openProfile": "Open author profile", + "openReactions": "Open reaction palette (Misskey)", + "openStatus": "Open post popup (or select item in open menus)", + "openTimelineMenu": "Open timeline menu", + "reply": "Reply", + "title": "Selected post controls" + }, + "suggestions": { + "close": "Close suggestions", + "insert": "Insert suggested emoji", + "navigate": "Navigate suggestions", + "note": "Works only when the suggestion list is open.", + "title": "Emoji suggestions" + }, + "timeline": { + "clearSelection": "Clear selection", + "moveColumn": "Move to adjacent column", + "moveVertical": "Move up/down within selected posts", + "selectLeftmost": "Select leftmost post when nothing is selected", + "title": "Timeline navigation" + } + }, + "status": { + "boostedBy": "Boosted by ", + "boostedByInline": "Boosted by ", + "boostedByMe": "Boosted by me", + "boostDisabled": "Private posts cannot be boosted.", + "deleteTitle": "Delete post", + "hideContent": "Hide", + "menuOpenAria": "Open post menu", + "noContent": "(No content)", + "openOrigin": "View on origin server", + "replyTo": "Reply sent to {{name}}", + "showContent": "Show content", + "threadLoading": "Loading thread...", + "title": "Post", + "viewAria": "View post", + "viewBoostThread": "View boosted post thread", + "viewReplyThread": "View reply thread" + }, + "themes": { + "christmas": "Christmas", + "default": "Default", + "matchaCore": "Matcha Core", + "monochrome": "Monochrome", + "skyPink": "Sky Pink" + }, + "translation": { + "completed": "Translation completed.", + "failed": "Translation failed.", + "hide": "Hide translation", + "hideAria": "Hide translation", + "inProgress": "Translating...", + "label": "Translation", + "meta": { + "detected": "Detected language: {{language}}", + "provider": "Provider: {{provider}}", + "target": "Target language: {{language}}" + }, + "noContent": "(No content)", + "resultAria": "Translation result", + "retry": "Retry translation" + }, + "timeline": { + "accountRequired": "Select an account to load the timeline.", + "actionsAria": "Timeline actions", + "bookmarks": "Bookmarks", + "empty": { + "bookmarks": "No bookmarked posts.", + "default": "No posts to display.", + "notifications": "No notifications to display." + }, + "federated": "Federated", + "global": "Global", + "home": "Home", + "loadingMore": "Loading more...", + "local": "Local", + "notifications": "Notifications", + "pending": { + "action": "View", + "aria": "Show {{label}} new posts", + "label": "{{label}} new posts", + "sr": "{{label}} new posts have arrived.", + "title": "Show new posts" + }, + "scrollTop": "Scroll to top", + "selectAria": "Select timeline: {{label}}", + "selectHint": "Select timeline (T)", + "selectMenuAria": "Select timeline", + "social": "Social" + }, + "toast": { + "bookmarkAdded": "Bookmarked.", + "bookmarkRemoved": "Bookmark removed.", + "close": "Close", + "closeAria": "Close toast", + "followRequestCancelled": "Follow request cancelled.", + "followRequestSent": "Follow request sent.", + "followed": "Followed.", + "unfollowed": "Unfollowed.", + "muted": "Muted.", + "unmuted": "Unmuted.", + "blocked": "Blocked.", + "unblocked": "Unblocked." + } +} diff --git a/public/locales/ko/common.json b/public/locales/ko/common.json new file mode 100644 index 0000000..90705d3 --- /dev/null +++ b/public/locales/ko/common.json @@ -0,0 +1,709 @@ +{ + "actions": { + "add": "추가", + "cancel": "취소", + "close": "닫기", + "reload": "다시 불러오기", + "remove": "삭제", + "reply": "답글", + "selectHint": "선택 (Enter)", + "send": "전송" + }, + "accountAdd": { + "closeAria": "계정 추가 닫기", + "closeLabel": "계정 추가 닫기", + "connectWithOAuth": "OAuth로 연결", + "connecting": "연결 중...", + "oauthRedirectHint": "OAuth 승인 후 자동으로 돌아옵니다.", + "openAria": "계정 추가", + "openLabel": "계정 추가", + "serverAddress": "서버 주소", + "serverPlaceholder": "mastodon.social" + }, + "accountLabel": { + "unknown": "알 수 없음" + }, + "accountSelector": { + "empty": "등록된 계정이 없습니다.", + "placeholder": "계정을 선택하세요.", + "shortcutHint": "계정 선택 (Ctrl+Shift+A)", + "shortcutHintCompact": "계정 선택 (A)" + }, + "app": { + "logoAlt": "Deck 로고", + "logoHomeAria": "Deck 홈", + "mobileBlocker": { + "aria": "모바일 안내", + "description": "멀티 컬럼 인터페이스 특성상 모바일 지원이 제한됩니다. 데스크톱 또는 태블릿에서 이용해 주세요.", + "title": "모바일 환경에서는 사용이 불가능합니다 🙇‍♂️" + }, + "sidebarDescription": "여러 계정을 전환하고 타임라인을 실시간으로 확인할 수 있습니다.", + "sourceCode": "소스 코드", + "tagline": "오픈소스 페디버스 웹 클라이언트" + }, + "colorScheme": { + "dark": "다크", + "light": "라이트", + "system": "시스템" + }, + "compose": { + "aria": "글 작성", + "attachments": { + "addAria": "이미지 추가", + "addHint": "이미지 추가 (Ctrl+Shift+I)", + "previewAria": "이미지 미리보기", + "selectedImageAlt": "선택한 이미지", + "selectedImageOriginalAlt": "선택한 이미지 원본" + }, + "cw": { + "aria": "콘텐츠 경고", + "placeholder": "CW 내용을 입력하세요", + "toggleAria": "콘텐츠 경고 입력", + "toggleHint": "콘텐츠 경고 입력 (Ctrl+Shift+W)" + }, + "closeAria": "글쓰기 닫기", + "emojiPanel": { + "aria": "이모지 팔렛트", + "emojiAria": "이모지 {{label}}", + "empty": "사용할 수 있는 이모지가 없습니다.", + "loading": "이모지를 불러오는 중...", + "noSearchResults": "검색 결과가 없습니다.", + "openAria": "이모지 팔렛트 열기", + "openHint": "이모지 팔렛트 열기 (Ctrl+Shift+E)", + "searchAria": "이모지 검색", + "searchPlaceholder": "이모지 검색", + "searchResults": "검색 결과", + "selectAccount": "계정을 선택해주세요.", + "standardDividerAria": "표준 이모지 구분선", + "standardTitle": "표준 이모지" + }, + "emojiSuggestions": { + "aria": "이모지 추천", + "itemAria": "이모지 {{label}}" + }, + "inputHint": "글 작성 (N / Ctrl+Shift+N)", + "openAria": "글쓰기 열기", + "placeholder": "지금 무슨 생각을 하고 있나요?", + "replyingTo": "답글 대상: {{summary}}", + "submitAria": "게시", + "submitHint": "전송 (Ctrl+Enter)", + "submitting": "게시 중...", + "title": "글쓰기", + "visibility": { + "direct": "DM", + "hint": "공개 범위 (Ctrl+Shift+O)", + "private": "팔로워", + "public": "전체 공개", + "publicWarning": "전체 공개로 게시됩니다. 민감한 내용은 주의해주세요.", + "unlisted": "미등록" + } + }, + "confirm": { + "clearLocalStorage": "로컬 저장소의 모든 데이터를 삭제할까요? 계정과 설정 정보가 모두 초기화됩니다.", + "removeAccount": "이 계정을 삭제할까요?" + }, + "emojiCategory": { + "activities": "활동", + "animals": "동물/자연", + "flags": "국기", + "food": "음식", + "objects": "사물", + "other": "기타", + "people": "사람/손", + "recent": "최근 사용", + "smileys": "표정", + "symbols": "기호", + "travel": "여행/장소" + }, + "emoji": { + "alt": "{{label}} 이모지" + }, + "favourite": { + "addLabel": "즐겨찾기", + "added": "즐겨찾기에 추가했습니다.", + "error": "즐겨찾기 처리에 실패했습니다.", + "loading": "로딩...", + "removeLabel": "즐겨찾기 취소", + "removed": "즐겨찾기에서 해제했습니다." + }, + "bookmark": { + "label": "북마크", + "removeLabel": "북마크 취소" + }, + "like": { + "add": "좋아요", + "remove": "좋아요 취소" + }, + "boost": { + "add": "부스트", + "remove": "부스트 취소" + }, + "media": { + "attachment": "첨부 미디어", + "floatingCloseAria": "고정 플레이어 닫기", + "imageAttachment": "첨부 이미지", + "nextImage": "다음 이미지", + "openAttachment": "첨부 파일 열기", + "prevImage": "이전 이미지", + "selectedImageOriginalAlt": "선택한 이미지 원본", + "videoAttachment": "첨부 동영상", + "viewImage": "이미지 보기", + "viewImageWithDescription": "이미지 보기: {{description}}" + }, + "errors": { + "accountAlreadyRegistered": "이미 등록된 계정입니다.", + "accountReauthNotFound": "재인증할 계정을 찾지 못했습니다.", + "accountRequired": "계정을 선택해주세요.", + "appContextRequired": "AppContext provider가 필요합니다.", + "appRegistrationLoadFailed": "앱 등록 정보를 불러오지 못했습니다.", + "api": { + "accountVerifyFailed": "계정 인증에 실패했습니다.", + "bookmarksLoadFailed": "북마크를 불러오지 못했습니다.", + "conversationLoadFailed": "대화를 불러오지 못했습니다.", + "createdNoteNotFound": "생성된 노트를 찾을 수 없습니다.", + "customEmojisLoadFailed": "이모지를 불러오지 못했습니다.", + "followFailed": "팔로우에 실패했습니다.", + "instanceInfoLoadFailed": "인스턴스 정보를 불러오지 못했습니다.", + "mediaUploadFailed": "이미지 업로드에 실패했습니다.", + "mediaUploadNotFound": "업로드된 미디어 정보를 찾을 수 없습니다.", + "muteFailed": "뮤트에 실패했습니다.", + "noteStateLoadFailed": "게시물 상태를 불러오지 못했습니다.", + "notificationsLoadFailed": "알림을 불러오지 못했습니다.", + "profileLoadFailed": "프로필 정보를 불러오지 못했습니다.", + "relationshipLoadFailed": "관계 정보를 불러오지 못했습니다.", + "renoteNotFound": "취소할 리노트를 찾지 못했습니다.", + "replyLoadFailed": "답글을 불러오지 못했습니다.", + "requestFailed": "요청에 실패했습니다.", + "statusInfoLoadFailed": "게시글 정보를 불러오지 못했습니다.", + "statusesLoadFailed": "게시글을 불러오지 못했습니다.", + "translationFailed": "번역에 실패했습니다.", + "translationResultMissing": "번역 결과를 확인할 수 없습니다.", + "unblockFailed": "차단 해제에 실패했습니다.", + "unfollowFailed": "언팔로우에 실패했습니다.", + "unmuteFailed": "뮤트 해제에 실패했습니다.", + "blockFailed": "차단에 실패했습니다." + }, + "bookmarkFailed": "북마크 처리에 실패했습니다.", + "boostFailed": "부스트 처리에 실패했습니다.", + "characterLimitExceeded": "글자 수 제한({{limit}}자)을 초과했습니다.", + "composeFailed": "글 작성에 실패했습니다.", + "emojisLoadFailed": "이모지를 불러오지 못했습니다.", + "followFailed": "팔로우에 실패했습니다.", + "followRequestCancelFailed": "팔로우 요청을 취소하지 못했습니다.", + "likeFailed": "좋아요 처리에 실패했습니다.", + "blockFailed": "차단에 실패했습니다.", + "muteFailed": "뮤트에 실패했습니다.", + "unblockFailed": "차단 해제에 실패했습니다.", + "unfollowFailed": "언팔로우에 실패했습니다.", + "unmuteFailed": "뮤트 해제에 실패했습니다.", + "oauth": { + "appRegisterFailed": "앱 등록에 실패했습니다.", + "appRegisterInvalid": "앱 등록 정보가 올바르지 않습니다.", + "failed": "OAuth 처리에 실패했습니다.", + "invalidState": "OAuth 상태가 유효하지 않습니다. 다시 시도해주세요.", + "mastodonInfoRequired": "마스토돈 OAuth 정보가 필요합니다.", + "misskeyInfoRequired": "미스키 OAuth 정보가 필요합니다.", + "misskeyMissingSession": "미스키 세션 정보를 받지 못했습니다. 다시 시도해주세요.", + "misskeySessionMissing": "미스키 세션 정보를 찾지 못했습니다.", + "misskeySessionMismatch": "미스키 세션 정보가 일치하지 않습니다. 다시 시도해주세요.", + "misskeyTokenInvalid": "미스키 토큰 응답이 올바르지 않습니다.", + "misskeyTokenMissing": "미스키 토큰을 받지 못했습니다.", + "missingCode": "OAuth 코드를 받지 못했습니다. 다시 시도해주세요." + }, + "oauthConnectFailed": "OAuth 연결에 실패했습니다.", + "reactionAlreadyExists": "이미 리액션을 남겼습니다. 먼저 취소해주세요.", + "reactionFailed": "리액션 처리에 실패했습니다.", + "reactionMisskeyOnly": "리액션은 미스키 계정에서만 사용할 수 있습니다.", + "serverAddressRequired": "서버 주소를 입력해주세요.", + "statusesLoadFailed": "게시글을 불러오지 못했습니다.", + "statusDeleteFailed": "게시글 삭제에 실패했습니다.", + "relationshipLoadFailed": "관계 정보를 불러오지 못했습니다.", + "profileLoadFailed": "프로필 정보를 불러오지 못했습니다.", + "threadLoadFailed": "스레드를 불러오지 못했습니다.", + "timelineLoadFailed": "타임라인을 불러오지 못했습니다.", + "timelineLoadMoreFailed": "추가 글을 불러오지 못했습니다." + }, + "infoModal": { + "closeAria": "{{title}} 닫기" + }, + "infoPages": { + "backToTimeline": "타임라인으로 돌아가기", + "license": "라이선스", + "oss": "오픈소스 목록", + "shortcuts": "단축키", + "terms": "이용약관" + }, + "language": { + "english": "영어", + "korean": "한국어" + }, + "mention": { + "viewProfile": "{{name}} 프로필 보기" + }, + "menu": { + "closeAria": "메뉴 닫기", + "openAria": "메뉴 열기", + "title": "메뉴" + }, + "notifications": { + "actorAction": " 님이 {{action}}", + "badgeCount": "{{count}}개", + "badgeOver": "99개 이상", + "close": "알림 닫기", + "empty": "표시할 알림이 없습니다.", + "loading": "알림을 불러오는 중...", + "open": "알림 열기", + "openHint": "알림 열기 (G)", + "openWithCount": "알림 열기 (새 알림 {{badge}})", + "title": "알림", + "toast": "새 알림이 도착했습니다.", + "toastAction": "알림 받은 컬럼으로 이동", + "toastActionAria": "알림이 도착한 컬럼으로 이동", + "labels": { + "default": "알림", + "favourite": "좋아요함", + "follow": "팔로우함", + "followRequest": "팔로우 요청함", + "mention": "멘션함", + "poll": "투표함", + "reblog": "부스트함", + "status": "글 작성함", + "update": "게시글 수정함" + }, + "fallback": { + "default": "알림이 도착했습니다.", + "favourite": "좋아요를 눌렀습니다.", + "follow": "팔로우했습니다.", + "followRequest": "팔로우 요청을 보냈습니다.", + "mention": "멘션했습니다.", + "poll": "투표했습니다.", + "reblog": "부스트했습니다.", + "status": "새 글을 올렸습니다.", + "update": "게시글을 수정했습니다." + }, + "export": { + "antenna": "안테나", + "blocking": "차단", + "clip": "클립", + "customEmoji": "커스텀 이모지", + "favorite": "즐겨찾기", + "following": "팔로잉", + "muting": "뮤트", + "note": "노트", + "userList": "사용자 리스트" + }, + "login": { + "browser": "브라우저: {{value}}", + "location": "위치: {{value}}" + }, + "system": { + "app": "앱", + "default": "시스템", + "grouped": "여러 사용자" + }, + "misskey": { + "achievementEarned": { + "label": "도전과제 달성함", + "fallback": "도전과제를 달성했습니다.", + "fallbackWithDetail": "도전과제를 달성했습니다: {{detail}}" + }, + "announcement": { + "label": "공지 알림", + "fallback": "새 공지가 있습니다.", + "fallbackWithAnnouncement": "공지: {{announcement}}" + }, + "app": { + "label": "앱 알림", + "fallback": "앱 알림이 도착했습니다." + }, + "chatRoomInvitationReceived": { + "label": "채팅방 초대됨", + "fallback": "채팅방에 초대되었습니다.", + "fallbackWithRoom": "채팅방에 초대되었습니다: {{room}}" + }, + "createToken": { + "label": "토큰 생성됨", + "fallback": "새 토큰이 생성되었습니다." + }, + "exportCompleted": { + "label": "내보내기 완료됨", + "fallback": "내보내기가 완료되었습니다.", + "fallbackWithEntity": "내보내기가 완료되었습니다: {{entity}}" + }, + "follow": { + "label": "팔로우함", + "fallback": "팔로우했습니다." + }, + "followRequestAccepted": { + "label": "팔로우 요청 승인함", + "fallback": "팔로우 요청이 승인되었습니다.", + "fallbackWithMessage": "팔로우 요청이 승인되었습니다. {{message}}" + }, + "login": { + "label": "로그인 알림", + "fallback": "로그인 알림입니다.", + "fallbackWithDetail": "로그인 알림입니다. {{detail}}" + }, + "mention": { + "label": "멘션함", + "fallback": "멘션했습니다." + }, + "note": { + "label": "글 작성함", + "fallback": "새 글을 올렸습니다." + }, + "pollEnded": { + "label": "투표 종료됨", + "fallback": "투표가 종료되었습니다." + }, + "pollVote": { + "label": "투표함", + "fallback": "투표했습니다." + }, + "quote": { + "label": "인용함", + "fallback": "인용했습니다." + }, + "reaction": { + "label": "리액션함", + "labelWithReaction": "리액션함 {{reaction}}", + "fallback": "리액션했습니다.", + "fallbackWithReaction": "리액션했습니다. {{reaction}}" + }, + "reactionGrouped": { + "label": "리액션 모음", + "fallback": "여러 명이 리액션했습니다." + }, + "receiveFollowRequest": { + "label": "팔로우 요청함", + "fallback": "팔로우 요청을 보냈습니다." + }, + "renote": { + "label": "리노트함", + "fallback": "리노트했습니다." + }, + "renoteGrouped": { + "label": "리노트 모음", + "fallback": "여러 명이 리노트했습니다." + }, + "reply": { + "label": "답글 남김", + "fallback": "답글을 남겼습니다." + }, + "roleAssigned": { + "label": "역할 부여됨", + "fallback": "새 역할이 부여되었습니다.", + "fallbackWithRole": "새 역할이 부여되었습니다: {{role}}" + }, + "scheduledNotePosted": { + "label": "예약 글 게시됨", + "fallback": "예약 글이 게시되었습니다." + }, + "scheduledNotePostFailed": { + "label": "예약 글 게시 실패", + "fallback": "예약 글 게시에 실패했습니다.", + "fallbackWithPreview": "예약 글 게시에 실패했습니다: {{preview}}" + }, + "test": { + "label": "테스트 알림", + "fallback": "테스트 알림입니다." + } + } + }, + "oauth": { + "authenticating": "OAuth 인증 중..." + }, + "pomodoro": { + "controls": { + "reset": "리셋", + "resetHint": "리셋 (X)", + "start": "시작", + "stop": "정지", + "toggleHint": "시작/정지 (S)" + }, + "focusSession": { + "description": "뽀모도로 타이머가 동작 중입니다.
타임라인은 집중이 끝날 때까지 숨겨집니다.", + "title": "🎯 집중 세션 진행 중" + }, + "nextSession": "다음 세션으로 전환", + "progress": { + "completed": "세션 {{session}} ({{label}} 완료)", + "pending": "세션 {{session}} (진행 전)" + }, + "session": { + "break": "휴식", + "focus": "집중", + "longBreak": "긴 휴식" + }, + "todos": { + "addAria": "투두 추가", + "aria": "뽀모도로 투두", + "completeAria": "할 일 완료: {{text}}", + "inputAria": "뽀모도로 투두 입력", + "inputHint": "할 일 추가 (F) · ↑ 목록 이동 · ESC 포커스 해제", + "listHint": "↑/↓ 이동 · Space 완료 · D 삭제 · → 타임라인 이동 · ESC 선택 해제", + "placeholder": "할 일 추가", + "removeAria": "할 일 삭제: {{text}}" + } + }, + "profile": { + "aria": "사용자 프로필", + "block": "차단하기", + "closeAria": "프로필 닫기", + "follow": "팔로우", + "followAction": "팔로우하기", + "followAriaRequested": "팔로우 요청됨", + "followRequest": "팔로우 요청", + "followRequestSend": "팔로우 요청 보내기", + "following": "팔로잉", + "imageAlt": "{{name}} 프로필 이미지", + "loading": "프로필을 불러오는 중...", + "menuOpenAria": "프로필 메뉴 열기", + "mute": "뮤트하기", + "openOrigin": "원본 사이트에서 보기", + "postsLoading": "게시글을 불러오는 중...", + "postsTitle": "작성한 글", + "requested": "요청됨", + "title": "프로필", + "unblock": "차단 해제", + "unfollow": "언팔로우", + "unfollowConfirmAria": "언팔로우 확인", + "unfollowConfirmMessage": "정말 언팔로우할까요?", + "unmute": "뮤트 해제", + "unknownUser": "알 수 없는 사용자", + "viewAria": "{{name}} 프로필 보기" + }, + "reactions": { + "add": "리액션", + "addAria": "리액션 추가", + "panelAria": "리액션 선택", + "receivedAria": "받은 리액션", + "countTitle": "{{label}} {{count}}개", + "toggleAria": "{{label}} 리액션 {{action}}" + }, + "sectionMenu": { + "addLeft": "왼쪽 섹션 추가", + "addRight": "오른쪽 섹션 추가", + "moveLeft": "왼쪽으로 이동", + "moveRight": "오른쪽으로 이동", + "openAria": "섹션 메뉴 열기", + "openHint": "섹션 메뉴 열기 (M)", + "openOrigin": "원본 서버에서 보기", + "refresh": "새로고침", + "remove": "섹션 삭제", + "settings": "섹션 설정" + }, + "sectionSettings": { + "customEmojis": { + "description": "사용자 이름과 본문에 커스텀 이모지를 표시합니다.", + "title": "커스텀 이모지 표시" + }, + "profileImages": { + "description": "이 섹션에서만 프로필 이미지를 보여줍니다.", + "title": "프로필 이미지 표시" + }, + "reactions": { + "description": "리액션을 지원하는 서버에서 받은 리액션을 보여줍니다.", + "title": "리액션 표시" + }, + "sectionSize": { + "aria": "섹션 폭 설정", + "description": "이 섹션의 가로 폭을 조절합니다.", + "large": "대", + "medium": "중", + "small": "소", + "title": "섹션 폭" + }, + "title": "섹션 설정" + }, + "settings": { + "account": { + "description": "계정을 선택하여 재인증하거나 삭제합니다.", + "reauth": "재인증", + "reauthAria": "계정 재인증", + "reauthing": "재인증 중...", + "removeAria": "계정 삭제", + "title": "계정 관리" + }, + "colorScheme": { + "aria": "색상 모드 선택", + "description": "시스템 설정을 따르거나 라이트/다크 모드를 고정합니다.", + "title": "색상 모드" + }, + "language": { + "aria": "언어 선택", + "description": "표시 언어를 선택합니다.", + "title": "언어" + }, + "pomodoro": { + "break": "휴식", + "description": "사이드바에 뽀모도로 타이머를 표시합니다.", + "focus": "집중", + "longBreak": "긴 휴식", + "timerDescription": "집중, 휴식, 긴 휴식 시간을 분 단위로 설정합니다.", + "timerTitle": "뽀모도로 시간 설정", + "title": "뽀모도로 타이머" + }, + "storage": { + "clear": "모두 삭제", + "clearAria": "로컬 저장소 초기화", + "description": "계정과 설정을 포함한 모든 로컬 데이터를 삭제합니다.", + "title": "로컬 저장소 초기화" + }, + "theme": { + "aria": "테마 선택", + "description": "기본, 크리스마스, 하늘핑크, 모노톤 테마를 선택합니다.", + "title": "테마" + }, + "open": "설정 열기", + "title": "설정" + }, + "shortcuts": { + "compose": { + "attachMedia": "미디어 첨부", + "blur": "글쓰기 입력 포커스 해제", + "focus": "글쓰기 입력으로 이동", + "focusWhileActive": "글쓰기 입력으로 이동 (포커스 중)", + "note": "글쓰기 영역 기준으로 동작합니다.", + "openAccountSelector": "계정 선택 열기", + "openVisibility": "공개 범위 선택", + "submit": "글 올리기", + "title": "글쓰기", + "toggleContentWarning": "내용 경고 토글", + "toggleEmojiPanel": "이모지 패널 토글" + }, + "emojiPanel": { + "close": "이모지 선택 닫기", + "navigate": "이모지/카테고리 이동", + "note": "이모지 선택 팝오버가 열려 있을 때만 동작합니다.", + "select": "선택된 이모지 입력/리액션", + "title": "이모지 패널/리액션", + "toggleCategory": "카테고리 접기/펼치기" + }, + "imageViewer": { + "close": "이미지 보기 닫기", + "navigate": "이미지 이동", + "title": "이미지 뷰어" + }, + "pomodoro": { + "focusTask": "할 일 추가 입력으로 이동", + "reset": "뽀모도로 타이머 리셋", + "title": "뽀모도로 타이머", + "toggle": "뽀모도로 타이머 시작/정지" + }, + "selected": { + "boost": "부스트", + "closeMenu": "열린 메뉴 닫기", + "likeOrReaction": "좋아요 (마스토돈) / ❤️ 리액션 (미스키)", + "navigateMenu": "열린 메뉴에서 항목 이동", + "note": "글을 선택한 상태에서만 동작합니다.", + "openAccount": "계정 선택 열기", + "openColumnMenu": "컬럼 메뉴 열기", + "openMedia": "첨부 이미지 열기", + "openNotifications": "알림 열기", + "openProfile": "작성자 프로필 팝업 열기", + "openReactions": "리액션 팔레트 열기 (미스키)", + "openStatus": "글 팝업 열기 (열린 메뉴에서는 항목 선택)", + "openTimelineMenu": "타임라인 메뉴 열기", + "reply": "답글 작성", + "title": "선택된 글 컨트롤" + }, + "suggestions": { + "close": "추천 닫기", + "insert": "추천 이모지 입력", + "navigate": "추천 항목 이동", + "note": "추천 목록이 열려 있을 때만 동작합니다.", + "title": "이모지 추천" + }, + "timeline": { + "clearSelection": "선택 해제", + "moveColumn": "이웃 컬럼으로 이동", + "moveVertical": "선택된 글 위아래 이동", + "selectLeftmost": "선택이 없을 때 왼쪽 첫 글을 선택", + "title": "타임라인 이동" + } + }, + "status": { + "boostedBy": " 님이 부스트함", + "boostedByInline": "이 부스트함", + "boostedByMe": "내가 부스트함", + "boostDisabled": "비공개 글은 부스트할 수 없습니다.", + "deleteTitle": "게시글 삭제", + "hideContent": "가리기", + "menuOpenAria": "게시글 메뉴 열기", + "noContent": "(내용 없음)", + "openOrigin": "원본 서버에서 보기", + "replyTo": "{{name}}에게 보낸 답글", + "showContent": "내용보기", + "threadLoading": "스레드 불러오는 중", + "title": "게시글", + "viewAria": "글 보기", + "viewBoostThread": "부스트한 글 스레드 보기", + "viewReplyThread": "댓글 스레드 보기" + }, + "themes": { + "christmas": "크리스마스", + "default": "기본", + "matchaCore": "말차코어", + "monochrome": "모노톤", + "skyPink": "하늘핑크" + }, + "translation": { + "completed": "번역이 완료되었습니다.", + "failed": "번역에 실패했습니다.", + "hide": "번역 숨기기", + "hideAria": "번역 숨기기", + "inProgress": "번역 중...", + "label": "번역", + "meta": { + "detected": "감지 언어: {{language}}", + "provider": "제공: {{provider}}", + "target": "번역 대상: {{language}}" + }, + "noContent": "(내용 없음)", + "resultAria": "번역 결과", + "retry": "번역 다시하기" + }, + "timeline": { + "accountRequired": "계정을 선택하면 타임라인을 불러옵니다.", + "actionsAria": "타임라인 작업", + "bookmarks": "북마크", + "empty": { + "bookmarks": "북마크한 글이 없습니다.", + "default": "표시할 글이 없습니다.", + "notifications": "표시할 알림이 없습니다." + }, + "federated": "연합", + "global": "글로벌", + "home": "홈", + "loadingMore": "더 불러오는 중...", + "local": "로컬", + "notifications": "알림", + "pending": { + "action": "보기", + "aria": "새 글 {{label}}개 표시", + "label": "새 글 {{label}}개", + "sr": "새 글 {{label}}개가 새로 도착했습니다.", + "title": "새 글 표시" + }, + "scrollTop": "최상단으로 이동", + "selectAria": "타임라인 선택: {{label}}", + "selectHint": "타임라인 선택 (T)", + "selectMenuAria": "타임라인 선택", + "social": "소셜" + }, + "toast": { + "bookmarkAdded": "북마크했습니다.", + "bookmarkRemoved": "북마크를 취소했습니다.", + "close": "닫기", + "closeAria": "토스트 닫기", + "followRequestCancelled": "팔로우 요청을 취소했습니다.", + "followRequestSent": "팔로우 요청을 보냈습니다.", + "followed": "팔로우했습니다.", + "unfollowed": "언팔로우했습니다.", + "muted": "뮤트했습니다.", + "unmuted": "뮤트를 해제했습니다.", + "blocked": "차단했습니다.", + "unblocked": "차단을 해제했습니다." + } +} diff --git a/src/App.tsx b/src/App.tsx index 79dafc5..d3a4184 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import type { Account, ReactionInput, Status, TimelineType } from "./domain/types"; import { AccountAdd } from "./ui/components/AccountAdd"; import { AccountSelector } from "./ui/components/AccountSelector"; @@ -82,6 +83,7 @@ const parseRoute = (): Route => { }; export const App = () => { + const { t } = useTranslation(); const [themeMode, setThemeMode] = useState(() => getStoredTheme()); const [colorScheme, setColorScheme] = useState(() => getStoredColorScheme()); const [showPomodoro, setShowPomodoro] = useState(() => { @@ -297,23 +299,23 @@ export const App = () => { if (!pending || !state || pending.state !== state) { clearPendingOAuth(); - setActionError("OAuth 상태가 유효하지 않습니다. 다시 시도해주세요."); + setActionError(t("errors.oauth.invalidState")); return; } if (pending.platform === "mastodon" && !code) { clearPendingOAuth(); - setActionError("OAuth 코드를 받지 못했습니다. 다시 시도해주세요."); + setActionError(t("errors.oauth.missingCode")); return; } if (pending.platform === "misskey") { if (!session) { clearPendingOAuth(); - setActionError("미스키 세션 정보를 받지 못했습니다. 다시 시도해주세요."); + setActionError(t("errors.oauth.misskeyMissingSession")); return; } if (session !== pending.sessionId) { clearPendingOAuth(); - setActionError("미스키 세션 정보가 일치하지 않습니다. 다시 시도해주세요."); + setActionError(t("errors.oauth.misskeySessionMismatch")); return; } } @@ -344,7 +346,7 @@ export const App = () => { if (pending.accountId) { const existing = accountsState.accounts.find((account) => account.id === pending.accountId); if (!existing) { - setActionError("재인증할 계정을 찾지 못했습니다."); + setActionError(t("errors.accountReauthNotFound")); return; } const updated: Account = { @@ -369,7 +371,7 @@ export const App = () => { account.handle === fullHandle ); if (existing) { - setActionError("이미 등록된 계정입니다."); + setActionError(t("errors.accountAlreadyRegistered")); accountsState.setActiveAccount(existing.id); return; } @@ -382,7 +384,7 @@ export const App = () => { emojis: verified.emojis ?? [] }); } catch (err) { - setActionError(err instanceof Error ? err.message : "OAuth 처리에 실패했습니다."); + setActionError(err instanceof Error ? err.message : t("errors.oauth.failed")); } finally { clearPendingOAuth(); setOauthLoading(false); @@ -390,7 +392,7 @@ export const App = () => { }; void addAccountWithToken(); - }, [accountsState, services.api, services.oauth]); + }, [accountsState, services.api, services.oauth, t]); useEffect(() => { const value = themeMode === "default" ? "" : themeMode; @@ -465,7 +467,7 @@ export const App = () => { const needsRegister = !cached || cached.redirectUri !== redirectUri || cached.platform === "misskey"; const registered = needsRegister ? await services.oauth.registerApp(normalizedUrl, redirectUri) : cached; if (!registered) { - throw new Error("앱 등록 정보를 불러오지 못했습니다."); + throw new Error(t("errors.appRegistrationLoadFailed")); } if (needsRegister && registered.platform === "mastodon") { saveRegisteredApp(registered); @@ -477,21 +479,19 @@ export const App = () => { } catch { setReauthLoading(false); } - }, [accountsState.accounts, settingsAccountId, services.oauth]); + }, [accountsState.accounts, settingsAccountId, services.oauth, t]); const handleSettingsRemove = useCallback(() => { if (!settingsAccountId) return; - const confirmed = window.confirm("이 계정을 삭제할까요?"); + const confirmed = window.confirm(t("confirm.removeAccount")); if (confirmed) { accountsState.removeAccount(settingsAccountId); setSettingsAccountId(null); } - }, [settingsAccountId, accountsState]); + }, [accountsState, settingsAccountId, t]); const handleClearLocalStorage = useCallback(() => { - const confirmed = window.confirm( - "로컬 저장소의 모든 데이터를 삭제할까요? 계정과 설정 정보가 모두 초기화됩니다." - ); + const confirmed = window.confirm(t("confirm.clearLocalStorage")); if (!confirmed) { return; } @@ -501,7 +501,7 @@ export const App = () => { /* noop */ } window.location.reload(); - }, []); + }, [t]); const isEditableElement = useCallback((element: Element | null) => { if (!element) { @@ -898,7 +898,7 @@ export const App = () => { setMentionSeed(null); return true; } catch (err) { - setActionError(err instanceof Error ? err.message : "글 작성에 실패했습니다."); + setActionError(err instanceof Error ? err.message : t("errors.composeFailed")); return false; } }; @@ -957,16 +957,16 @@ export const App = () => { const handleReaction = useCallback( async (account: Account | null, status: Status, reaction: ReactionInput) => { if (!account) { - setActionError("계정을 선택해주세요."); + setActionError(t("errors.accountRequired")); return; } if (account.platform !== "misskey") { - setActionError("리액션은 미스키 계정에서만 사용할 수 있습니다."); + setActionError(t("errors.reactionMisskeyOnly")); return; } const target = status.reblog ?? status; if (target.myReaction && target.myReaction !== reaction.name) { - setActionError("이미 리액션을 남겼습니다. 먼저 취소해주세요."); + setActionError(t("errors.reactionAlreadyExists")); return; } setActionError(null); @@ -981,11 +981,11 @@ export const App = () => { updateStatusEverywhere(account.id, updated); } } catch (err) { - setActionError(err instanceof Error ? err.message : "리액션 처리에 실패했습니다."); + setActionError(err instanceof Error ? err.message : t("errors.reactionFailed")); updateStatusEverywhere(account.id, target); } }, - [services.api, updateStatusEverywhere] + [services.api, t, updateStatusEverywhere] ); const composeAccountSelector = ( @@ -1104,14 +1104,14 @@ export const App = () => { return (
- - Deck 로고 + + {t("app.logoAlt")}
-
+
-

모바일 환경에서는 사용이 불가능합니다 🙇‍♂️

+

{t("app.mobileBlocker.title")}

- 멀티 컬럼 인터페이스 특성상 모바일 지원이 제한됩니다. 데스크톱 또는 태블릿에서 이용해 주세요. + {t("app.mobileBlocker.description")}

@@ -1175,14 +1175,14 @@ export const App = () => { {route === "home" ? (
- Deck 로고 + {t("app.logoAlt")}

Deck

-

오픈소스 페디버스 웹 클라이언트

+

{t("app.tagline")}

- 여러 계정을 전환하고 타임라인을 실시간으로 확인할 수 있습니다. + {t("app.sidebarDescription")}

@@ -1252,14 +1252,16 @@ export const App = () => { {hasAccounts ? (
- {oauthLoading ?

OAuth 인증 중...

: null} + {oauthLoading ?

{t("oauth.authenticating")}

: null} {route === "home" ? (
{showPomodoro && pomodoroSessionType === "focus" && pomodoroIsRunning ? (
-

🎯 집중 세션 진행 중

-

뽀모도로 타이머가 동작 중입니다.
타임라인은 집중이 끝날 때까지 숨겨집니다.

+

{t("pomodoro.focusSession.title")}

+

+ }} /> +

) : null} @@ -1426,7 +1428,7 @@ export const App = () => { }} onToggleFavourite={async (status) => { if (!composeAccount) { - setActionError("계정을 선택해주세요."); + setActionError(t("errors.accountRequired")); return; } setActionError(null); @@ -1437,12 +1439,12 @@ export const App = () => { // Update the status in modal setSelectedStatus(updated); } catch (err) { - setActionError(err instanceof Error ? err.message : "좋아요 처리에 실패했습니다."); + setActionError(err instanceof Error ? err.message : t("errors.likeFailed")); } }} onToggleReblog={async (status) => { if (!composeAccount) { - setActionError("계정을 선택해주세요."); + setActionError(t("errors.accountRequired")); return; } setActionError(null); @@ -1452,12 +1454,12 @@ export const App = () => { : await services.api.reblog(composeAccount, status.id); setSelectedStatus(updated); } catch (err) { - setActionError(err instanceof Error ? err.message : "부스트 처리에 실패했습니다."); + setActionError(err instanceof Error ? err.message : t("errors.boostFailed")); } }} onToggleBookmark={async (status) => { if (!composeAccount) { - setActionError("계정을 선택해주세요."); + setActionError(t("errors.accountRequired")); return; } setActionError(null); @@ -1468,12 +1470,12 @@ export const App = () => { : await services.api.bookmark(composeAccount, status.id); setSelectedStatus(updated); if (isBookmarking) { - showToast("북마크했습니다."); + showToast(t("toast.bookmarkAdded")); } else { - showToast("북마크를 취소했습니다."); + showToast(t("toast.bookmarkRemoved")); } } catch (err) { - setActionError(err instanceof Error ? err.message : "북마크 처리에 실패했습니다."); + setActionError(err instanceof Error ? err.message : t("errors.bookmarkFailed")); } }} onDelete={async (status) => { @@ -1485,7 +1487,7 @@ export const App = () => { await services.api.deleteStatus(composeAccount, status.id); setSelectedStatus(null); } catch (err) { - setActionError(err instanceof Error ? err.message : "게시글 삭제에 실패했습니다."); + setActionError(err instanceof Error ? err.message : t("errors.statusDeleteFailed")); } }} activeHandle={ diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..65ea33e --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,29 @@ +import i18n from "i18next"; +import HttpBackend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { initReactI18next } from "react-i18next"; + +export const SUPPORTED_LANGUAGES = ["ko", "en"] as const; + +void i18n + .use(HttpBackend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: "ko", + supportedLngs: SUPPORTED_LANGUAGES, + defaultNS: "common", + ns: ["common"], + backend: { + loadPath: "/locales/{{lng}}/{{ns}}.json" + }, + detection: { + order: ["localStorage", "navigator"], + caches: ["localStorage"] + }, + interpolation: { + escapeValue: false + } + }); + +export default i18n; diff --git a/src/infra/MastodonHttpClient.ts b/src/infra/MastodonHttpClient.ts index c9b37cf..672aa26 100644 --- a/src/infra/MastodonHttpClient.ts +++ b/src/infra/MastodonHttpClient.ts @@ -10,6 +10,7 @@ import type { } from "../domain/types"; import type { CreateStatusInput, MastodonApi } from "../services/MastodonApi"; import { mapAccountProfile, mapAccountRelationship, mapNotificationToStatus, mapStatus } from "./mastodonMapper"; +import i18n from "../i18n"; const buildHeaders = (account: Account): HeadersInit => ({ Authorization: `Bearer ${account.accessToken}`, @@ -53,7 +54,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("계정 인증에 실패했습니다."); + throw new Error(i18n.t("errors.api.accountVerifyFailed")); } const data = (await response.json()) as Record; return { @@ -74,7 +75,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("타임라인을 불러오지 못했습니다."); + throw new Error(i18n.t("errors.timelineLoadFailed")); } const data = (await response.json()) as unknown[]; return data.map(mapStatus); @@ -104,7 +105,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("타임라인을 불러오지 못했습니다."); + throw new Error(i18n.t("errors.timelineLoadFailed")); } const data = (await response.json()) as unknown[]; return data.map(mapStatus); @@ -120,7 +121,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("북마크를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.bookmarksLoadFailed")); } const data = (await response.json()) as unknown[]; return data.map(mapStatus); @@ -131,7 +132,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("이모지를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.customEmojisLoadFailed")); } const data = (await response.json()) as unknown; return mapCustomEmojis(data); @@ -171,7 +172,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!v1Response.ok) { - throw new Error("인스턴스 정보를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.instanceInfoLoadFailed")); } const data = (await v1Response.json()) as Record; @@ -203,7 +204,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("프로필 정보를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.profileLoadFailed")); } const data = (await response.json()) as unknown; return mapAccountProfile(data); @@ -216,7 +217,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("관계 정보를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.relationshipLoadFailed")); } const data = (await response.json()) as unknown[]; const relationship = data[0]; @@ -229,7 +230,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("팔로우에 실패했습니다."); + throw new Error(i18n.t("errors.api.followFailed")); } const data = (await response.json()) as unknown; return mapAccountRelationship(data); @@ -241,7 +242,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("언팔로우에 실패했습니다."); + throw new Error(i18n.t("errors.api.unfollowFailed")); } const data = (await response.json()) as unknown; return mapAccountRelationship(data); @@ -257,7 +258,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("뮤트에 실패했습니다."); + throw new Error(i18n.t("errors.api.muteFailed")); } const data = (await response.json()) as unknown; return mapAccountRelationship(data); @@ -269,7 +270,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("뮤트 해제에 실패했습니다."); + throw new Error(i18n.t("errors.api.unmuteFailed")); } const data = (await response.json()) as unknown; return mapAccountRelationship(data); @@ -281,7 +282,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("차단에 실패했습니다."); + throw new Error(i18n.t("errors.api.blockFailed")); } const data = (await response.json()) as unknown; return mapAccountRelationship(data); @@ -293,7 +294,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("차단 해제에 실패했습니다."); + throw new Error(i18n.t("errors.api.unblockFailed")); } const data = (await response.json()) as unknown; return mapAccountRelationship(data); @@ -318,7 +319,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("게시글을 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.statusesLoadFailed")); } const data = (await response.json()) as unknown[]; return data.map(mapStatus); @@ -335,12 +336,12 @@ export class MastodonHttpClient implements MastodonApi { body: formData }); if (!response.ok) { - throw new Error("이미지 업로드에 실패했습니다."); + throw new Error(i18n.t("errors.api.mediaUploadFailed")); } const data = (await response.json()) as Record; const id = String(data.id ?? ""); if (!id) { - throw new Error("업로드된 미디어 정보를 찾을 수 없습니다."); + throw new Error(i18n.t("errors.api.mediaUploadNotFound")); } return id; } @@ -350,7 +351,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("스레드 컨텍스트를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.threadLoadFailed")); } const data = (await response.json()) as Record; @@ -382,7 +383,7 @@ export class MastodonHttpClient implements MastodonApi { }) }); if (!response.ok) { - throw new Error("글 작성에 실패했습니다."); + throw new Error(i18n.t("errors.composeFailed")); } const data = (await response.json()) as unknown; return mapStatus(data); @@ -394,7 +395,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("게시글 삭제에 실패했습니다."); + throw new Error(i18n.t("errors.statusDeleteFailed")); } } @@ -415,11 +416,11 @@ export class MastodonHttpClient implements MastodonApi { } async createReaction(_account: Account, _statusId: string, _reaction: string): Promise { - throw new Error("리액션은 미스키 계정에서만 사용할 수 있습니다."); + throw new Error(i18n.t("errors.reactionMisskeyOnly")); } async deleteReaction(_account: Account, _statusId: string): Promise { - throw new Error("리액션은 미스키 계정에서만 사용할 수 있습니다."); + throw new Error(i18n.t("errors.reactionMisskeyOnly")); } async fetchNoteState( @@ -432,7 +433,7 @@ export class MastodonHttpClient implements MastodonApi { } }); if (!response.ok) { - throw new Error("게시물 상태를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.noteStateLoadFailed")); } const data = (await response.json()) as unknown; const status = mapStatus(data); @@ -449,7 +450,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("번역에 실패했습니다."); + throw new Error(i18n.t("errors.api.translationFailed")); } const data = (await response.json()) as Record; const htmlContent = typeof data.content === "string" ? data.content : ""; @@ -488,7 +489,7 @@ export class MastodonHttpClient implements MastodonApi { headers: buildHeaders(account) }); if (!response.ok) { - throw new Error("알림을 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.notificationsLoadFailed")); } const data = (await response.json()) as unknown[]; return data @@ -514,7 +515,7 @@ export class MastodonHttpClient implements MastodonApi { throw new Error(errorBody); } } - throw new Error("요청에 실패했습니다."); + throw new Error(i18n.t("errors.api.requestFailed")); } const data = (await response.json()) as unknown; return mapStatus(data); diff --git a/src/infra/MastodonOAuthClient.ts b/src/infra/MastodonOAuthClient.ts index 0192b80..7dcaaa8 100644 --- a/src/infra/MastodonOAuthClient.ts +++ b/src/infra/MastodonOAuthClient.ts @@ -4,6 +4,7 @@ import type { OAuthClient, RegisteredApp } from "../services/OAuthClient"; +import i18n from "../i18n"; const OAUTH_SCOPE = "read write follow"; @@ -26,14 +27,14 @@ export class MastodonOAuthClient implements OAuthClient { }); if (!response.ok) { - throw new Error("앱 등록에 실패했습니다."); + throw new Error(i18n.t("errors.oauth.appRegisterFailed")); } const data = (await response.json()) as Record; const clientId = String(data.client_id ?? ""); const clientSecret = String(data.client_secret ?? ""); if (!clientId || !clientSecret) { - throw new Error("앱 등록 정보가 올바르지 않습니다."); + throw new Error(i18n.t("errors.oauth.appRegisterInvalid")); } return { platform: "mastodon", @@ -47,7 +48,7 @@ export class MastodonOAuthClient implements OAuthClient { buildAuthorizeUrl(app: RegisteredApp, state: string): string { if (app.platform !== "mastodon") { - throw new Error("마스토돈 OAuth 정보가 필요합니다."); + throw new Error(i18n.t("errors.oauth.mastodonInfoRequired")); } const authorizeUrl = new URL(`${app.instanceUrl}/oauth/authorize`); authorizeUrl.searchParams.set("client_id", app.clientId); @@ -61,10 +62,10 @@ export class MastodonOAuthClient implements OAuthClient { async exchangeToken(params: { app: RegisteredApp; callback: OAuthCallbackParams }): Promise { if (params.app.platform !== "mastodon") { - throw new Error("마스토돈 OAuth 정보가 필요합니다."); + throw new Error(i18n.t("errors.oauth.mastodonInfoRequired")); } if (!params.callback.code) { - throw new Error("OAuth 코드를 찾지 못했습니다."); + throw new Error(i18n.t("errors.oauth.missingCode")); } const body = new URLSearchParams({ client_id: params.app.clientId, @@ -83,13 +84,13 @@ export class MastodonOAuthClient implements OAuthClient { }); if (!response.ok) { - throw new Error("토큰 교환에 실패했습니다."); + throw new Error(i18n.t("errors.oauth.tokenExchangeFailed")); } const data = (await response.json()) as Record; const token = String(data.access_token ?? ""); if (!token) { - throw new Error("토큰을 받지 못했습니다."); + throw new Error(i18n.t("errors.oauth.tokenMissing")); } return token; } diff --git a/src/infra/MisskeyHttpClient.ts b/src/infra/MisskeyHttpClient.ts index 5af3ef7..a513d85 100644 --- a/src/infra/MisskeyHttpClient.ts +++ b/src/infra/MisskeyHttpClient.ts @@ -15,6 +15,7 @@ import { mapMisskeyStatusWithInstance, mapMisskeyUserProfile } from "./misskeyMapper"; +import i18n from "../i18n"; const normalizeInstanceUrl = (instanceUrl: string): string => instanceUrl.replace(/\/$/, ""); @@ -95,7 +96,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, { noteId, limit })) }); if (!response.ok) { - throw new Error("답글을 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.replyLoadFailed")); } const data = (await response.json()) as unknown[]; return data @@ -146,7 +147,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, {})) }); if (!response.ok) { - throw new Error("계정 인증에 실패했습니다."); + throw new Error(i18n.t("errors.api.accountVerifyFailed")); } const data = (await response.json()) as Record; return { @@ -195,7 +196,7 @@ export class MisskeyHttpClient implements MastodonApi { if (!response.ok) { const fallback = await fetch(url); if (!fallback.ok) { - throw new Error("이모지를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.customEmojisLoadFailed")); } const data = (await fallback.json()) as unknown; return mapMisskeyEmojis(data); @@ -213,7 +214,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, {})) }); if (!response.ok) { - throw new Error("인스턴스 정보를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.instanceInfoLoadFailed")); } const data = (await response.json()) as Record; return { @@ -234,7 +235,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, { userId: accountId })) }); if (!response.ok) { - throw new Error("프로필 정보를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.profileLoadFailed")); } const data = (await response.json()) as unknown; return mapMisskeyUserProfile(data, account.instanceUrl); @@ -249,7 +250,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, { userId: accountId })) }); if (!response.ok) { - throw new Error("관계 정보를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.relationshipLoadFailed")); } const data = (await response.json()) as unknown; return mapMisskeyRelationship(data); @@ -310,7 +311,7 @@ export class MisskeyHttpClient implements MastodonApi { ) }); if (!response.ok) { - throw new Error("게시글을 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.statusesLoadFailed")); } const data = (await response.json()) as unknown[]; return data.map((item) => mapMisskeyStatusWithInstance(item, account.instanceUrl)); @@ -330,7 +331,7 @@ export class MisskeyHttpClient implements MastodonApi { ) }); if (!response.ok) { - throw new Error("북마크를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.bookmarksLoadFailed")); } const data = (await response.json()) as unknown[]; return data.map((item) => { @@ -352,12 +353,12 @@ export class MisskeyHttpClient implements MastodonApi { } ); if (!response.ok) { - throw new Error("이미지 업로드에 실패했습니다."); + throw new Error(i18n.t("errors.api.mediaUploadFailed")); } const data = (await response.json()) as Record; const id = String(data.id ?? ""); if (!id) { - throw new Error("업로드된 미디어 정보를 찾을 수 없습니다."); + throw new Error(i18n.t("errors.api.mediaUploadNotFound")); } return id; } @@ -375,7 +376,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, { noteId, limit: 100 })) }); if (!response.ok) { - throw new Error("대화를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.conversationLoadFailed")); } const data = (await response.json()) as unknown[]; @@ -425,12 +426,12 @@ export class MisskeyHttpClient implements MastodonApi { ) }); if (!response.ok) { - throw new Error("글 작성에 실패했습니다."); + throw new Error(i18n.t("errors.composeFailed")); } const data = (await response.json()) as { createdNote?: unknown; note?: unknown }; const created = data.createdNote ?? data.note; if (!created) { - throw new Error("생성된 노트를 찾을 수 없습니다."); + throw new Error(i18n.t("errors.api.createdNoteNotFound")); } return mapMisskeyStatusWithInstance(created, account.instanceUrl); } @@ -444,7 +445,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, { noteId: statusId })) }); if (!response.ok) { - throw new Error("게시글 삭제에 실패했습니다."); + throw new Error(i18n.t("errors.statusDeleteFailed")); } } @@ -493,7 +494,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, { noteId })) }); if (!response.ok) { - throw new Error("게시물 상태를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.noteStateLoadFailed")); } const state = (await response.json()) as Record; const isFavorited = Boolean(state.isFavorited ?? false); @@ -514,7 +515,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, { noteId: statusId })) }); if (!response.ok) { - throw new Error("번역에 실패했습니다."); + throw new Error(i18n.t("errors.api.translationFailed")); } const data = (await response.json()) as Record; const content = @@ -528,7 +529,7 @@ export class MisskeyHttpClient implements MastodonApi { ? data.translation : ""; if (!content) { - throw new Error("번역 결과를 확인할 수 없습니다."); + throw new Error(i18n.t("errors.api.translationResultMissing")); } const sourceLanguage = typeof data.sourceLang === "string" @@ -561,7 +562,7 @@ export class MisskeyHttpClient implements MastodonApi { const note = await this.fetchNoteRaw(account, statusId); const renoteId = typeof note.myRenoteId === "string" ? note.myRenoteId : ""; if (!renoteId) { - throw new Error("취소할 리노트를 찾지 못했습니다."); + throw new Error(i18n.t("errors.api.renoteNotFound")); } await this.postSimple(account, "/api/notes/delete", { noteId: renoteId }); return this.fetchNote(account, statusId); @@ -576,7 +577,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, { noteId })) }); if (!response.ok) { - throw new Error("게시글 정보를 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.statusInfoLoadFailed")); } return (await response.json()) as Record; } @@ -595,7 +596,7 @@ export class MisskeyHttpClient implements MastodonApi { body: JSON.stringify(buildBody(account, payload)) }); if (!response.ok) { - throw new Error("요청에 실패했습니다."); + throw new Error(i18n.t("errors.api.requestFailed")); } } @@ -618,7 +619,7 @@ export class MisskeyHttpClient implements MastodonApi { ) }); if (!response.ok) { - throw new Error("타임라인을 불러오지 못했습니다."); + throw new Error(i18n.t("errors.timelineLoadFailed")); } const data = (await response.json()) as unknown[]; return data.map((item) => mapMisskeyStatusWithInstance(item, account.instanceUrl)); @@ -642,7 +643,7 @@ export class MisskeyHttpClient implements MastodonApi { ) }); if (!response.ok) { - throw new Error("알림을 불러오지 못했습니다."); + throw new Error(i18n.t("errors.api.notificationsLoadFailed")); } const data = (await response.json()) as unknown[]; return data diff --git a/src/infra/MisskeyOAuthClient.ts b/src/infra/MisskeyOAuthClient.ts index 57c5c33..3dc1828 100644 --- a/src/infra/MisskeyOAuthClient.ts +++ b/src/infra/MisskeyOAuthClient.ts @@ -4,6 +4,7 @@ import type { OAuthClient, RegisteredApp } from "../services/OAuthClient"; +import i18n from "../i18n"; const APP_NAME = "Deck"; const MIAUTH_PERMISSIONS = [ @@ -35,7 +36,7 @@ export class MisskeyOAuthClient implements OAuthClient { buildAuthorizeUrl(app: RegisteredApp, state: string): string { if (app.platform !== "misskey") { - throw new Error("미스키 OAuth 정보가 필요합니다."); + throw new Error(i18n.t("errors.oauth.misskeyInfoRequired")); } const authorizeUrl = new URL(`${normalizeInstanceUrl(app.instanceUrl)}/miauth/${app.sessionId}`); const callbackUrl = new URL(app.redirectUri); @@ -48,11 +49,11 @@ export class MisskeyOAuthClient implements OAuthClient { async exchangeToken(params: { app: RegisteredApp; callback: OAuthCallbackParams }): Promise { if (params.app.platform !== "misskey") { - throw new Error("미스키 OAuth 정보가 필요합니다."); + throw new Error(i18n.t("errors.oauth.misskeyInfoRequired")); } const sessionId = params.callback.session ?? params.app.sessionId; if (!sessionId) { - throw new Error("미스키 세션 정보를 찾지 못했습니다."); + throw new Error(i18n.t("errors.oauth.misskeySessionMissing")); } const response = await fetch( `${normalizeInstanceUrl(params.app.instanceUrl)}/api/miauth/${sessionId}/check`, @@ -65,11 +66,11 @@ export class MisskeyOAuthClient implements OAuthClient { } ); if (!response.ok) { - throw new Error("미스키 토큰을 받지 못했습니다."); + throw new Error(i18n.t("errors.oauth.misskeyTokenMissing")); } const data = (await response.json()) as { ok?: boolean; token?: string }; if (!data.ok || !data.token) { - throw new Error("미스키 토큰 응답이 올바르지 않습니다."); + throw new Error(i18n.t("errors.oauth.misskeyTokenInvalid")); } return data.token; } diff --git a/src/infra/mastodonMapper.ts b/src/infra/mastodonMapper.ts index 65c0a78..b377485 100644 --- a/src/infra/mastodonMapper.ts +++ b/src/infra/mastodonMapper.ts @@ -1,4 +1,5 @@ import type { AccountRelationship, MediaAttachment, ProfileField, Reaction, Status, UserProfile } from "../domain/types"; +import i18n from "../i18n"; const htmlToText = (html: string): string => { // Preserve links as "text (url)" format before DOM parsing @@ -308,23 +309,26 @@ const STATUS_LIKE_NOTIFICATION_TYPES = new Set(["mention", "status", "up const getNotificationDescriptor = (type: string): { label: string; fallback: string } => { switch (type) { case "follow": - return { label: "팔로우함", fallback: "팔로우했습니다." }; + return { label: i18n.t("notifications.labels.follow"), fallback: i18n.t("notifications.fallback.follow") }; case "follow_request": - return { label: "팔로우 요청함", fallback: "팔로우 요청을 보냈습니다." }; + return { + label: i18n.t("notifications.labels.followRequest"), + fallback: i18n.t("notifications.fallback.followRequest") + }; case "favourite": - return { label: "좋아요함", fallback: "좋아요를 눌렀습니다." }; + return { label: i18n.t("notifications.labels.favourite"), fallback: i18n.t("notifications.fallback.favourite") }; case "reblog": - return { label: "부스트함", fallback: "부스트했습니다." }; + return { label: i18n.t("notifications.labels.reblog"), fallback: i18n.t("notifications.fallback.reblog") }; case "poll": - return { label: "투표함", fallback: "투표했습니다." }; + return { label: i18n.t("notifications.labels.poll"), fallback: i18n.t("notifications.fallback.poll") }; case "status": - return { label: "글 작성함", fallback: "새 글을 올렸습니다." }; + return { label: i18n.t("notifications.labels.status"), fallback: i18n.t("notifications.fallback.status") }; case "update": - return { label: "게시글 수정함", fallback: "게시글을 수정했습니다." }; + return { label: i18n.t("notifications.labels.update"), fallback: i18n.t("notifications.fallback.update") }; case "mention": - return { label: "멘션함", fallback: "멘션했습니다." }; + return { label: i18n.t("notifications.labels.mention"), fallback: i18n.t("notifications.fallback.mention") }; default: - return { label: "알림", fallback: "알림이 도착했습니다." }; + return { label: i18n.t("notifications.labels.default"), fallback: i18n.t("notifications.fallback.default") }; } }; diff --git a/src/infra/misskeyMapper.ts b/src/infra/misskeyMapper.ts index c3d8131..7053258 100644 --- a/src/infra/misskeyMapper.ts +++ b/src/infra/misskeyMapper.ts @@ -9,6 +9,7 @@ import type { UserProfile, Visibility } from "../domain/types"; +import i18n from "../i18n"; const mapVisibility = (visibility: string): Visibility => { switch (visibility) { @@ -598,23 +599,23 @@ const getExportEntityLabel = (value: Record): string => { const entity = typeof value.exportedEntity === "string" ? value.exportedEntity : ""; switch (entity) { case "antenna": - return "안테나"; + return i18n.t("notifications.export.antenna"); case "blocking": - return "차단"; + return i18n.t("notifications.export.blocking"); case "clip": - return "클립"; + return i18n.t("notifications.export.clip"); case "customEmoji": - return "커스텀 이모지"; + return i18n.t("notifications.export.customEmoji"); case "favorite": - return "즐겨찾기"; + return i18n.t("notifications.export.favorite"); case "following": - return "팔로잉"; + return i18n.t("notifications.export.following"); case "muting": - return "뮤트"; + return i18n.t("notifications.export.muting"); case "note": - return "노트"; + return i18n.t("notifications.export.note"); case "userList": - return "사용자 리스트"; + return i18n.t("notifications.export.userList"); default: return entity; } @@ -629,10 +630,10 @@ const getLoginDetail = (value: Record): string => { parts.push(`IP: ${ip}`); } if (location) { - parts.push(`위치: ${location}`); + parts.push(i18n.t("notifications.login.location", { value: location })); } if (userAgent) { - parts.push(`브라우저: ${userAgent}`); + parts.push(i18n.t("notifications.login.browser", { value: userAgent })); } return parts.join(", "); }; @@ -644,113 +645,172 @@ const getNotificationDescriptor = ( ): { label: string; fallback: string } => { switch (type) { case "follow": - return { label: "팔로우함", fallback: "팔로우했습니다." }; + return { + label: i18n.t("notifications.misskey.follow.label"), + fallback: i18n.t("notifications.misskey.follow.fallback") + }; case "receiveFollowRequest": - return { label: "팔로우 요청함", fallback: "팔로우 요청을 보냈습니다." }; + return { + label: i18n.t("notifications.misskey.receiveFollowRequest.label"), + fallback: i18n.t("notifications.misskey.receiveFollowRequest.fallback") + }; case "followRequestAccepted": { const followMessage = pickTextField(value, ["message"]); return { - label: "팔로우 요청 승인함", + label: i18n.t("notifications.misskey.followRequestAccepted.label"), fallback: followMessage - ? `팔로우 요청이 승인되었습니다. ${followMessage}` - : "팔로우 요청이 승인되었습니다." + ? i18n.t("notifications.misskey.followRequestAccepted.fallbackWithMessage", { + message: followMessage + }) + : i18n.t("notifications.misskey.followRequestAccepted.fallback") }; } case "renote": - return { label: "리노트함", fallback: "리노트했습니다." }; + return { + label: i18n.t("notifications.misskey.renote.label"), + fallback: i18n.t("notifications.misskey.renote.fallback") + }; case "reaction": return { - label: reaction ? `리액션함 ${reaction}` : "리액션함", - fallback: reaction ? `리액션했습니다. ${reaction}` : "리액션했습니다." + label: reaction + ? i18n.t("notifications.misskey.reaction.labelWithReaction", { reaction }) + : i18n.t("notifications.misskey.reaction.label"), + fallback: reaction + ? i18n.t("notifications.misskey.reaction.fallbackWithReaction", { reaction }) + : i18n.t("notifications.misskey.reaction.fallback") }; case "pollEnded": - return { label: "투표 종료됨", fallback: "투표가 종료되었습니다." }; + return { + label: i18n.t("notifications.misskey.pollEnded.label"), + fallback: i18n.t("notifications.misskey.pollEnded.fallback") + }; case "pollVote": - return { label: "투표함", fallback: "투표했습니다." }; + return { + label: i18n.t("notifications.misskey.pollVote.label"), + fallback: i18n.t("notifications.misskey.pollVote.fallback") + }; case "note": - return { label: "글 작성함", fallback: "새 글을 올렸습니다." }; + return { + label: i18n.t("notifications.misskey.note.label"), + fallback: i18n.t("notifications.misskey.note.fallback") + }; case "quote": - return { label: "인용함", fallback: "인용했습니다." }; + return { + label: i18n.t("notifications.misskey.quote.label"), + fallback: i18n.t("notifications.misskey.quote.fallback") + }; case "reply": - return { label: "답글 남김", fallback: "답글을 남겼습니다." }; + return { + label: i18n.t("notifications.misskey.reply.label"), + fallback: i18n.t("notifications.misskey.reply.fallback") + }; case "mention": - return { label: "멘션함", fallback: "멘션했습니다." }; + return { + label: i18n.t("notifications.misskey.mention.label"), + fallback: i18n.t("notifications.misskey.mention.fallback") + }; case "scheduledNotePosted": - return { label: "예약 글 게시됨", fallback: "예약 글이 게시되었습니다." }; + return { + label: i18n.t("notifications.misskey.scheduledNotePosted.label"), + fallback: i18n.t("notifications.misskey.scheduledNotePosted.fallback") + }; case "scheduledNotePostFailed": { const preview = getNoteDraftPreview(value); return { - label: "예약 글 게시 실패", - fallback: preview ? `예약 글 게시에 실패했습니다: ${preview}` : "예약 글 게시에 실패했습니다." + label: i18n.t("notifications.misskey.scheduledNotePostFailed.label"), + fallback: preview + ? i18n.t("notifications.misskey.scheduledNotePostFailed.fallbackWithPreview", { preview }) + : i18n.t("notifications.misskey.scheduledNotePostFailed.fallback") }; } case "achievementEarned": { const detail = getAchievementDetail(value) || getNotificationMessage(value); return { - label: "도전과제 달성함", - fallback: detail ? `도전과제를 달성했습니다: ${detail}` : "도전과제를 달성했습니다." + label: i18n.t("notifications.misskey.achievementEarned.label"), + fallback: detail + ? i18n.t("notifications.misskey.achievementEarned.fallbackWithDetail", { detail }) + : i18n.t("notifications.misskey.achievementEarned.fallback") }; } case "login": { const detail = getLoginDetail(value) || getNotificationMessage(value); return { - label: "로그인 알림", - fallback: detail ? `로그인 알림입니다. ${detail}` : "로그인 알림입니다." + label: i18n.t("notifications.misskey.login.label"), + fallback: detail + ? i18n.t("notifications.misskey.login.fallbackWithDetail", { detail }) + : i18n.t("notifications.misskey.login.fallback") }; } case "test": { const message = getNotificationMessage(value); return { - label: "테스트 알림", - fallback: message || "테스트 알림입니다." + label: i18n.t("notifications.misskey.test.label"), + fallback: message || i18n.t("notifications.misskey.test.fallback") }; } case "roleAssigned": { const roleName = getRoleName(value) || getNotificationMessage(value); return { - label: "역할 부여됨", - fallback: roleName ? `새 역할이 부여되었습니다: ${roleName}` : "새 역할이 부여되었습니다." + label: i18n.t("notifications.misskey.roleAssigned.label"), + fallback: roleName + ? i18n.t("notifications.misskey.roleAssigned.fallbackWithRole", { role: roleName }) + : i18n.t("notifications.misskey.roleAssigned.fallback") }; } case "announcement": case "unreadAnnouncement": { const announcement = getAnnouncementMessage(value) || getNotificationMessage(value); return { - label: "공지 알림", - fallback: announcement ? `공지: ${announcement}` : "새 공지가 있습니다." + label: i18n.t("notifications.misskey.announcement.label"), + fallback: announcement + ? i18n.t("notifications.misskey.announcement.fallbackWithAnnouncement", { announcement }) + : i18n.t("notifications.misskey.announcement.fallback") }; } case "app": { const message = getNotificationMessage(value); return { - label: "앱 알림", - fallback: message || "앱 알림이 도착했습니다." + label: i18n.t("notifications.misskey.app.label"), + fallback: message || i18n.t("notifications.misskey.app.fallback") }; } case "chatRoomInvitationReceived": { const roomName = getChatRoomInvitationDetail(value) || getNotificationMessage(value); return { - label: "채팅방 초대됨", - fallback: roomName ? `채팅방에 초대되었습니다: ${roomName}` : "채팅방에 초대되었습니다." + label: i18n.t("notifications.misskey.chatRoomInvitationReceived.label"), + fallback: roomName + ? i18n.t("notifications.misskey.chatRoomInvitationReceived.fallbackWithRoom", { room: roomName }) + : i18n.t("notifications.misskey.chatRoomInvitationReceived.fallback") }; } case "exportCompleted": { const entity = getExportEntityLabel(value); return { - label: "내보내기 완료됨", - fallback: entity ? `내보내기가 완료되었습니다: ${entity}` : "내보내기가 완료되었습니다." + label: i18n.t("notifications.misskey.exportCompleted.label"), + fallback: entity + ? i18n.t("notifications.misskey.exportCompleted.fallbackWithEntity", { entity }) + : i18n.t("notifications.misskey.exportCompleted.fallback") }; } case "createToken": - return { label: "토큰 생성됨", fallback: "새 토큰이 생성되었습니다." }; + return { + label: i18n.t("notifications.misskey.createToken.label"), + fallback: i18n.t("notifications.misskey.createToken.fallback") + }; case "reaction:grouped": - return { label: "리액션 모음", fallback: "여러 명이 리액션했습니다." }; + return { + label: i18n.t("notifications.misskey.reactionGrouped.label"), + fallback: i18n.t("notifications.misskey.reactionGrouped.fallback") + }; case "renote:grouped": - return { label: "리노트 모음", fallback: "여러 명이 리노트했습니다." }; + return { + label: i18n.t("notifications.misskey.renoteGrouped.label"), + fallback: i18n.t("notifications.misskey.renoteGrouped.fallback") + }; default: return { - label: "알림", - fallback: getNotificationMessage(value) || "알림이 도착했습니다." + label: i18n.t("notifications.labels.default"), + fallback: getNotificationMessage(value) || i18n.t("notifications.fallback.default") }; } }; @@ -781,8 +841,8 @@ export const mapMisskeyNotification = (raw: unknown, instanceUrl?: string): Stat const appHeader = pickTextField(value, ["header"]); const appIcon = typeof value.icon === "string" ? value.icon : null; const isGroupedNotification = type === "reaction:grouped" || type === "renote:grouped"; - const systemActorName = isGroupedNotification ? "여러 사용자" : "시스템"; - const appActorName = appHeader || "앱"; + const systemActorName = isGroupedNotification ? i18n.t("notifications.system.grouped") : i18n.t("notifications.system.default"); + const appActorName = appHeader || i18n.t("notifications.system.app"); const actor = isSystemNotification ? { name: type === "app" ? appActorName : systemActorName, @@ -796,7 +856,7 @@ export const mapMisskeyNotification = (raw: unknown, instanceUrl?: string): Stat url: accountUrl, avatarUrl: accountAvatarUrl }; - const normalizedAccountName = isSystemNotification ? actor.name || "시스템" : accountName; + const normalizedAccountName = isSystemNotification ? actor.name || i18n.t("notifications.system.default") : accountName; const systemAccountHandle = type === "app" ? "app" : isGroupedNotification ? "grouped" : "system"; const normalizedAccountHandle = isSystemNotification ? systemAccountHandle : accountHandle; const normalizedAccountUrl = isSystemNotification ? null : accountUrl; diff --git a/src/main.tsx b/src/main.tsx index 2e4212b..5c21ee7 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { App } from "./App"; +import "./i18n"; import { MastodonHttpClient } from "./infra/MastodonHttpClient"; import { MastodonStreamingClient } from "./infra/MastodonStreamingClient"; import { MastodonOAuthClient } from "./infra/MastodonOAuthClient"; diff --git a/src/ui/components/AccountAdd.tsx b/src/ui/components/AccountAdd.tsx index 8b8ef7e..2e6c2e1 100644 --- a/src/ui/components/AccountAdd.tsx +++ b/src/ui/components/AccountAdd.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; import type { OAuthClient } from "../../services/OAuthClient"; import { normalizeInstanceUrl } from "../utils/account"; import { createOauthState, loadRegisteredApp, saveRegisteredApp, storePendingOAuth } from "../utils/oauth"; @@ -13,12 +14,13 @@ export const AccountAdd = ({ const [loading, setLoading] = useState(false); const [showForm, setShowForm] = useState(false); const { showToast } = useToast(); + const { t } = useTranslation(); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); const normalizedUrl = normalizeInstanceUrl(instanceUrl); if (!normalizedUrl) { - showToast("서버 주소를 입력해주세요.", { tone: "error" }); + showToast(t("errors.serverAddressRequired"), { tone: "error" }); return; } @@ -32,7 +34,7 @@ export const AccountAdd = ({ const needsRegister = !cached || cached.redirectUri !== redirectUri || cached.platform === "misskey"; const registered = needsRegister ? await oauth.registerApp(normalizedUrl, redirectUri) : cached; if (!registered) { - throw new Error("앱 등록 정보를 불러오지 못했습니다."); + throw new Error(t("errors.appRegistrationLoadFailed")); } if (needsRegister && registered.platform === "mastodon") { saveRegisteredApp(registered); @@ -42,7 +44,7 @@ export const AccountAdd = ({ const authorizeUrl = oauth.buildAuthorizeUrl(registered, state); window.location.assign(authorizeUrl); } catch (err) { - showToast(err instanceof Error ? err.message : "OAuth 연결에 실패했습니다.", { tone: "error" }); + showToast(err instanceof Error ? err.message : t("errors.oauthConnectFailed"), { tone: "error" }); } finally { setLoading(false); } @@ -54,7 +56,7 @@ export const AccountAdd = ({ type="button" className="account-add-button header button-with-icon" onClick={() => setShowForm((prev) => !prev)} - aria-label={showForm ? "계정 추가 닫기" : "계정 추가"} + aria-label={showForm ? t("accountAdd.closeAria") : t("accountAdd.openAria")} aria-expanded={showForm} > {showForm ? ( @@ -68,26 +70,26 @@ export const AccountAdd = ({ )} - {showForm ? "계정 추가 닫기" : "계정 추가"} + {showForm ? t("accountAdd.closeLabel") : t("accountAdd.openLabel")} {showForm ? (
-

OAuth 승인 후 자동으로 돌아옵니다.

+

{t("accountAdd.oauthRedirectHint")}

) : null}
diff --git a/src/ui/components/AccountLabel.tsx b/src/ui/components/AccountLabel.tsx index 46eed39..18e02f6 100644 --- a/src/ui/components/AccountLabel.tsx +++ b/src/ui/components/AccountLabel.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import type { CustomEmoji } from "../../domain/types"; export interface AccountLabelProps { @@ -125,7 +126,8 @@ export const AccountLabel: React.FC = ({ textAsDiv = false, boldName = false }) => { - const effectiveDisplayName = displayName || name || instanceUrl || "알 수 없음"; + const { t } = useTranslation(); + const effectiveDisplayName = displayName || name || instanceUrl || t("accountLabel.unknown"); const isInteractive = !!(onClick || accountUrl); // 툴팁에 표시할 전체 텍스트 계산 diff --git a/src/ui/components/AccountSelector.tsx b/src/ui/components/AccountSelector.tsx index 90052d2..d81027a 100644 --- a/src/ui/components/AccountSelector.tsx +++ b/src/ui/components/AccountSelector.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import type { Account } from "../../domain/types"; import type { Ref } from "react"; import { formatHandle } from "../utils/account"; @@ -27,6 +28,7 @@ export const AccountSelector = ({ const detailsRef = useRef(null); const dropdownRef = useRef(null); const selectionChangeRef = useRef(false); + const { t } = useTranslation(); useClickOutside(dropdownRef, dropdownOpen, () => setDropdownOpen(false)); @@ -123,7 +125,7 @@ export const AccountSelector = ({ {activeAccount ? ( ) : ( - 계정을 선택하세요. + {t("accountSelector.placeholder")} )}
diff --git a/src/ui/components/ComposeBox.tsx b/src/ui/components/ComposeBox.tsx index 67e947c..873e4fb 100644 --- a/src/ui/components/ComposeBox.tsx +++ b/src/ui/components/ComposeBox.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import type { Account, Visibility } from "../../domain/types"; import type { MastodonApi } from "../../services/MastodonApi"; import { useEmojiManager, type EmojiItem } from "../hooks/useEmojiManager"; @@ -24,13 +25,6 @@ const parseVisibility = (value: string | null): Visibility | null => { const getVisibilityStorageKey = (accountId: string | null | undefined) => accountId ? `${VISIBILITY_KEY_PREFIX}.${accountId}` : VISIBILITY_KEY_PREFIX; -const visibilityOptions: { value: Visibility; label: string }[] = [ - { value: "public", label: "전체 공개" }, - { value: "unlisted", label: "미등록" }, - { value: "private", label: "팔로워" }, - { value: "direct", label: "DM" } -]; - const ZERO_WIDTH_SPACE = "\u200b"; export const ComposeBox = ({ @@ -56,6 +50,7 @@ export const ComposeBox = ({ account: Account | null; api: MastodonApi; }) => { + const { t } = useTranslation(); const [text, setText] = useState(""); const [emojiQuery, setEmojiQuery] = useState<{ value: string; @@ -105,6 +100,16 @@ export const ComposeBox = ({ const { showToast } = useToast(); const lastEmojiErrorRef = useRef(null); + const visibilityOptions = useMemo( + () => [ + { value: "public" as const, label: t("compose.visibility.public") }, + { value: "unlisted" as const, label: t("compose.visibility.unlisted") }, + { value: "private" as const, label: t("compose.visibility.private") }, + { value: "direct" as const, label: t("compose.visibility.direct") } + ], + [t] + ); + const handleAccountSelectionDone = useCallback(() => { requestAnimationFrame(() => { textareaRef.current?.focus(); @@ -137,7 +142,7 @@ export const ComposeBox = ({ lastEmojiErrorRef.current = null; return; } - const message = emojiError ?? "이모지를 불러오지 못했습니다."; + const message = emojiError ?? t("errors.emojisLoadFailed"); if (message === lastEmojiErrorRef.current) { return; } @@ -260,7 +265,9 @@ export const ComposeBox = ({ // 문자 수 제한 검사 if (characterLimit && currentCharCount > characterLimit) { - showToast(`글자 수 제한(${characterLimit.toLocaleString()}자)을 초과했습니다.`, { tone: "error" }); + showToast(t("errors.characterLimitExceeded", { limit: characterLimit.toLocaleString() }), { + tone: "error" + }); return; } @@ -851,9 +858,9 @@ export const ComposeBox = ({ ) : null} {replyingTo ? (
- 답글 대상: {replyingTo.summary} + {t("compose.replyingTo", { summary: replyingTo.summary })}
) : null} @@ -865,8 +872,8 @@ export const ComposeBox = ({ type="text" value={cwText} onChange={(event) => setCwText(event.target.value)} - placeholder="CW 내용을 입력하세요" - aria-label="콘텐츠 경고" + placeholder={t("compose.cw.placeholder")} + aria-label={t("compose.cw.aria")} disabled={isSubmitting} /> @@ -880,9 +887,9 @@ export const ComposeBox = ({ setText(nextValue); updateEmojiQuery(nextValue, event.target.selectionStart ?? nextValue.length); }} - placeholder="지금 무슨 생각을 하고 있나요?" - aria-label="글 작성" - title="글 작성 (N / Ctrl+Shift+N)" + placeholder={t("compose.placeholder")} + aria-label={t("compose.aria")} + title={t("compose.inputHint")} rows={4} onPaste={handlePaste} disabled={isSubmitting} @@ -932,7 +939,7 @@ export const ComposeBox = ({ }} /> {emojiSuggestions.length > 0 ? ( -
+
{emojiSuggestions.map((emoji, index) => { const isActive = index === emojiSuggestionIndex; const label = emoji.shortcode ? `:${emoji.shortcode}:` : emoji.label; @@ -943,7 +950,7 @@ export const ComposeBox = ({ className={`compose-emoji-suggestion${isActive ? " is-active" : ""}`} role="option" aria-selected={isActive} - aria-label={`이모지 ${label}`} + aria-label={t("compose.emojiSuggestions.itemAria", { label })} onMouseDown={(event) => event.preventDefault()} onClick={() => handleEmojiSuggestionSelect(emoji)} > @@ -972,17 +979,17 @@ export const ComposeBox = ({ resetImageZoom(); setActiveImageId(item.id); }} - aria-label="이미지 미리보기" + aria-label={t("compose.attachments.previewAria")} > - 선택한 이미지 + {t("compose.attachments.selectedImageAlt")} ))} {/* 이미지 추가 버튼 */}
{visibilityState.visibility === "public" ? (

- 전체 공개로 게시됩니다. 민감한 내용은 주의해주세요. + {t("compose.visibility.publicWarning")}

) : null}
@@ -1021,7 +1028,7 @@ export const ComposeBox = ({ value={visibilityState.visibility} onChange={(event) => setVisibilityState(prev => ({ ...prev, visibility: event.target.value as Visibility }))} disabled={isSubmitting} - title="공개 범위 (Ctrl+Shift+O)" + title={t("compose.visibility.hint")} > {visibilityOptions.map((option) => (
@@ -1080,45 +1087,45 @@ export const ComposeBox = ({
- {!account ?

계정을 선택해주세요.

: null} + {!account ?

{t("compose.emojiPanel.selectAccount")}

: null} {account ? (
setEmojiSearchQuery(event.target.value)} - placeholder="이모지 검색" - aria-label="이모지 검색" + placeholder={t("compose.emojiPanel.searchPlaceholder")} + aria-label={t("compose.emojiPanel.searchAria")} disabled={emojiStatus === "loading"} />
) : null} {account && emojiStatus === "loading" ? ( -

이모지를 불러오는 중...

+

{t("compose.emojiPanel.loading")}

) : null} {account && emojiStatus === "error" ? (
-

{emojiError ?? "이모지를 불러오지 못했습니다."}

+

{emojiError ?? t("errors.emojisLoadFailed")}

) : null} {account && emojiCategories.length === 0 ? ( -

사용할 수 있는 이모지가 없습니다.

+

{t("compose.emojiPanel.empty")}

) : null} {account && emojiCategories.length > 0 ? ( <> {hasEmojiSearch ? (
- 검색 결과 + {t("compose.emojiPanel.searchResults")} {emojiSearchResults.length}
{emojiSearchResults.length > 0 ? ( @@ -1129,7 +1136,7 @@ export const ComposeBox = ({ type="button" className="compose-emoji-button" onClick={() => handleEmojiSelect(emoji)} - aria-label={`이모지 ${emoji.label}`} + aria-label={t("compose.emojiPanel.emojiAria", { label: emoji.label })} title={emoji.shortcode ? `:${emoji.shortcode}:` : undefined} data-emoji-nav="emoji" data-emoji-id={emoji.id} @@ -1145,7 +1152,7 @@ export const ComposeBox = ({ ))}
) : ( -

검색 결과가 없습니다.

+

{t("compose.emojiPanel.noSearchResults")}

)} ) : null} @@ -1175,7 +1182,7 @@ export const ComposeBox = ({ type="button" className="compose-emoji-button" onClick={() => handleEmojiSelect(emoji)} - aria-label={`이모지 ${emoji.label}`} + aria-label={t("compose.emojiPanel.emojiAria", { label: emoji.label })} title={emoji.shortcode ? `:${emoji.shortcode}:` : undefined} data-emoji-nav="emoji" data-emoji-id={emoji.id} @@ -1215,14 +1222,14 @@ export const ComposeBox = ({ {category.emojis.map((emoji) => ( 선택한 이미지 원본 { - switch (type) { - case "terms": - return "이용약관"; - case "license": - return "라이선스"; - case "oss": - return "오픈소스 목록"; - case "shortcuts": - return "단축키"; - default: - return ""; - } +const INFO_MODAL_TITLE_KEYS: Record = { + terms: "infoPages.terms", + license: "infoPages.license", + oss: "infoPages.oss", + shortcuts: "infoPages.shortcuts" }; const InfoModalContent = ({ type }: { type: InfoModalType }) => { @@ -32,15 +25,16 @@ const InfoModalContent = ({ type }: { type: InfoModalType }) => { }; export const InfoModal = ({ type, onClose }: { type: InfoModalType; onClose: () => void }) => { - const title = getInfoModalTitle(type); + const { t } = useTranslation(); + const title = t(INFO_MODAL_TITLE_KEYS[type]); return (

{title}

-
diff --git a/src/ui/components/MobileMenus.tsx b/src/ui/components/MobileMenus.tsx index 81b3992..7cf57d5 100644 --- a/src/ui/components/MobileMenus.tsx +++ b/src/ui/components/MobileMenus.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; import type { Account, Visibility } from "../../domain/types"; import type { MastodonApi } from "../../services/MastodonApi"; import type { OAuthClient } from "../../services/OAuthClient"; @@ -34,6 +35,7 @@ export const MobileComposeMenu = ({ onCancelReply, mentionText }: MobileComposeMenuProps) => { + const { t } = useTranslation(); if (!open) { return null; } @@ -43,14 +45,14 @@ export const MobileComposeMenu = ({
-

글쓰기

+

{t("compose.title")}

{composeAccount ? ( @@ -77,6 +79,7 @@ type MobileMenuProps = { }; export const MobileMenu = ({ open, onClose, onOpenSettings, oauth }: MobileMenuProps) => { + const { t } = useTranslation(); if (!open) { return null; } @@ -86,9 +89,9 @@ export const MobileMenu = ({ open, onClose, onOpenSettings, oauth }: MobileMenuP
-

메뉴

-
@@ -108,7 +111,7 @@ export const MobileMenu = ({ open, onClose, onOpenSettings, oauth }: MobileMenuP - 설정 열기 + {t("settings.open")}
diff --git a/src/ui/components/PomodoroTimer.tsx b/src/ui/components/PomodoroTimer.tsx index 98ce46d..2fa60e9 100644 --- a/src/ui/components/PomodoroTimer.tsx +++ b/src/ui/components/PomodoroTimer.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; type SessionType = "focus" | "break" | "longBreak"; @@ -21,17 +22,6 @@ type PomodoroTimerProps = { // TOTAL_SESSIONS을 targetCycles에 따라 동적으로 계산 -const getSessionLabel = (type: SessionType): string => { - switch (type) { - case "focus": - return "집중"; - case "break": - return "휴식"; - case "longBreak": - return "긴 휴식"; - } -}; - const formatTime = (seconds: number): string => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; @@ -48,11 +38,17 @@ export const PomodoroTimer = ({ onRequestClearTimelineSelection, onRequestSelectTimelineAtY, }: PomodoroTimerProps) => { + const { t } = useTranslation(); const targetCycles = 4; // 고정된 4사이클 const focusDuration = focusMinutes * 60; const breakDuration = breakMinutes * 60; const longBreakDuration = longBreakMinutes * 60; + const getSessionLabel = useCallback( + (type: SessionType) => t(`pomodoro.session.${type}`), + [t] + ); + const getSessionInfo = useCallback( (sess: number): { type: SessionType; duration: number } => { const totalSessions = targetCycles * 2; @@ -406,7 +402,11 @@ export const PomodoroTimer = ({
); } @@ -582,7 +582,7 @@ export const PomodoroTimer = ({ type="button" className={`pomodoro-mode-toggle${sessionInfo.type === "break" ? " break" : ""}${sessionInfo.type === "longBreak" ? " long-break" : ""}`} onClick={handleSessionToggle} - aria-label="다음 세션으로 전환" + aria-label={t("pomodoro.nextSession")} > {getSessionLabel(sessionInfo.type)} {sessionInfo.type === "focus" ? focusCount : ""} @@ -601,29 +601,29 @@ export const PomodoroTimer = ({ type="button" className="pomodoro-button pomodoro-start" onClick={handleStart} - title="시작/정지 (S)" + title={t("pomodoro.controls.toggleHint")} > - {isRunning ? "정지" : "시작"} + {isRunning ? t("pomodoro.controls.stop") : t("pomodoro.controls.start")}
-
+
{displayedTodos.length > 0 ? (
0 ? 0 : -1} onKeyDownCapture={handleTodoKeyDown} - title="↑/↓ 이동 · Space 완료 · D 삭제 · → 타임라인 이동 · ESC 선택 해제" + title={t("pomodoro.todos.listHint")} > {displayedTodos.map((item) => (
handleToggleTodo(item.id)} - aria-label={`할 일 완료: ${item.text}`} + aria-label={t("pomodoro.todos.completeAria", { text: item.text })} /> {item.text}
diff --git a/src/ui/components/ProfileModal.tsx b/src/ui/components/ProfileModal.tsx index 723990e..52d73bc 100644 --- a/src/ui/components/ProfileModal.tsx +++ b/src/ui/components/ProfileModal.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import type { Account, AccountRelationship, @@ -164,6 +165,7 @@ export const ProfileModal = ({ showReactions: boolean; sectionSettings: SectionDisplaySettings; }) => { + const { t } = useTranslation(); const [profile, setProfile] = useState(null); const [profileError, setProfileError] = useState(null); const [profileLoading, setProfileLoading] = useState(false); @@ -210,7 +212,7 @@ export const ProfileModal = ({ useEffect(() => { if (!account || !targetAccountId) { setProfile(null); - setProfileError("프로필 정보를 불러올 수 없습니다."); + setProfileError(t("errors.profileLoadFailed")); setProfileLoading(false); return; } @@ -226,7 +228,7 @@ export const ProfileModal = ({ }) .catch((error) => { if (cancelled) return; - setProfileError(error instanceof Error ? error.message : "프로필 정보를 불러오지 못했습니다."); + setProfileError(error instanceof Error ? error.message : t("errors.profileLoadFailed")); }) .finally(() => { if (cancelled) return; @@ -235,7 +237,7 @@ export const ProfileModal = ({ return () => { cancelled = true; }; - }, [account, api, targetAccountId]); + }, [account, api, t, targetAccountId]); useEffect(() => { if (!account || !targetAccountId || account.id === targetAccountId) { @@ -256,12 +258,12 @@ export const ProfileModal = ({ .catch((error) => { if (cancelled) return; setRelationship(null); - setFollowError(error instanceof Error ? error.message : "관계 정보를 불러오지 못했습니다."); + setFollowError(error instanceof Error ? error.message : t("errors.relationshipLoadFailed")); }); return () => { cancelled = true; }; - }, [account, api, targetAccountId]); + }, [account, api, t, targetAccountId]); useEffect(() => { if (!account || !targetAccountId) { @@ -286,7 +288,7 @@ export const ProfileModal = ({ }) .catch((error) => { if (cancelled) return; - setItemsError(error instanceof Error ? error.message : "게시글을 불러오지 못했습니다."); + setItemsError(error instanceof Error ? error.message : t("errors.statusesLoadFailed")); setItems([]); }) .finally(() => { @@ -296,7 +298,7 @@ export const ProfileModal = ({ return () => { cancelled = true; }; - }, [account, api, targetAccountId]); + }, [account, api, t, targetAccountId]); useEffect(() => { if (!profileError) { @@ -333,7 +335,7 @@ export const ProfileModal = ({ const handleToggleFavourite = useCallback( async (target: Status) => { if (!account) { - setItemsError("계정을 선택해 주세요."); + setItemsError(t("errors.accountRequired")); return; } setItemsError(null); @@ -343,16 +345,16 @@ export const ProfileModal = ({ : await api.favourite(account, target.id); updateItem(updated); } catch (error) { - setItemsError(error instanceof Error ? error.message : "좋아요 처리에 실패했습니다."); + setItemsError(error instanceof Error ? error.message : t("errors.likeFailed")); } }, - [account, api, updateItem] + [account, api, t, updateItem] ); const handleToggleReblog = useCallback( async (target: Status) => { if (!account) { - setItemsError("계정을 선택해 주세요."); + setItemsError(t("errors.accountRequired")); return; } setItemsError(null); @@ -362,16 +364,16 @@ export const ProfileModal = ({ : await api.reblog(account, target.id); updateItem(updated); } catch (error) { - setItemsError(error instanceof Error ? error.message : "부스트 처리에 실패했습니다."); + setItemsError(error instanceof Error ? error.message : t("errors.boostFailed")); } }, - [account, api, updateItem] + [account, api, t, updateItem] ); const handleToggleBookmark = useCallback( async (target: Status) => { if (!account) { - setItemsError("계정을 선택해 주세요."); + setItemsError(t("errors.accountRequired")); return; } setItemsError(null); @@ -381,12 +383,12 @@ export const ProfileModal = ({ ? await api.unbookmark(account, target.id) : await api.bookmark(account, target.id); updateItem(updated); - showToast(isBookmarking ? "북마크했습니다." : "북마크를 취소했습니다."); + showToast(isBookmarking ? t("toast.bookmarkAdded") : t("toast.bookmarkRemoved")); } catch (error) { - setItemsError(error instanceof Error ? error.message : "북마크 처리에 실패했습니다."); + setItemsError(error instanceof Error ? error.message : t("errors.bookmarkFailed")); } }, - [account, api, updateItem, showToast] + [account, api, showToast, t, updateItem] ); const handleDeleteStatus = useCallback( @@ -399,24 +401,24 @@ export const ProfileModal = ({ await api.deleteStatus(account, target.id); removeItem(target.id); } catch (error) { - setItemsError(error instanceof Error ? error.message : "게시글 삭제에 실패했습니다."); + setItemsError(error instanceof Error ? error.message : t("errors.statusDeleteFailed")); } }, - [account, api, removeItem] + [account, api, removeItem, t] ); const handleReact = useCallback( async (target: Status, reaction: ReactionInput) => { if (!account) { - setItemsError("계정을 선택해 주세요."); + setItemsError(t("errors.accountRequired")); return; } if (account.platform !== "misskey") { - setItemsError("리액션은 미스키 계정에서만 사용할 수 있습니다."); + setItemsError(t("errors.reactionMisskeyOnly")); return; } if (target.myReaction && target.myReaction !== reaction.name) { - setItemsError("다른 리액션을 선택했습니다. 먼저 취소해 주세요."); + setItemsError(t("errors.reactionAlreadyExists")); return; } setItemsError(null); @@ -427,10 +429,10 @@ export const ProfileModal = ({ : await api.createReaction(account, target.id, reaction.name); updateItem(updated); } catch (error) { - setItemsError(error instanceof Error ? error.message : "리액션 처리에 실패했습니다."); + setItemsError(error instanceof Error ? error.message : t("errors.reactionFailed")); } }, - [account, api, updateItem] + [account, api, t, updateItem] ); const loadMore = useCallback(async () => { @@ -450,11 +452,11 @@ export const ProfileModal = ({ setHasMore(false); } } catch (error) { - setItemsError(error instanceof Error ? error.message : "게시글을 불러오지 못했습니다."); + setItemsError(error instanceof Error ? error.message : t("errors.statusesLoadFailed")); } finally { setItemsLoadingMore(false); } - }, [account, api, targetAccountId, hasMore, items, itemsLoading, itemsLoadingMore]); + }, [account, api, hasMore, items, itemsLoading, itemsLoadingMore, t, targetAccountId]); const handleScroll = useCallback(() => { const el = scrollRef.current; @@ -601,20 +603,20 @@ export const ProfileModal = ({ const canOpenProfileMenu = Boolean(profileOriginUrl) || canInteractFollow; const followLabel = followState === "following" - ? "팔로잉" + ? t("profile.following") : followState === "requested" - ? "요청됨" + ? t("profile.requested") : displayProfile.locked - ? "팔로우 요청" - : "팔로우"; + ? t("profile.followRequest") + : t("profile.follow"); const followAriaLabel = followState === "requested" - ? "팔로우 요청됨" + ? t("profile.followAriaRequested") : followState === "following" - ? "언팔로우" + ? t("profile.unfollow") : displayProfile.locked - ? "팔로우 요청 보내기" - : "팔로우하기"; + ? t("profile.followRequestSend") + : t("profile.followAction"); const buildNextRelationship = useCallback( (updates: Partial): AccountRelationship => ({ @@ -635,7 +637,7 @@ export const ProfileModal = ({ successTone: "success" | "info" = "success" ) => { if (!account || !targetAccountId) { - setFollowError("계정을 선택해 주세요."); + setFollowError(t("errors.accountRequired")); return; } const previous = relationship; @@ -657,7 +659,7 @@ export const ProfileModal = ({ setFollowLoading(false); } }, - [account, relationship, showToast, targetAccountId] + [account, relationship, showToast, t, targetAccountId] ); const handleFollowClick = useCallback(() => { @@ -665,7 +667,7 @@ export const ProfileModal = ({ return; } if (!account || !targetAccountId) { - setFollowError("계정을 선택해 주세요."); + setFollowError(t("errors.accountRequired")); return; } if (followState === "following") { @@ -676,8 +678,8 @@ export const ProfileModal = ({ updateRelationshipOptimistically( buildNextRelationship({ following: false, requested: false }), () => api.cancelFollowRequest(account, targetAccountId), - "팔로우 요청을 취소하지 못했습니다.", - "팔로우 요청을 취소했습니다." + t("errors.followRequestCancelFailed"), + t("toast.followRequestCancelled") ); return; } @@ -685,8 +687,8 @@ export const ProfileModal = ({ updateRelationshipOptimistically( buildNextRelationship({ following: !shouldRequest, requested: shouldRequest }), () => api.followAccount(account, targetAccountId), - "팔로우에 실패했습니다.", - shouldRequest ? "팔로우 요청을 보냈습니다." : "팔로우했습니다." + t("errors.followFailed"), + shouldRequest ? t("toast.followRequestSent") : t("toast.followed") ); }, [ account, @@ -695,6 +697,7 @@ export const ProfileModal = ({ canInteractFollow, displayProfile.locked, followState, + t, targetAccountId, updateRelationshipOptimistically ]); @@ -704,23 +707,23 @@ export const ProfileModal = ({ return; } if (!account || !targetAccountId) { - setFollowError("계정을 선택해 주세요."); + setFollowError(t("errors.accountRequired")); return; } updateRelationshipOptimistically( buildNextRelationship({ following: false, requested: false }), () => api.unfollowAccount(account, targetAccountId), - "언팔로우에 실패했습니다.", - "언팔로우했습니다." + t("errors.unfollowFailed"), + t("toast.unfollowed") ); - }, [account, api, buildNextRelationship, canInteractFollow, targetAccountId, updateRelationshipOptimistically]); + }, [account, api, buildNextRelationship, canInteractFollow, t, targetAccountId, updateRelationshipOptimistically]); const handleMuteToggle = useCallback(() => { if (!canInteractFollow) { return; } if (!account || !targetAccountId) { - setFollowError("계정을 선택해 주세요."); + setFollowError(t("errors.accountRequired")); return; } const next = buildNextRelationship({ muting: !isMuted }); @@ -730,8 +733,8 @@ export const ProfileModal = ({ isMuted ? api.unmuteAccount(account, targetAccountId) : api.muteAccount(account, targetAccountId), - isMuted ? "뮤트 해제에 실패했습니다." : "뮤트에 실패했습니다.", - isMuted ? "뮤트를 해제했습니다." : "뮤트했습니다." + isMuted ? t("errors.unmuteFailed") : t("errors.muteFailed"), + isMuted ? t("toast.unmuted") : t("toast.muted") ); setProfileMenuOpen(false); }, [ @@ -740,6 +743,7 @@ export const ProfileModal = ({ buildNextRelationship, canInteractFollow, isMuted, + t, targetAccountId, updateRelationshipOptimistically ]); @@ -749,7 +753,7 @@ export const ProfileModal = ({ return; } if (!account || !targetAccountId) { - setFollowError("계정을 선택해 주세요."); + setFollowError(t("errors.accountRequired")); return; } const next = buildNextRelationship({ @@ -763,8 +767,8 @@ export const ProfileModal = ({ isBlocked ? api.unblockAccount(account, targetAccountId) : api.blockAccount(account, targetAccountId), - isBlocked ? "차단 해제에 실패했습니다." : "차단에 실패했습니다.", - isBlocked ? "차단을 해제했습니다." : "차단했습니다." + isBlocked ? t("errors.unblockFailed") : t("errors.blockFailed"), + isBlocked ? t("toast.unblocked") : t("toast.blocked") ); setProfileMenuOpen(false); }, [ @@ -773,6 +777,7 @@ export const ProfileModal = ({ buildNextRelationship, canInteractFollow, isBlocked, + t, targetAccountId, updateRelationshipOptimistically ]); @@ -802,18 +807,18 @@ export const ProfileModal = ({ className="profile-modal" role="dialog" aria-modal="true" - aria-label="사용자 프로필" + aria-label={t("profile.aria")} style={zIndex ? { zIndex } : undefined} >
-

프로필

+

{t("profile.title")}

@@ -914,7 +923,7 @@ export const ProfileModal = ({ ref={profileMenuButtonRef} type="button" className="icon-button" - aria-label="프로필 메뉴 열기" + aria-label={t("profile.menuOpenAria")} aria-haspopup="menu" aria-expanded={profileMenuOpen} onClick={() => { @@ -934,10 +943,10 @@ export const ProfileModal = ({
- {profileLoading ?

프로필을 불러오는 중...

: null} + {profileLoading ?

{t("profile.loading")}

: null} {bioContent ? bioContent.type === "html" ?
@@ -973,9 +982,9 @@ export const ProfileModal = ({ ) : null}
-

작성한 글

- {itemsLoading && items.length === 0 ?

게시글을 불러오는 중...

: null} - {!itemsLoading && items.length === 0 ?

표시할 글이 없습니다.

: null} +

{t("profile.postsTitle")}

+ {itemsLoading && items.length === 0 ?

{t("profile.postsLoading")}

: null} + {!itemsLoading && items.length === 0 ?

{t("timeline.empty.default")}

: null} {items.length > 0 ? (
{items.map((item) => ( @@ -1003,7 +1012,7 @@ export const ProfileModal = ({ ))}
) : null} - {itemsLoadingMore ?

더 불러오는 중...

: null} + {itemsLoadingMore ?

{t("timeline.loadingMore")}

: null}
diff --git a/src/ui/components/ReactionPicker.tsx b/src/ui/components/ReactionPicker.tsx index 96391bb..9c7447c 100644 --- a/src/ui/components/ReactionPicker.tsx +++ b/src/ui/components/ReactionPicker.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import type { Account, ReactionInput } from "../../domain/types"; import type { MastodonApi } from "../../services/MastodonApi"; import { useClickOutside } from "../hooks/useClickOutside"; @@ -18,6 +19,7 @@ export const ReactionPicker = ({ onSelect: (reaction: ReactionInput) => void; buttonDataAction?: string; }) => { + const { t } = useTranslation(); const [open, setOpen] = useState(false); const [panelStyle, setPanelStyle] = useState({}); const [recentOpen, setRecentOpen] = useState(true); @@ -49,13 +51,13 @@ export const ReactionPicker = ({ lastEmojiErrorRef.current = null; return; } - const message = emojiError ?? "이모지를 불러오지 못했습니다."; + const message = emojiError ?? t("errors.emojisLoadFailed"); if (message === lastEmojiErrorRef.current) { return; } lastEmojiErrorRef.current = message; showToast(message, { tone: "error" }); - }, [emojiError, emojiStatus, showToast]); + }, [emojiError, emojiStatus, showToast, t]); const emojiSearchResults = useMemo(() => { if (!emojiSearchQuery.trim()) { @@ -383,11 +385,11 @@ export const ReactionPicker = ({ disabled={disabled} ref={buttonRef} data-action={buttonDataAction} - aria-label="리액션 추가" + aria-label={t("reactions.addAria")} aria-haspopup="dialog" aria-expanded={open} > - 리액션 + {t("reactions.add")} {open ? ( <> @@ -396,7 +398,7 @@ export const ReactionPicker = ({ className="reaction-picker-panel" role="dialog" aria-modal="true" - aria-label="리액션 선택" + aria-label={t("reactions.panelAria")} ref={panelRef} style={panelStyle} onKeyDown={handleEmojiPanelKeyDown} @@ -404,39 +406,39 @@ export const ReactionPicker = ({ tabIndex={-1} >
- {!account ?

계정을 선택해주세요.

: null} + {!account ?

{t("errors.accountRequired")}

: null} {account ? (
setEmojiSearchQuery(event.target.value)} - placeholder="이모지 검색" - aria-label="이모지 검색" + placeholder={t("compose.emojiPanel.searchPlaceholder")} + aria-label={t("compose.emojiPanel.searchAria")} disabled={emojiStatus === "loading"} />
) : null} {account && emojiStatus === "loading" ? ( -

이모지를 불러오는 중...

+

{t("compose.emojiPanel.loading")}

) : null} {account && emojiStatus === "error" ? (
-

{emojiError ?? "이모지를 불러오지 못했습니다."}

+

{emojiError ?? t("errors.emojisLoadFailed")}

) : null} {account && emojiCategories.length === 0 ? ( -

사용할 수 있는 이모지가 없습니다.

+

{t("compose.emojiPanel.empty")}

) : null} {account && emojiCategories.length > 0 ? ( <> {hasEmojiSearch ? (
- 검색 결과 + {t("compose.emojiPanel.searchResults")} {emojiSearchResults.length}
{emojiSearchResults.length > 0 ? ( @@ -447,7 +449,7 @@ export const ReactionPicker = ({ type="button" className="compose-emoji-button" onClick={() => handleSelect(emoji)} - aria-label={`이모지 ${emoji.label}`} + aria-label={t("compose.emojiPanel.emojiAria", { label: emoji.label })} title={emoji.shortcode ? `:${emoji.shortcode}:` : undefined} data-emoji-nav="emoji" data-emoji-id={emoji.id} @@ -463,7 +465,7 @@ export const ReactionPicker = ({ ))}
) : ( -

검색 결과가 없습니다.

+

{t("compose.emojiPanel.noSearchResults")}

)} ) : null} @@ -493,7 +495,7 @@ export const ReactionPicker = ({ type="button" className="compose-emoji-button" onClick={() => handleSelect(emoji)} - aria-label={`이모지 ${emoji.label}`} + aria-label={t("compose.emojiPanel.emojiAria", { label: emoji.label })} title={emoji.shortcode ? `:${emoji.shortcode}:` : undefined} data-emoji-nav="emoji" data-emoji-id={emoji.id} @@ -536,7 +538,7 @@ export const ReactionPicker = ({ type="button" className="compose-emoji-button" onClick={() => handleSelect(emoji)} - aria-label={`이모지 ${emoji.label}`} + aria-label={t("compose.emojiPanel.emojiAria", { label: emoji.label })} title={emoji.shortcode ? `:${emoji.shortcode}:` : undefined} data-emoji-nav="emoji" data-emoji-id={emoji.id} @@ -559,9 +561,9 @@ export const ReactionPicker = ({
- 표준 이모지 + {t("compose.emojiPanel.standardTitle")}
) : null} {standardEmojiCategories.map((category) => { @@ -588,7 +590,7 @@ export const ReactionPicker = ({ type="button" className="compose-emoji-button" onClick={() => handleSelect(emoji)} - aria-label={`이모지 ${emoji.label}`} + aria-label={t("compose.emojiPanel.emojiAria", { label: emoji.label })} title={emoji.shortcode ? `:${emoji.shortcode}:` : undefined} data-emoji-nav="emoji" data-emoji-id={emoji.id} diff --git a/src/ui/components/SettingsModal.tsx b/src/ui/components/SettingsModal.tsx index ac3cc08..69294fe 100644 --- a/src/ui/components/SettingsModal.tsx +++ b/src/ui/components/SettingsModal.tsx @@ -1,6 +1,8 @@ import type { AccountsState } from "../state/AppContext"; import type { ColorScheme, ThemeMode } from "../utils/theme"; import { AccountSelector } from "./AccountSelector"; +import { useTranslation } from "react-i18next"; +import { SUPPORTED_LANGUAGES } from "../../i18n"; type SettingsModalProps = { open: boolean; @@ -49,6 +51,7 @@ export const SettingsModal = ({ onPomodoroBreakChange, onPomodoroLongBreakChange }: SettingsModalProps) => { + const { t, i18n } = useTranslation(); if (!open) { return null; } @@ -58,20 +61,20 @@ export const SettingsModal = ({
-

설정

+

{t("settings.title")}

- 계정 관리 -

계정을 선택하여 재인증하거나 삭제합니다.

+ {t("settings.account.title")} +

{t("settings.account.description")}

- {reauthLoading ? "재인증 중..." : "재인증"} + {reauthLoading ? t("settings.account.reauthing") : t("settings.account.reauth")}
- 테마 -

기본, 크리스마스, 하늘핑크, 모노톤 테마를 선택합니다.

+ {t("settings.theme.title")} +

{t("settings.theme.description")}

- 색상 모드 -

시스템 설정을 따르거나 라이트/다크 모드를 고정합니다.

+ {t("settings.colorScheme.title")} +

{t("settings.colorScheme.description")}

- 뽀모도로 타이머 -

사이드바에 뽀모도로 타이머를 표시합니다.

+ {t("settings.language.title")} +

{t("settings.language.description")}

+
+ +
+
+
+ {t("settings.pomodoro.title")} +

{t("settings.pomodoro.description")}