From f7fb68a7987f72a08deb30bed61cbf506bb1bd7d Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 25 Feb 2026 13:13:35 -0300 Subject: [PATCH] feat: add dashboard i18n with next-intl (EN + PT-BR), language selector in header --- next.config.mjs | 6 +- package-lock.json | 696 ++++++++++++++++++++- package.json | 1 + src/app/layout.tsx | 13 +- src/i18n/config.ts | 15 + src/i18n/messages/en.json | 74 +++ src/i18n/messages/pt-BR.json | 74 +++ src/i18n/request.ts | 28 + src/shared/components/Header.tsx | 52 +- src/shared/components/LanguageSelector.tsx | 90 +++ src/shared/components/Sidebar.tsx | 70 ++- 11 files changed, 1062 insertions(+), 57 deletions(-) create mode 100644 src/i18n/config.ts create mode 100644 src/i18n/messages/en.json create mode 100644 src/i18n/messages/pt-BR.json create mode 100644 src/i18n/request.ts create mode 100644 src/shared/components/LanguageSelector.tsx diff --git a/next.config.mjs b/next.config.mjs index 8fe8475d..5448ec6e 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,3 +1,7 @@ +import createNextIntlPlugin from "next-intl/plugin"; + +const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); + /** @type {import('next').NextConfig} */ const nextConfig = { turbopack: {}, @@ -63,4 +67,4 @@ const nextConfig = { }, }; -export default nextConfig; +export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index 43232c5a..51734d39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "lowdb": "^7.0.1", "monaco-editor": "^0.55.1", "next": "^16.1.6", + "next-intl": "^4.8.3", "node-machine-id": "^1.1.12", "open": "^11.0.0", "ora": "^9.1.0", @@ -950,6 +951,58 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz", + "integrity": "sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/intl-localematcher": "0.8.1", + "decimal.js": "^10.6.0", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz", + "integrity": "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.1.tgz", + "integrity": "sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/icu-skeleton-parser": "2.1.1", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.1.tgz", + "integrity": "sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "tslib": "^2.8.1" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz", + "integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.0", + "tslib": "^2.8.1" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1762,6 +1815,313 @@ "resolved": "open-sse", "link": true }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@peculiar/asn1-cms": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", @@ -1975,6 +2335,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1987,6 +2353,172 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.13.tgz", + "integrity": "sha512-ztXusRuC5NV2w+a6pDhX13CGioMLq8CjX5P4XgVJ21ocqz9t19288Do0y8LklplDtwcEhYGTNdMbkmUT7+lDTg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.13.tgz", + "integrity": "sha512-cVifxQUKhaE7qcO/y9Mq6PEhoyvN9tSLzCnnFZ4EIabFHBuLtDDO6a+vLveOy98hAs5Qu1+bb5Nv0oa1Pihe3Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.13.tgz", + "integrity": "sha512-t+xxEzZ48enl/wGGy7SRYd7kImWQ/+wvVFD7g5JZo234g6/QnIgZ+YdfIyjHB+ZJI3F7a2IQHS7RNjxF29UkWw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.13.tgz", + "integrity": "sha512-VndeGvKmTXFn6AGwjy0Kg8i7HccOCE7Jt/vmZwRxGtOfNZM1RLYRQ7MfDLo6T0h1Bq6eYzps3L5Ma4zBmjOnOg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.13.tgz", + "integrity": "sha512-SmZ9m+XqCB35NddHCctvHFLqPZDAs5j8IgD36GoutufDJmeq2VNfgk5rQoqNqKmAK3Y7iFdEmI76QoHIWiCLyw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.13.tgz", + "integrity": "sha512-5rij+vB9a29aNkHq72EXI2ihDZPszJb4zlApJY4aCC/q6utgqFA6CkrfTfIb+O8hxtG3zP5KERETz8mfFK6A0A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.13.tgz", + "integrity": "sha512-OlSlaOK9JplQ5qn07WiBLibkOw7iml2++ojEXhhR3rbWrNEKCD7sd8+6wSavsInyFdw4PhLA+Hy6YyDBIE23Yw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.13.tgz", + "integrity": "sha512-zwQii5YVdsfG8Ti9gIKgBKZg8qMkRZxl+OlYWUT5D93Jl4NuNBRausP20tfEkQdAPSRrMCSUZBM6FhW7izAZRg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.13.tgz", + "integrity": "sha512-hYXvyVVntqRlYoAIDwNzkS3tL2ijP3rxyWQMNKaxcCxxkCDto/w3meOK/OB6rbQSkNw0qTUcBfU9k+T0ptYdfQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.13.tgz", + "integrity": "sha512-XTzKs7c/vYCcjmcwawnQvlHHNS1naJEAzcBckMI5OJlnrcgW8UtcX9NHFYvNjGtXuKv0/9KvqL4fuahdvlNGKw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1996,6 +2528,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", @@ -4124,6 +4665,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -5769,6 +6316,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/icu-minify": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.3.tgz", + "integrity": "sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -5872,6 +6434,18 @@ "node": ">=12" } }, + "node_modules/intl-messageformat": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.1.2.tgz", + "integrity": "sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "3.1.1", + "@formatjs/fast-memoize": "3.1.0", + "@formatjs/icu-messageformat-parser": "3.5.1", + "tslib": "^2.8.1" + } + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -7302,6 +7876,93 @@ } } }, + "node_modules/next-intl": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.3.tgz", + "integrity": "sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.8.1", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "icu-minify": "^4.8.3", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.8.3", + "po-parser": "^2.1.1", + "use-intl": "^4.8.3" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.3.tgz", + "integrity": "sha512-YcaT+R9z69XkGhpDarVFWUprrCMbxgIQYPUaXoE6LGVnLjGdo8hu3gL6bramDVjNKViYY8a/pXPy7Bna0mXORg==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.13", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.13.tgz", + "integrity": "sha512-0l1gl/72PErwUZuavcRpRAQN9uSst+Nk++niC5IX6lmMWpXoScYx3oq/narT64/sKv/eRiPTaAjBFGDEQiWJIw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.13", + "@swc/core-darwin-x64": "1.15.13", + "@swc/core-linux-arm-gnueabihf": "1.15.13", + "@swc/core-linux-arm64-gnu": "1.15.13", + "@swc/core-linux-arm64-musl": "1.15.13", + "@swc/core-linux-x64-gnu": "1.15.13", + "@swc/core-linux-x64-musl": "1.15.13", + "@swc/core-win32-arm64-msvc": "1.15.13", + "@swc/core-win32-ia32-msvc": "1.15.13", + "@swc/core-win32-x64-msvc": "1.15.13" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -7354,6 +8015,12 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-machine-id": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", @@ -7868,6 +8535,12 @@ "node": ">=18" } }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9616,7 +10289,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -9769,6 +10442,27 @@ "punycode": "^2.1.0" } }, + "node_modules/use-intl": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz", + "integrity": "sha512-nLxlC/RH+le6g3amA508Itnn/00mE+J22ui21QhOWo5V9hCEC43+WtnRAITbJW0ztVZphev5X9gvOf2/Dk9PLA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^3.1.0", + "@schummar/icu-type-parser": "1.21.5", + "icu-minify": "^4.8.3", + "intl-messageformat": "^11.1.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", diff --git a/package.json b/package.json index 9b672ca2..0af61932 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "lowdb": "^7.0.1", "monaco-editor": "^0.55.1", "next": "^16.1.6", + "next-intl": "^4.8.3", "node-machine-id": "^1.1.12", "open": "^11.0.0", "ora": "^9.1.0", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 490ebacd..fcbb4c0a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,8 @@ import { Inter } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/shared/components/ThemeProvider"; import "@/lib/initCloudSync"; // Auto-initialize cloud sync +import { NextIntlClientProvider } from "next-intl"; +import { getMessages, getLocale } from "next-intl/server"; const inter = Inter({ subsets: ["latin"], @@ -18,9 +20,12 @@ export const metadata = { }, }; -export default function RootLayout({ children }) { +export default async function RootLayout({ children }) { + const locale = await getLocale(); + const messages = await getMessages(); + return ( - + @@ -37,7 +42,9 @@ export default function RootLayout({ children }) { > Skip to content - {children} + + {children} + ); diff --git a/src/i18n/config.ts b/src/i18n/config.ts new file mode 100644 index 00000000..546b16bf --- /dev/null +++ b/src/i18n/config.ts @@ -0,0 +1,15 @@ +export const LOCALES = ["en", "pt-BR"] as const; +export type Locale = (typeof LOCALES)[number]; +export const DEFAULT_LOCALE: Locale = "en"; + +export const LANGUAGES: readonly { + code: Locale; + label: string; + name: string; + flag: string; +}[] = [ + { code: "en", label: "EN", name: "English", flag: "🇺🇸" }, + { code: "pt-BR", label: "PT", name: "Português", flag: "🇧🇷" }, +] as const; + +export const LOCALE_COOKIE = "NEXT_LOCALE"; diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json new file mode 100644 index 00000000..b8a19c1f --- /dev/null +++ b/src/i18n/messages/en.json @@ -0,0 +1,74 @@ +{ + "common": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "loading": "Loading...", + "error": "An error occurred", + "success": "Success", + "confirm": "Are you sure?", + "refresh": "Refresh", + "close": "Close", + "add": "Add", + "edit": "Edit", + "search": "Search", + "back": "Back", + "next": "Next", + "submit": "Submit", + "reset": "Reset", + "copy": "Copy", + "copied": "Copied!", + "enabled": "Enabled", + "disabled": "Disabled", + "active": "Active", + "inactive": "Inactive", + "noData": "No data available" + }, + "sidebar": { + "home": "Home", + "dashboard": "Dashboard", + "providers": "Providers", + "combos": "Combos", + "usage": "Usage", + "analytics": "Analytics", + "costs": "Costs", + "health": "Health", + "limits": "Limits & Quotas", + "cliTools": "CLI Tools", + "settings": "Settings", + "translator": "Translator", + "docs": "Docs", + "issues": "Issues", + "endpoint": "Endpoint", + "apiManager": "API Manager", + "logs": "Logs", + "auditLog": "Audit Log", + "shutdown": "Shutdown", + "restart": "Restart", + "shutdownConfirm": "Shut down OmniRoute?", + "restartConfirm": "Restart OmniRoute?", + "version": "v{version}" + }, + "header": { + "logout": "Logout", + "language": "Language", + "providers": "Providers", + "providerDescription": "Manage your AI provider connections", + "combos": "Combos", + "comboDescription": "Model combos with fallback", + "usage": "Usage & Analytics", + "usageDescription": "Monitor your API usage, token consumption, and request logs", + "analytics": "Analytics", + "analyticsDescription": "Charts, trends, and evaluation insights", + "cliTools": "CLI Tools", + "cliToolsDescription": "Configure CLI tools", + "home": "Home", + "homeDescription": "Welcome to OmniRoute", + "endpoint": "Endpoint", + "endpointDescription": "API endpoint configuration", + "settings": "Settings", + "settingsDescription": "Manage your preferences", + "openaiCompatible": "OpenAI Compatible", + "anthropicCompatible": "Anthropic Compatible" + } +} diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json new file mode 100644 index 00000000..8a588a26 --- /dev/null +++ b/src/i18n/messages/pt-BR.json @@ -0,0 +1,74 @@ +{ + "common": { + "save": "Salvar", + "cancel": "Cancelar", + "delete": "Excluir", + "loading": "Carregando...", + "error": "Ocorreu um erro", + "success": "Sucesso", + "confirm": "Tem certeza?", + "refresh": "Atualizar", + "close": "Fechar", + "add": "Adicionar", + "edit": "Editar", + "search": "Pesquisar", + "back": "Voltar", + "next": "Próximo", + "submit": "Enviar", + "reset": "Resetar", + "copy": "Copiar", + "copied": "Copiado!", + "enabled": "Ativado", + "disabled": "Desativado", + "active": "Ativo", + "inactive": "Inativo", + "noData": "Nenhum dado disponível" + }, + "sidebar": { + "home": "Início", + "dashboard": "Painel", + "providers": "Provedores", + "combos": "Combos", + "usage": "Uso", + "analytics": "Análises", + "costs": "Custos", + "health": "Saúde", + "limits": "Limites e Cotas", + "cliTools": "Ferramentas CLI", + "settings": "Configurações", + "translator": "Tradutor", + "docs": "Documentação", + "issues": "Problemas", + "endpoint": "Endpoint", + "apiManager": "Gerenciador API", + "logs": "Logs", + "auditLog": "Log de Auditoria", + "shutdown": "Desligar", + "restart": "Reiniciar", + "shutdownConfirm": "Desligar o OmniRoute?", + "restartConfirm": "Reiniciar o OmniRoute?", + "version": "v{version}" + }, + "header": { + "logout": "Sair", + "language": "Idioma", + "providers": "Provedores", + "providerDescription": "Gerencie suas conexões de provedores de IA", + "combos": "Combos", + "comboDescription": "Combos de modelos com fallback", + "usage": "Uso e Análises", + "usageDescription": "Monitore seu uso de API, consumo de tokens e logs de requisições", + "analytics": "Análises", + "analyticsDescription": "Gráficos, tendências e insights de avaliação", + "cliTools": "Ferramentas CLI", + "cliToolsDescription": "Configurar ferramentas de linha de comando", + "home": "Início", + "homeDescription": "Bem-vindo ao OmniRoute", + "endpoint": "Endpoint", + "endpointDescription": "Configuração de endpoint da API", + "settings": "Configurações", + "settingsDescription": "Gerencie suas preferências", + "openaiCompatible": "Compatível com OpenAI", + "anthropicCompatible": "Compatível com Anthropic" + } +} diff --git a/src/i18n/request.ts b/src/i18n/request.ts new file mode 100644 index 00000000..e5eb2099 --- /dev/null +++ b/src/i18n/request.ts @@ -0,0 +1,28 @@ +import { getRequestConfig } from "next-intl/server"; +import { cookies, headers } from "next/headers"; +import { LOCALES, DEFAULT_LOCALE, LOCALE_COOKIE } from "./config"; +import type { Locale } from "./config"; + +export default getRequestConfig(async () => { + // 1. Try cookie + const cookieStore = await cookies(); + let locale: string = cookieStore.get(LOCALE_COOKIE)?.value || ""; + + // 2. Try custom header (set by middleware) + if (!locale) { + const headerStore = await headers(); + locale = headerStore.get("x-locale") || ""; + } + + // 3. Validate & fallback + if (!LOCALES.includes(locale as Locale)) { + locale = DEFAULT_LOCALE; + } + + const messages = (await import(`./messages/${locale}.json`)).default; + + return { + locale, + messages, + }; +}); diff --git a/src/shared/components/Header.tsx b/src/shared/components/Header.tsx index 739fa23c..b9e45cc3 100644 --- a/src/shared/components/Header.tsx +++ b/src/shared/components/Header.tsx @@ -6,6 +6,8 @@ import Image from "next/image"; import PropTypes from "prop-types"; import { ThemeToggle } from "@/shared/components"; import TokenHealthBadge from "./TokenHealthBadge"; +import LanguageSelector from "./LanguageSelector"; +import { useTranslations } from "next-intl"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, @@ -14,7 +16,9 @@ import { ANTHROPIC_COMPATIBLE_PREFIX, } from "@/shared/constants/providers"; -const getPageInfo = (pathname) => { +function usePageInfo(pathname: string | null) { + const t = useTranslations("header"); + if (!pathname) return { title: "", description: "", breadcrumbs: [] }; // Provider detail page: /dashboard/providers/[id] @@ -29,7 +33,7 @@ const getPageInfo = (pathname) => { title: providerInfo.name, description: "", breadcrumbs: [ - { label: "Providers", href: "/dashboard/providers" }, + { label: t("providers"), href: "/dashboard/providers" }, { label: providerInfo.name, image: `/providers/${providerInfo.id}.png` }, ], }; @@ -37,22 +41,22 @@ const getPageInfo = (pathname) => { if (providerId.startsWith(OPENAI_COMPATIBLE_PREFIX)) { return { - title: "OpenAI Compatible", + title: t("openaiCompatible"), description: "", breadcrumbs: [ - { label: "Providers", href: "/dashboard/providers" }, - { label: "OpenAI Compatible", image: "/providers/oai-cc.png" }, + { label: t("providers"), href: "/dashboard/providers" }, + { label: t("openaiCompatible"), image: "/providers/oai-cc.png" }, ], }; } if (providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX)) { return { - title: "Anthropic Compatible", + title: t("anthropicCompatible"), description: "", breadcrumbs: [ - { label: "Providers", href: "/dashboard/providers" }, - { label: "Anthropic Compatible", image: "/providers/anthropic-m.png" }, + { label: t("providers"), href: "/dashboard/providers" }, + { label: t("anthropicCompatible"), image: "/providers/anthropic-m.png" }, ], }; } @@ -60,40 +64,41 @@ const getPageInfo = (pathname) => { if (pathname.includes("/providers")) return { - title: "Providers", - description: "Manage your AI provider connections", + title: t("providers"), + description: t("providerDescription"), breadcrumbs: [], }; if (pathname.includes("/combos")) - return { title: "Combos", description: "Model combos with fallback", breadcrumbs: [] }; + return { title: t("combos"), description: t("comboDescription"), breadcrumbs: [] }; if (pathname.includes("/usage")) return { - title: "Usage & Analytics", - description: "Monitor your API usage, token consumption, and request logs", + title: t("usage"), + description: t("usageDescription"), breadcrumbs: [], }; if (pathname.includes("/analytics")) return { - title: "Analytics", - description: "Charts, trends, and evaluation insights", + title: t("analytics"), + description: t("analyticsDescription"), breadcrumbs: [], }; if (pathname.includes("/cli-tools")) - return { title: "CLI Tools", description: "Configure CLI tools", breadcrumbs: [] }; + return { title: t("cliTools"), description: t("cliToolsDescription"), breadcrumbs: [] }; if (pathname === "/dashboard") - return { title: "Home", description: "Welcome to OmniRoute", breadcrumbs: [] }; + return { title: t("home"), description: t("homeDescription"), breadcrumbs: [] }; if (pathname.includes("/endpoint")) - return { title: "Endpoint", description: "API endpoint configuration", breadcrumbs: [] }; + return { title: t("endpoint"), description: t("endpointDescription"), breadcrumbs: [] }; if (pathname.includes("/profile")) - return { title: "Settings", description: "Manage your preferences", breadcrumbs: [] }; + return { title: t("settings"), description: t("settingsDescription"), breadcrumbs: [] }; return { title: "", description: "", breadcrumbs: [] }; -}; +} export default function Header({ onMenuClick, showMenuButton = true }) { const pathname = usePathname(); const router = useRouter(); - const { title, description, breadcrumbs } = getPageInfo(pathname); + const t = useTranslations("header"); + const { title, description, breadcrumbs } = usePageInfo(pathname); const handleLogout = async () => { try { @@ -175,6 +180,9 @@ export default function Header({ onMenuClick, showMenuButton = true }) { {/* Right actions */}
+ {/* Language selector */} + + {/* Theme toggle */} @@ -185,7 +193,7 @@ export default function Header({ onMenuClick, showMenuButton = true }) { diff --git a/src/shared/components/LanguageSelector.tsx b/src/shared/components/LanguageSelector.tsx new file mode 100644 index 00000000..416f43db --- /dev/null +++ b/src/shared/components/LanguageSelector.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { LANGUAGES, LOCALE_COOKIE } from "@/i18n/config"; +import type { Locale } from "@/i18n/config"; +import { useLocale } from "next-intl"; + +/** Persist locale preference in cookie + localStorage (outside component scope for ESLint) */ +function persistLocale(code: Locale) { + document.cookie = `${LOCALE_COOKIE}=${code};path=/;max-age=${365 * 24 * 60 * 60};samesite=lax`; + try { + localStorage.setItem(LOCALE_COOKIE, code); + } catch { + // Ignore + } +} + +export default function LanguageSelector() { + const locale = useLocale(); + const router = useRouter(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + const currentLang = LANGUAGES.find((l) => l.code === locale) || LANGUAGES[0]; + + // Close dropdown on outside click + useEffect(() => { + const handler = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, []); + + const handleSelect = (code: Locale) => { + if (code === locale) { + setOpen(false); + return; + } + + persistLocale(code); + setOpen(false); + router.refresh(); + }; + + return ( +
+ {/* Trigger button */} + + + {/* Dropdown */} + {open && ( +
+ {LANGUAGES.map((lang) => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/shared/components/Sidebar.tsx b/src/shared/components/Sidebar.tsx index d61268b1..e402262e 100644 --- a/src/shared/components/Sidebar.tsx +++ b/src/shared/components/Sidebar.tsx @@ -10,31 +10,32 @@ import OmniRouteLogo from "./OmniRouteLogo"; import Button from "./Button"; import { ConfirmModal } from "./Modal"; import CloudSyncStatus from "./CloudSyncStatus"; +import { useTranslations } from "next-intl"; -const navItems = [ - { href: "/dashboard", label: "Home", icon: "home", exact: true }, - { href: "/dashboard/endpoint", label: "Endpoint", icon: "api" }, - { href: "/dashboard/api-manager", label: "API Manager", icon: "vpn_key" }, - { href: "/dashboard/providers", label: "Providers", icon: "dns" }, - { href: "/dashboard/combos", label: "Combos", icon: "layers" }, - { href: "/dashboard/logs", label: "Logs", icon: "description" }, - { href: "/dashboard/costs", label: "Costs", icon: "account_balance_wallet" }, - { href: "/dashboard/analytics", label: "Analytics", icon: "analytics" }, - { href: "/dashboard/limits", label: "Limits & Quotas", icon: "tune" }, - { href: "/dashboard/health", label: "Health", icon: "health_and_safety" }, - { href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" }, +// Nav items use i18n keys resolved inside the component +const navItemDefs = [ + { href: "/dashboard", i18nKey: "home", icon: "home", exact: true }, + { href: "/dashboard/endpoint", i18nKey: "endpoint", icon: "api" }, + { href: "/dashboard/api-manager", i18nKey: "apiManager", icon: "vpn_key" }, + { href: "/dashboard/providers", i18nKey: "providers", icon: "dns" }, + { href: "/dashboard/combos", i18nKey: "combos", icon: "layers" }, + { href: "/dashboard/logs", i18nKey: "logs", icon: "description" }, + { href: "/dashboard/costs", i18nKey: "costs", icon: "account_balance_wallet" }, + { href: "/dashboard/analytics", i18nKey: "analytics", icon: "analytics" }, + { href: "/dashboard/limits", i18nKey: "limits", icon: "tune" }, + { href: "/dashboard/health", i18nKey: "health", icon: "health_and_safety" }, + { href: "/dashboard/cli-tools", i18nKey: "cliTools", icon: "terminal" }, ]; -// Debug items (only show when ENABLE_REQUEST_LOGS=true) -const debugItems = [{ href: "/dashboard/translator", label: "Translator", icon: "translate" }]; +const debugItemDefs = [{ href: "/dashboard/translator", i18nKey: "translator", icon: "translate" }]; -const systemItems = [{ href: "/dashboard/settings", label: "Settings", icon: "settings" }]; +const systemItemDefs = [{ href: "/dashboard/settings", i18nKey: "settings", icon: "settings" }]; -const helpItems = [ - { href: "/docs", label: "Docs", icon: "menu_book" }, +const helpItemDefs = [ + { href: "/docs", i18nKey: "docs", icon: "menu_book" }, { href: "https://github.com/diegosouzapw/OmniRoute/issues", - label: "Issues", + i18nKey: "issues", icon: "bug_report", external: true, }, @@ -50,6 +51,8 @@ export default function Sidebar({ onToggleCollapse?: any; }) { const pathname = usePathname(); + const t = useTranslations("sidebar"); + const tc = useTranslations("common"); const [showShutdownModal, setShowShutdownModal] = useState(false); const [showRestartModal, setShowRestartModal] = useState(false); const [isShuttingDown, setIsShuttingDown] = useState(false); @@ -100,6 +103,13 @@ export default function Sidebar({ }, 3000); }; + // Resolve i18n keys → labels + const resolveItems = (defs) => defs.map((d) => ({ ...d, label: t(d.i18nKey) })); + const navItems = resolveItems(navItemDefs); + const debugItems = resolveItems(debugItemDefs); + const systemItems = resolveItems(systemItemDefs); + const helpItems = resolveItems(helpItemDefs); + const renderNavLink = (item) => { const active = !item.external && isActive(item.href, item.exact); const className = cn( @@ -271,7 +281,7 @@ export default function Sidebar({ >
@@ -301,10 +311,10 @@ export default function Sidebar({ isOpen={showShutdownModal} onClose={() => setShowShutdownModal(false)} onConfirm={handleShutdown} - title="Close Proxy" - message="Are you sure you want to close the proxy server?" - confirmText="Close" - cancelText="Cancel" + title={t("shutdown")} + message={t("shutdownConfirm")} + confirmText={t("shutdown")} + cancelText={tc("cancel")} variant="danger" loading={isShuttingDown} /> @@ -314,10 +324,10 @@ export default function Sidebar({ isOpen={showRestartModal} onClose={() => setShowRestartModal(false)} onConfirm={handleRestart} - title="Restart Proxy" - message="Are you sure you want to restart the proxy server? It will be back online in a few seconds." - confirmText="Restart" - cancelText="Cancel" + title={t("restart")} + message={t("restartConfirm")} + confirmText={t("restart")} + cancelText={tc("cancel")} variant="warning" loading={isRestarting} />