diff --git a/bun.lock b/bun.lock index 941ea88..c6dc6d5 100644 --- a/bun.lock +++ b/bun.lock @@ -5,31 +5,34 @@ "": { "name": "holistream", "dependencies": { - "@pinia/colada": "^0.21.2", - "@unhead/vue": "^2.1.2", - "@vueuse/core": "^14.2.0", + "@pinia/colada": "^0.21.7", + "@unhead/vue": "^2.1.10", + "@vueuse/core": "^14.2.1", "aws4fetch": "^1.0.20", "clsx": "^2.1.1", - "hono": "^4.11.7", + "hono": "^4.12.5", + "i18next": "^25.8.14", + "i18next-browser-languagedetector": "^8.2.1", + "i18next-http-backend": "^3.0.2", + "i18next-vue": "^5.4.0", "is-mobile": "^5.0.0", "pinia": "^3.0.4", - "tailwind-merge": "^3.4.0", - "vue": "^3.5.27", - "vue-i18n": "^11.2.8", - "vue-router": "^5.0.2", + "tailwind-merge": "^3.5.0", + "vue": "^3.5.29", + "vue-router": "^5.0.3", "zod": "^4.3.6", }, "devDependencies": { - "@cloudflare/vite-plugin": "^1.23.0", - "@types/node": "^25.2.0", + "@cloudflare/vite-plugin": "^1.26.0", + "@types/node": "^25.3.3", "@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue-jsx": "^5.1.4", - "unocss": "^66.6.0", + "unocss": "^66.6.5", "unplugin-auto-import": "^21.0.0", "unplugin-vue-components": "^31.0.0", "vite": "^8.0.0-beta.16", "vite-ssr-components": "^0.5.2", - "wrangler": "^4.62.0", + "wrangler": "^4.70.0", }, }, }, @@ -82,6 +85,8 @@ "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], @@ -92,17 +97,17 @@ "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.14.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260218.0" }, "optionalPeers": ["workerd"] }, "sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg=="], - "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.25.5", "", { "dependencies": { "@cloudflare/unenv-preset": "2.14.0", "miniflare": "4.20260302.0", "unenv": "2.0.0-rc.24", "wrangler": "4.68.1", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-dWnJtp/4/m2XQ5Ssnxrh6rb+Jvlkd9pTZhX8MS5sNhdzoULB6vzPkdKaKnaLnYC97iL3j1I2m0gIr15QznKRjA=="], + "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.26.0", "", { "dependencies": { "@cloudflare/unenv-preset": "2.14.0", "miniflare": "4.20260301.1", "unenv": "2.0.0-rc.24", "wrangler": "4.70.0", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-F5jSOj9JeWMp9iQa2x+Ocjz++SCfK6Phcca/YLkaddPw5ie7W1VvEWudQ/gxYtRd47mQ/PfCLkE9QGyy6OGEng=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260302.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cGtxPByeVrgoqxbmd8qs631wuGwf8yTm/FY44dEW4HdoXrb5jhlE4oWYHFafedkQCvGjY1Vbs3puAiKnuMxTXQ=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260301.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+kJvwociLrvy1JV9BAvoSVsMEIYD982CpFmo/yMEvBwxDIjltYsLTE8DLi0mCkGsQ8Ygidv2fD9wavzXeiY7OQ=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260302.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WRGqV6RNXM3xoQblJJw1EHKwx9exyhB18cdnToSCUFPObFhk3fzMLoQh7S+nUHUpto6aUrXPVj6R/4G3UPjCxw=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260301.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PPIetY3e67YBr9O4UhILK8nbm5TqUDl14qx4rwFNrRSBOvlzuczzbd4BqgpAtbGVFxKp1PWpjAnBvGU/OI/tLQ=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260302.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gG423mtUjrmlQT+W2+KisLc6qcGcBLR+QcK5x1gje3bu/dF3oNiYuqY7o58A+sQk6IB849UC4UyNclo1RhP2xw=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260301.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Gu5vaVTZuYl3cHa+u5CDzSVDBvSkfNyuAHi6Mdfut7TTUdcb3V5CIcR/mXRSyMXzEy9YxEWIfdKMxOMBjupvYQ=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260302.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7M25noGI4WlSBOhrIaY8xZrnn87OQKtJg9YWAO2EFqGjF1Su5QXGaLlQVF4fAKbqTywbHnI8BAuIsIlUSNkhCg=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260301.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-igL1pkyCXW6GiGpjdOAvqMi87UW0LMc/+yIQe/CSzuZJm5GzXoAMrwVTkCFnikk6JVGELrM5x0tGYlxa0sk5Iw=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260302.0", "", { "os": "win32", "cpu": "x64" }, "sha512-jK1L3ADkiWxFzlqZTq2iHW1Bd2Nzu1fmMWCGZw4sMZ2W1B2WCm2wHwO2SX/py4BgylyEN3wuF+5zagbkNKht9A=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260301.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Q0wMJ4kcujXILwQKQFc1jaYamVsNvjuECzvRrTI8OxGFMx2yq9aOsswViE4X1gaS2YQQ5u0JGwuGi5WdT1Lt7A=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -218,12 +223,6 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - "@intlify/core-base": ["@intlify/core-base@11.2.8", "", { "dependencies": { "@intlify/message-compiler": "11.2.8", "@intlify/shared": "11.2.8" } }, "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA=="], - - "@intlify/message-compiler": ["@intlify/message-compiler@11.2.8", "", { "dependencies": { "@intlify/shared": "11.2.8", "source-map-js": "^1.0.2" } }, "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ=="], - - "@intlify/shared": ["@intlify/shared@11.2.8", "", {}, "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw=="], - "@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=="], @@ -236,11 +235,51 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.115.0", "", { "os": "android", "cpu": "arm" }, "sha512-VoB2rhgoqgYf64d6Qs5emONQW8ASiTc0xp+aUE4JUhxjX+0pE3gblTYDO0upcN5vt9UlBNmUhAwfSifkfre7nw=="], + + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.115.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lWRX75u+gqfB4TF3pWCHuvhaeneAmRl2b2qNBcl4S6yJ0HtnT4VXOMEZrq747i4Zby1ZTxj6mtOe678Bg8gRLw=="], + + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.115.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ii/oOZjfGY1aszXTy29Z5DRyCEnBOrAXDVCvfdfXFQsOZlbbOa7NMHD7D+06YFe5qdxfmbWAYv4yn6QJi/0d2g=="], + + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.115.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-R/sW/p8l77wglbjpMcF+h/3rWbp9zk1mRP3U14mxTYIC2k3m+aLBpXXgk2zksqf9qKk5mcc4GIYsuCn9l8TgDg=="], + + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.115.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-CSJ5ldNm9wIGGkhaIJeGmxRMZbgxThRN+X1ufYQQUNi5jZDV/U3C2QDMywpP93fczNBj961hXtcUPO/oVGq4Pw=="], + + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.115.0", "", { "os": "linux", "cpu": "arm" }, "sha512-uWFwssE5dHfQ8lH+ktrsD9JA49+Qa0gtxZHUs62z1e91NgGz6O7jefHGI6aygNyKNS45pnnBSDSP/zV977MsOQ=="], + + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.115.0", "", { "os": "linux", "cpu": "arm" }, "sha512-fZbqt8y/sKQ+v6bBCuv/mYYFoC0+fZI3mGDDEemmDOhT78+aUs2+4ZMdbd2btlXmnLaScl37r8IRbhnok5Ka9w=="], + + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.115.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1ej/MjuTY9tJEunU/hUPIFmgH5PqgMQoRjNOvOkibtJ3Zqlw/+Lc+HGHDNET8sjbgIkWzdhX+p4J96A5CPdbag=="], + + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.115.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HjsZbJPH9mMd4swJRywVMsDZsJX0hyKb1iNHo5ijRl5yhtbO3lj7ImSrrL1oZ1VEg0te4iKmDGGz/6YPLd1G8w=="], + + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.115.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zhhePoBrd7kQx3oClX/W6NldsuCbuMqaN9rRsY+6/WoorAb4j490PG/FjqgAXscWp2uSW2WV9L+ksn0wHrvsrg=="], + + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.115.0", "", { "os": "linux", "cpu": "none" }, "sha512-t/IRojvUE9XrKu+/H1b8YINug+7Q6FLls5rsm2lxB5mnS8GN/eYAYrPgHkcg9/1SueRDSzGpDYu3lGWTObk1zw=="], + + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.115.0", "", { "os": "linux", "cpu": "none" }, "sha512-79jBHSSh/YpQRAmvYoaCfpyToRbJ/HBrdB7hxK2ku2JMehjopTVo+xMJss/RV7/ZYqeezgjvKDQzapJbgcjVZA=="], + + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.115.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-nA1TpxkhNTIOMMyiSSsa7XIVJVoOU/SsVrHIz3gHvWweB5PHCQfO7w+Lb2EP0lBWokv7HtA/KbF7aLDoXzmuMw=="], + + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.115.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9iVX789DoC3SaOOG+X6NcF/tVChgLp2vcHffzOC2/Z1JTPlz6bMG2ogvcW6/9s0BG2qvhNQImd+gbWYeQbOwVw=="], + + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.115.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RmQmk+mjCB0nMNfEYhaCxwofLo1Z95ebHw1AGvRiWGCd4zhCNOyskgCbMogIcQzSB3SuEKWgkssyaiQYVAA4hQ=="], + + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.115.0", "", { "os": "none", "cpu": "arm64" }, "sha512-viigraWWQhhDvX5aGq+wrQq58k00Xq3MHz/0R4AFMxGlZ8ogNonpEfNc73Q5Ly87Z6sU9BvxEdG0dnYTfVnmew=="], + + "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.115.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-IzGCrMwXhpb4kTXy/8lnqqqwjI7eOvy+r9AhVw+hsr8t1ecBBEHprcNy0aKatFHN6hsX7UMHHQmBAQjVvL/p1A=="], + + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.115.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-/ym+Absk/TLFvbhh3se9XYuI1D7BrUVHw4RaG/2dmWKgBenrZHaJsgnRb7NJtaOyjEOLIPtULx1wDdVL0SX2eg=="], + + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.115.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-AQSZjIR+b+Te7uaO/hGTMjT8/oxlYrvKrOTi4KTHF/O6osjHEatUQ3y6ZW2+8+lJxy20zIcGz6iQFmFq/qDKkg=="], + + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.115.0", "", { "os": "win32", "cpu": "x64" }, "sha512-oxUl82N+fIO9jIaXPph8SPPHQXrA08BHokBBJW8ct9F/x6o6bZE6eUAhUtWajbtvFhL8UYcCWRMba+kww6MBlA=="], + "@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="], "@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="], - "@pinia/colada": ["@pinia/colada@0.21.6", "", { "peerDependencies": { "pinia": "^2.2.6 || ^3.0.0", "vue": "^3.5.17" } }, "sha512-DppfAYky3Uavlpdx2iZHgd/+ZVPyBGTR+x+kFfAUz8h9l1DIQgf2cw/QZg0RZ4GAUNnKf6Ue6FzfWttwqhZXUQ=="], + "@pinia/colada": ["@pinia/colada@0.21.7", "", { "peerDependencies": { "pinia": "^2.2.6 || ^3.0.0", "vue": "^3.5.17" } }, "sha512-b8dJgRSjh7o6NnPXuvMbqv6JhoD/m/CwdadKl5SQvygsbUveYCBoqtnWzPch8AEW/UK0I3rFoATE8WrfI2cgKA=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], @@ -288,53 +327,53 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/node": ["@types/node@25.3.1", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw=="], + "@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="], "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], - "@unhead/vue": ["@unhead/vue@2.1.9", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.9" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-7SqqDEn5zFID1PnEdjLCLa/kOhoAlzol0JdYfVr2Ejek+H4ON4s8iyExv2QQ8bReMosbXQ/Bw41j2CF1NUuGSA=="], + "@unhead/vue": ["@unhead/vue@2.1.10", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.10" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-VP78Onh2HNezLPfhYjfHqn4dxlcQsE6PJgTTs61NksO/thvilNswtgBq0N0MWCLtn43N5akEPGW2y2zxM3PWgQ=="], - "@unocss/cli": ["@unocss/cli@66.6.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.2", "@unocss/core": "66.6.2", "@unocss/preset-wind3": "66.6.2", "@unocss/preset-wind4": "66.6.2", "@unocss/transformer-directives": "66.6.2", "cac": "^6.7.14", "chokidar": "^5.0.0", "colorette": "^2.0.20", "consola": "^3.4.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "bin": { "unocss": "bin/unocss.mjs" } }, "sha512-N7nKnOJ/36FRs3PE7+CFbzg7UBhIsucYYAK5xjJScX0H2q8O6rODaNM5uvc77Qh4q+y1S/Bt5ArOwIewzdpP4w=="], + "@unocss/cli": ["@unocss/cli@66.6.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.4", "@unocss/core": "66.6.5", "@unocss/preset-wind3": "66.6.5", "@unocss/preset-wind4": "66.6.5", "@unocss/transformer-directives": "66.6.5", "cac": "^6.7.14", "chokidar": "^5.0.0", "colorette": "^2.0.20", "consola": "^3.4.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "bin": { "unocss": "bin/unocss.mjs" } }, "sha512-UlETATpAZ+A5gOfj+z+BMXuIUcXCMjvlQteQE0VR2Yf0VIxz4sVO4z0VCXwXsxLTMfQiIMDpKVrGeczcYicvTA=="], - "@unocss/config": ["@unocss/config@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "colorette": "^2.0.20", "consola": "^3.4.2", "unconfig": "^7.5.0" } }, "sha512-qny2bRW1OA+MZbWShVZdBg6fJundm1LqQwCxJnIpeK3McpPHS3pnHBiwD1wfZHY2z5Pe+XgZOZkozNmG/eyyqg=="], + "@unocss/config": ["@unocss/config@66.6.4", "", { "dependencies": { "@unocss/core": "66.6.4", "colorette": "^2.0.20", "consola": "^3.4.2", "unconfig": "^7.5.0" } }, "sha512-iwHl5FG81cOAMalqigjw21Z2tMa0xjN0doQxnGOLx8KP+BllruXSjBj8CRk3m6Ny9fDxfpFY0ruYbIBA5AGwDQ=="], - "@unocss/core": ["@unocss/core@66.6.2", "", {}, "sha512-IOvN1BLRP0VTjjS5afSxmXhvKRDko2Shisp8spU+A9qiH1tXEFP3phyVevm/SuGwBHO1lC+SJ451/4oFkCAwJA=="], + "@unocss/core": ["@unocss/core@66.6.5", "", {}, "sha512-hzjo+0EF+pNbf+tb0OjRNZRF9BJoKECcZZgtufxRPpWJdlv+aYmNkH1p9fldlHHzYcn3ZqVnnHnmk7HwaolJbg=="], - "@unocss/extractor-arbitrary-variants": ["@unocss/extractor-arbitrary-variants@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2" } }, "sha512-D2tK/8QClrVViSuoH5eLjXwlVOK1UgXx7ukz/D260+R6vhCmjv97RXPouZkq40sxGzfxzaQZUyPEjXLjtnO3bw=="], + "@unocss/extractor-arbitrary-variants": ["@unocss/extractor-arbitrary-variants@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5" } }, "sha512-wqzRtbyy3I595WCwwb8VBmznJTHWcTdylzVT+WBgacJDjRlT1sXaq2fRlOsHvtTRj1qG70t3PwKc6XgU0hutNg=="], - "@unocss/inspector": ["@unocss/inspector@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/rule-utils": "66.6.2", "colorette": "^2.0.20", "gzip-size": "^6.0.0", "sirv": "^3.0.2" } }, "sha512-q0kktb01dXeeXyNnNwYM1SkSHxrEOQhCZ/YQ5aCdC7BWNGF4yZMK0YrJXmGUTEHN4RhEPLN/rAIsDBsKcoFaAQ=="], + "@unocss/inspector": ["@unocss/inspector@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5", "@unocss/rule-utils": "66.6.5", "colorette": "^2.0.20", "gzip-size": "^6.0.0", "sirv": "^3.0.2" } }, "sha512-rrXPlSeRfYajEL65FL1Ok9Hfhjy9zvuZZwqXh9P0qCJlou2r2IqDFO/Gf9j5yO89tnKIfJ8ff6jEyqUmzbKSMQ=="], - "@unocss/preset-attributify": ["@unocss/preset-attributify@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2" } }, "sha512-pRry38qO1kJvj5/cekbDk0QLosty+UFQ3fhNiph88D//jkT5tsUCn77nB/RTSe7oTqw/FqNwxPgbGz/wfNWqZg=="], + "@unocss/preset-attributify": ["@unocss/preset-attributify@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5" } }, "sha512-fx+pKMZ0WgT+dfinVaLkNXlx6oZFwtMbZj5O/1SQia0UcfhnyS+G35HYpbgoc9GEAl3DclxxotzZjveZm++9fA=="], - "@unocss/preset-icons": ["@unocss/preset-icons@66.6.2", "", { "dependencies": { "@iconify/utils": "^3.1.0", "@unocss/core": "66.6.2", "ofetch": "^1.5.1" } }, "sha512-FjhxvYX+21HefYdMIxJCq8C9v/K7fSlO1DMqDQgtrCp0/WvHyFncHILLOwp064M7m3AqzOVJx7Vw/zCvKy0Jrg=="], + "@unocss/preset-icons": ["@unocss/preset-icons@66.6.5", "", { "dependencies": { "@iconify/utils": "^3.1.0", "@unocss/core": "66.6.5", "ofetch": "^1.5.1" } }, "sha512-03ppAcTWD77w1WZhORT8c9beTHBtWu3cx+c4qfShOfY6LQmZgx5i7DhCij5Wcj/U1zYA4Vrh13CDEmpsdZO3Cw=="], - "@unocss/preset-mini": ["@unocss/preset-mini@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/extractor-arbitrary-variants": "66.6.2", "@unocss/rule-utils": "66.6.2" } }, "sha512-mybpiAq9htF7PWPH1Mnb4y7hrxVwpsBg8VfbjSglY3SfLca8RrJtvBT+DVh7YUDRiYsZGfihRWkfD0AN68gkcA=="], + "@unocss/preset-mini": ["@unocss/preset-mini@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5", "@unocss/extractor-arbitrary-variants": "66.6.5", "@unocss/rule-utils": "66.6.5" } }, "sha512-Ber3k2jlE8JP0y507hw/lvdDvcxfY0t4zaGA7hVZdEqlH6Eus/TqIVZ9tdMH4u0VDWYeAs98YV+auUJmMqGXpg=="], - "@unocss/preset-tagify": ["@unocss/preset-tagify@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2" } }, "sha512-ybb45So2x87P3bssLRp1uIS+VHAeNSecwkHqiv93PnuBDJ38/9XlqWF98uga2MEfNM3zvMj9plX9MauidxiPrw=="], + "@unocss/preset-tagify": ["@unocss/preset-tagify@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5" } }, "sha512-YYk/eg1OWX4Nx7rK1YZLMHXXntzNRDHp6BIInJteQmlXw0sFgrtdMKj7fnxrORsBDHwxWMp4sWEucPvfCtTlVQ=="], - "@unocss/preset-typography": ["@unocss/preset-typography@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/rule-utils": "66.6.2" } }, "sha512-1f/ZfeuLQOnO48mRz1+6UdoJxa13ZYcamaLz7ft96n7D1eWvkOUAC/AUUke/kbHh3vvqwRVimC9OpdXxdGFQAQ=="], + "@unocss/preset-typography": ["@unocss/preset-typography@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5", "@unocss/rule-utils": "66.6.5" } }, "sha512-Cb63tdC0P2rgj/4t4DrSCl6RHebNpjUp9FQArg0KCnFnW75nWtKlsKpHuEXpi7KwrgOIx+rjlkwC1bDcsdNLHw=="], - "@unocss/preset-uno": ["@unocss/preset-uno@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/preset-wind3": "66.6.2" } }, "sha512-Wy3V25ZF29OmVHJk5ghP6HCCRNBJXm0t+bKLKJJknOjD+/D51DZbUsDqZBtTpVtgi/SOPDbw7cX3lY2oqt4Hnw=="], + "@unocss/preset-uno": ["@unocss/preset-uno@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5", "@unocss/preset-wind3": "66.6.5" } }, "sha512-feZfGyzt3dH4h6yP2kjsx5MuoI1gU7vY/VL5O+ObosaB7HzzOFCsu2WzlvWn/FTRBi+scvdq436hsfflVyHYfQ=="], - "@unocss/preset-web-fonts": ["@unocss/preset-web-fonts@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "ofetch": "^1.5.1" } }, "sha512-0ckqiE8HkhETeghhxCXVGf96sNPhgBsB5q32iAuMM0HFR4x+ANiLqyfKrm/iqxKUw6rVO4+ItTV0RUWKcZvkXg=="], + "@unocss/preset-web-fonts": ["@unocss/preset-web-fonts@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5", "ofetch": "^1.5.1" } }, "sha512-u5jEHYTMeseykqinXd2VY2n7q9yFQlZotREpfSAft8ENNJdV7Yg/6It3lL68zT/k1AV/A8gk94KEuDh0fnoSxQ=="], - "@unocss/preset-wind": ["@unocss/preset-wind@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/preset-wind3": "66.6.2" } }, "sha512-G0H4baUizmTByEowqGuYbKpU2TTisDhZ9W7hrIpYFbRkFv0i1kN2mIxCwj/FLmdY/6x8iSRJ7rO8Nez63YYhnw=="], + "@unocss/preset-wind": ["@unocss/preset-wind@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5", "@unocss/preset-wind3": "66.6.5" } }, "sha512-GLu7LzVF0LHqdZoHFZ8dbsCv8TD5ZH/r10CQbrL5qwmp4a/uyfDEmsre4Nsqim7JktRyXn3HK2XQmTB8AmXpgQ=="], - "@unocss/preset-wind3": ["@unocss/preset-wind3@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/preset-mini": "66.6.2", "@unocss/rule-utils": "66.6.2" } }, "sha512-UqdU2Obx3wXid9xeBHGY1MWxedXa43MGuP5Z2FA9modcXptReux4Zhy764SeQwx6acOUEql2/CTvOBwelZzheQ=="], + "@unocss/preset-wind3": ["@unocss/preset-wind3@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5", "@unocss/preset-mini": "66.6.5", "@unocss/rule-utils": "66.6.5" } }, "sha512-0ccQoJmHq4tTnn5C0UKhP598B/gG65AjqlfgfRpwt059yAWYqizGy6MRUGdLklyEK4H06E6qbMBqIjla2rOexQ=="], - "@unocss/preset-wind4": ["@unocss/preset-wind4@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/extractor-arbitrary-variants": "66.6.2", "@unocss/rule-utils": "66.6.2" } }, "sha512-XU+4NN9QIMefawDB9FqOeKONXeGDUJQuQgOeBcpbV/jwOYtyqRrHiqQg++fy1hRbluM+S+KqwRHYjvje8zCTow=="], + "@unocss/preset-wind4": ["@unocss/preset-wind4@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5", "@unocss/extractor-arbitrary-variants": "66.6.5", "@unocss/rule-utils": "66.6.5" } }, "sha512-JT57CU60PY3/PHBvxY+UG53I9K+awin/TodZTn4lqQNnF2v6fjkeBKiys9cxeoP4wbHuQWorrW4GqRLNDWIMcw=="], - "@unocss/rule-utils": ["@unocss/rule-utils@66.6.2", "", { "dependencies": { "@unocss/core": "^66.6.2", "magic-string": "^0.30.21" } }, "sha512-cygfCtkeMrqMM6si1cnyOF16sS7M2gCAqgmZybAhGV7tmH7V8Izn52JZiZIrxVRNMz9dWMVWerHEI9nLbFdbrg=="], + "@unocss/rule-utils": ["@unocss/rule-utils@66.6.5", "", { "dependencies": { "@unocss/core": "^66.6.5", "magic-string": "^0.30.21" } }, "sha512-eDGXoMebb5aeEAFa2y4gnGLC+CHZPx93JYCt6uvEyf9xOoetwDcZaYC8brWdjaSKn+WVgsfxiZreC7F0rJywOQ=="], - "@unocss/transformer-attributify-jsx": ["@unocss/transformer-attributify-jsx@66.6.2", "", { "dependencies": { "@babel/parser": "7.27.7", "@babel/traverse": "7.27.7", "@unocss/core": "66.6.2" } }, "sha512-WiAEdEowGjQWu1ayhkGGBNGyw3mZLzZ+V5o3zx5U2GPuqvP67YIUfvY+/gTkCnd4+A8unkb+a1VeVgr4cHUkQw=="], + "@unocss/transformer-attributify-jsx": ["@unocss/transformer-attributify-jsx@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5", "oxc-parser": "^0.115.0", "oxc-walker": "^0.7.0" } }, "sha512-/dVaRR7V/2Alskb2rUPmP/lhyb/YCxYyYNxp30kxxW0ew6mZWXQRzsxOJJVmGp23Uw7HxUW63t8zXzUdoI0b+g=="], - "@unocss/transformer-compile-class": ["@unocss/transformer-compile-class@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2" } }, "sha512-L0yaQAmvWkm6LVLXMviqhHIi4c7WQpZFBgJF8jfsALyHihh8K9U9OrRJ81zfLH3Ltw5ZbGzoDE8m/2bB6aRhyw=="], + "@unocss/transformer-compile-class": ["@unocss/transformer-compile-class@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5" } }, "sha512-U/ukk5lyZOFNyz9hVzZBkxciayjgimyfPuQBa5PHSC4W3nDmnFd1zgXzUVaM6KduPmiTExzpJSDgELb2OTbpqg=="], - "@unocss/transformer-directives": ["@unocss/transformer-directives@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2", "@unocss/rule-utils": "66.6.2", "css-tree": "^3.1.0" } }, "sha512-gjLDLItTUJ4CV8K2AA0cw381a7rJ3U4kCHQmZmN3+956o2R7cEHSLyEczmMy04Mg2JBomrjIZjo+L66z5rvblQ=="], + "@unocss/transformer-directives": ["@unocss/transformer-directives@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5", "@unocss/rule-utils": "66.6.5", "css-tree": "^3.1.0" } }, "sha512-QgofDdDedNK6dQ246+RXhM6gTzRz7NuetQQ8UnNgArm4PBHngVrrkjCzG1ByDTtEtoE8WR70UMR4Vf5dXTcHPw=="], - "@unocss/transformer-variant-group": ["@unocss/transformer-variant-group@66.6.2", "", { "dependencies": { "@unocss/core": "66.6.2" } }, "sha512-Uoo6xthOHJ36NdN4b7s/Y7R3fZOf4JYgKzuldHEyHAo0LL204Ss+Ah0+TEt4v72aq+Z86vrLJPyYCeGNKdr8cA=="], + "@unocss/transformer-variant-group": ["@unocss/transformer-variant-group@66.6.5", "", { "dependencies": { "@unocss/core": "66.6.5" } }, "sha512-k6vQgn/P7ObHBRYw6o1+xwdQIfwc6b9O5TFFe87UmBB6hJ2zaHWRVuPB6oky7F9Gz8bPfXC3WJuv7UyIwRmBQQ=="], - "@unocss/vite": ["@unocss/vite@66.6.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.2", "@unocss/core": "66.6.2", "@unocss/inspector": "66.6.2", "chokidar": "^5.0.0", "magic-string": "^0.30.21", "pathe": "^2.0.3", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0" } }, "sha512-HLmzDvde3BJ2C6iromHVE21lmNm4SmGSMlbSbFuLPOmWV11XhhHBkAOzytSxPBRG0dbuo+InSGUM14Ek2d6UDg=="], + "@unocss/vite": ["@unocss/vite@66.6.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.4", "@unocss/core": "66.6.5", "@unocss/inspector": "66.6.5", "chokidar": "^5.0.0", "magic-string": "^0.30.21", "pathe": "^2.0.3", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0" } }, "sha512-J/QZa6h94ordZlZytIKQkuYa+G2GiWiS3y9O1uoHAAN2tzFSkgCXNUif7lHu1h4eCrgC0AOHJSYWg1LIASNDkg=="], "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="], @@ -414,6 +453,8 @@ "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], + "cross-fetch": ["cross-fetch@4.0.0", "", { "dependencies": { "node-fetch": "^2.6.12" } }, "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g=="], + "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -450,14 +491,20 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], - "gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="], - "hono": ["hono@4.12.2", "", {}, "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg=="], + "hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="], "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], + "i18next": ["i18next@25.8.14", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA=="], + + "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="], + + "i18next-http-backend": ["i18next-http-backend@3.0.2", "", { "dependencies": { "cross-fetch": "4.0.0" } }, "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g=="], + + "i18next-vue": ["i18next-vue@5.4.0", "", { "peerDependencies": { "i18next": ">=23", "vue": "^3.4.38" } }, "sha512-GDj0Xvmis5Xgcvo9gMBJMgJCtewYMLZP6gAEPDDGCMjA+QeB4uS4qUf1MK79mkz/FukhaJdC+nlj0y1qk6NO2Q=="], + "is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="], "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], @@ -500,13 +547,15 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "magic-regexp": ["magic-regexp@0.10.0", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.12", "mlly": "^1.7.2", "regexp-tree": "^0.1.27", "type-level-regexp": "~0.1.17", "ufo": "^1.5.4", "unplugin": "^2.0.0" } }, "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magic-string-ast": ["magic-string-ast@1.0.3", "", { "dependencies": { "magic-string": "^0.30.19" } }, "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA=="], "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], - "miniflare": ["miniflare@4.20260302.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260302.0", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-joGFywlo7HdfHXXGOkc6tDCVkwjEncM0mwEsMOLWcl+vDVJPj9HRV7JtEa0+lCpNOLdYw7mZNHYe12xz9KtJOw=="], + "miniflare": ["miniflare@4.20260301.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260301.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-fqkHx0QMKswRH9uqQQQOU/RoaS3Wjckxy3CUX3YGJr0ZIMu7ObvI+NovdYi6RIsSPthNtq+3TPmRNxjeRiasog=="], "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], @@ -520,6 +569,8 @@ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "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-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -528,6 +579,10 @@ "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + "oxc-parser": ["oxc-parser@0.115.0", "", { "dependencies": { "@oxc-project/types": "^0.115.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.115.0", "@oxc-parser/binding-android-arm64": "0.115.0", "@oxc-parser/binding-darwin-arm64": "0.115.0", "@oxc-parser/binding-darwin-x64": "0.115.0", "@oxc-parser/binding-freebsd-x64": "0.115.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.115.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.115.0", "@oxc-parser/binding-linux-arm64-gnu": "0.115.0", "@oxc-parser/binding-linux-arm64-musl": "0.115.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.115.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.115.0", "@oxc-parser/binding-linux-riscv64-musl": "0.115.0", "@oxc-parser/binding-linux-s390x-gnu": "0.115.0", "@oxc-parser/binding-linux-x64-gnu": "0.115.0", "@oxc-parser/binding-linux-x64-musl": "0.115.0", "@oxc-parser/binding-openharmony-arm64": "0.115.0", "@oxc-parser/binding-wasm32-wasi": "0.115.0", "@oxc-parser/binding-win32-arm64-msvc": "0.115.0", "@oxc-parser/binding-win32-ia32-msvc": "0.115.0", "@oxc-parser/binding-win32-x64-msvc": "0.115.0" } }, "sha512-2w7Xn3CbS/zwzSY82S5WLemrRu3CT57uF7Lx8llrE/2bul6iMTcJE4Rbls7GDNbLn3ttATI68PfOz2Pt3KZ2cQ=="], + + "oxc-walker": ["oxc-walker@0.7.0", "", { "dependencies": { "magic-regexp": "^0.10.0" }, "peerDependencies": { "oxc-parser": ">=0.98.0" } }, "sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], @@ -550,6 +605,8 @@ "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], "rolldown": ["rolldown@1.0.0-rc.6", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.6" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.6", "@rolldown/binding-darwin-arm64": "1.0.0-rc.6", "@rolldown/binding-darwin-x64": "1.0.0-rc.6", "@rolldown/binding-freebsd-x64": "1.0.0-rc.6", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.6", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.6", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.6", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.6", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.6", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.6", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.6", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.6", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.6" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-B8vFPV1ADyegoYfhg+E7RAucYKv0xdVlwYYsIJgfPNeiSxZGWNxts9RqhyGzC11ULK/VaeXyKezGCwpMiH8Ktw=="], @@ -580,8 +637,12 @@ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-level-regexp": ["type-level-regexp@0.1.17", "", {}, "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg=="], + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], "unconfig": ["unconfig@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "defu": "^6.1.4", "jiti": "^2.6.1", "quansync": "^1.0.0", "unconfig-core": "7.5.0" } }, "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA=="], @@ -594,11 +655,11 @@ "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], - "unhead": ["unhead@2.1.9", "", { "dependencies": { "hookable": "^6.0.1" } }, "sha512-4GvP6YeJQzo9J3g9fFZUJOH6jacUp5JgJ0/zC8eZrt8Dwompg9SuOSfrYbZaEzsfMPgQc4fsEjMoY9WzGPOChg=="], + "unhead": ["unhead@2.1.10", "", { "dependencies": { "hookable": "^6.0.1" } }, "sha512-We8l9uNF8zz6U8lfQaVG70+R/QBfQx1oPIgXin4BtZnK2IQpz6yazQ0qjMNVBDw2ADgF2ea58BtvSK+XX5AS7g=="], "unimport": ["unimport@5.6.0", "", { "dependencies": { "acorn": "^8.15.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" } }, "sha512-8rqAmtJV8o60x46kBAJKtHpJDJWkA2xcBqWKPI14MgUb05o1pnpnCnXSxedUXyeq7p8fR5g3pTo2BaswZ9lD9A=="], - "unocss": ["unocss@66.6.2", "", { "dependencies": { "@unocss/cli": "66.6.2", "@unocss/core": "66.6.2", "@unocss/preset-attributify": "66.6.2", "@unocss/preset-icons": "66.6.2", "@unocss/preset-mini": "66.6.2", "@unocss/preset-tagify": "66.6.2", "@unocss/preset-typography": "66.6.2", "@unocss/preset-uno": "66.6.2", "@unocss/preset-web-fonts": "66.6.2", "@unocss/preset-wind": "66.6.2", "@unocss/preset-wind3": "66.6.2", "@unocss/preset-wind4": "66.6.2", "@unocss/transformer-attributify-jsx": "66.6.2", "@unocss/transformer-compile-class": "66.6.2", "@unocss/transformer-directives": "66.6.2", "@unocss/transformer-variant-group": "66.6.2", "@unocss/vite": "66.6.2" }, "peerDependencies": { "@unocss/astro": "66.6.2", "@unocss/postcss": "66.6.2", "@unocss/webpack": "66.6.2" }, "optionalPeers": ["@unocss/astro", "@unocss/postcss", "@unocss/webpack"] }, "sha512-ulkfFBFm++/yTdgDn/clpxtm3GxynZi57F4KETQkMQWRXUI7FwqPKGn0xooscvbtldlX67pkovwj/mzkwExitQ=="], + "unocss": ["unocss@66.6.5", "", { "dependencies": { "@unocss/cli": "66.6.5", "@unocss/core": "66.6.5", "@unocss/preset-attributify": "66.6.5", "@unocss/preset-icons": "66.6.5", "@unocss/preset-mini": "66.6.5", "@unocss/preset-tagify": "66.6.5", "@unocss/preset-typography": "66.6.5", "@unocss/preset-uno": "66.6.5", "@unocss/preset-web-fonts": "66.6.5", "@unocss/preset-wind": "66.6.5", "@unocss/preset-wind3": "66.6.5", "@unocss/preset-wind4": "66.6.5", "@unocss/transformer-attributify-jsx": "66.6.5", "@unocss/transformer-compile-class": "66.6.5", "@unocss/transformer-directives": "66.6.5", "@unocss/transformer-variant-group": "66.6.5", "@unocss/vite": "66.6.5" }, "peerDependencies": { "@unocss/astro": "66.6.5", "@unocss/postcss": "66.6.5", "@unocss/webpack": "66.6.5" }, "optionalPeers": ["@unocss/astro", "@unocss/postcss", "@unocss/webpack"] }, "sha512-WlpPlV7yAzEPREcwaKeacP+1jOm6ImhyKJRkK18tIW2b2BRZZDKln7X8P+NzJtAr0kziNY/ttUKZNZRnSmzP1A=="], "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], @@ -616,15 +677,17 @@ "vue": ["vue@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/compiler-sfc": "3.5.29", "@vue/runtime-dom": "3.5.29", "@vue/server-renderer": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA=="], - "vue-i18n": ["vue-i18n@11.2.8", "", { "dependencies": { "@intlify/core-base": "11.2.8", "@intlify/shared": "11.2.8", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg=="], - "vue-router": ["vue-router@5.0.3", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], - "workerd": ["workerd@1.20260302.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260302.0", "@cloudflare/workerd-darwin-arm64": "1.20260302.0", "@cloudflare/workerd-linux-64": "1.20260302.0", "@cloudflare/workerd-linux-arm64": "1.20260302.0", "@cloudflare/workerd-windows-64": "1.20260302.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-FhNdC8cenMDllI6bTktFgxP5Bn5ZEnGtofgKipY6pW9jtq708D1DeGI6vGad78KQLBGaDwFy1eThjCoLYgFfog=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "wrangler": ["wrangler@4.68.1", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.14.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260302.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260302.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260302.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-G+TI3k/olEGBAVkPtUlhAX/DIbL/190fv3aK+r+45/wPclNEymjxCc35T8QGTDhc2fEMXiw51L5bH9aNsBg+yQ=="], + "workerd": ["workerd@1.20260301.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260301.1", "@cloudflare/workerd-darwin-arm64": "1.20260301.1", "@cloudflare/workerd-linux-64": "1.20260301.1", "@cloudflare/workerd-linux-arm64": "1.20260301.1", "@cloudflare/workerd-windows-64": "1.20260301.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oterQ1IFd3h7PjCfT4znSFOkJCvNQ6YMOyZ40YsnO3nrSpgB4TbJVYWFOnyJAw71/RQuupfVqZZWKvsy8GO3fw=="], + + "wrangler": ["wrangler@4.70.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.14.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260301.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260226.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-PNDZ9o4e+B5x+1bUbz62Hmwz6G9lw+I9pnYe/AguLddJFjfIyt2cmFOUOb3eOZSoXsrhcEPUg2YidYIbVwUkfw=="], "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], @@ -642,9 +705,7 @@ "@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], - "@unocss/transformer-attributify-jsx/@babel/parser": ["@babel/parser@7.27.7", "", { "dependencies": { "@babel/types": "^7.27.7" }, "bin": "./bin/babel-parser.js" }, "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q=="], - - "@unocss/transformer-attributify-jsx/@babel/traverse": ["@babel/traverse@7.27.7", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/template": "^7.27.2", "@babel/types": "^7.27.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw=="], + "@unocss/config/@unocss/core": ["@unocss/core@66.6.4", "", {}, "sha512-Fii3lhVJVFrKUz6hMGAkq3sXBfNnXB2G8bldNHuBHJpDAoP1F0oO/SU/oSqSjCYvtcD5RtOn8qwzcHuuN3B/mg=="], "@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="], @@ -668,14 +729,10 @@ "unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], - "vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], - "vue-router/@vue/devtools-api": ["@vue/devtools-api@8.0.6", "", { "dependencies": { "@vue/devtools-kit": "^8.0.6" } }, "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA=="], "vue-router/unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="], - "@unocss/transformer-attributify-jsx/@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "vue-router/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@8.0.6", "", { "dependencies": { "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^2.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw=="], diff --git a/package.json b/package.json index 110dee5..dd2c089 100644 --- a/package.json +++ b/package.json @@ -10,30 +10,33 @@ "tail": "wrangler tail" }, "dependencies": { - "@pinia/colada": "^0.21.2", - "@unhead/vue": "^2.1.2", - "@vueuse/core": "^14.2.0", + "@pinia/colada": "^0.21.7", + "@unhead/vue": "^2.1.10", + "@vueuse/core": "^14.2.1", "aws4fetch": "^1.0.20", "clsx": "^2.1.1", - "hono": "^4.11.7", + "hono": "^4.12.5", + "i18next": "^25.8.14", + "i18next-browser-languagedetector": "^8.2.1", + "i18next-http-backend": "^3.0.2", + "i18next-vue": "^5.4.0", "is-mobile": "^5.0.0", "pinia": "^3.0.4", - "tailwind-merge": "^3.4.0", - "vue": "^3.5.27", - "vue-i18n": "^11.2.8", - "vue-router": "^5.0.2", + "tailwind-merge": "^3.5.0", + "vue": "^3.5.29", + "vue-router": "^5.0.3", "zod": "^4.3.6" }, "devDependencies": { - "@cloudflare/vite-plugin": "^1.23.0", - "@types/node": "^25.2.0", + "@cloudflare/vite-plugin": "^1.26.0", + "@types/node": "^25.3.3", "@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue-jsx": "^5.1.4", - "unocss": "^66.6.0", + "unocss": "^66.6.5", "unplugin-auto-import": "^21.0.0", "unplugin-vue-components": "^31.0.0", "vite": "^8.0.0-beta.16", "vite-ssr-components": "^0.5.2", - "wrangler": "^4.62.0" + "wrangler": "^4.70.0" } } diff --git a/public/locales/en/en.json b/public/locales/en/en.json new file mode 100644 index 0000000..2743f1d --- /dev/null +++ b/public/locales/en/en.json @@ -0,0 +1,1052 @@ +{ + "common": { + "save": "Save", + "close": "Close", + "cancel": "Cancel", + "connect": "Connect", + "disconnect": "Disconnect", + "upload": "Upload", + "loading": "Loading", + "actions": "Actions", + "status": "Status", + "videos": "Videos", + "selected": "{count} selected", + "copy": "Copy" + }, + "app": { + "name": "EcoStream" + }, + "auth": { + "layout": { + "login": { + "headTitle": "Login to your account", + "title": "Sign in to your dashboard", + "subtitle": "Please enter your details to sign in." + }, + "signup": { + "headTitle": "Create your account", + "title": "Create your account", + "subtitle": "Please fill in the information to create your account." + }, + "forgot": { + "headTitle": "Reset your password", + "title": "Forgot your password?", + "subtitle": "Enter your email address and we'll send you a link to reset your password." + } + }, + "login": { + "email": "Email", + "password": "Password", + "forgotPassword": "Forgot password?", + "signIn": "Sign in", + "google": "Google", + "noAccount": "Don't have an account?", + "signUp": "Sign up", + "errors": { + "emailRequired": "Email is required.", + "emailInvalid": "Invalid email address.", + "passwordRequired": "Password is required." + } + }, + "signup": { + "fullName": "Full Name", + "email": "Email address", + "password": "Password", + "passwordHint": "Must be at least 8 characters.", + "createAccount": "Create Account", + "alreadyHave": "Already have an account?", + "signIn": "Sign in", + "placeholders": { + "name": "John Doe", + "email": "you@example.com", + "password": "Create a password" + }, + "errors": { + "nameRequired": "Name is required.", + "emailRequired": "Email is required.", + "emailInvalid": "Invalid email address.", + "passwordMin": "Password must be at least 8 characters." + } + }, + "forgot": { + "description": "Enter your email address and we'll send you a link to reset your password.", + "email": "Email address", + "sendResetLink": "Send Reset Link", + "backToSignIn": "Back to Sign in", + "placeholders": { + "email": "you@example.com" + }, + "errors": { + "emailRequired": "Email is required.", + "emailInvalid": "Invalid email address." + }, + "toast": { + "successSummary": "Success", + "successDetail": "Reset link sent", + "errorSummary": "Error", + "errorDetail": "An error occurred" + } + }, + "errors": { + "loginNoUserData": "Login failed: No user data received", + "loginFailed": "Login failed: {error}", + "registrationFailedFallback": "Registration failed", + "registrationFailed": "Registration failed: {error}", + "updateProfileFailed": "Failed to update profile: {error}", + "changePasswordFailed": "Failed to change password: {error}", + "unknown": "Unknown error" + } + }, + "nav": { + "overview": "Overview", + "videos": "Videos", + "notification": "Notification", + "settings": "Settings" + }, + "settings": { + "menu": { + "securityGroup": "Security", + "preferencesGroup": "Preferences", + "integrationsGroup": "Integrations", + "dangerGroup": "Danger Zone", + "security": "Security", + "billing": "Billing & Plans", + "notifications": "Notifications", + "player": "Player", + "domains": "Allowed Domains", + "ads": "Ads & VAST", + "danger": "Danger Zone" + }, + "content": { + "fallbackTitle": "Settings", + "fallbackSubtitle": "Manage your account settings and preferences.", + "security": { + "title": "Security & Connected Apps", + "subtitle": "Manage your security settings and connected applications." + }, + "notifications": { + "title": "Notifications", + "subtitle": "Choose how you want to receive notifications and updates." + }, + "player": { + "title": "Player Settings", + "subtitle": "Configure default video player behavior and features." + }, + "billing": { + "title": "Billing & Plans", + "subtitle": "Your current subscription and billing information." + }, + "domains": { + "title": "Allowed Domains", + "subtitle": "Add domains to your whitelist to allow embedding content via iframe." + }, + "ads": { + "title": "Ads & VAST", + "subtitle": "Create and manage VAST ad templates for your videos." + }, + "danger": { + "title": "Danger Zone", + "subtitle": "Irreversible and destructive actions. Be careful!" + } + }, + "notificationSettings": { + "saveChanges": "Save Changes", + "types": { + "email": { + "title": "Email Notifications", + "description": "Receive updates and alerts via email" + }, + "push": { + "title": "Push Notifications", + "description": "Get instant alerts in your browser" + }, + "marketing": { + "title": "Marketing Emails", + "description": "Receive promotions and product updates" + }, + "telegram": { + "title": "Telegram Notifications", + "description": "Receive updates via Telegram" + } + }, + "toast": { + "savedSummary": "Settings Saved", + "savedDetail": "Your notification settings have been saved.", + "failedSummary": "Save Failed", + "failedDetail": "Failed to save settings." + } + }, + "playerSettings": { + "toast": { + "savedSummary": "Settings Saved", + "savedDetail": "Your player settings have been saved.", + "failedSummary": "Save Failed", + "failedDetail": "Failed to save settings." + }, + "items": { + "autoplay": { + "title": "Autoplay", + "description": "Automatically start videos when loaded" + }, + "loop": { + "title": "Loop", + "description": "Repeat video when it ends" + }, + "muted": { + "title": "Muted", + "description": "Start videos with sound muted" + }, + "showControls": { + "title": "Show Controls", + "description": "Display player controls (play, pause, volume)" + }, + "pip": { + "title": "Picture in Picture", + "description": "Enable Picture-in-Picture mode" + }, + "airplay": { + "title": "AirPlay", + "description": "Allow streaming to Apple devices via AirPlay" + }, + "chromecast": { + "title": "Chromecast", + "description": "Allow casting to Chromecast devices" + } + } + }, + "dangerZone": { + "deleteAccount": { + "title": "Delete Account", + "description": "Permanently delete your account and all associated data.", + "button": "Delete Account" + }, + "clearData": { + "title": "Clear All Data", + "description": "Remove all your videos, playlists, and activity history.", + "button": "Clear Data" + }, + "warning": { + "title": "Warning", + "description": "These actions are permanent and cannot be undone. Make sure you have backed up any important data before proceeding." + }, + "confirm": { + "deleteAccountMessage": "Are you sure you want to delete your account? This action cannot be undone.", + "deleteAccountHeader": "Delete Account", + "deleteAccountAccept": "Delete", + "deleteAccountReject": "Cancel", + "clearDataMessage": "Are you sure you want to clear all your data? This action cannot be undone.", + "clearDataHeader": "Clear All Data", + "clearDataAccept": "Clear", + "clearDataReject": "Cancel" + }, + "toast": { + "deleteAccountSummary": "Account deletion requested", + "deleteAccountDetail": "Your account deletion request has been submitted.", + "clearDataSummary": "Data cleared", + "clearDataDetail": "All your data has been permanently deleted." + } + }, + "domainsDns": { + "addDomain": "Add Domain", + "infoBanner": "Only domains in your whitelist can embed your content using iframe.", + "table": { + "domain": "Domain", + "addedDate": "Added Date" + }, + "emptyTitle": "No domains in whitelist", + "emptySubtitle": "Add a domain to allow iframe embedding", + "embedCodeTitle": "Embed Code", + "copyCode": "Copy Code", + "embedCodeHint": "Use this iframe code to embed content on your whitelisted domains.", + "dialog": { + "title": "Add Domain to Whitelist", + "domainLabel": "Domain Name", + "domainPlaceholder": "example.com", + "domainHint": "Enter domain without www or https:// (e.g., example.com)", + "importantTitle": "Important", + "importantDetail": "Only add domains that you own and control." + }, + "confirm": { + "removeMessage": "Are you sure you want to remove {domain} from your whitelist? Embedded iframes from this domain will no longer work.", + "removeHeader": "Remove Domain", + "removeAccept": "Remove", + "removeReject": "Cancel" + }, + "toast": { + "invalidSummary": "Invalid Domain", + "invalidDetail": "Please enter a valid domain name.", + "duplicateSummary": "Domain Already Added", + "duplicateDetail": "This domain is already in your whitelist.", + "addedSummary": "Domain Added", + "addedDetail": "{domain} has been added to your whitelist.", + "removedSummary": "Domain Removed", + "removedDetail": "{domain} has been removed from your whitelist.", + "copiedSummary": "Copied", + "copiedDetail": "Embed code copied to clipboard." + } + }, + "adsVast": { + "createTemplate": "Create Template", + "infoBanner": "VAST (Video Ad Serving Template) is an XML schema for serving ad tags to video players.", + "createdOn": "Created {date}", + "emptyTitle": "No VAST templates yet", + "emptySubtitle": "Create a template to start monetizing your videos", + "formats": { + "preRoll": "Pre-roll", + "midRoll": "Mid-roll", + "postRoll": "Post-roll" + }, + "state": { + "enabled": "enabled", + "disabled": "disabled" + }, + "table": { + "template": "Template", + "format": "Format", + "vastUrl": "VAST URL" + }, + "dialog": { + "editTitle": "Edit Template", + "createTitle": "Create VAST Template", + "templateName": "Template Name", + "templateNamePlaceholder": "e.g., Main Pre-roll Ad", + "vastUrlLabel": "VAST Tag URL", + "vastUrlPlaceholder": "https://ads.example.com/vast/tag.xml", + "adFormat": "Ad Format", + "adInterval": "Ad Interval (seconds)", + "adIntervalPlaceholder": "30", + "update": "Update", + "create": "Create" + }, + "confirm": { + "deleteMessage": "Are you sure you want to delete \"{name}\"?", + "deleteHeader": "Delete Template", + "deleteAccept": "Delete", + "deleteReject": "Cancel" + }, + "toast": { + "nameRequiredSummary": "Name Required", + "nameRequiredDetail": "Please enter a template name.", + "urlRequiredSummary": "VAST URL Required", + "urlRequiredDetail": "Please enter the VAST tag URL.", + "invalidUrlSummary": "Invalid URL", + "invalidUrlDetail": "Please enter a valid URL.", + "durationRequiredSummary": "Duration Required", + "durationRequiredDetail": "Mid-roll ads require a duration/interval.", + "updatedSummary": "Template Updated", + "updatedDetail": "VAST template has been updated.", + "createdSummary": "Template Created", + "createdDetail": "VAST template has been created.", + "enabledSummary": "Template Enabled", + "disabledSummary": "Template Disabled", + "toggleDetail": "{name} has been {state}.", + "deletedSummary": "Template Deleted", + "deletedDetail": "VAST template has been removed.", + "copiedSummary": "Copied", + "copiedDetail": "URL copied to clipboard." + } + }, + "profile": { + "title": "Profile Information", + "subtitle": "Manage your personal information and account details.", + "userFallback": "User", + "username": "Username", + "email": "Email Address", + "storageUsage": "Storage Usage", + "storageUsedOfLimit": "{used} of {limit} used", + "editProfile": "Edit Profile", + "changePassword": "Change Password" + }, + "connectedAccounts": { + "title": "Connected Accounts", + "email": { + "label": "Email", + "connected": "Connected", + "notConnected": "Not connected", + "disconnected": "Disconnected" + }, + "telegram": { + "label": "Telegram", + "hint": "Get notified via Telegram", + "connectedFallback": "Connected" + } + }, + "billing": { + "walletBalance": "Wallet Balance", + "currentBalance": "Current balance: {balance}", + "topUp": "Top Up", + "availablePlans": "Available Plans", + "availablePlansHint": "Choose the plan that best fits your needs", + "planStorage": "{storage} Storage", + "planDuration": "{duration} Max Duration", + "planUploads": "{count} Uploads / day", + "currentPlan": "Current Plan", + "processing": "Processing...", + "upgrade": "Upgrade", + "storage": "Storage", + "storageUsedOfLimit": "{used} of {limit} used", + "monthlyUploads": "Monthly Uploads", + "uploadsUsedOfLimit": "{used} of {limit} uploads", + "paymentHistory": "Payment History", + "paymentHistorySubtitle": "Your past payments and invoices", + "noPaymentHistory": "No payment history found.", + "download": "Download", + "durationMinutes": "{minutes} mins", + "unknownPlan": "Unknown", + "table": { + "date": "Date", + "amount": "Amount", + "plan": "Plan", + "status": "Status", + "invoice": "Invoice" + }, + "status": { + "success": "Success", + "failed": "Failed", + "pending": "Pending" + }, + "topupDialog": { + "title": "Top Up Wallet", + "subtitle": "Select an amount or enter a custom amount to add to your wallet.", + "customAmount": "Custom Amount", + "enterAmount": "Enter amount", + "hint": "Minimum top-up amount is $1. Funds will be added to your wallet immediately after payment.", + "proceed": "Proceed to Payment" + }, + "toast": { + "subscriptionSuccessSummary": "Subscription Successful", + "subscriptionSuccessDetail": "Successfully subscribed to {plan}", + "subscriptionFailedSummary": "Subscription Failed", + "subscriptionFailedDetail": "Failed to subscribe", + "topupSuccessSummary": "Top-up Successful", + "topupSuccessDetail": "{amount} has been added to your wallet.", + "topupFailedSummary": "Top-up Failed", + "topupFailedDetail": "Failed to process top-up.", + "downloadingSummary": "Downloading", + "downloadingDetail": "Downloading invoice #{invoiceId}...", + "downloadedSummary": "Downloaded", + "downloadedDetail": "Invoice #{invoiceId} downloaded successfully" + } + }, + "securityConnected": { + "header": { + "title": "Security & Connected Accounts", + "subtitle": "Manage your security settings and connected services." + }, + "accountStatus": { + "label": "Account Status", + "detail": "Your account is in good standing", + "badge": "Active" + }, + "language": { + "label": "Language", + "detail": "Choose your preferred display language", + "save": "Save Language", + "options": { + "en": "English", + "vi": "Tiếng Việt" + }, + "toast": { + "successSummary": "Language Saved", + "successDetail": "Language has been updated.", + "errorSummary": "Save Failed", + "errorDetail": "Failed to save language on server. Applied from cookie fallback." + } + }, + "twoFactor": { + "label": "Two-Factor Authentication", + "enabled": "2FA is enabled", + "disabled": "Add an extra layer of security" + }, + "changePassword": { + "label": "Change Password", + "detail": "Update your account password", + "button": "Change Password", + "dialog": { + "title": "Change Password", + "subtitle": "Enter your current password and choose a new password.", + "current": "Current Password", + "new": "New Password", + "confirm": "Confirm New Password", + "currentPlaceholder": "Enter current password", + "newPlaceholder": "Enter new password", + "confirmPlaceholder": "Confirm new password", + "submit": "Change Password", + "cancel": "Cancel", + "errors": { + "mismatch": "Passwords do not match", + "minLength": "Password must be at least 6 characters", + "default": "Failed to change password" + } + }, + "toast": { + "successSummary": "Password Changed", + "successDetail": "Your password has been changed successfully." + } + }, + "logout": { + "label": "Logout", + "detail": "Sign out of your account on this device.", + "button": "Logout", + "confirm": { + "message": "Are you sure you want to log out of your account?", + "header": "Log Out", + "accept": "Log Out", + "reject": "Cancel" + } + }, + "email": { + "label": "Email", + "connected": "Connected", + "disconnected": "Not connected", + "badgeConnected": "Connected", + "badgeDisconnected": "Disconnected" + }, + "telegram": { + "label": "Telegram", + "detailDisconnected": "Get notified via Telegram", + "connectedFallback": "Connected", + "connect": "Connect", + "disconnect": "Disconnect" + }, + "twoFactorDialog": { + "title": "Enable Two-Factor Authentication", + "subtitle": "Scan the QR code below with your authenticator app (Google Authenticator, Authy, etc.)", + "secret": "Secret Key:", + "codeLabel": "Verification Code", + "codePlaceholder": "Enter 6-digit code", + "cancel": "Cancel", + "verify": "Verify & Enable" + }, + "toast": { + "twoFactorEnabledSummary": "2FA Enabled", + "twoFactorEnabledDetail": "Two-factor authentication has been enabled successfully.", + "twoFactorEnableFailedSummary": "Enable 2FA Failed", + "twoFactorEnableFailedDetail": "Failed to enable two-factor authentication.", + "twoFactorDisableFailedSummary": "Disable 2FA Failed", + "twoFactorDisableFailedDetail": "Failed to disable two-factor authentication.", + "twoFactorDisabledSummary": "2FA Disabled", + "twoFactorDisabledDetail": "Two-factor authentication has been disabled.", + "twoFactorInvalidCodeSummary": "Enable 2FA Failed", + "twoFactorInvalidCodeDetail": "Invalid verification code. Please try again.", + "telegramConnectedSummary": "Telegram Connected", + "telegramConnectedDetail": "Connected to {username}", + "telegramConnectFailedSummary": "Connection Failed", + "telegramConnectFailedDetail": "Failed to connect Telegram account.", + "telegramDisconnectedSummary": "Telegram Disconnected", + "telegramDisconnectedDetail": "Your Telegram account has been disconnected.", + "telegramDisconnectFailedSummary": "Disconnect Failed", + "telegramDisconnectFailedDetail": "Failed to disconnect Telegram account." + } + } + }, + "pageHeader": { + "dashboard": "Dashboard", + "settings": "Settings" + }, + "confirm": { + "defaultHeader": "Confirm", + "defaultAccept": "OK", + "defaultReject": "Cancel" + }, + "toast": { + "dismissAria": "Dismiss" + }, + "overview": { + "welcome": { + "title": "Welcome back, {name}! 👋", + "subtitle": "Here's what's happening with your content today." + }, + "stats": { + "totalVideos": "Total Videos", + "totalViews": "Total Views", + "storageUsed": "Storage Used", + "uploadsThisMonth": "Uploads This Month", + "trendVsLastMonth": "vs last month" + }, + "quickActions": { + "title": "Quick Actions", + "uploadVideo": { + "title": "Upload Video", + "description": "Upload a new video to your library" + }, + "videoLibrary": { + "title": "Video Library", + "description": "Browse all your videos" + }, + "analytics": { + "title": "Analytics", + "description": "Track performance & insights" + }, + "managePlan": { + "title": "Manage Plan", + "description": "Upgrade or change your plan" + } + }, + "referral": { + "title": "Referral Link", + "subtitle": "Share your referral link and earn commissions from referred users!" + }, + "recentVideos": { + "title": "Recent Videos", + "viewAll": "View all", + "emptyTitle": "No videos found", + "emptyDescription": "You haven't uploaded any videos yet. Start by uploading your first video!", + "emptyAction": "Upload Video", + "table": { + "video": "Video", + "status": "Status", + "duration": "Duration", + "uploadDate": "Upload Date", + "actions": "Actions" + }, + "noDescription": "No description", + "unknownStatus": "Unknown", + "actionEdit": "Edit", + "actionShare": "Share", + "actionDelete": "Delete" + }, + "storage": { + "title": "Storage Usage", + "usedOfLimit": "{used} of {limit} used", + "breakdown": { + "videos": "Videos", + "thumbnails": "Thumbnails & Assets", + "other": "Other Files" + }, + "lowStorage": { + "title": "Storage running low", + "message": "Consider upgrading your plan to get more storage.", + "viewPlans": "View plans" + } + } + }, + "video": { + "page": { + "title": "My Videos", + "description": "Manage and organize your video library", + "uploadAction": "Upload Video", + "uploadDropTitle": "Drop to upload", + "uploadDropSubtitle": "Files will be added to the upload queue", + "deleteSelectedConfirm": "Delete {count} videos?", + "deleteSingleConfirm": "Are you sure you want to delete this video?", + "retry": "Try Again", + "emptyTitle": "No videos found", + "emptyDescription": "You haven't uploaded any videos yet. Start by uploading your first video!", + "emptyAction": "Upload Video", + "duplicateSummary": "Duplicate files skipped", + "duplicateDetailOne": "{count} file is already in the queue.", + "duplicateDetailOther": "{count} files are already in the queue." + }, + "filters": { + "searchPlaceholder": "Search videos...", + "rangeOfTotal": "{first}–{last} of {total}", + "previousPageAria": "Previous page", + "nextPageAria": "Next page", + "allStatus": "All Status", + "ready": "Ready", + "processing": "Processing", + "failed": "Failed" + }, + "table": { + "video": "Video", + "status": "Status", + "size": "Size", + "created": "Created", + "actions": "Actions", + "noDescription": "No description", + "copyLink": "Copy link", + "edit": "Edit", + "delete": "Delete" + }, + "bulk": { + "selected": "{count} selected", + "delete": "Delete" + }, + "copyModal": { + "title": "Get sharing address", + "playerAddress": "Player address", + "embedPlayer": "Embed player (recommended)", + "thumbnail": "Thumbnail URL", + "hls": "HLS link (VIP only)", + "hlsPlaceholder": "HLS link available for VIP with whitelisted domain", + "hlsHint": "This link redirects to a signed HLS URL and only works on whitelisted domains.", + "warningTitle": "Warning", + "warningDetail": "Make sure shared files comply with local laws and confirm you understand the responsibilities involved when distributing content.", + "reminderTitle": "Reminder", + "reminderDetail": "The embed player can auto switch fallback nodes and works well on mobile. Raw HLS links rely on your own player and must be used only on whitelisted domains.", + "toastCopiedSummary": "Copied", + "toastCopiedDetail": "Copied to clipboard", + "toastErrorSummary": "Error", + "toastErrorDetail": "Failed to load video details" + }, + "detailModal": { + "title": "Edit video", + "titleLabel": "Title", + "titlePlaceholder": "Enter video title", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Enter video description", + "subtitlesTitle": "Subtitles", + "subtitleTracks": "{count} tracks", + "noSubtitles": "No subtitles uploaded yet", + "uploadSubtitle": "Upload Subtitle", + "subtitleFile": "Subtitle File (VTT, SRT, ASS, SSA)", + "languageCode": "Language Code *", + "languagePlaceholder": "en, vi, etc.", + "displayName": "Display Name (Optional)", + "displayNamePlaceholder": "English, Tiếng Việt, etc.", + "uploadSubtitleButton": "Upload Subtitle", + "cancel": "Cancel", + "saveChanges": "Save Changes", + "errors": { + "titleRequired": "Title is required." + }, + "toast": { + "loadErrorSummary": "Error", + "loadErrorDetail": "Failed to load video details", + "saveSuccessSummary": "Success", + "saveSuccessDetail": "Video updated successfully", + "saveErrorSummary": "Error", + "saveErrorDetail": "Failed to save changes", + "subtitleInfoSummary": "Info", + "subtitleInfoDetail": "Subtitle upload not yet implemented" + } + }, + "detailPage": { + "title": "Video Detail", + "description": "View and manage video details", + "loadingBreadcrumb": "Loading...", + "detailsTitle": "Video Details", + "copyValueTitle": "Copy value", + "videoTagFallback": "Your browser does not support the video tag.", + "saving": "Saving...", + "cancelEditTitle": "Cancel editing", + "reloadTitle": "Reload video", + "reloadButton": "Reload", + "confirmDelete": { + "message": "Are you sure you want to delete this video? This action cannot be undone.", + "header": "Confirm Delete", + "accept": "Delete", + "reject": "Cancel" + }, + "toast": { + "reloadSummary": "Info", + "reloadDetail": "Reloading video...", + "deleteSuccessSummary": "Success", + "deleteSuccessDetail": "Video deleted successfully", + "deleteErrorSummary": "Error", + "deleteErrorDetail": "Failed to delete video", + "copySummary": "Copied", + "copyDetail": "{label} copied to clipboard" + }, + "videoInfo": { + "videoId": "Video ID", + "thumbnailUrl": "Thumbnail URL", + "embedUrl": "Embed URL", + "iframeCode": "Iframe Code", + "shareLink": "Share Link" + } + }, + "cardPopover": { + "download": "Download", + "copyLink": "Copy link", + "edit": "Edit", + "delete": "Delete", + "toast": { + "copySuccessSummary": "Success", + "copySuccessDetail": "Video link copied", + "copyErrorSummary": "Error", + "copyErrorDetail": "Failed to copy link", + "downloadSuccessSummary": "Success", + "downloadSuccessDetail": "Downloading video...", + "downloadErrorSummary": "Error", + "downloadErrorDetail": "Video file not found" + } + } + }, + "notification": { + "title": "Notifications", + "subtitle": "Stay updated with your latest activities and alerts.", + "tabs": { + "all": "All", + "unread": "Unread", + "videos": "Videos", + "payments": "Payments" + }, + "stats": { + "total": "{count} notifications", + "unread": "{count} unread" + }, + "actions": { + "markAllRead": "Mark all read", + "clearAll": "Clear all", + "viewAll": "View all notifications", + "viewVideo": "View video", + "viewReceipt": "View receipt", + "upgradePlan": "Upgrade plan", + "tryNow": "Try it now" + }, + "item": { + "viewDetails": "View Details", + "markAsRead": "Mark as read", + "delete": "Delete" + }, + "empty": { + "title": "No notifications", + "subtitle": "You're all caught up! Check back later." + }, + "time": { + "minutesAgo": "{count} minutes ago", + "hoursAgo": "{count} hours ago", + "daysAgo": "{count} days ago" + }, + "mocks": { + "videoProcessed": { + "title": "Video processing complete", + "message": "Your video \"Summer Vacation 2024\" has been successfully processed and is now ready to stream." + }, + "paymentSuccess": { + "title": "Payment successful", + "message": "Your subscription to Pro Plan has been renewed successfully. Next billing date: Feb 25, 2026." + }, + "storageWarning": { + "title": "Storage almost full", + "message": "You have used 85% of your storage quota. Consider upgrading your plan for more space." + }, + "uploadSuccess": { + "title": "Upload successful", + "message": "Your video \"Product Demo v2\" has been uploaded successfully." + }, + "maintenance": { + "title": "Scheduled maintenance", + "message": "We will perform scheduled maintenance on Jan 30, 2026 from 2:00 AM to 4:00 AM UTC." + }, + "newFeature": { + "title": "New feature available", + "message": "We just launched video analytics! Track your video performance with detailed insights." + } + } + }, + "upload": { + "dialog": { + "title": "Upload Videos", + "subtitle": "Add up to {maxItems} videos per batch", + "mode": { + "local": "Local", + "remote": "Remote URL" + }, + "queueFullTitle": "Queue is full", + "queueFullDescription": "Maximum {maxItems} videos per batch. Start or clear the current queue first.", + "slotsRemaining": "{remaining} / {maxItems} slots remaining", + "formatsHint": "MP4, MOV, MKV · max 10 GB per file", + "close": "Close", + "startUpload": "Start Upload ({count})", + "duplicateFilesSummary": "Duplicate files skipped", + "duplicateFilesDetailOne": "{count} file is already in the queue.", + "duplicateFilesDetailOther": "{count} files are already in the queue.", + "duplicateUrlsSummary": "Duplicate URLs skipped", + "duplicateUrlsDetailOne": "{count} URL is already in the queue.", + "duplicateUrlsDetailOther": "{count} URLs are already in the queue." + }, + "dropzone": { + "releaseToAdd": "Release to add", + "dropHere": "Drop videos here", + "browse": "or click anywhere to browse" + }, + "remote": { + "placeholder": "Paste video URLs here, one per line\n\nhttps://example.com/video.mp4\nhttps://drive.google.com/...", + "providersHint": "Google Drive, Dropbox supported", + "addUrls": "Add URLs" + }, + "queue": { + "empty": "Empty queue!", + "totalSize": "Total size:", + "zeroSize": "0 MB" + }, + "queueItem": { + "remoteFileName": "Remote File", + "unknownSize": "Unknown", + "status": { + "pending": "Pending", + "uploading": "Uploading...", + "uploadingThreads": "Uploading · {threads} threads", + "processing": "Processing...", + "complete": "Done", + "error": "Failed", + "fetching": "Fetching..." + } + }, + "infoTip": { + "title": "Tip: For fastest processing", + "description": "Upload videos in H.264 video codec + AAC audio codec format (e.g., MP4 with H.264/AAC). Videos in this format will be processed much faster (seconds instead of minutes) because they do not need re-encoding." + }, + "bulkActions": { + "title": "Quick Settings", + "applyToPending": "Apply to {count} pending files", + "selectCategory": "Select category...", + "category": { + "learning": "Learning", + "entertainment": "Entertainment" + }, + "visibility": { + "public": "Public", + "private": "Private" + } + }, + "indicator": { + "allDone": "All done", + "uploading": "Uploading {count} files...", + "waiting": "{count} files waiting", + "completeProgress": "{complete} of {total} complete", + "start": "Start", + "viewVideos": "View Videos", + "addMoreFiles": "Add more files" + }, + "errors": { + "chunkUploadFailed": "Failed to upload chunk {index}", + "mergeFailed": "Merge failed" + } + }, + "home": { + "nav": { + "features": "Features", + "pricing": "Pricing", + "login": "Log in", + "startFree": "Start for free" + }, + "hero": { + "titleLine1": "Video infrastructure for", + "titleLine2": "modern internet.", + "subtitle": "Seamlessly host, encode, and stream video with our developer-first API. Optimized for speed, built for scale.", + "getStarted": "Get Started", + "uploadVideo": "Upload video" + }, + "features": { + "heading": "Everything you need to ship video", + "subtitle": "Focus on building your product. We'll handle the complex video infrastructure.", + "global": { + "title": "Global Edge Network", + "description": "Content delivered from 200+ PoPs worldwide. Automatic region selection ensures the lowest latency for every viewer." + }, + "live": { + "title": "Live Streaming API", + "description": "Scale to millions of concurrent viewers with ultra-low latency. RTMP ingest and HLS playback supported natively.", + "status": "Live Status", + "onAir": "On Air", + "bitrate": "Bitrate:", + "fps": "FPS:", + "latency": "Latency:", + "bitrateValue": "6000 kbps", + "fpsValue": "60", + "latencyValue": "~2s" + }, + "encoding": { + "title": "Instant Encoding", + "description": "Upload raw files and get optimized HLS/DASH streams in seconds." + }, + "analytics": { + "title": "Deep Analytics", + "description": "Session-level insights, quality of experience (QoE) metrics, and more." + } + }, + "pricing": { + "title": "Simple, transparent pricing", + "subtitle": "Choose the plan that fits your needs. No hidden fees.", + "perMonth": "/mo", + "hobby": { + "name": "Hobby", + "features": [ + "Unlimited upload", + "1 Hour of Storage", + "Standard Support" + ], + "button": "Start Free" + }, + "pro": { + "name": "Pro", + "features": [ + "Ads free player", + "Support M3U8", + "Unlimited upload", + "Custom ads" + ], + "button": "Get Started", + "tag": "POPULAR" + }, + "scale": { + "name": "Scale", + "features": ["5 TB Bandwidth", "500 Hours Storage", "Priority Support"], + "button": "Contact Sales", + "tag": "Best Value" + } + }, + "footer": { + "description": "Building the video layer of the internet. Designed for developers.", + "product": "Product", + "productFeatures": "Features", + "productPricing": "Pricing", + "productShowcase": "Showcase", + "company": "Company", + "companyAbout": "About", + "companyBlog": "Blog", + "companyCareers": "Careers", + "legal": "Legal", + "privacy": "Privacy", + "terms": "Terms", + "copyright": "© {year} EcoStream Inc. All rights reserved." + }, + "head": { + "title": "EcoStream - Video infrastructure for modern internet", + "description": "Seamlessly host, encode, and stream video with our developer-first API. Optimized for speed, built for scale." + } + }, + "legal": { + "common": { + "heading": "Legal & Privacy Policy", + "subheading": "Legal & Privacy Policy", + "description": "Our legal and privacy policy." + }, + "terms": { + "title": "Terms and Conditions - Ecostream", + "description": "Read Ecostream's terms and conditions for using our video hosting and streaming services.", + "pageHeading": "Terms and Conditions Details", + "pageSubheading": "Terms and Conditions", + "pageDescription": "Our terms and conditions set forth important guidelines and rules for using Ecostream's services.", + "sections": { + "acceptanceTitle": "1. Acceptance of Terms", + "acceptanceText": "By accessing and using Ecostream, you accept and agree to be bound by the terms and provision of this agreement.", + "usageTitle": "2. Service Usage", + "usageText": "You agree to use our service only for lawful purposes. You are prohibited from posting or transmitting any unlawful, threatening, libelous, defamatory, obscene, or profane material. We reserve the right to terminate accounts that violate these terms.", + "ownershipTitle": "3. Content Ownership", + "ownershipText": "You retain all rights and ownership of the content you upload to Ecostream. However, by uploading content, you grant us a license to host, store, and display the content as necessary to provide our services.", + "liabilityTitle": "4. Limitation of Liability", + "liabilityText": "Ecostream shall not be liable for any direct, indirect, incidental, special, or consequential damages resulting from the use or inability to use our service.", + "changesTitle": "5. Changes to Terms", + "changesText": "We reserve the right to modify these terms at any time. Your continued use of the service after any such changes constitutes your acceptance of the new terms." + } + }, + "privacy": { + "title": "Privacy Policy - Ecostream", + "description": "Read about Ecostream's commitment to protecting your privacy and data security.", + "pageHeading": "Legal & Privacy Policy", + "pageSubheading": "Legal & Privacy Policy", + "pageDescription": "Our legal and privacy policy.", + "sections": { + "policyTitle": "1. Privacy Policy", + "policyText": "At Ecostream, we take your privacy seriously. This policy describes how we collect, use, and protect your personal information. We only collect information that is necessary for the operation of our service, including email addresses for account creation and payment information for subscription processing.", + "dataCollectionTitle": "2. Data Collection", + "dataCollectionText": "We collect data such as IP addresses, browser types, and access times to analyze trends and improve our service. Uploaded content is stored securely and is only accessed as required for the delivery of our hosting services.", + "cookieTitle": "3. Cookie Policy", + "cookieText": "We use cookies to maintain user sessions and preferences. By using our website, you consent to the use of cookies in accordance with this policy.", + "dmcaTitle": "4. DMCA & Copyright", + "dmcaText": "Ecostream respects the intellectual property rights of others. We respond to notices of alleged copyright infringement in accordance with the Digital Millennium Copyright Act (DMCA). Please report any copyright violations to our support team." + } + } + }, + "notFound": { + "headTitle": "404 - Page Not Found", + "title": "404 - Page Not Found", + "description": "The page you are looking for does not exist.", + "backHome": "Go back to Home" + } +} diff --git a/public/locales/vi/vi.json b/public/locales/vi/vi.json new file mode 100644 index 0000000..e89386c --- /dev/null +++ b/public/locales/vi/vi.json @@ -0,0 +1,1052 @@ +{ + "common": { + "save": "Lưu", + "close": "Đóng", + "cancel": "Hủy", + "connect": "Kết nối", + "disconnect": "Ngắt kết nối", + "upload": "Tải lên", + "loading": "Đang tải", + "actions": "Hành động", + "status": "Trạng thái", + "videos": "Video", + "selected": "{count} mục đã chọn", + "copy": "Sao chép" + }, + "app": { + "name": "EcoStream" + }, + "auth": { + "layout": { + "login": { + "headTitle": "Đăng nhập vào tài khoản", + "title": "Đăng nhập vào bảng điều khiển", + "subtitle": "Vui lòng nhập thông tin để đăng nhập." + }, + "signup": { + "headTitle": "Tạo tài khoản", + "title": "Tạo tài khoản", + "subtitle": "Vui lòng điền thông tin để tạo tài khoản." + }, + "forgot": { + "headTitle": "Đặt lại mật khẩu", + "title": "Quên mật khẩu?", + "subtitle": "Nhập email và chúng tôi sẽ gửi liên kết đặt lại mật khẩu." + } + }, + "login": { + "email": "Email", + "password": "Mật khẩu", + "forgotPassword": "Quên mật khẩu?", + "signIn": "Đăng nhập", + "google": "Google", + "noAccount": "Chưa có tài khoản?", + "signUp": "Đăng ký", + "errors": { + "emailRequired": "Email là bắt buộc.", + "emailInvalid": "Địa chỉ email không hợp lệ.", + "passwordRequired": "Mật khẩu là bắt buộc." + } + }, + "signup": { + "fullName": "Họ và tên", + "email": "Địa chỉ email", + "password": "Mật khẩu", + "passwordHint": "Tối thiểu 8 ký tự.", + "createAccount": "Tạo tài khoản", + "alreadyHave": "Đã có tài khoản?", + "signIn": "Đăng nhập", + "placeholders": { + "name": "Nguyễn Văn A", + "email": "ban@example.com", + "password": "Tạo mật khẩu" + }, + "errors": { + "nameRequired": "Tên là bắt buộc.", + "emailRequired": "Email là bắt buộc.", + "emailInvalid": "Địa chỉ email không hợp lệ.", + "passwordMin": "Mật khẩu phải có ít nhất 8 ký tự." + } + }, + "forgot": { + "description": "Nhập email và chúng tôi sẽ gửi liên kết đặt lại mật khẩu.", + "email": "Địa chỉ email", + "sendResetLink": "Gửi liên kết đặt lại", + "backToSignIn": "Quay lại đăng nhập", + "placeholders": { + "email": "ban@example.com" + }, + "errors": { + "emailRequired": "Email là bắt buộc.", + "emailInvalid": "Địa chỉ email không hợp lệ." + }, + "toast": { + "successSummary": "Thành công", + "successDetail": "Đã gửi liên kết đặt lại", + "errorSummary": "Lỗi", + "errorDetail": "Đã xảy ra lỗi" + } + }, + "errors": { + "loginNoUserData": "Đăng nhập thất bại: Không nhận được dữ liệu người dùng", + "loginFailed": "Đăng nhập thất bại: {error}", + "registrationFailedFallback": "Đăng ký thất bại", + "registrationFailed": "Đăng ký thất bại: {error}", + "updateProfileFailed": "Cập nhật hồ sơ thất bại: {error}", + "changePasswordFailed": "Đổi mật khẩu thất bại: {error}", + "unknown": "Lỗi không xác định" + } + }, + "nav": { + "overview": "Tổng quan", + "videos": "Video", + "notification": "Thông báo", + "settings": "Cài đặt" + }, + "settings": { + "menu": { + "securityGroup": "Bảo mật", + "preferencesGroup": "Tùy chọn", + "integrationsGroup": "Tích hợp", + "dangerGroup": "Vùng nguy hiểm", + "security": "Bảo mật", + "billing": "Thanh toán & Gói", + "notifications": "Thông báo", + "player": "Trình phát", + "domains": "Tên miền được phép", + "ads": "Quảng cáo & VAST", + "danger": "Vùng nguy hiểm" + }, + "content": { + "fallbackTitle": "Cài đặt", + "fallbackSubtitle": "Quản lý cài đặt và tùy chọn tài khoản của bạn.", + "security": { + "title": "Bảo mật & Tài khoản liên kết", + "subtitle": "Quản lý bảo mật và ứng dụng đã liên kết." + }, + "notifications": { + "title": "Thông báo", + "subtitle": "Chọn cách bạn muốn nhận thông báo và cập nhật." + }, + "player": { + "title": "Cài đặt trình phát", + "subtitle": "Cấu hình hành vi và tính năng mặc định của trình phát video." + }, + "billing": { + "title": "Thanh toán & Gói", + "subtitle": "Thông tin gói và thanh toán hiện tại của bạn." + }, + "domains": { + "title": "Tên miền được phép", + "subtitle": "Thêm tên miền vào whitelist để cho phép nhúng nội dung qua iframe." + }, + "ads": { + "title": "Quảng cáo & VAST", + "subtitle": "Tạo và quản lý mẫu quảng cáo VAST cho video." + }, + "danger": { + "title": "Vùng nguy hiểm", + "subtitle": "Hành động không thể hoàn tác và có tính phá hủy. Hãy cẩn thận!" + } + }, + "notificationSettings": { + "saveChanges": "Lưu thay đổi", + "types": { + "email": { + "title": "Thông báo Email", + "description": "Nhận cập nhật và cảnh báo qua email" + }, + "push": { + "title": "Thông báo đẩy", + "description": "Nhận cảnh báo tức thì trên trình duyệt" + }, + "marketing": { + "title": "Email marketing", + "description": "Nhận khuyến mãi và cập nhật sản phẩm" + }, + "telegram": { + "title": "Thông báo Telegram", + "description": "Nhận cập nhật qua Telegram" + } + }, + "toast": { + "savedSummary": "Đã lưu cài đặt", + "savedDetail": "Cài đặt thông báo của bạn đã được lưu.", + "failedSummary": "Lưu thất bại", + "failedDetail": "Không thể lưu cài đặt." + } + }, + "playerSettings": { + "toast": { + "savedSummary": "Đã lưu cài đặt", + "savedDetail": "Cài đặt trình phát của bạn đã được lưu.", + "failedSummary": "Lưu thất bại", + "failedDetail": "Không thể lưu cài đặt." + }, + "items": { + "autoplay": { + "title": "Tự phát", + "description": "Tự động phát video khi tải xong" + }, + "loop": { + "title": "Lặp lại", + "description": "Phát lại video khi kết thúc" + }, + "muted": { + "title": "Tắt tiếng", + "description": "Bắt đầu video với âm thanh tắt" + }, + "showControls": { + "title": "Hiển thị điều khiển", + "description": "Hiển thị thanh điều khiển (phát, tạm dừng, âm lượng)" + }, + "pip": { + "title": "Picture in Picture", + "description": "Bật chế độ Picture-in-Picture" + }, + "airplay": { + "title": "AirPlay", + "description": "Cho phép phát tới thiết bị Apple qua AirPlay" + }, + "chromecast": { + "title": "Chromecast", + "description": "Cho phép cast tới thiết bị Chromecast" + } + } + }, + "dangerZone": { + "deleteAccount": { + "title": "Xóa tài khoản", + "description": "Xóa vĩnh viễn tài khoản và toàn bộ dữ liệu liên quan.", + "button": "Xóa tài khoản" + }, + "clearData": { + "title": "Xóa toàn bộ dữ liệu", + "description": "Xóa tất cả video, danh sách phát và lịch sử hoạt động của bạn.", + "button": "Xóa dữ liệu" + }, + "warning": { + "title": "Cảnh báo", + "description": "Các hành động này là vĩnh viễn và không thể hoàn tác. Hãy đảm bảo bạn đã sao lưu dữ liệu quan trọng trước khi tiếp tục." + }, + "confirm": { + "deleteAccountMessage": "Bạn có chắc muốn xóa tài khoản? Hành động này không thể hoàn tác.", + "deleteAccountHeader": "Xóa tài khoản", + "deleteAccountAccept": "Xóa", + "deleteAccountReject": "Hủy", + "clearDataMessage": "Bạn có chắc muốn xóa toàn bộ dữ liệu? Hành động này không thể hoàn tác.", + "clearDataHeader": "Xóa toàn bộ dữ liệu", + "clearDataAccept": "Xóa", + "clearDataReject": "Hủy" + }, + "toast": { + "deleteAccountSummary": "Đã gửi yêu cầu xóa tài khoản", + "deleteAccountDetail": "Yêu cầu xóa tài khoản của bạn đã được gửi.", + "clearDataSummary": "Đã xóa dữ liệu", + "clearDataDetail": "Toàn bộ dữ liệu của bạn đã bị xóa vĩnh viễn." + } + }, + "domainsDns": { + "addDomain": "Thêm tên miền", + "infoBanner": "Chỉ các tên miền trong whitelist mới có thể nhúng nội dung của bạn qua iframe.", + "table": { + "domain": "Tên miền", + "addedDate": "Ngày thêm" + }, + "emptyTitle": "Chưa có tên miền trong whitelist", + "emptySubtitle": "Thêm tên miền để cho phép nhúng iframe", + "embedCodeTitle": "Mã nhúng", + "copyCode": "Sao chép mã", + "embedCodeHint": "Dùng đoạn iframe này để nhúng nội dung trên các tên miền đã whitelist.", + "dialog": { + "title": "Thêm tên miền vào whitelist", + "domainLabel": "Tên miền", + "domainPlaceholder": "example.com", + "domainHint": "Nhập tên miền không kèm www hoặc https:// (ví dụ: example.com)", + "importantTitle": "Quan trọng", + "importantDetail": "Chỉ thêm những tên miền bạn sở hữu và kiểm soát." + }, + "confirm": { + "removeMessage": "Bạn có chắc muốn xóa {domain} khỏi whitelist? Các iframe nhúng từ tên miền này sẽ không còn hoạt động.", + "removeHeader": "Xóa tên miền", + "removeAccept": "Xóa", + "removeReject": "Hủy" + }, + "toast": { + "invalidSummary": "Tên miền không hợp lệ", + "invalidDetail": "Vui lòng nhập tên miền hợp lệ.", + "duplicateSummary": "Tên miền đã tồn tại", + "duplicateDetail": "Tên miền này đã có trong whitelist.", + "addedSummary": "Đã thêm tên miền", + "addedDetail": "{domain} đã được thêm vào whitelist.", + "removedSummary": "Đã xóa tên miền", + "removedDetail": "{domain} đã được xóa khỏi whitelist.", + "copiedSummary": "Đã sao chép", + "copiedDetail": "Đã sao chép mã nhúng vào clipboard." + } + }, + "adsVast": { + "createTemplate": "Tạo mẫu", + "infoBanner": "VAST (Video Ad Serving Template) là schema XML dùng để phân phối ad tags cho trình phát video.", + "createdOn": "Tạo ngày {date}", + "emptyTitle": "Chưa có mẫu VAST", + "emptySubtitle": "Tạo mẫu để bắt đầu kiếm tiền từ video", + "formats": { + "preRoll": "Pre-roll", + "midRoll": "Mid-roll", + "postRoll": "Post-roll" + }, + "state": { + "enabled": "bật", + "disabled": "tắt" + }, + "table": { + "template": "Mẫu", + "format": "Định dạng", + "vastUrl": "VAST URL" + }, + "dialog": { + "editTitle": "Sửa mẫu", + "createTitle": "Tạo mẫu VAST", + "templateName": "Tên mẫu", + "templateNamePlaceholder": "ví dụ: Main Pre-roll Ad", + "vastUrlLabel": "VAST Tag URL", + "vastUrlPlaceholder": "https://ads.example.com/vast/tag.xml", + "adFormat": "Định dạng quảng cáo", + "adInterval": "Khoảng cách quảng cáo (giây)", + "adIntervalPlaceholder": "30", + "update": "Cập nhật", + "create": "Tạo" + }, + "confirm": { + "deleteMessage": "Bạn có chắc muốn xóa \"{name}\"?", + "deleteHeader": "Xóa mẫu", + "deleteAccept": "Xóa", + "deleteReject": "Hủy" + }, + "toast": { + "nameRequiredSummary": "Thiếu tên mẫu", + "nameRequiredDetail": "Vui lòng nhập tên mẫu.", + "urlRequiredSummary": "Thiếu VAST URL", + "urlRequiredDetail": "Vui lòng nhập VAST tag URL.", + "invalidUrlSummary": "URL không hợp lệ", + "invalidUrlDetail": "Vui lòng nhập URL hợp lệ.", + "durationRequiredSummary": "Thiếu thời lượng", + "durationRequiredDetail": "Quảng cáo mid-roll yêu cầu thời lượng/khoảng cách.", + "updatedSummary": "Đã cập nhật mẫu", + "updatedDetail": "Mẫu VAST đã được cập nhật.", + "createdSummary": "Đã tạo mẫu", + "createdDetail": "Mẫu VAST đã được tạo.", + "enabledSummary": "Đã bật mẫu", + "disabledSummary": "Đã tắt mẫu", + "toggleDetail": "{name} đã được {state}.", + "deletedSummary": "Đã xóa mẫu", + "deletedDetail": "Mẫu VAST đã được gỡ bỏ.", + "copiedSummary": "Đã sao chép", + "copiedDetail": "Đã sao chép URL vào clipboard." + } + }, + "profile": { + "title": "Thông tin hồ sơ", + "subtitle": "Quản lý thông tin cá nhân và chi tiết tài khoản của bạn.", + "userFallback": "Người dùng", + "username": "Tên người dùng", + "email": "Địa chỉ email", + "storageUsage": "Dung lượng sử dụng", + "storageUsedOfLimit": "Đã dùng {used} trên {limit}", + "editProfile": "Chỉnh sửa hồ sơ", + "changePassword": "Đổi mật khẩu" + }, + "connectedAccounts": { + "title": "Tài khoản liên kết", + "email": { + "label": "Email", + "connected": "Đã kết nối", + "notConnected": "Chưa kết nối", + "disconnected": "Đã ngắt kết nối" + }, + "telegram": { + "label": "Telegram", + "hint": "Nhận thông báo qua Telegram", + "connectedFallback": "Đã kết nối" + } + }, + "billing": { + "walletBalance": "Số dư ví", + "currentBalance": "Số dư hiện tại: {balance}", + "topUp": "Nạp tiền", + "availablePlans": "Các gói khả dụng", + "availablePlansHint": "Chọn gói phù hợp nhất với nhu cầu của bạn", + "planStorage": "{storage} dung lượng", + "planDuration": "{duration} thời lượng tối đa", + "planUploads": "{count} lượt tải / ngày", + "currentPlan": "Gói hiện tại", + "processing": "Đang xử lý...", + "upgrade": "Nâng cấp", + "storage": "Dung lượng", + "storageUsedOfLimit": "Đã dùng {used} trên {limit}", + "monthlyUploads": "Lượt tải tháng này", + "uploadsUsedOfLimit": "{used} trên {limit} lượt tải", + "paymentHistory": "Lịch sử thanh toán", + "paymentHistorySubtitle": "Các khoản thanh toán và hóa đơn trước đây của bạn", + "noPaymentHistory": "Không tìm thấy lịch sử thanh toán.", + "download": "Tải xuống", + "durationMinutes": "{minutes} phút", + "unknownPlan": "Không xác định", + "table": { + "date": "Ngày", + "amount": "Số tiền", + "plan": "Gói", + "status": "Trạng thái", + "invoice": "Hóa đơn" + }, + "status": { + "success": "Thành công", + "failed": "Thất bại", + "pending": "Đang chờ" + }, + "topupDialog": { + "title": "Nạp tiền vào ví", + "subtitle": "Chọn số tiền hoặc nhập số tiền tùy chỉnh để nạp vào ví.", + "customAmount": "Số tiền tùy chỉnh", + "enterAmount": "Nhập số tiền", + "hint": "Số tiền nạp tối thiểu là $1. Tiền sẽ được cộng vào ví ngay sau khi thanh toán.", + "proceed": "Tiếp tục thanh toán" + }, + "toast": { + "subscriptionSuccessSummary": "Đăng ký thành công", + "subscriptionSuccessDetail": "Đăng ký gói {plan} thành công", + "subscriptionFailedSummary": "Đăng ký thất bại", + "subscriptionFailedDetail": "Không thể đăng ký gói", + "topupSuccessSummary": "Nạp tiền thành công", + "topupSuccessDetail": "{amount} đã được cộng vào ví của bạn.", + "topupFailedSummary": "Nạp tiền thất bại", + "topupFailedDetail": "Không thể xử lý nạp tiền.", + "downloadingSummary": "Đang tải", + "downloadingDetail": "Đang tải hóa đơn #{invoiceId}...", + "downloadedSummary": "Đã tải xong", + "downloadedDetail": "Hóa đơn #{invoiceId} đã được tải thành công" + } + }, + "securityConnected": { + "header": { + "title": "Bảo mật & Tài khoản liên kết", + "subtitle": "Quản lý cài đặt bảo mật và dịch vụ đã kết nối." + }, + "accountStatus": { + "label": "Trạng thái tài khoản", + "detail": "Tài khoản của bạn đang hoạt động tốt", + "badge": "Đang hoạt động" + }, + "language": { + "label": "Ngôn ngữ", + "detail": "Chọn ngôn ngữ hiển thị ưu tiên", + "save": "Lưu ngôn ngữ", + "options": { + "en": "English", + "vi": "Tiếng Việt" + }, + "toast": { + "successSummary": "Đã lưu ngôn ngữ", + "successDetail": "Ngôn ngữ đã được cập nhật.", + "errorSummary": "Lưu thất bại", + "errorDetail": "Không thể lưu ngôn ngữ lên server. Đã áp dụng theo cookie dự phòng." + } + }, + "twoFactor": { + "label": "Xác thực hai lớp", + "enabled": "2FA đã bật", + "disabled": "Thêm một lớp bảo mật" + }, + "changePassword": { + "label": "Đổi mật khẩu", + "detail": "Cập nhật mật khẩu tài khoản", + "button": "Đổi mật khẩu", + "dialog": { + "title": "Đổi mật khẩu", + "subtitle": "Nhập mật khẩu hiện tại và chọn mật khẩu mới.", + "current": "Mật khẩu hiện tại", + "new": "Mật khẩu mới", + "confirm": "Xác nhận mật khẩu mới", + "currentPlaceholder": "Nhập mật khẩu hiện tại", + "newPlaceholder": "Nhập mật khẩu mới", + "confirmPlaceholder": "Xác nhận mật khẩu mới", + "submit": "Đổi mật khẩu", + "cancel": "Hủy", + "errors": { + "mismatch": "Mật khẩu xác nhận không khớp", + "minLength": "Mật khẩu phải có ít nhất 6 ký tự", + "default": "Không thể đổi mật khẩu" + } + }, + "toast": { + "successSummary": "Đổi mật khẩu thành công", + "successDetail": "Mật khẩu của bạn đã được cập nhật." + } + }, + "logout": { + "label": "Đăng xuất", + "detail": "Đăng xuất khỏi thiết bị này.", + "button": "Đăng xuất", + "confirm": { + "message": "Bạn có chắc muốn đăng xuất?", + "header": "Đăng xuất", + "accept": "Đăng xuất", + "reject": "Hủy" + } + }, + "email": { + "label": "Email", + "connected": "Đã kết nối", + "disconnected": "Chưa kết nối", + "badgeConnected": "Đã kết nối", + "badgeDisconnected": "Đã ngắt kết nối" + }, + "telegram": { + "label": "Telegram", + "detailDisconnected": "Nhận thông báo qua Telegram", + "connectedFallback": "Đã kết nối", + "connect": "Kết nối", + "disconnect": "Ngắt kết nối" + }, + "twoFactorDialog": { + "title": "Bật xác thực hai lớp", + "subtitle": "Quét mã QR bằng ứng dụng xác thực (Google Authenticator, Authy, v.v.)", + "secret": "Khóa bí mật:", + "codeLabel": "Mã xác thực", + "codePlaceholder": "Nhập mã 6 chữ số", + "cancel": "Hủy", + "verify": "Xác minh & Bật" + }, + "toast": { + "twoFactorEnabledSummary": "Đã bật 2FA", + "twoFactorEnabledDetail": "Xác thực hai lớp đã được bật thành công.", + "twoFactorEnableFailedSummary": "Bật 2FA thất bại", + "twoFactorEnableFailedDetail": "Không thể bật xác thực hai lớp.", + "twoFactorDisableFailedSummary": "Tắt 2FA thất bại", + "twoFactorDisableFailedDetail": "Không thể tắt xác thực hai lớp.", + "twoFactorDisabledSummary": "Đã tắt 2FA", + "twoFactorDisabledDetail": "Xác thực hai lớp đã bị tắt.", + "twoFactorInvalidCodeSummary": "Bật 2FA thất bại", + "twoFactorInvalidCodeDetail": "Mã xác thực không hợp lệ. Vui lòng thử lại.", + "telegramConnectedSummary": "Đã kết nối Telegram", + "telegramConnectedDetail": "Đã kết nối với {username}", + "telegramConnectFailedSummary": "Kết nối thất bại", + "telegramConnectFailedDetail": "Không thể kết nối tài khoản Telegram.", + "telegramDisconnectedSummary": "Đã ngắt Telegram", + "telegramDisconnectedDetail": "Tài khoản Telegram đã được ngắt.", + "telegramDisconnectFailedSummary": "Ngắt kết nối thất bại", + "telegramDisconnectFailedDetail": "Không thể ngắt kết nối Telegram." + } + } + }, + "pageHeader": { + "dashboard": "Bảng điều khiển", + "settings": "Cài đặt" + }, + "confirm": { + "defaultHeader": "Xác nhận", + "defaultAccept": "Đồng ý", + "defaultReject": "Hủy" + }, + "toast": { + "dismissAria": "Đóng" + }, + "overview": { + "welcome": { + "title": "Chào mừng trở lại, {name}! 👋", + "subtitle": "Đây là tình hình nội dung của bạn hôm nay." + }, + "stats": { + "totalVideos": "Tổng số video", + "totalViews": "Tổng lượt xem", + "storageUsed": "Dung lượng đã dùng", + "uploadsThisMonth": "Lượt tải lên tháng này", + "trendVsLastMonth": "so với tháng trước" + }, + "quickActions": { + "title": "Thao tác nhanh", + "uploadVideo": { + "title": "Tải video lên", + "description": "Tải video mới vào thư viện" + }, + "videoLibrary": { + "title": "Thư viện video", + "description": "Duyệt tất cả video của bạn" + }, + "analytics": { + "title": "Phân tích", + "description": "Theo dõi hiệu suất & thông tin chi tiết" + }, + "managePlan": { + "title": "Quản lý gói", + "description": "Nâng cấp hoặc thay đổi gói" + } + }, + "referral": { + "title": "Liên kết giới thiệu", + "subtitle": "Chia sẻ liên kết giới thiệu và nhận hoa hồng từ người dùng được giới thiệu!" + }, + "recentVideos": { + "title": "Video gần đây", + "viewAll": "Xem tất cả", + "emptyTitle": "Không có video", + "emptyDescription": "Bạn chưa tải video nào. Hãy bắt đầu với video đầu tiên!", + "emptyAction": "Tải video lên", + "table": { + "video": "Video", + "status": "Trạng thái", + "duration": "Thời lượng", + "uploadDate": "Ngày tải lên", + "actions": "Hành động" + }, + "noDescription": "Không có mô tả", + "unknownStatus": "Không xác định", + "actionEdit": "Sửa", + "actionShare": "Chia sẻ", + "actionDelete": "Xóa" + }, + "storage": { + "title": "Sử dụng dung lượng", + "usedOfLimit": "Đã dùng {used} trên {limit}", + "breakdown": { + "videos": "Video", + "thumbnails": "Thumbnail & tài nguyên", + "other": "Tệp khác" + }, + "lowStorage": { + "title": "Dung lượng sắp đầy", + "message": "Hãy cân nhắc nâng cấp gói để có thêm dung lượng.", + "viewPlans": "Xem các gói" + } + } + }, + "video": { + "page": { + "title": "Video của tôi", + "description": "Quản lý và sắp xếp thư viện video", + "uploadAction": "Tải video lên", + "uploadDropTitle": "Thả để tải lên", + "uploadDropSubtitle": "Tệp sẽ được thêm vào hàng đợi tải lên", + "deleteSelectedConfirm": "Xóa {count} video?", + "deleteSingleConfirm": "Bạn có chắc muốn xóa video này?", + "retry": "Thử lại", + "emptyTitle": "Không có video", + "emptyDescription": "Bạn chưa tải video nào. Hãy bắt đầu với video đầu tiên!", + "emptyAction": "Tải video lên", + "duplicateSummary": "Đã bỏ qua tệp trùng lặp", + "duplicateDetailOne": "{count} tệp đã có trong hàng đợi.", + "duplicateDetailOther": "{count} tệp đã có trong hàng đợi." + }, + "filters": { + "searchPlaceholder": "Tìm kiếm video...", + "rangeOfTotal": "{first}–{last} / {total}", + "previousPageAria": "Trang trước", + "nextPageAria": "Trang sau", + "allStatus": "Tất cả trạng thái", + "ready": "Sẵn sàng", + "processing": "Đang xử lý", + "failed": "Thất bại" + }, + "table": { + "video": "Video", + "status": "Trạng thái", + "size": "Dung lượng", + "created": "Ngày tạo", + "actions": "Hành động", + "noDescription": "Không có mô tả", + "copyLink": "Sao chép liên kết", + "edit": "Chỉnh sửa", + "delete": "Xóa" + }, + "bulk": { + "selected": "{count} mục đã chọn", + "delete": "Xóa" + }, + "copyModal": { + "title": "Lấy địa chỉ chia sẻ", + "playerAddress": "Địa chỉ phát", + "embedPlayer": "Nhúng trình phát (khuyến nghị)", + "thumbnail": "URL thumbnail", + "hls": "Liên kết HLS (chỉ VIP)", + "hlsPlaceholder": "Liên kết HLS cho VIP với domain được whitelist", + "hlsHint": "Liên kết này chuyển hướng tới URL HLS đã ký và chỉ hoạt động trên domain đã whitelist.", + "warningTitle": "Cảnh báo", + "warningDetail": "Hãy đảm bảo tệp chia sẻ tuân thủ pháp luật địa phương và bạn hiểu rõ trách nhiệm khi phân phối nội dung.", + "reminderTitle": "Lưu ý", + "reminderDetail": "Trình phát nhúng có thể tự chuyển node dự phòng và hoạt động tốt trên di động. Liên kết HLS thô phụ thuộc vào trình phát riêng của bạn và chỉ dùng trên domain đã whitelist.", + "toastCopiedSummary": "Đã sao chép", + "toastCopiedDetail": "Đã sao chép vào clipboard", + "toastErrorSummary": "Lỗi", + "toastErrorDetail": "Không thể tải chi tiết video" + }, + "detailModal": { + "title": "Chỉnh sửa video", + "titleLabel": "Tiêu đề", + "titlePlaceholder": "Nhập tiêu đề video", + "descriptionLabel": "Mô tả", + "descriptionPlaceholder": "Nhập mô tả video", + "subtitlesTitle": "Phụ đề", + "subtitleTracks": "{count} track", + "noSubtitles": "Chưa có phụ đề", + "uploadSubtitle": "Tải phụ đề", + "subtitleFile": "Tệp phụ đề (VTT, SRT, ASS, SSA)", + "languageCode": "Mã ngôn ngữ *", + "languagePlaceholder": "en, vi, v.v.", + "displayName": "Tên hiển thị (Tùy chọn)", + "displayNamePlaceholder": "English, Tiếng Việt, v.v.", + "uploadSubtitleButton": "Tải phụ đề", + "cancel": "Hủy", + "saveChanges": "Lưu thay đổi", + "errors": { + "titleRequired": "Tiêu đề là bắt buộc." + }, + "toast": { + "loadErrorSummary": "Lỗi", + "loadErrorDetail": "Không thể tải chi tiết video", + "saveSuccessSummary": "Thành công", + "saveSuccessDetail": "Cập nhật video thành công", + "saveErrorSummary": "Lỗi", + "saveErrorDetail": "Không thể lưu thay đổi", + "subtitleInfoSummary": "Thông tin", + "subtitleInfoDetail": "Tải phụ đề chưa được hỗ trợ" + } + }, + "detailPage": { + "title": "Chi tiết video", + "description": "Xem và quản lý thông tin video", + "loadingBreadcrumb": "Đang tải...", + "detailsTitle": "Thông tin video", + "copyValueTitle": "Sao chép giá trị", + "videoTagFallback": "Trình duyệt của bạn không hỗ trợ thẻ video.", + "saving": "Đang lưu...", + "cancelEditTitle": "Hủy chỉnh sửa", + "reloadTitle": "Tải lại video", + "reloadButton": "Tải lại", + "confirmDelete": { + "message": "Bạn có chắc muốn xóa video này? Hành động này không thể hoàn tác.", + "header": "Xác nhận xóa", + "accept": "Xóa", + "reject": "Hủy" + }, + "toast": { + "reloadSummary": "Thông tin", + "reloadDetail": "Đang tải lại video...", + "deleteSuccessSummary": "Thành công", + "deleteSuccessDetail": "Xóa video thành công", + "deleteErrorSummary": "Lỗi", + "deleteErrorDetail": "Không thể xóa video", + "copySummary": "Đã sao chép", + "copyDetail": "Đã sao chép {label} vào clipboard" + }, + "videoInfo": { + "videoId": "ID video", + "thumbnailUrl": "URL thumbnail", + "embedUrl": "URL nhúng", + "iframeCode": "Mã iframe", + "shareLink": "Liên kết chia sẻ" + } + }, + "cardPopover": { + "download": "Tải xuống", + "copyLink": "Sao chép liên kết", + "edit": "Chỉnh sửa", + "delete": "Xóa", + "toast": { + "copySuccessSummary": "Thành công", + "copySuccessDetail": "Đã sao chép liên kết video", + "copyErrorSummary": "Lỗi", + "copyErrorDetail": "Không thể sao chép liên kết", + "downloadSuccessSummary": "Thành công", + "downloadSuccessDetail": "Đang tải xuống video...", + "downloadErrorSummary": "Lỗi", + "downloadErrorDetail": "Không tìm thấy tệp video" + } + } + }, + "notification": { + "title": "Thông báo", + "subtitle": "Luôn cập nhật các hoạt động và cảnh báo mới nhất của bạn.", + "tabs": { + "all": "Tất cả", + "unread": "Chưa đọc", + "videos": "Video", + "payments": "Thanh toán" + }, + "stats": { + "total": "{count} thông báo", + "unread": "{count} chưa đọc" + }, + "actions": { + "markAllRead": "Đánh dấu đã đọc tất cả", + "clearAll": "Xóa tất cả", + "viewAll": "Xem tất cả thông báo", + "viewVideo": "Xem video", + "viewReceipt": "Xem biên lai", + "upgradePlan": "Nâng cấp gói", + "tryNow": "Dùng thử ngay" + }, + "item": { + "viewDetails": "Xem chi tiết", + "markAsRead": "Đánh dấu đã đọc", + "delete": "Xóa" + }, + "empty": { + "title": "Không có thông báo", + "subtitle": "Bạn đã xem hết! Hãy quay lại sau." + }, + "time": { + "minutesAgo": "{count} phút trước", + "hoursAgo": "{count} giờ trước", + "daysAgo": "{count} ngày trước" + }, + "mocks": { + "videoProcessed": { + "title": "Xử lý video hoàn tất", + "message": "Video \"Summer Vacation 2024\" của bạn đã được xử lý thành công và sẵn sàng phát." + }, + "paymentSuccess": { + "title": "Thanh toán thành công", + "message": "Gói Pro Plan của bạn đã được gia hạn thành công. Kỳ thanh toán tiếp theo: 25/02/2026." + }, + "storageWarning": { + "title": "Dung lượng sắp đầy", + "message": "Bạn đã sử dụng 85% hạn mức lưu trữ. Hãy cân nhắc nâng cấp gói để có thêm dung lượng." + }, + "uploadSuccess": { + "title": "Tải lên thành công", + "message": "Video \"Product Demo v2\" của bạn đã được tải lên thành công." + }, + "maintenance": { + "title": "Bảo trì định kỳ", + "message": "Chúng tôi sẽ bảo trì hệ thống vào ngày 30/01/2026 từ 02:00 đến 04:00 UTC." + }, + "newFeature": { + "title": "Tính năng mới", + "message": "Chúng tôi vừa ra mắt phân tích video! Theo dõi hiệu suất video với dữ liệu chi tiết." + } + } + }, + "upload": { + "dialog": { + "title": "Tải video lên", + "subtitle": "Thêm tối đa {maxItems} video mỗi đợt", + "mode": { + "local": "Tệp cục bộ", + "remote": "URL từ xa" + }, + "queueFullTitle": "Hàng đợi đã đầy", + "queueFullDescription": "Tối đa {maxItems} video mỗi đợt. Hãy bắt đầu hoặc xóa hàng đợi hiện tại trước.", + "slotsRemaining": "Còn {remaining} / {maxItems} vị trí", + "formatsHint": "MP4, MOV, MKV · tối đa 10 GB mỗi tệp", + "close": "Đóng", + "startUpload": "Bắt đầu tải ({count})", + "duplicateFilesSummary": "Đã bỏ qua tệp trùng lặp", + "duplicateFilesDetailOne": "{count} tệp đã có trong hàng đợi.", + "duplicateFilesDetailOther": "{count} tệp đã có trong hàng đợi.", + "duplicateUrlsSummary": "Đã bỏ qua URL trùng lặp", + "duplicateUrlsDetailOne": "{count} URL đã có trong hàng đợi.", + "duplicateUrlsDetailOther": "{count} URL đã có trong hàng đợi." + }, + "dropzone": { + "releaseToAdd": "Thả để thêm", + "dropHere": "Thả video vào đây", + "browse": "hoặc bấm để chọn tệp" + }, + "remote": { + "placeholder": "Dán URL video, mỗi dòng một URL\n\nhttps://example.com/video.mp4\nhttps://drive.google.com/...", + "providersHint": "Hỗ trợ Google Drive, Dropbox", + "addUrls": "Thêm URL" + }, + "queue": { + "empty": "Hàng đợi trống!", + "totalSize": "Tổng dung lượng:", + "zeroSize": "0 MB" + }, + "queueItem": { + "remoteFileName": "Tệp từ URL", + "unknownSize": "Không xác định", + "status": { + "pending": "Chờ tải", + "uploading": "Đang tải lên...", + "uploadingThreads": "Đang tải · {threads} luồng", + "processing": "Đang xử lý...", + "complete": "Hoàn tất", + "error": "Thất bại", + "fetching": "Đang lấy dữ liệu..." + } + }, + "infoTip": { + "title": "Mẹo: Để xử lý nhanh nhất", + "description": "Hãy tải video với codec H.264 + codec âm thanh AAC (ví dụ MP4 dùng H.264/AAC). Video theo định dạng này sẽ được xử lý nhanh hơn nhiều (giây thay vì phút) vì không cần encode lại." + }, + "bulkActions": { + "title": "Thiết lập nhanh", + "applyToPending": "Áp dụng cho {count} tệp đang chờ", + "selectCategory": "Chọn danh mục...", + "category": { + "learning": "Học tập", + "entertainment": "Giải trí" + }, + "visibility": { + "public": "Công khai", + "private": "Riêng tư" + } + }, + "indicator": { + "allDone": "Hoàn tất", + "uploading": "Đang tải lên {count} tệp...", + "waiting": "{count} tệp đang chờ", + "completeProgress": "Hoàn tất {complete} / {total}", + "start": "Bắt đầu", + "viewVideos": "Xem video", + "addMoreFiles": "Thêm tệp" + }, + "errors": { + "chunkUploadFailed": "Không thể tải phần {index}", + "mergeFailed": "Gộp tệp thất bại" + } + }, + "home": { + "nav": { + "features": "Tính năng", + "pricing": "Bảng giá", + "login": "Đăng nhập", + "startFree": "Dùng thử miễn phí" + }, + "hero": { + "titleLine1": "Hạ tầng video cho", + "titleLine2": "internet hiện đại.", + "subtitle": "Lưu trữ, mã hóa và phát video mượt mà với API ưu tiên cho developer. Tối ưu tốc độ, sẵn sàng mở rộng.", + "getStarted": "Bắt đầu", + "uploadVideo": "Tải video" + }, + "features": { + "heading": "Mọi thứ bạn cần để triển khai video", + "subtitle": "Tập trung xây dựng sản phẩm. Chúng tôi lo phần hạ tầng video phức tạp.", + "global": { + "title": "Mạng edge toàn cầu", + "description": "Nội dung được phân phối từ hơn 200 PoP trên toàn thế giới. Tự động chọn vùng để có độ trễ thấp nhất cho mọi người xem." + }, + "live": { + "title": "API livestream", + "description": "Mở rộng tới hàng triệu người xem đồng thời với độ trễ cực thấp. Hỗ trợ RTMP ingest và HLS playback sẵn có.", + "status": "Trạng thái trực tiếp", + "onAir": "Đang phát", + "bitrate": "Bitrate:", + "fps": "FPS:", + "latency": "Độ trễ:", + "bitrateValue": "6000 kbps", + "fpsValue": "60", + "latencyValue": "~2 giây" + }, + "encoding": { + "title": "Mã hóa tức thì", + "description": "Tải tệp thô và nhận luồng HLS/DASH tối ưu chỉ trong vài giây." + }, + "analytics": { + "title": "Phân tích chuyên sâu", + "description": "Insight theo phiên, chỉ số chất lượng trải nghiệm (QoE), và nhiều hơn nữa." + } + }, + "pricing": { + "title": "Bảng giá đơn giản, minh bạch", + "subtitle": "Chọn gói phù hợp với nhu cầu. Không phí ẩn.", + "perMonth": "/tháng", + "hobby": { + "name": "Hobby", + "features": [ + "Tải lên không giới hạn", + "1 giờ lưu trữ", + "Hỗ trợ tiêu chuẩn" + ], + "button": "Bắt đầu miễn phí" + }, + "pro": { + "name": "Pro", + "features": [ + "Trình phát không quảng cáo", + "Hỗ trợ M3U8", + "Tải lên không giới hạn", + "Quảng cáo tùy chỉnh" + ], + "button": "Bắt đầu ngay", + "tag": "PHỔ BIẾN" + }, + "scale": { + "name": "Scale", + "features": ["5 TB băng thông", "500 giờ lưu trữ", "Hỗ trợ ưu tiên"], + "button": "Liên hệ kinh doanh", + "tag": "Giá trị cao" + } + }, + "footer": { + "description": "Xây dựng lớp hạ tầng video cho internet. Thiết kế cho developer.", + "product": "Sản phẩm", + "productFeatures": "Tính năng", + "productPricing": "Bảng giá", + "productShowcase": "Trình diễn", + "company": "Công ty", + "companyAbout": "Giới thiệu", + "companyBlog": "Blog", + "companyCareers": "Tuyển dụng", + "legal": "Pháp lý", + "privacy": "Quyền riêng tư", + "terms": "Điều khoản", + "copyright": "© {year} EcoStream Inc. Bảo lưu mọi quyền." + }, + "head": { + "title": "EcoStream - Hạ tầng video cho internet hiện đại", + "description": "Lưu trữ, mã hóa và phát video mượt mà với API ưu tiên cho developer. Tối ưu tốc độ, sẵn sàng mở rộng." + } + }, + "legal": { + "common": { + "heading": "Chính sách pháp lý & quyền riêng tư", + "subheading": "Pháp lý & quyền riêng tư", + "description": "Chính sách pháp lý và quyền riêng tư của chúng tôi." + }, + "terms": { + "title": "Điều khoản và điều kiện - Ecostream", + "description": "Đọc điều khoản và điều kiện sử dụng dịch vụ lưu trữ và phát video của Ecostream.", + "pageHeading": "Chi tiết điều khoản và điều kiện", + "pageSubheading": "Điều khoản và điều kiện", + "pageDescription": "Điều khoản của chúng tôi nêu rõ các hướng dẫn và quy định quan trọng khi sử dụng dịch vụ Ecostream.", + "sections": { + "acceptanceTitle": "1. Chấp nhận điều khoản", + "acceptanceText": "Bằng việc truy cập và sử dụng Ecostream, bạn đồng ý bị ràng buộc bởi các điều khoản trong thỏa thuận này.", + "usageTitle": "2. Sử dụng dịch vụ", + "usageText": "Bạn đồng ý chỉ sử dụng dịch vụ cho mục đích hợp pháp. Nghiêm cấm đăng hoặc truyền nội dung bất hợp pháp, đe dọa, phỉ báng, tục tĩu hoặc phản cảm. Chúng tôi có quyền chấm dứt tài khoản vi phạm điều khoản.", + "ownershipTitle": "3. Quyền sở hữu nội dung", + "ownershipText": "Bạn giữ toàn bộ quyền sở hữu đối với nội dung tải lên Ecostream. Tuy nhiên, khi tải lên, bạn cấp cho chúng tôi quyền lưu trữ, lưu giữ và hiển thị nội dung cần thiết để cung cấp dịch vụ.", + "liabilityTitle": "4. Giới hạn trách nhiệm", + "liabilityText": "Ecostream không chịu trách nhiệm cho bất kỳ thiệt hại trực tiếp, gián tiếp, ngẫu nhiên, đặc biệt hoặc hệ quả do việc sử dụng hoặc không thể sử dụng dịch vụ.", + "changesTitle": "5. Thay đổi điều khoản", + "changesText": "Chúng tôi có quyền sửa đổi điều khoản bất kỳ lúc nào. Việc bạn tiếp tục sử dụng dịch vụ sau khi có thay đổi đồng nghĩa với việc chấp nhận điều khoản mới." + } + }, + "privacy": { + "title": "Chính sách quyền riêng tư - Ecostream", + "description": "Tìm hiểu cam kết của Ecostream trong việc bảo vệ quyền riêng tư và dữ liệu của bạn.", + "pageHeading": "Chính sách pháp lý & quyền riêng tư", + "pageSubheading": "Pháp lý & quyền riêng tư", + "pageDescription": "Chính sách pháp lý và quyền riêng tư của chúng tôi.", + "sections": { + "policyTitle": "1. Chính sách quyền riêng tư", + "policyText": "Tại Ecostream, chúng tôi coi trọng quyền riêng tư của bạn. Chính sách này mô tả cách chúng tôi thu thập, sử dụng và bảo vệ thông tin cá nhân. Chúng tôi chỉ thu thập thông tin cần thiết để vận hành dịch vụ, bao gồm email tạo tài khoản và thông tin thanh toán xử lý gói dịch vụ.", + "dataCollectionTitle": "2. Thu thập dữ liệu", + "dataCollectionText": "Chúng tôi thu thập dữ liệu như địa chỉ IP, loại trình duyệt và thời gian truy cập để phân tích xu hướng và cải thiện dịch vụ. Nội dung tải lên được lưu trữ an toàn và chỉ truy cập khi cần cho việc cung cấp dịch vụ lưu trữ.", + "cookieTitle": "3. Chính sách cookie", + "cookieText": "Chúng tôi sử dụng cookie để duy trì phiên đăng nhập và tùy chọn người dùng. Khi sử dụng website, bạn đồng ý với việc sử dụng cookie theo chính sách này.", + "dmcaTitle": "4. DMCA & bản quyền", + "dmcaText": "Ecostream tôn trọng quyền sở hữu trí tuệ của người khác. Chúng tôi phản hồi các thông báo vi phạm bản quyền theo Đạo luật DMCA. Vui lòng báo cáo vi phạm bản quyền tới bộ phận hỗ trợ." + } + } + }, + "notFound": { + "headTitle": "404 - Không tìm thấy trang", + "title": "404 - Không tìm thấy trang", + "description": "Trang bạn đang tìm không tồn tại.", + "backHome": "Quay về trang chủ" + } +} diff --git a/src/client.ts b/src/client.ts index 2a0199c..b7461c4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9,8 +9,7 @@ const readAppData = () => { async function render() { const appData = readAppData(); - const { app, router, queryCache, pinia } = createApp(appData.$locale); - + const { app, router, queryCache, pinia } = await createApp(appData.$locale); pinia.use(PiniaSharedState({ enable: true, initialize: true })); hydrateQueryCache(queryCache, appData.$colada || {}); diff --git a/src/i18n/constants.ts b/src/i18n/constants.ts deleted file mode 100644 index 0fdcb3c..0000000 --- a/src/i18n/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const supportedLocales = ['en', 'vi'] as const; - -export type SupportedLocale = (typeof supportedLocales)[number]; - -export const defaultLocale: SupportedLocale = 'en'; - -export const localeCookieKey = 'lang'; diff --git a/src/i18n/index.ts b/src/i18n/index.ts deleted file mode 100644 index 667e29a..0000000 --- a/src/i18n/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createI18n as createVueI18n } from 'vue-i18n'; -import type { SupportedLocale } from './constants'; -import { defaultLocale, supportedLocales } from './constants'; -import en from './messages/en'; -import vi from './messages/vi'; - -export const i18nMessages = { - en, - vi, -} as const; - -let activeI18n: ReturnType | null = null; - -const normalizeLocaleToken = (locale?: string | null): string | undefined => { - if (!locale) return undefined; - return locale - .trim() - .toLowerCase() - .replace('_', '-'); -}; - -export const toSupportedLocale = (locale?: string | null): SupportedLocale | undefined => { - const normalized = normalizeLocaleToken(locale); - if (!normalized) return undefined; - - const direct = supportedLocales.find(item => item === normalized); - if (direct) return direct; - - const base = normalized.split('-')[0]; - return supportedLocales.find(item => item === base); -}; - -export const normalizeLocale = (locale?: string | null): SupportedLocale => { - return toSupportedLocale(locale) ?? defaultLocale; -}; - -export const resolveLocaleFromAcceptLanguage = (acceptLanguage?: string | null): SupportedLocale | undefined => { - if (!acceptLanguage) return undefined; - - const candidates = acceptLanguage - .split(',') - .map((part) => { - const [rawLocale, ...params] = part.trim().split(';'); - const qParam = params.find(param => param.trim().startsWith('q=')); - const quality = qParam ? Number.parseFloat(qParam.split('=')[1] ?? '1') : 1; - return { - locale: rawLocale, - quality: Number.isFinite(quality) ? quality : 1, - }; - }) - .sort((a, b) => b.quality - a.quality); - - for (const candidate of candidates) { - const matched = toSupportedLocale(candidate.locale); - if (matched) return matched; - } - - return undefined; -}; - -export const createI18n = (initialLocale?: string | null) => { - const locale = normalizeLocale(initialLocale); - const i18n = createVueI18n({ - legacy: false, - locale, - fallbackLocale: defaultLocale, - messages: i18nMessages, - }); - activeI18n = i18n; - return i18n; -}; - -export const getActiveI18n = () => activeI18n; - -export type AppI18n = ReturnType; diff --git a/src/i18n/messages/en.ts b/src/i18n/messages/en.ts deleted file mode 100644 index fe3bcb0..0000000 --- a/src/i18n/messages/en.ts +++ /dev/null @@ -1,1045 +0,0 @@ -const en = { - common: { - save: 'Save', - close: 'Close', - cancel: 'Cancel', - connect: 'Connect', - disconnect: 'Disconnect', - upload: 'Upload', - loading: 'Loading', - actions: 'Actions', - status: 'Status', - videos: 'Videos', - selected: '{count} selected', - copy: 'Copy', - }, - app: { - name: 'EcoStream', - }, - auth: { - layout: { - login: { - headTitle: 'Login to your account', - title: 'Sign in to your dashboard', - subtitle: 'Please enter your details to sign in.', - }, - signup: { - headTitle: 'Create your account', - title: 'Create your account', - subtitle: 'Please fill in the information to create your account.', - }, - forgot: { - headTitle: 'Reset your password', - title: 'Forgot your password?', - subtitle: "Enter your email address and we'll send you a link to reset your password.", - }, - }, - login: { - email: 'Email', - password: 'Password', - forgotPassword: 'Forgot password?', - signIn: 'Sign in', - google: 'Google', - noAccount: "Don't have an account?", - signUp: 'Sign up', - errors: { - emailRequired: 'Email is required.', - emailInvalid: 'Invalid email address.', - passwordRequired: 'Password is required.', - }, - }, - signup: { - fullName: 'Full Name', - email: 'Email address', - password: 'Password', - passwordHint: 'Must be at least 8 characters.', - createAccount: 'Create Account', - alreadyHave: 'Already have an account?', - signIn: 'Sign in', - placeholders: { - name: 'John Doe', - email: 'you@example.com', - password: 'Create a password', - }, - errors: { - nameRequired: 'Name is required.', - emailRequired: 'Email is required.', - emailInvalid: 'Invalid email address.', - passwordMin: 'Password must be at least 8 characters.', - }, - }, - forgot: { - description: "Enter your email address and we'll send you a link to reset your password.", - email: 'Email address', - sendResetLink: 'Send Reset Link', - backToSignIn: 'Back to Sign in', - placeholders: { - email: 'you@example.com', - }, - errors: { - emailRequired: 'Email is required.', - emailInvalid: 'Invalid email address.', - }, - toast: { - successSummary: 'Success', - successDetail: 'Reset link sent', - errorSummary: 'Error', - errorDetail: 'An error occurred', - }, - }, - errors: { - loginNoUserData: 'Login failed: No user data received', - loginFailed: 'Login failed: {error}', - registrationFailedFallback: 'Registration failed', - registrationFailed: 'Registration failed: {error}', - updateProfileFailed: 'Failed to update profile: {error}', - changePasswordFailed: 'Failed to change password: {error}', - unknown: 'Unknown error', - }, - }, - nav: { - overview: 'Overview', - videos: 'Videos', - notification: 'Notification', - settings: 'Settings', - }, - settings: { - menu: { - securityGroup: 'Security', - preferencesGroup: 'Preferences', - integrationsGroup: 'Integrations', - dangerGroup: 'Danger Zone', - security: 'Security', - billing: 'Billing & Plans', - notifications: 'Notifications', - player: 'Player', - domains: 'Allowed Domains', - ads: 'Ads & VAST', - danger: 'Danger Zone', - }, - content: { - fallbackTitle: 'Settings', - fallbackSubtitle: 'Manage your account settings and preferences.', - security: { - title: 'Security & Connected Apps', - subtitle: 'Manage your security settings and connected applications.', - }, - notifications: { - title: 'Notifications', - subtitle: 'Choose how you want to receive notifications and updates.', - }, - player: { - title: 'Player Settings', - subtitle: 'Configure default video player behavior and features.', - }, - billing: { - title: 'Billing & Plans', - subtitle: 'Your current subscription and billing information.', - }, - domains: { - title: 'Allowed Domains', - subtitle: 'Add domains to your whitelist to allow embedding content via iframe.', - }, - ads: { - title: 'Ads & VAST', - subtitle: 'Create and manage VAST ad templates for your videos.', - }, - danger: { - title: 'Danger Zone', - subtitle: 'Irreversible and destructive actions. Be careful!', - }, - }, - notificationSettings: { - saveChanges: 'Save Changes', - types: { - email: { - title: 'Email Notifications', - description: 'Receive updates and alerts via email', - }, - push: { - title: 'Push Notifications', - description: 'Get instant alerts in your browser', - }, - marketing: { - title: 'Marketing Emails', - description: 'Receive promotions and product updates', - }, - telegram: { - title: 'Telegram Notifications', - description: 'Receive updates via Telegram', - }, - }, - toast: { - savedSummary: 'Settings Saved', - savedDetail: 'Your notification settings have been saved.', - failedSummary: 'Save Failed', - failedDetail: 'Failed to save settings.', - }, - }, - playerSettings: { - toast: { - savedSummary: 'Settings Saved', - savedDetail: 'Your player settings have been saved.', - failedSummary: 'Save Failed', - failedDetail: 'Failed to save settings.', - }, - items: { - autoplay: { - title: 'Autoplay', - description: 'Automatically start videos when loaded', - }, - loop: { - title: 'Loop', - description: 'Repeat video when it ends', - }, - muted: { - title: 'Muted', - description: 'Start videos with sound muted', - }, - showControls: { - title: 'Show Controls', - description: 'Display player controls (play, pause, volume)', - }, - pip: { - title: 'Picture in Picture', - description: 'Enable Picture-in-Picture mode', - }, - airplay: { - title: 'AirPlay', - description: 'Allow streaming to Apple devices via AirPlay', - }, - chromecast: { - title: 'Chromecast', - description: 'Allow casting to Chromecast devices', - }, - }, - }, - dangerZone: { - deleteAccount: { - title: 'Delete Account', - description: 'Permanently delete your account and all associated data.', - button: 'Delete Account', - }, - clearData: { - title: 'Clear All Data', - description: 'Remove all your videos, playlists, and activity history.', - button: 'Clear Data', - }, - warning: { - title: 'Warning', - description: 'These actions are permanent and cannot be undone. Make sure you have backed up any important data before proceeding.', - }, - confirm: { - deleteAccountMessage: 'Are you sure you want to delete your account? This action cannot be undone.', - deleteAccountHeader: 'Delete Account', - deleteAccountAccept: 'Delete', - deleteAccountReject: 'Cancel', - clearDataMessage: 'Are you sure you want to clear all your data? This action cannot be undone.', - clearDataHeader: 'Clear All Data', - clearDataAccept: 'Clear', - clearDataReject: 'Cancel', - }, - toast: { - deleteAccountSummary: 'Account deletion requested', - deleteAccountDetail: 'Your account deletion request has been submitted.', - clearDataSummary: 'Data cleared', - clearDataDetail: 'All your data has been permanently deleted.', - }, - }, - domainsDns: { - addDomain: 'Add Domain', - infoBanner: 'Only domains in your whitelist can embed your content using iframe.', - table: { - domain: 'Domain', - addedDate: 'Added Date', - }, - emptyTitle: 'No domains in whitelist', - emptySubtitle: 'Add a domain to allow iframe embedding', - embedCodeTitle: 'Embed Code', - copyCode: 'Copy Code', - embedCodeHint: 'Use this iframe code to embed content on your whitelisted domains.', - dialog: { - title: 'Add Domain to Whitelist', - domainLabel: 'Domain Name', - domainPlaceholder: 'example.com', - domainHint: 'Enter domain without www or https:// (e.g., example.com)', - importantTitle: 'Important', - importantDetail: 'Only add domains that you own and control.', - }, - confirm: { - removeMessage: 'Are you sure you want to remove {domain} from your whitelist? Embedded iframes from this domain will no longer work.', - removeHeader: 'Remove Domain', - removeAccept: 'Remove', - removeReject: 'Cancel', - }, - toast: { - invalidSummary: 'Invalid Domain', - invalidDetail: 'Please enter a valid domain name.', - duplicateSummary: 'Domain Already Added', - duplicateDetail: 'This domain is already in your whitelist.', - addedSummary: 'Domain Added', - addedDetail: '{domain} has been added to your whitelist.', - removedSummary: 'Domain Removed', - removedDetail: '{domain} has been removed from your whitelist.', - copiedSummary: 'Copied', - copiedDetail: 'Embed code copied to clipboard.', - }, - }, - adsVast: { - createTemplate: 'Create Template', - infoBanner: 'VAST (Video Ad Serving Template) is an XML schema for serving ad tags to video players.', - createdOn: 'Created {date}', - emptyTitle: 'No VAST templates yet', - emptySubtitle: 'Create a template to start monetizing your videos', - formats: { - preRoll: 'Pre-roll', - midRoll: 'Mid-roll', - postRoll: 'Post-roll', - }, - state: { - enabled: 'enabled', - disabled: 'disabled', - }, - table: { - template: 'Template', - format: 'Format', - vastUrl: 'VAST URL', - }, - dialog: { - editTitle: 'Edit Template', - createTitle: 'Create VAST Template', - templateName: 'Template Name', - templateNamePlaceholder: 'e.g., Main Pre-roll Ad', - vastUrlLabel: 'VAST Tag URL', - vastUrlPlaceholder: 'https://ads.example.com/vast/tag.xml', - adFormat: 'Ad Format', - adInterval: 'Ad Interval (seconds)', - adIntervalPlaceholder: '30', - update: 'Update', - create: 'Create', - }, - confirm: { - deleteMessage: 'Are you sure you want to delete "{name}"?', - deleteHeader: 'Delete Template', - deleteAccept: 'Delete', - deleteReject: 'Cancel', - }, - toast: { - nameRequiredSummary: 'Name Required', - nameRequiredDetail: 'Please enter a template name.', - urlRequiredSummary: 'VAST URL Required', - urlRequiredDetail: 'Please enter the VAST tag URL.', - invalidUrlSummary: 'Invalid URL', - invalidUrlDetail: 'Please enter a valid URL.', - durationRequiredSummary: 'Duration Required', - durationRequiredDetail: 'Mid-roll ads require a duration/interval.', - updatedSummary: 'Template Updated', - updatedDetail: 'VAST template has been updated.', - createdSummary: 'Template Created', - createdDetail: 'VAST template has been created.', - enabledSummary: 'Template Enabled', - disabledSummary: 'Template Disabled', - toggleDetail: '{name} has been {state}.', - deletedSummary: 'Template Deleted', - deletedDetail: 'VAST template has been removed.', - copiedSummary: 'Copied', - copiedDetail: 'URL copied to clipboard.', - }, - }, - profile: { - title: 'Profile Information', - subtitle: 'Manage your personal information and account details.', - userFallback: 'User', - username: 'Username', - email: 'Email Address', - storageUsage: 'Storage Usage', - storageUsedOfLimit: '{used} of {limit} used', - editProfile: 'Edit Profile', - changePassword: 'Change Password', - }, - connectedAccounts: { - title: 'Connected Accounts', - email: { - label: 'Email', - connected: 'Connected', - notConnected: 'Not connected', - disconnected: 'Disconnected', - }, - telegram: { - label: 'Telegram', - hint: 'Get notified via Telegram', - connectedFallback: 'Connected', - }, - }, - billing: { - walletBalance: 'Wallet Balance', - currentBalance: 'Current balance: {balance}', - topUp: 'Top Up', - availablePlans: 'Available Plans', - availablePlansHint: 'Choose the plan that best fits your needs', - planStorage: '{storage} Storage', - planDuration: '{duration} Max Duration', - planUploads: '{count} Uploads / day', - currentPlan: 'Current Plan', - processing: 'Processing...', - upgrade: 'Upgrade', - storage: 'Storage', - storageUsedOfLimit: '{used} of {limit} used', - monthlyUploads: 'Monthly Uploads', - uploadsUsedOfLimit: '{used} of {limit} uploads', - paymentHistory: 'Payment History', - paymentHistorySubtitle: 'Your past payments and invoices', - noPaymentHistory: 'No payment history found.', - download: 'Download', - durationMinutes: '{minutes} mins', - unknownPlan: 'Unknown', - table: { - date: 'Date', - amount: 'Amount', - plan: 'Plan', - status: 'Status', - invoice: 'Invoice', - }, - status: { - success: 'Success', - failed: 'Failed', - pending: 'Pending', - }, - topupDialog: { - title: 'Top Up Wallet', - subtitle: 'Select an amount or enter a custom amount to add to your wallet.', - customAmount: 'Custom Amount', - enterAmount: 'Enter amount', - hint: 'Minimum top-up amount is $1. Funds will be added to your wallet immediately after payment.', - proceed: 'Proceed to Payment', - }, - toast: { - subscriptionSuccessSummary: 'Subscription Successful', - subscriptionSuccessDetail: 'Successfully subscribed to {plan}', - subscriptionFailedSummary: 'Subscription Failed', - subscriptionFailedDetail: 'Failed to subscribe', - topupSuccessSummary: 'Top-up Successful', - topupSuccessDetail: '{amount} has been added to your wallet.', - topupFailedSummary: 'Top-up Failed', - topupFailedDetail: 'Failed to process top-up.', - downloadingSummary: 'Downloading', - downloadingDetail: 'Downloading invoice #{invoiceId}...', - downloadedSummary: 'Downloaded', - downloadedDetail: 'Invoice #{invoiceId} downloaded successfully', - }, - }, - securityConnected: { - header: { - title: 'Security & Connected Accounts', - subtitle: 'Manage your security settings and connected services.', - }, - accountStatus: { - label: 'Account Status', - detail: 'Your account is in good standing', - badge: 'Active', - }, - language: { - label: 'Language', - detail: 'Choose your preferred display language', - save: 'Save Language', - options: { - en: 'English', - vi: 'Tiếng Việt', - }, - toast: { - successSummary: 'Language Saved', - successDetail: 'Language has been updated.', - errorSummary: 'Save Failed', - errorDetail: 'Failed to save language on server. Applied from cookie fallback.', - }, - }, - twoFactor: { - label: 'Two-Factor Authentication', - enabled: '2FA is enabled', - disabled: 'Add an extra layer of security', - }, - changePassword: { - label: 'Change Password', - detail: 'Update your account password', - button: 'Change Password', - dialog: { - title: 'Change Password', - subtitle: 'Enter your current password and choose a new password.', - current: 'Current Password', - new: 'New Password', - confirm: 'Confirm New Password', - currentPlaceholder: 'Enter current password', - newPlaceholder: 'Enter new password', - confirmPlaceholder: 'Confirm new password', - submit: 'Change Password', - cancel: 'Cancel', - errors: { - mismatch: 'Passwords do not match', - minLength: 'Password must be at least 6 characters', - default: 'Failed to change password', - }, - }, - toast: { - successSummary: 'Password Changed', - successDetail: 'Your password has been changed successfully.', - }, - }, - logout: { - label: 'Logout', - detail: 'Sign out of your account on this device.', - button: 'Logout', - confirm: { - message: 'Are you sure you want to log out of your account?', - header: 'Log Out', - accept: 'Log Out', - reject: 'Cancel', - }, - }, - email: { - label: 'Email', - connected: 'Connected', - disconnected: 'Not connected', - badgeConnected: 'Connected', - badgeDisconnected: 'Disconnected', - }, - telegram: { - label: 'Telegram', - detailDisconnected: 'Get notified via Telegram', - connectedFallback: 'Connected', - connect: 'Connect', - disconnect: 'Disconnect', - }, - twoFactorDialog: { - title: 'Enable Two-Factor Authentication', - subtitle: 'Scan the QR code below with your authenticator app (Google Authenticator, Authy, etc.)', - secret: 'Secret Key:', - codeLabel: 'Verification Code', - codePlaceholder: 'Enter 6-digit code', - cancel: 'Cancel', - verify: 'Verify & Enable', - }, - toast: { - twoFactorEnabledSummary: '2FA Enabled', - twoFactorEnabledDetail: 'Two-factor authentication has been enabled successfully.', - twoFactorEnableFailedSummary: 'Enable 2FA Failed', - twoFactorEnableFailedDetail: 'Failed to enable two-factor authentication.', - twoFactorDisableFailedSummary: 'Disable 2FA Failed', - twoFactorDisableFailedDetail: 'Failed to disable two-factor authentication.', - twoFactorDisabledSummary: '2FA Disabled', - twoFactorDisabledDetail: 'Two-factor authentication has been disabled.', - twoFactorInvalidCodeSummary: 'Enable 2FA Failed', - twoFactorInvalidCodeDetail: 'Invalid verification code. Please try again.', - telegramConnectedSummary: 'Telegram Connected', - telegramConnectedDetail: 'Connected to {username}', - telegramConnectFailedSummary: 'Connection Failed', - telegramConnectFailedDetail: 'Failed to connect Telegram account.', - telegramDisconnectedSummary: 'Telegram Disconnected', - telegramDisconnectedDetail: 'Your Telegram account has been disconnected.', - telegramDisconnectFailedSummary: 'Disconnect Failed', - telegramDisconnectFailedDetail: 'Failed to disconnect Telegram account.', - }, - }, - }, - pageHeader: { - dashboard: 'Dashboard', - settings: 'Settings', - }, - confirm: { - defaultHeader: 'Confirm', - defaultAccept: 'OK', - defaultReject: 'Cancel', - }, - toast: { - dismissAria: 'Dismiss', - }, - overview: { - welcome: { - title: 'Welcome back, {name}! 👋', - subtitle: "Here's what's happening with your content today.", - }, - stats: { - totalVideos: 'Total Videos', - totalViews: 'Total Views', - storageUsed: 'Storage Used', - uploadsThisMonth: 'Uploads This Month', - trendVsLastMonth: 'vs last month', - }, - quickActions: { - title: 'Quick Actions', - uploadVideo: { - title: 'Upload Video', - description: 'Upload a new video to your library', - }, - videoLibrary: { - title: 'Video Library', - description: 'Browse all your videos', - }, - analytics: { - title: 'Analytics', - description: 'Track performance & insights', - }, - managePlan: { - title: 'Manage Plan', - description: 'Upgrade or change your plan', - }, - }, - referral: { - title: 'Referral Link', - subtitle: 'Share your referral link and earn commissions from referred users!', - }, - recentVideos: { - title: 'Recent Videos', - viewAll: 'View all', - emptyTitle: 'No videos found', - emptyDescription: "You haven't uploaded any videos yet. Start by uploading your first video!", - emptyAction: 'Upload Video', - table: { - video: 'Video', - status: 'Status', - duration: 'Duration', - uploadDate: 'Upload Date', - actions: 'Actions', - }, - noDescription: 'No description', - unknownStatus: 'Unknown', - actionEdit: 'Edit', - actionShare: 'Share', - actionDelete: 'Delete', - }, - storage: { - title: 'Storage Usage', - usedOfLimit: '{used} of {limit} used', - breakdown: { - videos: 'Videos', - thumbnails: 'Thumbnails & Assets', - other: 'Other Files', - }, - lowStorage: { - title: 'Storage running low', - message: 'Consider upgrading your plan to get more storage.', - viewPlans: 'View plans', - }, - }, - }, - video: { - page: { - title: 'My Videos', - description: 'Manage and organize your video library', - uploadAction: 'Upload Video', - uploadDropTitle: 'Drop to upload', - uploadDropSubtitle: 'Files will be added to the upload queue', - deleteSelectedConfirm: 'Delete {count} videos?', - deleteSingleConfirm: 'Are you sure you want to delete this video?', - retry: 'Try Again', - emptyTitle: 'No videos found', - emptyDescription: "You haven't uploaded any videos yet. Start by uploading your first video!", - emptyAction: 'Upload Video', - duplicateSummary: 'Duplicate files skipped', - duplicateDetailOne: '{count} file is already in the queue.', - duplicateDetailOther: '{count} files are already in the queue.', - }, - filters: { - searchPlaceholder: 'Search videos...', - rangeOfTotal: '{first}–{last} of {total}', - previousPageAria: 'Previous page', - nextPageAria: 'Next page', - allStatus: 'All Status', - ready: 'Ready', - processing: 'Processing', - failed: 'Failed', - }, - table: { - video: 'Video', - status: 'Status', - size: 'Size', - created: 'Created', - actions: 'Actions', - noDescription: 'No description', - copyLink: 'Copy link', - edit: 'Edit', - delete: 'Delete', - }, - bulk: { - selected: '{count} selected', - delete: 'Delete', - }, - copyModal: { - title: 'Get sharing address', - playerAddress: 'Player address', - embedPlayer: 'Embed player (recommended)', - thumbnail: 'Thumbnail URL', - hls: 'HLS link (VIP only)', - hlsPlaceholder: 'HLS link available for VIP with whitelisted domain', - hlsHint: 'This link redirects to a signed HLS URL and only works on whitelisted domains.', - warningTitle: 'Warning', - warningDetail: 'Make sure shared files comply with local laws and confirm you understand the responsibilities involved when distributing content.', - reminderTitle: 'Reminder', - reminderDetail: 'The embed player can auto switch fallback nodes and works well on mobile. Raw HLS links rely on your own player and must be used only on whitelisted domains.', - toastCopiedSummary: 'Copied', - toastCopiedDetail: 'Copied to clipboard', - toastErrorSummary: 'Error', - toastErrorDetail: 'Failed to load video details', - }, - detailModal: { - title: 'Edit video', - titleLabel: 'Title', - titlePlaceholder: 'Enter video title', - descriptionLabel: 'Description', - descriptionPlaceholder: 'Enter video description', - subtitlesTitle: 'Subtitles', - subtitleTracks: '{count} tracks', - noSubtitles: 'No subtitles uploaded yet', - uploadSubtitle: 'Upload Subtitle', - subtitleFile: 'Subtitle File (VTT, SRT, ASS, SSA)', - languageCode: 'Language Code *', - languagePlaceholder: 'en, vi, etc.', - displayName: 'Display Name (Optional)', - displayNamePlaceholder: 'English, Tiếng Việt, etc.', - uploadSubtitleButton: 'Upload Subtitle', - cancel: 'Cancel', - saveChanges: 'Save Changes', - errors: { - titleRequired: 'Title is required.', - }, - toast: { - loadErrorSummary: 'Error', - loadErrorDetail: 'Failed to load video details', - saveSuccessSummary: 'Success', - saveSuccessDetail: 'Video updated successfully', - saveErrorSummary: 'Error', - saveErrorDetail: 'Failed to save changes', - subtitleInfoSummary: 'Info', - subtitleInfoDetail: 'Subtitle upload not yet implemented', - }, - }, - detailPage: { - title: 'Video Detail', - description: 'View and manage video details', - loadingBreadcrumb: 'Loading...', - detailsTitle: 'Video Details', - copyValueTitle: 'Copy value', - videoTagFallback: 'Your browser does not support the video tag.', - saving: 'Saving...', - cancelEditTitle: 'Cancel editing', - reloadTitle: 'Reload video', - reloadButton: 'Reload', - confirmDelete: { - message: 'Are you sure you want to delete this video? This action cannot be undone.', - header: 'Confirm Delete', - accept: 'Delete', - reject: 'Cancel', - }, - toast: { - reloadSummary: 'Info', - reloadDetail: 'Reloading video...', - deleteSuccessSummary: 'Success', - deleteSuccessDetail: 'Video deleted successfully', - deleteErrorSummary: 'Error', - deleteErrorDetail: 'Failed to delete video', - copySummary: 'Copied', - copyDetail: '{label} copied to clipboard', - }, - videoInfo: { - videoId: 'Video ID', - thumbnailUrl: 'Thumbnail URL', - embedUrl: 'Embed URL', - iframeCode: 'Iframe Code', - shareLink: 'Share Link', - }, - }, - cardPopover: { - download: 'Download', - copyLink: 'Copy link', - edit: 'Edit', - delete: 'Delete', - toast: { - copySuccessSummary: 'Success', - copySuccessDetail: 'Video link copied', - copyErrorSummary: 'Error', - copyErrorDetail: 'Failed to copy link', - downloadSuccessSummary: 'Success', - downloadSuccessDetail: 'Downloading video...', - downloadErrorSummary: 'Error', - downloadErrorDetail: 'Video file not found', - }, - }, - }, - notification: { - title: 'Notifications', - subtitle: 'Stay updated with your latest activities and alerts.', - tabs: { - all: 'All', - unread: 'Unread', - videos: 'Videos', - payments: 'Payments', - }, - stats: { - total: '{count} notifications', - unread: '{count} unread', - }, - actions: { - markAllRead: 'Mark all read', - clearAll: 'Clear all', - viewAll: 'View all notifications', - viewVideo: 'View video', - viewReceipt: 'View receipt', - upgradePlan: 'Upgrade plan', - tryNow: 'Try it now', - }, - item: { - viewDetails: 'View Details', - markAsRead: 'Mark as read', - delete: 'Delete', - }, - empty: { - title: 'No notifications', - subtitle: "You're all caught up! Check back later.", - }, - time: { - minutesAgo: '{count} minutes ago', - hoursAgo: '{count} hours ago', - daysAgo: '{count} days ago', - }, - mocks: { - videoProcessed: { - title: 'Video processing complete', - message: 'Your video "Summer Vacation 2024" has been successfully processed and is now ready to stream.', - }, - paymentSuccess: { - title: 'Payment successful', - message: 'Your subscription to Pro Plan has been renewed successfully. Next billing date: Feb 25, 2026.', - }, - storageWarning: { - title: 'Storage almost full', - message: 'You have used 85% of your storage quota. Consider upgrading your plan for more space.', - }, - uploadSuccess: { - title: 'Upload successful', - message: 'Your video "Product Demo v2" has been uploaded successfully.', - }, - maintenance: { - title: 'Scheduled maintenance', - message: 'We will perform scheduled maintenance on Jan 30, 2026 from 2:00 AM to 4:00 AM UTC.', - }, - newFeature: { - title: 'New feature available', - message: 'We just launched video analytics! Track your video performance with detailed insights.', - }, - }, - }, - upload: { - dialog: { - title: 'Upload Videos', - subtitle: 'Add up to {maxItems} videos per batch', - mode: { - local: 'Local', - remote: 'Remote URL', - }, - queueFullTitle: 'Queue is full', - queueFullDescription: 'Maximum {maxItems} videos per batch. Start or clear the current queue first.', - slotsRemaining: '{remaining} / {maxItems} slots remaining', - formatsHint: 'MP4, MOV, MKV · max 10 GB per file', - close: 'Close', - startUpload: 'Start Upload ({count})', - duplicateFilesSummary: 'Duplicate files skipped', - duplicateFilesDetailOne: '{count} file is already in the queue.', - duplicateFilesDetailOther: '{count} files are already in the queue.', - duplicateUrlsSummary: 'Duplicate URLs skipped', - duplicateUrlsDetailOne: '{count} URL is already in the queue.', - duplicateUrlsDetailOther: '{count} URLs are already in the queue.', - }, - dropzone: { - releaseToAdd: 'Release to add', - dropHere: 'Drop videos here', - browse: 'or click anywhere to browse', - }, - remote: { - placeholder: 'Paste video URLs here, one per line\n\nhttps://example.com/video.mp4\nhttps://drive.google.com/...', - providersHint: 'Google Drive, Dropbox supported', - addUrls: 'Add URLs', - }, - queue: { - empty: 'Empty queue!', - totalSize: 'Total size:', - zeroSize: '0 MB', - }, - queueItem: { - remoteFileName: 'Remote File', - unknownSize: 'Unknown', - status: { - pending: 'Pending', - uploading: 'Uploading...', - uploadingThreads: 'Uploading · {threads} threads', - processing: 'Processing...', - complete: 'Done', - error: 'Failed', - fetching: 'Fetching...', - }, - }, - infoTip: { - title: 'Tip: For fastest processing', - description: 'Upload videos in H.264 video codec + AAC audio codec format (e.g., MP4 with H.264/AAC). Videos in this format will be processed much faster (seconds instead of minutes) because they do not need re-encoding.', - }, - bulkActions: { - title: 'Quick Settings', - applyToPending: 'Apply to {count} pending files', - selectCategory: 'Select category...', - category: { - learning: 'Learning', - entertainment: 'Entertainment', - }, - visibility: { - public: 'Public', - private: 'Private', - }, - }, - indicator: { - allDone: 'All done', - uploading: 'Uploading {count} files...', - waiting: '{count} files waiting', - completeProgress: '{complete} of {total} complete', - start: 'Start', - viewVideos: 'View Videos', - addMoreFiles: 'Add more files', - }, - errors: { - chunkUploadFailed: 'Failed to upload chunk {index}', - mergeFailed: 'Merge failed', - }, - }, - home: { - nav: { - features: 'Features', - pricing: 'Pricing', - login: 'Log in', - startFree: 'Start for free', - }, - hero: { - titleLine1: 'Video infrastructure for', - titleLine2: 'modern internet.', - subtitle: 'Seamlessly host, encode, and stream video with our developer-first API. Optimized for speed, built for scale.', - getStarted: 'Get Started', - uploadVideo: 'Upload video', - }, - features: { - heading: 'Everything you need to ship video', - subtitle: "Focus on building your product. We'll handle the complex video infrastructure.", - global: { - title: 'Global Edge Network', - description: 'Content delivered from 200+ PoPs worldwide. Automatic region selection ensures the lowest latency for every viewer.', - }, - live: { - title: 'Live Streaming API', - description: 'Scale to millions of concurrent viewers with ultra-low latency. RTMP ingest and HLS playback supported natively.', - status: 'Live Status', - onAir: 'On Air', - bitrate: 'Bitrate:', - fps: 'FPS:', - latency: 'Latency:', - bitrateValue: '6000 kbps', - fpsValue: '60', - latencyValue: '~2s', - }, - encoding: { - title: 'Instant Encoding', - description: 'Upload raw files and get optimized HLS/DASH streams in seconds.', - }, - analytics: { - title: 'Deep Analytics', - description: 'Session-level insights, quality of experience (QoE) metrics, and more.', - }, - }, - pricing: { - title: 'Simple, transparent pricing', - subtitle: 'Choose the plan that fits your needs. No hidden fees.', - perMonth: '/mo', - hobby: { - name: 'Hobby', - features: ['Unlimited upload', '1 Hour of Storage', 'Standard Support'], - button: 'Start Free', - }, - pro: { - name: 'Pro', - features: ['Ads free player', 'Support M3U8', 'Unlimited upload', 'Custom ads'], - button: 'Get Started', - tag: 'POPULAR', - }, - scale: { - name: 'Scale', - features: ['5 TB Bandwidth', '500 Hours Storage', 'Priority Support'], - button: 'Contact Sales', - tag: 'Best Value', - }, - }, - footer: { - description: 'Building the video layer of the internet. Designed for developers.', - product: 'Product', - productFeatures: 'Features', - productPricing: 'Pricing', - productShowcase: 'Showcase', - company: 'Company', - companyAbout: 'About', - companyBlog: 'Blog', - companyCareers: 'Careers', - legal: 'Legal', - privacy: 'Privacy', - terms: 'Terms', - copyright: '© {year} EcoStream Inc. All rights reserved.', - }, - head: { - title: 'EcoStream - Video infrastructure for modern internet', - description: 'Seamlessly host, encode, and stream video with our developer-first API. Optimized for speed, built for scale.', - }, - }, - legal: { - common: { - heading: 'Legal & Privacy Policy', - subheading: 'Legal & Privacy Policy', - description: 'Our legal and privacy policy.', - }, - terms: { - title: 'Terms and Conditions - Ecostream', - description: "Read Ecostream's terms and conditions for using our video hosting and streaming services.", - pageHeading: 'Terms and Conditions Details', - pageSubheading: 'Terms and Conditions', - pageDescription: "Our terms and conditions set forth important guidelines and rules for using Ecostream's services.", - sections: { - acceptanceTitle: '1. Acceptance of Terms', - acceptanceText: 'By accessing and using Ecostream, you accept and agree to be bound by the terms and provision of this agreement.', - usageTitle: '2. Service Usage', - usageText: 'You agree to use our service only for lawful purposes. You are prohibited from posting or transmitting any unlawful, threatening, libelous, defamatory, obscene, or profane material. We reserve the right to terminate accounts that violate these terms.', - ownershipTitle: '3. Content Ownership', - ownershipText: 'You retain all rights and ownership of the content you upload to Ecostream. However, by uploading content, you grant us a license to host, store, and display the content as necessary to provide our services.', - liabilityTitle: '4. Limitation of Liability', - liabilityText: 'Ecostream shall not be liable for any direct, indirect, incidental, special, or consequential damages resulting from the use or inability to use our service.', - changesTitle: '5. Changes to Terms', - changesText: 'We reserve the right to modify these terms at any time. Your continued use of the service after any such changes constitutes your acceptance of the new terms.', - }, - }, - privacy: { - title: 'Privacy Policy - Ecostream', - description: "Read about Ecostream's commitment to protecting your privacy and data security.", - pageHeading: 'Legal & Privacy Policy', - pageSubheading: 'Legal & Privacy Policy', - pageDescription: 'Our legal and privacy policy.', - sections: { - policyTitle: '1. Privacy Policy', - policyText: 'At Ecostream, we take your privacy seriously. This policy describes how we collect, use, and protect your personal information. We only collect information that is necessary for the operation of our service, including email addresses for account creation and payment information for subscription processing.', - dataCollectionTitle: '2. Data Collection', - dataCollectionText: 'We collect data such as IP addresses, browser types, and access times to analyze trends and improve our service. Uploaded content is stored securely and is only accessed as required for the delivery of our hosting services.', - cookieTitle: '3. Cookie Policy', - cookieText: 'We use cookies to maintain user sessions and preferences. By using our website, you consent to the use of cookies in accordance with this policy.', - dmcaTitle: '4. DMCA & Copyright', - dmcaText: 'Ecostream respects the intellectual property rights of others. We respond to notices of alleged copyright infringement in accordance with the Digital Millennium Copyright Act (DMCA). Please report any copyright violations to our support team.', - }, - }, - }, - notFound: { - headTitle: '404 - Page Not Found', - title: '404 - Page Not Found', - description: 'The page you are looking for does not exist.', - backHome: 'Go back to Home', - }, -}; - -export default en; diff --git a/src/i18n/messages/vi.ts b/src/i18n/messages/vi.ts deleted file mode 100644 index 1044cdb..0000000 --- a/src/i18n/messages/vi.ts +++ /dev/null @@ -1,1045 +0,0 @@ -const vi = { - common: { - save: 'Lưu', - close: 'Đóng', - cancel: 'Hủy', - connect: 'Kết nối', - disconnect: 'Ngắt kết nối', - upload: 'Tải lên', - loading: 'Đang tải', - actions: 'Hành động', - status: 'Trạng thái', - videos: 'Video', - selected: '{count} mục đã chọn', - copy: 'Sao chép', - }, - app: { - name: 'EcoStream', - }, - auth: { - layout: { - login: { - headTitle: 'Đăng nhập vào tài khoản', - title: 'Đăng nhập vào bảng điều khiển', - subtitle: 'Vui lòng nhập thông tin để đăng nhập.', - }, - signup: { - headTitle: 'Tạo tài khoản', - title: 'Tạo tài khoản', - subtitle: 'Vui lòng điền thông tin để tạo tài khoản.', - }, - forgot: { - headTitle: 'Đặt lại mật khẩu', - title: 'Quên mật khẩu?', - subtitle: 'Nhập email và chúng tôi sẽ gửi liên kết đặt lại mật khẩu.', - }, - }, - login: { - email: 'Email', - password: 'Mật khẩu', - forgotPassword: 'Quên mật khẩu?', - signIn: 'Đăng nhập', - google: 'Google', - noAccount: 'Chưa có tài khoản?', - signUp: 'Đăng ký', - errors: { - emailRequired: 'Email là bắt buộc.', - emailInvalid: 'Địa chỉ email không hợp lệ.', - passwordRequired: 'Mật khẩu là bắt buộc.', - }, - }, - signup: { - fullName: 'Họ và tên', - email: 'Địa chỉ email', - password: 'Mật khẩu', - passwordHint: 'Tối thiểu 8 ký tự.', - createAccount: 'Tạo tài khoản', - alreadyHave: 'Đã có tài khoản?', - signIn: 'Đăng nhập', - placeholders: { - name: 'Nguyễn Văn A', - email: 'ban@example.com', - password: 'Tạo mật khẩu', - }, - errors: { - nameRequired: 'Tên là bắt buộc.', - emailRequired: 'Email là bắt buộc.', - emailInvalid: 'Địa chỉ email không hợp lệ.', - passwordMin: 'Mật khẩu phải có ít nhất 8 ký tự.', - }, - }, - forgot: { - description: 'Nhập email và chúng tôi sẽ gửi liên kết đặt lại mật khẩu.', - email: 'Địa chỉ email', - sendResetLink: 'Gửi liên kết đặt lại', - backToSignIn: 'Quay lại đăng nhập', - placeholders: { - email: 'ban@example.com', - }, - errors: { - emailRequired: 'Email là bắt buộc.', - emailInvalid: 'Địa chỉ email không hợp lệ.', - }, - toast: { - successSummary: 'Thành công', - successDetail: 'Đã gửi liên kết đặt lại', - errorSummary: 'Lỗi', - errorDetail: 'Đã xảy ra lỗi', - }, - }, - errors: { - loginNoUserData: 'Đăng nhập thất bại: Không nhận được dữ liệu người dùng', - loginFailed: 'Đăng nhập thất bại: {error}', - registrationFailedFallback: 'Đăng ký thất bại', - registrationFailed: 'Đăng ký thất bại: {error}', - updateProfileFailed: 'Cập nhật hồ sơ thất bại: {error}', - changePasswordFailed: 'Đổi mật khẩu thất bại: {error}', - unknown: 'Lỗi không xác định', - }, - }, - nav: { - overview: 'Tổng quan', - videos: 'Video', - notification: 'Thông báo', - settings: 'Cài đặt', - }, - settings: { - menu: { - securityGroup: 'Bảo mật', - preferencesGroup: 'Tùy chọn', - integrationsGroup: 'Tích hợp', - dangerGroup: 'Vùng nguy hiểm', - security: 'Bảo mật', - billing: 'Thanh toán & Gói', - notifications: 'Thông báo', - player: 'Trình phát', - domains: 'Tên miền được phép', - ads: 'Quảng cáo & VAST', - danger: 'Vùng nguy hiểm', - }, - content: { - fallbackTitle: 'Cài đặt', - fallbackSubtitle: 'Quản lý cài đặt và tùy chọn tài khoản của bạn.', - security: { - title: 'Bảo mật & Tài khoản liên kết', - subtitle: 'Quản lý bảo mật và ứng dụng đã liên kết.', - }, - notifications: { - title: 'Thông báo', - subtitle: 'Chọn cách bạn muốn nhận thông báo và cập nhật.', - }, - player: { - title: 'Cài đặt trình phát', - subtitle: 'Cấu hình hành vi và tính năng mặc định của trình phát video.', - }, - billing: { - title: 'Thanh toán & Gói', - subtitle: 'Thông tin gói và thanh toán hiện tại của bạn.', - }, - domains: { - title: 'Tên miền được phép', - subtitle: 'Thêm tên miền vào whitelist để cho phép nhúng nội dung qua iframe.', - }, - ads: { - title: 'Quảng cáo & VAST', - subtitle: 'Tạo và quản lý mẫu quảng cáo VAST cho video.', - }, - danger: { - title: 'Vùng nguy hiểm', - subtitle: 'Hành động không thể hoàn tác và có tính phá hủy. Hãy cẩn thận!', - }, - }, - notificationSettings: { - saveChanges: 'Lưu thay đổi', - types: { - email: { - title: 'Thông báo Email', - description: 'Nhận cập nhật và cảnh báo qua email', - }, - push: { - title: 'Thông báo đẩy', - description: 'Nhận cảnh báo tức thì trên trình duyệt', - }, - marketing: { - title: 'Email marketing', - description: 'Nhận khuyến mãi và cập nhật sản phẩm', - }, - telegram: { - title: 'Thông báo Telegram', - description: 'Nhận cập nhật qua Telegram', - }, - }, - toast: { - savedSummary: 'Đã lưu cài đặt', - savedDetail: 'Cài đặt thông báo của bạn đã được lưu.', - failedSummary: 'Lưu thất bại', - failedDetail: 'Không thể lưu cài đặt.', - }, - }, - playerSettings: { - toast: { - savedSummary: 'Đã lưu cài đặt', - savedDetail: 'Cài đặt trình phát của bạn đã được lưu.', - failedSummary: 'Lưu thất bại', - failedDetail: 'Không thể lưu cài đặt.', - }, - items: { - autoplay: { - title: 'Tự phát', - description: 'Tự động phát video khi tải xong', - }, - loop: { - title: 'Lặp lại', - description: 'Phát lại video khi kết thúc', - }, - muted: { - title: 'Tắt tiếng', - description: 'Bắt đầu video với âm thanh tắt', - }, - showControls: { - title: 'Hiển thị điều khiển', - description: 'Hiển thị thanh điều khiển (phát, tạm dừng, âm lượng)', - }, - pip: { - title: 'Picture in Picture', - description: 'Bật chế độ Picture-in-Picture', - }, - airplay: { - title: 'AirPlay', - description: 'Cho phép phát tới thiết bị Apple qua AirPlay', - }, - chromecast: { - title: 'Chromecast', - description: 'Cho phép cast tới thiết bị Chromecast', - }, - }, - }, - dangerZone: { - deleteAccount: { - title: 'Xóa tài khoản', - description: 'Xóa vĩnh viễn tài khoản và toàn bộ dữ liệu liên quan.', - button: 'Xóa tài khoản', - }, - clearData: { - title: 'Xóa toàn bộ dữ liệu', - description: 'Xóa tất cả video, danh sách phát và lịch sử hoạt động của bạn.', - button: 'Xóa dữ liệu', - }, - warning: { - title: 'Cảnh báo', - description: 'Các hành động này là vĩnh viễn và không thể hoàn tác. Hãy đảm bảo bạn đã sao lưu dữ liệu quan trọng trước khi tiếp tục.', - }, - confirm: { - deleteAccountMessage: 'Bạn có chắc muốn xóa tài khoản? Hành động này không thể hoàn tác.', - deleteAccountHeader: 'Xóa tài khoản', - deleteAccountAccept: 'Xóa', - deleteAccountReject: 'Hủy', - clearDataMessage: 'Bạn có chắc muốn xóa toàn bộ dữ liệu? Hành động này không thể hoàn tác.', - clearDataHeader: 'Xóa toàn bộ dữ liệu', - clearDataAccept: 'Xóa', - clearDataReject: 'Hủy', - }, - toast: { - deleteAccountSummary: 'Đã gửi yêu cầu xóa tài khoản', - deleteAccountDetail: 'Yêu cầu xóa tài khoản của bạn đã được gửi.', - clearDataSummary: 'Đã xóa dữ liệu', - clearDataDetail: 'Toàn bộ dữ liệu của bạn đã bị xóa vĩnh viễn.', - }, - }, - domainsDns: { - addDomain: 'Thêm tên miền', - infoBanner: 'Chỉ các tên miền trong whitelist mới có thể nhúng nội dung của bạn qua iframe.', - table: { - domain: 'Tên miền', - addedDate: 'Ngày thêm', - }, - emptyTitle: 'Chưa có tên miền trong whitelist', - emptySubtitle: 'Thêm tên miền để cho phép nhúng iframe', - embedCodeTitle: 'Mã nhúng', - copyCode: 'Sao chép mã', - embedCodeHint: 'Dùng đoạn iframe này để nhúng nội dung trên các tên miền đã whitelist.', - dialog: { - title: 'Thêm tên miền vào whitelist', - domainLabel: 'Tên miền', - domainPlaceholder: 'example.com', - domainHint: 'Nhập tên miền không kèm www hoặc https:// (ví dụ: example.com)', - importantTitle: 'Quan trọng', - importantDetail: 'Chỉ thêm những tên miền bạn sở hữu và kiểm soát.', - }, - confirm: { - removeMessage: 'Bạn có chắc muốn xóa {domain} khỏi whitelist? Các iframe nhúng từ tên miền này sẽ không còn hoạt động.', - removeHeader: 'Xóa tên miền', - removeAccept: 'Xóa', - removeReject: 'Hủy', - }, - toast: { - invalidSummary: 'Tên miền không hợp lệ', - invalidDetail: 'Vui lòng nhập tên miền hợp lệ.', - duplicateSummary: 'Tên miền đã tồn tại', - duplicateDetail: 'Tên miền này đã có trong whitelist.', - addedSummary: 'Đã thêm tên miền', - addedDetail: '{domain} đã được thêm vào whitelist.', - removedSummary: 'Đã xóa tên miền', - removedDetail: '{domain} đã được xóa khỏi whitelist.', - copiedSummary: 'Đã sao chép', - copiedDetail: 'Đã sao chép mã nhúng vào clipboard.', - }, - }, - adsVast: { - createTemplate: 'Tạo mẫu', - infoBanner: 'VAST (Video Ad Serving Template) là schema XML dùng để phân phối ad tags cho trình phát video.', - createdOn: 'Tạo ngày {date}', - emptyTitle: 'Chưa có mẫu VAST', - emptySubtitle: 'Tạo mẫu để bắt đầu kiếm tiền từ video', - formats: { - preRoll: 'Pre-roll', - midRoll: 'Mid-roll', - postRoll: 'Post-roll', - }, - state: { - enabled: 'bật', - disabled: 'tắt', - }, - table: { - template: 'Mẫu', - format: 'Định dạng', - vastUrl: 'VAST URL', - }, - dialog: { - editTitle: 'Sửa mẫu', - createTitle: 'Tạo mẫu VAST', - templateName: 'Tên mẫu', - templateNamePlaceholder: 'ví dụ: Main Pre-roll Ad', - vastUrlLabel: 'VAST Tag URL', - vastUrlPlaceholder: 'https://ads.example.com/vast/tag.xml', - adFormat: 'Định dạng quảng cáo', - adInterval: 'Khoảng cách quảng cáo (giây)', - adIntervalPlaceholder: '30', - update: 'Cập nhật', - create: 'Tạo', - }, - confirm: { - deleteMessage: 'Bạn có chắc muốn xóa "{name}"?', - deleteHeader: 'Xóa mẫu', - deleteAccept: 'Xóa', - deleteReject: 'Hủy', - }, - toast: { - nameRequiredSummary: 'Thiếu tên mẫu', - nameRequiredDetail: 'Vui lòng nhập tên mẫu.', - urlRequiredSummary: 'Thiếu VAST URL', - urlRequiredDetail: 'Vui lòng nhập VAST tag URL.', - invalidUrlSummary: 'URL không hợp lệ', - invalidUrlDetail: 'Vui lòng nhập URL hợp lệ.', - durationRequiredSummary: 'Thiếu thời lượng', - durationRequiredDetail: 'Quảng cáo mid-roll yêu cầu thời lượng/khoảng cách.', - updatedSummary: 'Đã cập nhật mẫu', - updatedDetail: 'Mẫu VAST đã được cập nhật.', - createdSummary: 'Đã tạo mẫu', - createdDetail: 'Mẫu VAST đã được tạo.', - enabledSummary: 'Đã bật mẫu', - disabledSummary: 'Đã tắt mẫu', - toggleDetail: '{name} đã được {state}.', - deletedSummary: 'Đã xóa mẫu', - deletedDetail: 'Mẫu VAST đã được gỡ bỏ.', - copiedSummary: 'Đã sao chép', - copiedDetail: 'Đã sao chép URL vào clipboard.', - }, - }, - profile: { - title: 'Thông tin hồ sơ', - subtitle: 'Quản lý thông tin cá nhân và chi tiết tài khoản của bạn.', - userFallback: 'Người dùng', - username: 'Tên người dùng', - email: 'Địa chỉ email', - storageUsage: 'Dung lượng sử dụng', - storageUsedOfLimit: 'Đã dùng {used} trên {limit}', - editProfile: 'Chỉnh sửa hồ sơ', - changePassword: 'Đổi mật khẩu', - }, - connectedAccounts: { - title: 'Tài khoản liên kết', - email: { - label: 'Email', - connected: 'Đã kết nối', - notConnected: 'Chưa kết nối', - disconnected: 'Đã ngắt kết nối', - }, - telegram: { - label: 'Telegram', - hint: 'Nhận thông báo qua Telegram', - connectedFallback: 'Đã kết nối', - }, - }, - billing: { - walletBalance: 'Số dư ví', - currentBalance: 'Số dư hiện tại: {balance}', - topUp: 'Nạp tiền', - availablePlans: 'Các gói khả dụng', - availablePlansHint: 'Chọn gói phù hợp nhất với nhu cầu của bạn', - planStorage: '{storage} dung lượng', - planDuration: '{duration} thời lượng tối đa', - planUploads: '{count} lượt tải / ngày', - currentPlan: 'Gói hiện tại', - processing: 'Đang xử lý...', - upgrade: 'Nâng cấp', - storage: 'Dung lượng', - storageUsedOfLimit: 'Đã dùng {used} trên {limit}', - monthlyUploads: 'Lượt tải tháng này', - uploadsUsedOfLimit: '{used} trên {limit} lượt tải', - paymentHistory: 'Lịch sử thanh toán', - paymentHistorySubtitle: 'Các khoản thanh toán và hóa đơn trước đây của bạn', - noPaymentHistory: 'Không tìm thấy lịch sử thanh toán.', - download: 'Tải xuống', - durationMinutes: '{minutes} phút', - unknownPlan: 'Không xác định', - table: { - date: 'Ngày', - amount: 'Số tiền', - plan: 'Gói', - status: 'Trạng thái', - invoice: 'Hóa đơn', - }, - status: { - success: 'Thành công', - failed: 'Thất bại', - pending: 'Đang chờ', - }, - topupDialog: { - title: 'Nạp tiền vào ví', - subtitle: 'Chọn số tiền hoặc nhập số tiền tùy chỉnh để nạp vào ví.', - customAmount: 'Số tiền tùy chỉnh', - enterAmount: 'Nhập số tiền', - hint: 'Số tiền nạp tối thiểu là $1. Tiền sẽ được cộng vào ví ngay sau khi thanh toán.', - proceed: 'Tiếp tục thanh toán', - }, - toast: { - subscriptionSuccessSummary: 'Đăng ký thành công', - subscriptionSuccessDetail: 'Đăng ký gói {plan} thành công', - subscriptionFailedSummary: 'Đăng ký thất bại', - subscriptionFailedDetail: 'Không thể đăng ký gói', - topupSuccessSummary: 'Nạp tiền thành công', - topupSuccessDetail: '{amount} đã được cộng vào ví của bạn.', - topupFailedSummary: 'Nạp tiền thất bại', - topupFailedDetail: 'Không thể xử lý nạp tiền.', - downloadingSummary: 'Đang tải', - downloadingDetail: 'Đang tải hóa đơn #{invoiceId}...', - downloadedSummary: 'Đã tải xong', - downloadedDetail: 'Hóa đơn #{invoiceId} đã được tải thành công', - }, - }, - securityConnected: { - header: { - title: 'Bảo mật & Tài khoản liên kết', - subtitle: 'Quản lý cài đặt bảo mật và dịch vụ đã kết nối.', - }, - accountStatus: { - label: 'Trạng thái tài khoản', - detail: 'Tài khoản của bạn đang hoạt động tốt', - badge: 'Đang hoạt động', - }, - language: { - label: 'Ngôn ngữ', - detail: 'Chọn ngôn ngữ hiển thị ưu tiên', - save: 'Lưu ngôn ngữ', - options: { - en: 'English', - vi: 'Tiếng Việt', - }, - toast: { - successSummary: 'Đã lưu ngôn ngữ', - successDetail: 'Ngôn ngữ đã được cập nhật.', - errorSummary: 'Lưu thất bại', - errorDetail: 'Không thể lưu ngôn ngữ lên server. Đã áp dụng theo cookie dự phòng.', - }, - }, - twoFactor: { - label: 'Xác thực hai lớp', - enabled: '2FA đã bật', - disabled: 'Thêm một lớp bảo mật', - }, - changePassword: { - label: 'Đổi mật khẩu', - detail: 'Cập nhật mật khẩu tài khoản', - button: 'Đổi mật khẩu', - dialog: { - title: 'Đổi mật khẩu', - subtitle: 'Nhập mật khẩu hiện tại và chọn mật khẩu mới.', - current: 'Mật khẩu hiện tại', - new: 'Mật khẩu mới', - confirm: 'Xác nhận mật khẩu mới', - currentPlaceholder: 'Nhập mật khẩu hiện tại', - newPlaceholder: 'Nhập mật khẩu mới', - confirmPlaceholder: 'Xác nhận mật khẩu mới', - submit: 'Đổi mật khẩu', - cancel: 'Hủy', - errors: { - mismatch: 'Mật khẩu xác nhận không khớp', - minLength: 'Mật khẩu phải có ít nhất 6 ký tự', - default: 'Không thể đổi mật khẩu', - }, - }, - toast: { - successSummary: 'Đổi mật khẩu thành công', - successDetail: 'Mật khẩu của bạn đã được cập nhật.', - }, - }, - logout: { - label: 'Đăng xuất', - detail: 'Đăng xuất khỏi thiết bị này.', - button: 'Đăng xuất', - confirm: { - message: 'Bạn có chắc muốn đăng xuất?', - header: 'Đăng xuất', - accept: 'Đăng xuất', - reject: 'Hủy', - }, - }, - email: { - label: 'Email', - connected: 'Đã kết nối', - disconnected: 'Chưa kết nối', - badgeConnected: 'Đã kết nối', - badgeDisconnected: 'Đã ngắt kết nối', - }, - telegram: { - label: 'Telegram', - detailDisconnected: 'Nhận thông báo qua Telegram', - connectedFallback: 'Đã kết nối', - connect: 'Kết nối', - disconnect: 'Ngắt kết nối', - }, - twoFactorDialog: { - title: 'Bật xác thực hai lớp', - subtitle: 'Quét mã QR bằng ứng dụng xác thực (Google Authenticator, Authy, v.v.)', - secret: 'Khóa bí mật:', - codeLabel: 'Mã xác thực', - codePlaceholder: 'Nhập mã 6 chữ số', - cancel: 'Hủy', - verify: 'Xác minh & Bật', - }, - toast: { - twoFactorEnabledSummary: 'Đã bật 2FA', - twoFactorEnabledDetail: 'Xác thực hai lớp đã được bật thành công.', - twoFactorEnableFailedSummary: 'Bật 2FA thất bại', - twoFactorEnableFailedDetail: 'Không thể bật xác thực hai lớp.', - twoFactorDisableFailedSummary: 'Tắt 2FA thất bại', - twoFactorDisableFailedDetail: 'Không thể tắt xác thực hai lớp.', - twoFactorDisabledSummary: 'Đã tắt 2FA', - twoFactorDisabledDetail: 'Xác thực hai lớp đã bị tắt.', - twoFactorInvalidCodeSummary: 'Bật 2FA thất bại', - twoFactorInvalidCodeDetail: 'Mã xác thực không hợp lệ. Vui lòng thử lại.', - telegramConnectedSummary: 'Đã kết nối Telegram', - telegramConnectedDetail: 'Đã kết nối với {username}', - telegramConnectFailedSummary: 'Kết nối thất bại', - telegramConnectFailedDetail: 'Không thể kết nối tài khoản Telegram.', - telegramDisconnectedSummary: 'Đã ngắt Telegram', - telegramDisconnectedDetail: 'Tài khoản Telegram đã được ngắt.', - telegramDisconnectFailedSummary: 'Ngắt kết nối thất bại', - telegramDisconnectFailedDetail: 'Không thể ngắt kết nối Telegram.', - }, - }, - }, - pageHeader: { - dashboard: 'Bảng điều khiển', - settings: 'Cài đặt', - }, - confirm: { - defaultHeader: 'Xác nhận', - defaultAccept: 'Đồng ý', - defaultReject: 'Hủy', - }, - toast: { - dismissAria: 'Đóng', - }, - overview: { - welcome: { - title: 'Chào mừng trở lại, {name}! 👋', - subtitle: 'Đây là tình hình nội dung của bạn hôm nay.', - }, - stats: { - totalVideos: 'Tổng số video', - totalViews: 'Tổng lượt xem', - storageUsed: 'Dung lượng đã dùng', - uploadsThisMonth: 'Lượt tải lên tháng này', - trendVsLastMonth: 'so với tháng trước', - }, - quickActions: { - title: 'Thao tác nhanh', - uploadVideo: { - title: 'Tải video lên', - description: 'Tải video mới vào thư viện', - }, - videoLibrary: { - title: 'Thư viện video', - description: 'Duyệt tất cả video của bạn', - }, - analytics: { - title: 'Phân tích', - description: 'Theo dõi hiệu suất & thông tin chi tiết', - }, - managePlan: { - title: 'Quản lý gói', - description: 'Nâng cấp hoặc thay đổi gói', - }, - }, - referral: { - title: 'Liên kết giới thiệu', - subtitle: 'Chia sẻ liên kết giới thiệu và nhận hoa hồng từ người dùng được giới thiệu!', - }, - recentVideos: { - title: 'Video gần đây', - viewAll: 'Xem tất cả', - emptyTitle: 'Không có video', - emptyDescription: 'Bạn chưa tải video nào. Hãy bắt đầu với video đầu tiên!', - emptyAction: 'Tải video lên', - table: { - video: 'Video', - status: 'Trạng thái', - duration: 'Thời lượng', - uploadDate: 'Ngày tải lên', - actions: 'Hành động', - }, - noDescription: 'Không có mô tả', - unknownStatus: 'Không xác định', - actionEdit: 'Sửa', - actionShare: 'Chia sẻ', - actionDelete: 'Xóa', - }, - storage: { - title: 'Sử dụng dung lượng', - usedOfLimit: 'Đã dùng {used} trên {limit}', - breakdown: { - videos: 'Video', - thumbnails: 'Thumbnail & tài nguyên', - other: 'Tệp khác', - }, - lowStorage: { - title: 'Dung lượng sắp đầy', - message: 'Hãy cân nhắc nâng cấp gói để có thêm dung lượng.', - viewPlans: 'Xem các gói', - }, - }, - }, - video: { - page: { - title: 'Video của tôi', - description: 'Quản lý và sắp xếp thư viện video', - uploadAction: 'Tải video lên', - uploadDropTitle: 'Thả để tải lên', - uploadDropSubtitle: 'Tệp sẽ được thêm vào hàng đợi tải lên', - deleteSelectedConfirm: 'Xóa {count} video?', - deleteSingleConfirm: 'Bạn có chắc muốn xóa video này?', - retry: 'Thử lại', - emptyTitle: 'Không có video', - emptyDescription: 'Bạn chưa tải video nào. Hãy bắt đầu với video đầu tiên!', - emptyAction: 'Tải video lên', - duplicateSummary: 'Đã bỏ qua tệp trùng lặp', - duplicateDetailOne: '{count} tệp đã có trong hàng đợi.', - duplicateDetailOther: '{count} tệp đã có trong hàng đợi.', - }, - filters: { - searchPlaceholder: 'Tìm kiếm video...', - rangeOfTotal: '{first}–{last} / {total}', - previousPageAria: 'Trang trước', - nextPageAria: 'Trang sau', - allStatus: 'Tất cả trạng thái', - ready: 'Sẵn sàng', - processing: 'Đang xử lý', - failed: 'Thất bại', - }, - table: { - video: 'Video', - status: 'Trạng thái', - size: 'Dung lượng', - created: 'Ngày tạo', - actions: 'Hành động', - noDescription: 'Không có mô tả', - copyLink: 'Sao chép liên kết', - edit: 'Chỉnh sửa', - delete: 'Xóa', - }, - bulk: { - selected: '{count} mục đã chọn', - delete: 'Xóa', - }, - copyModal: { - title: 'Lấy địa chỉ chia sẻ', - playerAddress: 'Địa chỉ phát', - embedPlayer: 'Nhúng trình phát (khuyến nghị)', - thumbnail: 'URL thumbnail', - hls: 'Liên kết HLS (chỉ VIP)', - hlsPlaceholder: 'Liên kết HLS cho VIP với domain được whitelist', - hlsHint: 'Liên kết này chuyển hướng tới URL HLS đã ký và chỉ hoạt động trên domain đã whitelist.', - warningTitle: 'Cảnh báo', - warningDetail: 'Hãy đảm bảo tệp chia sẻ tuân thủ pháp luật địa phương và bạn hiểu rõ trách nhiệm khi phân phối nội dung.', - reminderTitle: 'Lưu ý', - reminderDetail: 'Trình phát nhúng có thể tự chuyển node dự phòng và hoạt động tốt trên di động. Liên kết HLS thô phụ thuộc vào trình phát riêng của bạn và chỉ dùng trên domain đã whitelist.', - toastCopiedSummary: 'Đã sao chép', - toastCopiedDetail: 'Đã sao chép vào clipboard', - toastErrorSummary: 'Lỗi', - toastErrorDetail: 'Không thể tải chi tiết video', - }, - detailModal: { - title: 'Chỉnh sửa video', - titleLabel: 'Tiêu đề', - titlePlaceholder: 'Nhập tiêu đề video', - descriptionLabel: 'Mô tả', - descriptionPlaceholder: 'Nhập mô tả video', - subtitlesTitle: 'Phụ đề', - subtitleTracks: '{count} track', - noSubtitles: 'Chưa có phụ đề', - uploadSubtitle: 'Tải phụ đề', - subtitleFile: 'Tệp phụ đề (VTT, SRT, ASS, SSA)', - languageCode: 'Mã ngôn ngữ *', - languagePlaceholder: 'en, vi, v.v.', - displayName: 'Tên hiển thị (Tùy chọn)', - displayNamePlaceholder: 'English, Tiếng Việt, v.v.', - uploadSubtitleButton: 'Tải phụ đề', - cancel: 'Hủy', - saveChanges: 'Lưu thay đổi', - errors: { - titleRequired: 'Tiêu đề là bắt buộc.', - }, - toast: { - loadErrorSummary: 'Lỗi', - loadErrorDetail: 'Không thể tải chi tiết video', - saveSuccessSummary: 'Thành công', - saveSuccessDetail: 'Cập nhật video thành công', - saveErrorSummary: 'Lỗi', - saveErrorDetail: 'Không thể lưu thay đổi', - subtitleInfoSummary: 'Thông tin', - subtitleInfoDetail: 'Tải phụ đề chưa được hỗ trợ', - }, - }, - detailPage: { - title: 'Chi tiết video', - description: 'Xem và quản lý thông tin video', - loadingBreadcrumb: 'Đang tải...', - detailsTitle: 'Thông tin video', - copyValueTitle: 'Sao chép giá trị', - videoTagFallback: 'Trình duyệt của bạn không hỗ trợ thẻ video.', - saving: 'Đang lưu...', - cancelEditTitle: 'Hủy chỉnh sửa', - reloadTitle: 'Tải lại video', - reloadButton: 'Tải lại', - confirmDelete: { - message: 'Bạn có chắc muốn xóa video này? Hành động này không thể hoàn tác.', - header: 'Xác nhận xóa', - accept: 'Xóa', - reject: 'Hủy', - }, - toast: { - reloadSummary: 'Thông tin', - reloadDetail: 'Đang tải lại video...', - deleteSuccessSummary: 'Thành công', - deleteSuccessDetail: 'Xóa video thành công', - deleteErrorSummary: 'Lỗi', - deleteErrorDetail: 'Không thể xóa video', - copySummary: 'Đã sao chép', - copyDetail: 'Đã sao chép {label} vào clipboard', - }, - videoInfo: { - videoId: 'ID video', - thumbnailUrl: 'URL thumbnail', - embedUrl: 'URL nhúng', - iframeCode: 'Mã iframe', - shareLink: 'Liên kết chia sẻ', - }, - }, - cardPopover: { - download: 'Tải xuống', - copyLink: 'Sao chép liên kết', - edit: 'Chỉnh sửa', - delete: 'Xóa', - toast: { - copySuccessSummary: 'Thành công', - copySuccessDetail: 'Đã sao chép liên kết video', - copyErrorSummary: 'Lỗi', - copyErrorDetail: 'Không thể sao chép liên kết', - downloadSuccessSummary: 'Thành công', - downloadSuccessDetail: 'Đang tải xuống video...', - downloadErrorSummary: 'Lỗi', - downloadErrorDetail: 'Không tìm thấy tệp video', - }, - }, - }, - notification: { - title: 'Thông báo', - subtitle: 'Luôn cập nhật các hoạt động và cảnh báo mới nhất của bạn.', - tabs: { - all: 'Tất cả', - unread: 'Chưa đọc', - videos: 'Video', - payments: 'Thanh toán', - }, - stats: { - total: '{count} thông báo', - unread: '{count} chưa đọc', - }, - actions: { - markAllRead: 'Đánh dấu đã đọc tất cả', - clearAll: 'Xóa tất cả', - viewAll: 'Xem tất cả thông báo', - viewVideo: 'Xem video', - viewReceipt: 'Xem biên lai', - upgradePlan: 'Nâng cấp gói', - tryNow: 'Dùng thử ngay', - }, - item: { - viewDetails: 'Xem chi tiết', - markAsRead: 'Đánh dấu đã đọc', - delete: 'Xóa', - }, - empty: { - title: 'Không có thông báo', - subtitle: 'Bạn đã xem hết! Hãy quay lại sau.', - }, - time: { - minutesAgo: '{count} phút trước', - hoursAgo: '{count} giờ trước', - daysAgo: '{count} ngày trước', - }, - mocks: { - videoProcessed: { - title: 'Xử lý video hoàn tất', - message: 'Video "Summer Vacation 2024" của bạn đã được xử lý thành công và sẵn sàng phát.', - }, - paymentSuccess: { - title: 'Thanh toán thành công', - message: 'Gói Pro Plan của bạn đã được gia hạn thành công. Kỳ thanh toán tiếp theo: 25/02/2026.', - }, - storageWarning: { - title: 'Dung lượng sắp đầy', - message: 'Bạn đã sử dụng 85% hạn mức lưu trữ. Hãy cân nhắc nâng cấp gói để có thêm dung lượng.', - }, - uploadSuccess: { - title: 'Tải lên thành công', - message: 'Video "Product Demo v2" của bạn đã được tải lên thành công.', - }, - maintenance: { - title: 'Bảo trì định kỳ', - message: 'Chúng tôi sẽ bảo trì hệ thống vào ngày 30/01/2026 từ 02:00 đến 04:00 UTC.', - }, - newFeature: { - title: 'Tính năng mới', - message: 'Chúng tôi vừa ra mắt phân tích video! Theo dõi hiệu suất video với dữ liệu chi tiết.', - }, - }, - }, - upload: { - dialog: { - title: 'Tải video lên', - subtitle: 'Thêm tối đa {maxItems} video mỗi đợt', - mode: { - local: 'Tệp cục bộ', - remote: 'URL từ xa', - }, - queueFullTitle: 'Hàng đợi đã đầy', - queueFullDescription: 'Tối đa {maxItems} video mỗi đợt. Hãy bắt đầu hoặc xóa hàng đợi hiện tại trước.', - slotsRemaining: 'Còn {remaining} / {maxItems} vị trí', - formatsHint: 'MP4, MOV, MKV · tối đa 10 GB mỗi tệp', - close: 'Đóng', - startUpload: 'Bắt đầu tải ({count})', - duplicateFilesSummary: 'Đã bỏ qua tệp trùng lặp', - duplicateFilesDetailOne: '{count} tệp đã có trong hàng đợi.', - duplicateFilesDetailOther: '{count} tệp đã có trong hàng đợi.', - duplicateUrlsSummary: 'Đã bỏ qua URL trùng lặp', - duplicateUrlsDetailOne: '{count} URL đã có trong hàng đợi.', - duplicateUrlsDetailOther: '{count} URL đã có trong hàng đợi.', - }, - dropzone: { - releaseToAdd: 'Thả để thêm', - dropHere: 'Thả video vào đây', - browse: 'hoặc bấm để chọn tệp', - }, - remote: { - placeholder: 'Dán URL video, mỗi dòng một URL\n\nhttps://example.com/video.mp4\nhttps://drive.google.com/...', - providersHint: 'Hỗ trợ Google Drive, Dropbox', - addUrls: 'Thêm URL', - }, - queue: { - empty: 'Hàng đợi trống!', - totalSize: 'Tổng dung lượng:', - zeroSize: '0 MB', - }, - queueItem: { - remoteFileName: 'Tệp từ URL', - unknownSize: 'Không xác định', - status: { - pending: 'Chờ tải', - uploading: 'Đang tải lên...', - uploadingThreads: 'Đang tải · {threads} luồng', - processing: 'Đang xử lý...', - complete: 'Hoàn tất', - error: 'Thất bại', - fetching: 'Đang lấy dữ liệu...', - }, - }, - infoTip: { - title: 'Mẹo: Để xử lý nhanh nhất', - description: 'Hãy tải video với codec H.264 + codec âm thanh AAC (ví dụ MP4 dùng H.264/AAC). Video theo định dạng này sẽ được xử lý nhanh hơn nhiều (giây thay vì phút) vì không cần encode lại.', - }, - bulkActions: { - title: 'Thiết lập nhanh', - applyToPending: 'Áp dụng cho {count} tệp đang chờ', - selectCategory: 'Chọn danh mục...', - category: { - learning: 'Học tập', - entertainment: 'Giải trí', - }, - visibility: { - public: 'Công khai', - private: 'Riêng tư', - }, - }, - indicator: { - allDone: 'Hoàn tất', - uploading: 'Đang tải lên {count} tệp...', - waiting: '{count} tệp đang chờ', - completeProgress: 'Hoàn tất {complete} / {total}', - start: 'Bắt đầu', - viewVideos: 'Xem video', - addMoreFiles: 'Thêm tệp', - }, - errors: { - chunkUploadFailed: 'Không thể tải phần {index}', - mergeFailed: 'Gộp tệp thất bại', - }, - }, - home: { - nav: { - features: 'Tính năng', - pricing: 'Bảng giá', - login: 'Đăng nhập', - startFree: 'Dùng thử miễn phí', - }, - hero: { - titleLine1: 'Hạ tầng video cho', - titleLine2: 'internet hiện đại.', - subtitle: 'Lưu trữ, mã hóa và phát video mượt mà với API ưu tiên cho developer. Tối ưu tốc độ, sẵn sàng mở rộng.', - getStarted: 'Bắt đầu', - uploadVideo: 'Tải video', - }, - features: { - heading: 'Mọi thứ bạn cần để triển khai video', - subtitle: 'Tập trung xây dựng sản phẩm. Chúng tôi lo phần hạ tầng video phức tạp.', - global: { - title: 'Mạng edge toàn cầu', - description: 'Nội dung được phân phối từ hơn 200 PoP trên toàn thế giới. Tự động chọn vùng để có độ trễ thấp nhất cho mọi người xem.', - }, - live: { - title: 'API livestream', - description: 'Mở rộng tới hàng triệu người xem đồng thời với độ trễ cực thấp. Hỗ trợ RTMP ingest và HLS playback sẵn có.', - status: 'Trạng thái trực tiếp', - onAir: 'Đang phát', - bitrate: 'Bitrate:', - fps: 'FPS:', - latency: 'Độ trễ:', - bitrateValue: '6000 kbps', - fpsValue: '60', - latencyValue: '~2 giây', - }, - encoding: { - title: 'Mã hóa tức thì', - description: 'Tải tệp thô và nhận luồng HLS/DASH tối ưu chỉ trong vài giây.', - }, - analytics: { - title: 'Phân tích chuyên sâu', - description: 'Insight theo phiên, chỉ số chất lượng trải nghiệm (QoE), và nhiều hơn nữa.', - }, - }, - pricing: { - title: 'Bảng giá đơn giản, minh bạch', - subtitle: 'Chọn gói phù hợp với nhu cầu. Không phí ẩn.', - perMonth: '/tháng', - hobby: { - name: 'Hobby', - features: ['Tải lên không giới hạn', '1 giờ lưu trữ', 'Hỗ trợ tiêu chuẩn'], - button: 'Bắt đầu miễn phí', - }, - pro: { - name: 'Pro', - features: ['Trình phát không quảng cáo', 'Hỗ trợ M3U8', 'Tải lên không giới hạn', 'Quảng cáo tùy chỉnh'], - button: 'Bắt đầu ngay', - tag: 'PHỔ BIẾN', - }, - scale: { - name: 'Scale', - features: ['5 TB băng thông', '500 giờ lưu trữ', 'Hỗ trợ ưu tiên'], - button: 'Liên hệ kinh doanh', - tag: 'Giá trị cao', - }, - }, - footer: { - description: 'Xây dựng lớp hạ tầng video cho internet. Thiết kế cho developer.', - product: 'Sản phẩm', - productFeatures: 'Tính năng', - productPricing: 'Bảng giá', - productShowcase: 'Trình diễn', - company: 'Công ty', - companyAbout: 'Giới thiệu', - companyBlog: 'Blog', - companyCareers: 'Tuyển dụng', - legal: 'Pháp lý', - privacy: 'Quyền riêng tư', - terms: 'Điều khoản', - copyright: '© {year} EcoStream Inc. Bảo lưu mọi quyền.', - }, - head: { - title: 'EcoStream - Hạ tầng video cho internet hiện đại', - description: 'Lưu trữ, mã hóa và phát video mượt mà với API ưu tiên cho developer. Tối ưu tốc độ, sẵn sàng mở rộng.', - }, - }, - legal: { - common: { - heading: 'Chính sách pháp lý & quyền riêng tư', - subheading: 'Pháp lý & quyền riêng tư', - description: 'Chính sách pháp lý và quyền riêng tư của chúng tôi.', - }, - terms: { - title: 'Điều khoản và điều kiện - Ecostream', - description: 'Đọc điều khoản và điều kiện sử dụng dịch vụ lưu trữ và phát video của Ecostream.', - pageHeading: 'Chi tiết điều khoản và điều kiện', - pageSubheading: 'Điều khoản và điều kiện', - pageDescription: 'Điều khoản của chúng tôi nêu rõ các hướng dẫn và quy định quan trọng khi sử dụng dịch vụ Ecostream.', - sections: { - acceptanceTitle: '1. Chấp nhận điều khoản', - acceptanceText: 'Bằng việc truy cập và sử dụng Ecostream, bạn đồng ý bị ràng buộc bởi các điều khoản trong thỏa thuận này.', - usageTitle: '2. Sử dụng dịch vụ', - usageText: 'Bạn đồng ý chỉ sử dụng dịch vụ cho mục đích hợp pháp. Nghiêm cấm đăng hoặc truyền nội dung bất hợp pháp, đe dọa, phỉ báng, tục tĩu hoặc phản cảm. Chúng tôi có quyền chấm dứt tài khoản vi phạm điều khoản.', - ownershipTitle: '3. Quyền sở hữu nội dung', - ownershipText: 'Bạn giữ toàn bộ quyền sở hữu đối với nội dung tải lên Ecostream. Tuy nhiên, khi tải lên, bạn cấp cho chúng tôi quyền lưu trữ, lưu giữ và hiển thị nội dung cần thiết để cung cấp dịch vụ.', - liabilityTitle: '4. Giới hạn trách nhiệm', - liabilityText: 'Ecostream không chịu trách nhiệm cho bất kỳ thiệt hại trực tiếp, gián tiếp, ngẫu nhiên, đặc biệt hoặc hệ quả do việc sử dụng hoặc không thể sử dụng dịch vụ.', - changesTitle: '5. Thay đổi điều khoản', - changesText: 'Chúng tôi có quyền sửa đổi điều khoản bất kỳ lúc nào. Việc bạn tiếp tục sử dụng dịch vụ sau khi có thay đổi đồng nghĩa với việc chấp nhận điều khoản mới.', - }, - }, - privacy: { - title: 'Chính sách quyền riêng tư - Ecostream', - description: 'Tìm hiểu cam kết của Ecostream trong việc bảo vệ quyền riêng tư và dữ liệu của bạn.', - pageHeading: 'Chính sách pháp lý & quyền riêng tư', - pageSubheading: 'Pháp lý & quyền riêng tư', - pageDescription: 'Chính sách pháp lý và quyền riêng tư của chúng tôi.', - sections: { - policyTitle: '1. Chính sách quyền riêng tư', - policyText: 'Tại Ecostream, chúng tôi coi trọng quyền riêng tư của bạn. Chính sách này mô tả cách chúng tôi thu thập, sử dụng và bảo vệ thông tin cá nhân. Chúng tôi chỉ thu thập thông tin cần thiết để vận hành dịch vụ, bao gồm email tạo tài khoản và thông tin thanh toán xử lý gói dịch vụ.', - dataCollectionTitle: '2. Thu thập dữ liệu', - dataCollectionText: 'Chúng tôi thu thập dữ liệu như địa chỉ IP, loại trình duyệt và thời gian truy cập để phân tích xu hướng và cải thiện dịch vụ. Nội dung tải lên được lưu trữ an toàn và chỉ truy cập khi cần cho việc cung cấp dịch vụ lưu trữ.', - cookieTitle: '3. Chính sách cookie', - cookieText: 'Chúng tôi sử dụng cookie để duy trì phiên đăng nhập và tùy chọn người dùng. Khi sử dụng website, bạn đồng ý với việc sử dụng cookie theo chính sách này.', - dmcaTitle: '4. DMCA & bản quyền', - dmcaText: 'Ecostream tôn trọng quyền sở hữu trí tuệ của người khác. Chúng tôi phản hồi các thông báo vi phạm bản quyền theo Đạo luật DMCA. Vui lòng báo cáo vi phạm bản quyền tới bộ phận hỗ trợ.', - }, - }, - }, - notFound: { - headTitle: '404 - Không tìm thấy trang', - title: '404 - Không tìm thấy trang', - description: 'Trang bạn đang tìm không tồn tại.', - backHome: 'Quay về trang chủ', - }, -}; - -export default vi; diff --git a/src/lib/translation/index.ts b/src/lib/translation/index.ts new file mode 100644 index 0000000..70898ad --- /dev/null +++ b/src/lib/translation/index.ts @@ -0,0 +1,37 @@ +import i18next from "i18next"; +import I18NextHttpBackend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; +const i18n = i18next.createInstance(); + +i18n + .use(I18NextHttpBackend) + .use(LanguageDetector) + .init({ + supportedLngs: ["en", "vi"], + fallbackLng: "en", + defaultNS: "common", + ns: [ + "common", + "app", + "auth", + "nav", + "settings", + "pageHeader", + "confirm", + "toast", + "overview", + "video", + "notification", + "upload", + "home", + "legal", + "notFound", + ], + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: "/locales/{{lng}}/{{ns}}.json", // dynamic fetch JSON + }, + }); +export default i18n; diff --git a/src/main.ts b/src/main.ts index a52a41d..2a24e1e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,10 @@ import { createHead as SSRHead } from '@unhead/vue/server'; import { createPinia } from 'pinia'; import { createSSRApp } from 'vue'; import { RouterView } from 'vue-router'; -import { createI18n, normalizeLocale } from './i18n'; + +import I18NextVue from 'i18next-vue'; +import i18next from '@/lib/translation'; + import { withErrorBoundary } from './lib/hoc/withErrorBoundary'; import createAppRouter from './routes'; @@ -15,18 +18,13 @@ const getSerializedAppData = () => { return JSON.parse(document.getElementById('__APP_DATA__')?.innerText || '{}') as Record; }; -export function createApp(initialLocale?: string | null) { +export async function createApp(lng: string = 'en') { const pinia = createPinia(); const app = createSSRApp(withErrorBoundary(RouterView)); + const head = import.meta.env.SSR ? SSRHead() : CSRHead(); const appData = !import.meta.env.SSR ? getSerializedAppData() : ({} as Record); - const resolvedInitialLocale = initialLocale - ?? (!import.meta.env.SSR ? appData.$locale : undefined) - ?? undefined; - - const i18n = createI18n(normalizeLocale(resolvedInitialLocale)); - app.use(head); app.directive('nh', { created(el) { @@ -34,7 +32,8 @@ export function createApp(initialLocale?: string | null) { } }); app.use(pinia); - app.use(i18n); + await i18next.init({lng}); + app.use(I18NextVue, {i18next}); app.use(PiniaColada, { pinia, plugins: [ @@ -62,5 +61,5 @@ export function createApp(initialLocale?: string | null) { } } - return { app, router, head, pinia, bodyClass, queryCache, i18n }; + return { app, router, head, pinia, bodyClass, queryCache }; } diff --git a/src/routes/auth/layout.vue b/src/routes/auth/layout.vue index 56aef73..b4fcfec 100644 --- a/src/routes/auth/layout.vue +++ b/src/routes/auth/layout.vue @@ -8,9 +8,9 @@ {{ content[route.name as keyof typeof content.value]?.title || '' }} @@ -23,12 +23,12 @@