From af462c58725136bf913d896a462818af89c3a77e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 20:40:29 +0000 Subject: [PATCH 01/14] Initial plan From 51c2cc94abf83029955b0851849d9149ba60f1c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:00:35 +0000 Subject: [PATCH 02/14] feat: Add Bengali localization infrastructure with next-intl, translation files, and utilities Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com> --- next.config.ts | 5 +- package-lock.json | 682 ++++++++++++++++++++++- package.json | 1 + prisma/schema.prisma | 66 ++- src/components/language-switcher.tsx | 137 +++++ src/i18n.ts | 79 +++ src/lib/utils/bengali-numbers.ts | 340 ++++++++++++ src/lib/utils/index.ts | 36 ++ src/lib/utils/sms-counter.ts | 338 ++++++++++++ src/messages/bn.json | 797 +++++++++++++++++++++++++++ src/messages/en.json | 742 +++++++++++++++++++++++++ 11 files changed, 3209 insertions(+), 14 deletions(-) create mode 100644 src/components/language-switcher.tsx create mode 100644 src/i18n.ts create mode 100644 src/lib/utils/bengali-numbers.ts create mode 100644 src/lib/utils/index.ts create mode 100644 src/lib/utils/sms-counter.ts create mode 100644 src/messages/bn.json create mode 100644 src/messages/en.json diff --git a/next.config.ts b/next.config.ts index bfe3782e..2049ca21 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,7 @@ import type { NextConfig } from "next"; +import createNextIntlPlugin from 'next-intl/plugin'; + +const withNextIntl = createNextIntlPlugin('./src/i18n.ts'); const nextConfig: NextConfig = { /* config options here */ @@ -20,4 +23,4 @@ const nextConfig: NextConfig = { }, }; -export default nextConfig; +export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index d613e847..f57a71a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "lucide-react": "^0.553.0", "next": "16.1.0", "next-auth": "^4.24.13", + "next-intl": "^4.6.1", "next-themes": "^0.4.6", "nodemailer": "^7.0.10", "papaparse": "^5.5.3", @@ -1812,6 +1813,66 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", + "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.2", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", + "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", + "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/icu-skeleton-parser": "1.8.16", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", + "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", + "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, "node_modules/@hono/node-server": { "version": "1.19.7", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", @@ -2905,6 +2966,313 @@ "cuid2": "bin/cuid2.js" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "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/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/@playwright/test": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", @@ -4906,6 +5274,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/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -4944,6 +5318,172 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.7.tgz", + "integrity": "sha512-+hNVUfezUid7LeSHqnhoC6Gh3BROABxjlDNInuZ/fie1RUxaEX4qzDwdTgozJELgHhvYxyPIg1ro8ibnKtgO4g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.7.tgz", + "integrity": "sha512-ZAFuvtSYZTuXPcrhanaD5eyp27H8LlDzx2NAeVyH0FchYcuXf0h5/k3GL9ZU6Jw9eQ63R1E8KBgpXEJlgRwZUQ==", + "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.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.7.tgz", + "integrity": "sha512-K3HTYocpqnOw8KcD8SBFxiDHjIma7G/X+bLdfWqf+qzETNBrzOub/IEkq9UaeupaJiZJkPptr/2EhEXXWryS/A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.7.tgz", + "integrity": "sha512-HCnVIlsLnCtQ3uXcXgWrvQ6SAraskLA9QJo9ykTnqTH6TvUYqEta+TdTdGjzngD6TOE7XjlAiUs/RBtU8Z0t+Q==", + "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.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.7.tgz", + "integrity": "sha512-/OOp9UZBg4v2q9+x/U21Jtld0Wb8ghzBScwhscI7YvoSh4E8RALaJ1msV8V8AKkBkZH7FUAFB7Vbv0oVzZsezA==", + "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.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.7.tgz", + "integrity": "sha512-VBbs4gtD4XQxrHuQ2/2+TDZpPQQgrOHYRnS6SyJW+dw0Nj/OomRqH+n5Z4e/TgKRRbieufipeIGvADYC/90PYQ==", + "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.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.7.tgz", + "integrity": "sha512-kVuy2unodso6p0rMauS2zby8/bhzoGRYxBDyD6i2tls/fEYAE74oP0VPFzxIyHaIjK1SN6u5TgvV9MpyJ5xVug==", + "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.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.7.tgz", + "integrity": "sha512-uddYoo5Xmo1XKLhAnh4NBIyy5d0xk33x1sX3nIJboFySLNz878ksCFCZ3IBqrt1Za0gaoIWoOSSSk0eNhAc/sw==", + "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.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.7.tgz", + "integrity": "sha512-rqq8JjNMLx3QNlh0aPTtN/4+BGLEHC94rj9mkH1stoNRf3ra6IksNHMHy+V1HUqElEgcZyx+0yeXx3eLOTcoFw==", + "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.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.7.tgz", + "integrity": "sha512-4BK06EGdPnuplgcNhmSbOIiLdRgHYX3v1nl4HXo5uo4GZMfllXaCyBUes+0ePRfwbn9OFgVhCWPcYYjMT6hycQ==", + "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", @@ -4953,6 +5493,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/@tabler/icons": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.35.0.tgz", @@ -7134,7 +7683,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -9413,7 +9961,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -10161,6 +10708,18 @@ "node": ">=12" } }, + "node_modules/intl-messageformat": { + "version": "10.7.18", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", + "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.6", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.4", + "tslib": "^2.8.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -10356,7 +10915,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10412,7 +10970,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -10503,7 +11060,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -11563,7 +12119,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -11776,7 +12331,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11888,6 +12442,92 @@ "preact": ">=10" } }, + "node_modules/next-intl": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.6.1.tgz", + "integrity": "sha512-KlWgWtKLBPUsTPgxqwyjws1wCMD2QKxLlVjeeGj53DC1JWfKmBShKOrhIP0NznZrRQ0GleeoDUeHSETmyyIFeA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.6.1", + "po-parser": "^2.0.0", + "use-intl": "^4.6.1" + }, + "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.6.1", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.6.1.tgz", + "integrity": "sha512-+HHNeVERfSvuPDF7LYVn3pxst5Rf7EYdUTw7C7WIrYhcLaKiZ1b9oSRkTQddAN3mifDMCfHqO4kAQ/pcKiBl3A==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.7.tgz", + "integrity": "sha512-kTGB8XI7P+pTKW83tnUEDVP4zduF951u3UAOn5eTi0vyW6MvL56A3+ggMdfuVFtDI0/DsbSzf5z34HVBbuScWw==", + "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.7", + "@swc/core-darwin-x64": "1.15.7", + "@swc/core-linux-arm-gnueabihf": "1.15.7", + "@swc/core-linux-arm64-gnu": "1.15.7", + "@swc/core-linux-arm64-musl": "1.15.7", + "@swc/core-linux-x64-gnu": "1.15.7", + "@swc/core-linux-x64-musl": "1.15.7", + "@swc/core-win32-arm64-msvc": "1.15.7", + "@swc/core-win32-ia32-msvc": "1.15.7", + "@swc/core-win32-x64-msvc": "1.15.7" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -11926,6 +12566,12 @@ "node": "^10 || ^12 || >=14" } }, + "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-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -12664,7 +13310,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -12741,6 +13386,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/po-parser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.0.0.tgz", + "integrity": "sha512-SZvoKi3PoI/hHa2V9je9CW7Xgxl4dvO74cvaa6tWShIHT51FkPxje6pt0gTJznJrU67ix91nDaQp2hUxkOYhKA==", + "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", @@ -14671,7 +15322,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -15171,6 +15821,20 @@ } } }, + "node_modules/use-intl": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.6.1.tgz", + "integrity": "sha512-mUIj6QvJZ7Rk33mLDxRziz1YiBBAnIji8YW4TXXMdYHtaPEbVucrXD3iKQGAqJhbVn0VnjrEtIKYO1B18mfSJw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^2.2.0", + "@schummar/icu-type-parser": "1.21.5", + "intl-messageformat": "^10.5.14" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", diff --git a/package.json b/package.json index 661fda70..ca6d559c 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "lucide-react": "^0.553.0", "next": "16.1.0", "next-auth": "^4.24.13", + "next-intl": "^4.6.1", "next-themes": "^0.4.6", "nodemailer": "^7.0.10", "papaparse": "^5.5.3", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2d0e34e4..d8186708 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -511,11 +511,12 @@ model Product { reviews Review[] inventoryLogs InventoryLog[] @relation("InventoryLogs") inventoryReservations InventoryReservation[] - + translations ProductTranslation[] + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? - + @@unique([storeId, sku]) @@unique([storeId, slug]) @@index([storeId, status]) @@ -583,11 +584,12 @@ model Category { sortOrder Int @default(0) products Product[] - + translations CategoryTranslation[] + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? - + @@unique([storeId, slug]) @@index([storeId, parentId]) @@index([storeId, isPublished]) @@ -1264,4 +1266,60 @@ model StoreRequest { @@index([status, createdAt]) @@index([reviewedBy]) @@map("store_requests") +} + +// ============================================================================ +// INTERNATIONALIZATION (i18n) MODELS +// ============================================================================ + +/** + * Product translations for multi-language support + * Supports Bengali (bn) and English (en) locales + */ +model ProductTranslation { + id String @id @default(cuid()) + productId String + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + locale String // 'en' or 'bn' + name String // Translated product name + description String? // Translated description (supports long text) + shortDescription String? // Translated short description + + // SEO translations + metaTitle String? + metaDescription String? + metaKeywords String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([productId, locale]) + @@index([productId]) + @@index([locale]) + @@map("product_translations") +} + +/** + * Category translations for multi-language support + * Supports Bengali (bn) and English (en) locales + */ +model CategoryTranslation { + id String @id @default(cuid()) + categoryId String + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) + locale String // 'en' or 'bn' + name String // Translated category name + description String? // Translated description + + // SEO translations + metaTitle String? + metaDescription String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([categoryId, locale]) + @@index([categoryId]) + @@index([locale]) + @@map("category_translations") } \ No newline at end of file diff --git a/src/components/language-switcher.tsx b/src/components/language-switcher.tsx new file mode 100644 index 00000000..01c65d2a --- /dev/null +++ b/src/components/language-switcher.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { useLocale } from 'next-intl'; +import { usePathname, useRouter } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { Languages } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { locales, localeNames, type Locale } from '@/i18n'; + +/** + * Language Switcher Component + * + * Provides a dropdown menu to switch between Bengali (বাংলা) and English locales. + * Uses next-intl for locale management and updates the URL path accordingly. + * + * Usage: + * ```tsx + * + * ``` + */ +export function LanguageSwitcher() { + const locale = useLocale() as Locale; + const router = useRouter(); + const pathname = usePathname(); + + /** + * Switch to a new locale by updating the pathname + */ + const switchLocale = (newLocale: Locale) => { + if (newLocale === locale) return; + + // Build the new path with the new locale + // Handle both cases: with locale prefix (/en/page) and without (/page) + let newPathname = pathname; + + // Check if current path has a locale prefix + const hasLocalePrefix = locales.some( + (l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}` + ); + + if (hasLocalePrefix) { + // Replace the existing locale prefix + newPathname = pathname.replace(new RegExp(`^/(${locales.join('|')})`), `/${newLocale}`); + } else { + // Add the new locale prefix + newPathname = `/${newLocale}${pathname}`; + } + + router.push(newPathname); + }; + + const currentLocaleName = localeNames[locale] || localeNames.en; + + return ( + + + + + + {locales.map((l) => { + const { flag, native, english } = localeNames[l]; + const isActive = l === locale; + + return ( + switchLocale(l)} + className={isActive ? 'bg-accent' : ''} + > + {flag} + {native} + {native !== english && ( + ({english}) + )} + + ); + })} + + + ); +} + +/** + * Compact language toggle button + * Switches directly between Bengali and English without dropdown + */ +export function LanguageToggle() { + const locale = useLocale() as Locale; + const router = useRouter(); + const pathname = usePathname(); + + const toggleLocale = () => { + const newLocale: Locale = locale === 'bn' ? 'en' : 'bn'; + + // Build the new path with the new locale + let newPathname = pathname; + const hasLocalePrefix = locales.some( + (l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}` + ); + + if (hasLocalePrefix) { + newPathname = pathname.replace(new RegExp(`^/(${locales.join('|')})`), `/${newLocale}`); + } else { + newPathname = `/${newLocale}${pathname}`; + } + + router.push(newPathname); + }; + + const currentLocaleName = localeNames[locale]; + const targetLocale = locale === 'bn' ? 'en' : 'bn'; + const targetLocaleName = localeNames[targetLocale]; + + return ( + + ); +} diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 00000000..42b1d8c5 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,79 @@ +import { getRequestConfig } from 'next-intl/server'; +import { notFound } from 'next/navigation'; + +/** + * Supported locales for the application + * - 'en': English (US) + * - 'bn': Bengali (Bangladesh) + */ +export const locales = ['en', 'bn'] as const; +export type Locale = (typeof locales)[number]; + +/** + * Default locale for the application + * Bengali is the default for Bangladesh market where 85% prefer Bengali UI + */ +export const defaultLocale: Locale = 'bn'; + +/** + * Locale display names for UI + */ +export const localeNames: Record = { + en: { native: 'English', english: 'English', flag: '🇺🇸' }, + bn: { native: 'বাংলা', english: 'Bengali', flag: '🇧🇩' }, +}; + +/** + * Check if a string is a valid locale + */ +export function isValidLocale(locale: string): locale is Locale { + return locales.includes(locale as Locale); +} + +/** + * Get request configuration for next-intl + * This is called for each request to load the appropriate messages + */ +export default getRequestConfig(async ({ requestLocale }) => { + // Get the locale from the request (may be undefined for pages outside [locale] segment) + let locale = await requestLocale; + + // Validate that the incoming locale is valid, fallback to default + if (!locale || !isValidLocale(locale)) { + locale = defaultLocale; + } + + return { + locale, + messages: (await import(`./messages/${locale}.json`)).default, + timeZone: 'Asia/Dhaka', // Bangladesh timezone + now: new Date(), + // Format options for numbers and dates + formats: { + dateTime: { + short: { + day: 'numeric', + month: 'short', + year: 'numeric', + }, + long: { + day: 'numeric', + month: 'long', + year: 'numeric', + weekday: 'long', + }, + }, + number: { + currency: { + style: 'currency', + currency: 'BDT', + }, + percent: { + style: 'percent', + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }, + }, + }, + }; +}); diff --git a/src/lib/utils/bengali-numbers.ts b/src/lib/utils/bengali-numbers.ts new file mode 100644 index 00000000..2128fed5 --- /dev/null +++ b/src/lib/utils/bengali-numbers.ts @@ -0,0 +1,340 @@ +/** + * Bengali Number Formatting Utilities + * + * Provides utilities for converting and formatting numbers, currency, + * dates, and phone numbers in Bengali (বাংলা) format. + * + * Bengali numerals: ০১২৩৪৫৬৭৮৯ + * Western numerals: 0123456789 + */ + +/** + * Bengali digit mapping (index = Western digit) + */ +const BENGALI_DIGITS = ['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯'] as const; + +/** + * Western digit mapping (for reverse conversion) + */ +const WESTERN_DIGITS: Record = { + '০': '0', '১': '1', '২': '2', '৩': '3', '৪': '4', + '৫': '5', '৬': '6', '৭': '7', '৮': '8', '৯': '9', +}; + +/** + * Convert Western numerals to Bengali numerals + * + * @example + * toBengaliNumerals(12345.67) // "১২৩৪৫.৬৭" + * toBengaliNumerals("2025") // "২০২৫" + */ +export function toBengaliNumerals(num: number | string): string { + return String(num).replace(/\d/g, (digit) => BENGALI_DIGITS[parseInt(digit, 10)]); +} + +/** + * Convert Bengali numerals to Western numerals + * + * @example + * toWesternNumerals("১২৩৪৫") // "12345" + * toWesternNumerals("০১৮১২-৩৪৫৬৭৮") // "01812-345678" + */ +export function toWesternNumerals(str: string): string { + return str.replace(/[০-৯]/g, (digit) => WESTERN_DIGITS[digit] || digit); +} + +/** + * Check if a string contains Bengali numerals + * + * @example + * hasBengaliNumerals("১২৩") // true + * hasBengaliNumerals("123") // false + */ +export function hasBengaliNumerals(str: string): boolean { + return /[০-৯]/.test(str); +} + +/** + * Check if a string contains Bengali characters (not just numerals) + * + * @example + * hasBengaliText("হ্যালো") // true + * hasBengaliText("hello") // false + */ +export function hasBengaliText(str: string): boolean { + // Bengali Unicode range: U+0980 to U+09FF + return /[\u0980-\u09FF]/.test(str); +} + +/** + * Currency formatting options + */ +export interface BengaliCurrencyOptions { + /** Use Bengali numerals (০১২৩৪৫৬৭৮৯) instead of Western (0123456789) */ + useBengaliNumerals?: boolean; + /** Currency symbol (default: ৳ for BDT) */ + currency?: string; + /** Show currency symbol (default: true) */ + showSymbol?: boolean; + /** Minimum fraction digits (default: 2) */ + minimumFractionDigits?: number; + /** Maximum fraction digits (default: 2) */ + maximumFractionDigits?: number; + /** Use Indian/Bangladesh number grouping (12,34,567) vs Western (1,234,567) */ + useIndianGrouping?: boolean; +} + +/** + * Format currency in Bengali style + * Supports both Taka symbol (৳) and BDT text format + * + * @example + * formatBengaliCurrency(1234.50) // "৳1,234.50" + * formatBengaliCurrency(1234.50, { useBengaliNumerals: true }) // "৳১,২৩৪.৫০" + * formatBengaliCurrency(1234.50, { currency: "Tk " }) // "Tk 1,234.50" + * formatBengaliCurrency(1234567, { useIndianGrouping: true }) // "৳12,34,567.00" + */ +export function formatBengaliCurrency( + amount: number, + options: BengaliCurrencyOptions = {} +): string { + const { + useBengaliNumerals = false, + currency = '৳', + showSymbol = true, + minimumFractionDigits = 2, + maximumFractionDigits = 2, + useIndianGrouping = false, + } = options; + + let formatted: string; + + if (useIndianGrouping) { + // Indian/Bangladesh grouping: 12,34,567 (last 3, then 2-digit groups) + const [integerPart, decimalPart] = amount.toFixed(maximumFractionDigits).split('.'); + const absInteger = integerPart.replace('-', ''); + const isNegative = amount < 0; + + let result = ''; + const len = absInteger.length; + + if (len <= 3) { + result = absInteger; + } else { + // Last 3 digits + result = absInteger.slice(-3); + let remaining = absInteger.slice(0, -3); + + // Group remaining digits in pairs + while (remaining.length > 0) { + const group = remaining.slice(-2); + result = group + ',' + result; + remaining = remaining.slice(0, -2); + } + } + + formatted = (isNegative ? '-' : '') + result; + if (decimalPart) { + formatted += '.' + decimalPart; + } + } else { + // Standard Western grouping using Intl + formatted = new Intl.NumberFormat('en-BD', { + minimumFractionDigits, + maximumFractionDigits, + }).format(amount); + } + + if (useBengaliNumerals) { + formatted = toBengaliNumerals(formatted); + } + + return showSymbol ? `${currency}${formatted}` : formatted; +} + +/** + * Format a number with optional Bengali numerals + * + * @example + * formatBengaliNumber(12345.67) // "12,345.67" + * formatBengaliNumber(12345.67, true) // "১২,৩৪৫.৬৭" + */ +export function formatBengaliNumber( + num: number, + useBengaliNumerals = false, + locale = 'en-BD' +): string { + const formatted = new Intl.NumberFormat(locale, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }).format(num); + + return useBengaliNumerals ? toBengaliNumerals(formatted) : formatted; +} + +/** + * Format a date in Bengali + * + * @example + * formatBengaliDate(new Date('2025-11-25')) // "২৫ নভেম্বর ২০২৫" + * formatBengaliDate(new Date(), { useBengaliNumerals: false }) // "25 নভেম্বর 2025" + */ +export function formatBengaliDate( + date: Date, + options: { + useBengaliNumerals?: boolean; + format?: 'short' | 'long' | 'full'; + } = {} +): string { + const { useBengaliNumerals = true, format = 'long' } = options; + + let dateTimeFormat: Intl.DateTimeFormatOptions; + + switch (format) { + case 'short': + dateTimeFormat = { day: 'numeric', month: 'short', year: 'numeric' }; + break; + case 'full': + dateTimeFormat = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }; + break; + case 'long': + default: + dateTimeFormat = { day: 'numeric', month: 'long', year: 'numeric' }; + break; + } + + const formatted = new Intl.DateTimeFormat('bn-BD', dateTimeFormat).format(date); + + // Bengali Intl already uses Bengali numerals, but we may want to convert back + if (!useBengaliNumerals) { + return toWesternNumerals(formatted); + } + + return formatted; +} + +/** + * Format a phone number in Bengali + * Bangladesh phone numbers: +880 XXXX-XXXXXX + * + * @example + * formatBengaliPhoneNumber("01812345678") // "+৮৮০১৮১২-৩৪৫৬৭৮" + * formatBengaliPhoneNumber("+8801812345678") // "+৮৮০১৮১২-৩৪৫৬৭৮" + * formatBengaliPhoneNumber("01812345678", false) // "+8801812-345678" + */ +export function formatBengaliPhoneNumber( + phone: string, + useBengaliNumerals = true +): string { + // Remove all non-digit characters + const digits = phone.replace(/\D/g, ''); + + let normalized: string; + + // Handle different formats + if (digits.startsWith('880')) { + // Already has country code + normalized = digits; + } else if (digits.startsWith('0')) { + // Local format (01812345678) + normalized = '880' + digits.slice(1); + } else { + // Assume it's a local number without leading 0 + normalized = '880' + digits; + } + + // Format: +880 XXXX-XXXXXX (for 11-digit mobile numbers) + const countryCode = normalized.slice(0, 3); + const operatorCode = normalized.slice(3, 7); + const subscriberNumber = normalized.slice(7); + + const formatted = `+${countryCode}${operatorCode}-${subscriberNumber}`; + + return useBengaliNumerals ? toBengaliNumerals(formatted) : formatted; +} + +/** + * Format a percentage in Bengali + * + * @example + * formatBengaliPercentage(25.5) // "২৫.৫%" + * formatBengaliPercentage(25.5, false) // "25.5%" + */ +export function formatBengaliPercentage( + value: number, + useBengaliNumerals = true +): string { + const formatted = new Intl.NumberFormat('bn-BD', { + style: 'percent', + minimumFractionDigits: 0, + maximumFractionDigits: 1, + }).format(value / 100); + + if (!useBengaliNumerals) { + return toWesternNumerals(formatted); + } + + return formatted; +} + +/** + * Get Bengali ordinal suffix + * + * @example + * getBengaliOrdinal(1) // "১ম" + * getBengaliOrdinal(2) // "২য়" + * getBengaliOrdinal(3) // "৩য়" + * getBengaliOrdinal(4) // "৪র্থ" + */ +export function getBengaliOrdinal(num: number, useBengaliNumerals = true): string { + const bengaliNum = useBengaliNumerals ? toBengaliNumerals(num) : String(num); + + // Bengali ordinal suffixes + if (num === 1) return `${bengaliNum}ম`; + if (num === 2 || num === 3) return `${bengaliNum}য়`; + if (num === 4) return `${bengaliNum}র্থ`; + if (num === 5) return `${bengaliNum}ম`; + if (num === 6) return `${bengaliNum}ষ্ঠ`; + + // For numbers 7 and above, use generic suffix + return `${bengaliNum}তম`; +} + +/** + * Format time of day greeting in Bengali + * + * @example + * getBengaliGreeting(new Date('2025-01-01T08:00:00')) // "সুপ্রভাত" + * getBengaliGreeting(new Date('2025-01-01T14:00:00')) // "শুভ অপরাহ্ন" + * getBengaliGreeting(new Date('2025-01-01T18:00:00')) // "শুভ সন্ধ্যা" + * getBengaliGreeting(new Date('2025-01-01T22:00:00')) // "শুভ রাত্রি" + */ +export function getBengaliGreeting(date: Date = new Date()): string { + const hour = date.getHours(); + + if (hour >= 5 && hour < 12) { + return 'সুপ্রভাত'; // Good morning + } else if (hour >= 12 && hour < 17) { + return 'শুভ অপরাহ্ন'; // Good afternoon + } else if (hour >= 17 && hour < 20) { + return 'শুভ সন্ধ্যা'; // Good evening + } else { + return 'শুভ রাত্রি'; // Good night + } +} + +/** + * Format relative time in Bengali + * + * @example + * formatBengaliRelativeTime(-1, 'day') // "গতকাল" + * formatBengaliRelativeTime(1, 'day') // "আগামীকাল" + * formatBengaliRelativeTime(-5, 'minute') // "৫ মিনিট আগে" + */ +export function formatBengaliRelativeTime( + value: number, + unit: Intl.RelativeTimeFormatUnit +): string { + const rtf = new Intl.RelativeTimeFormat('bn', { numeric: 'auto' }); + return rtf.format(value, unit); +} diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts new file mode 100644 index 00000000..972b082a --- /dev/null +++ b/src/lib/utils/index.ts @@ -0,0 +1,36 @@ +/** + * Utility exports + * + * Centralized exports for all utility functions. + */ + +// Bengali number formatting utilities +export { + toBengaliNumerals, + toWesternNumerals, + hasBengaliNumerals, + hasBengaliText, + formatBengaliCurrency, + formatBengaliNumber, + formatBengaliDate, + formatBengaliPhoneNumber, + formatBengaliPercentage, + getBengaliOrdinal, + getBengaliGreeting, + formatBengaliRelativeTime, + type BengaliCurrencyOptions, +} from './bengali-numbers'; + +// SMS counter utilities +export { + calculateSMSCost, + requiresUTF16, + containsBengali, + getNonGSMChars, + formatSMSCost, + getSMSCounterText, + SMS_TEMPLATES, + getTemplateSMSCost, + type SMSEncoding, + type SMSCalculation, +} from './sms-counter'; diff --git a/src/lib/utils/sms-counter.ts b/src/lib/utils/sms-counter.ts new file mode 100644 index 00000000..57342917 --- /dev/null +++ b/src/lib/utils/sms-counter.ts @@ -0,0 +1,338 @@ +/** + * SMS Character Counter for Bengali Text + * + * This utility calculates SMS parts and costs for messages containing + * Bengali text, which requires UTF-16 encoding (70 chars/SMS vs 160 for GSM-7). + * + * Encoding rules: + * - GSM-7: Standard ASCII + extended characters (160 chars single, 153 chars multipart) + * - UTF-16: Required for Bengali/non-GSM characters (70 chars single, 67 chars multipart) + * + * Bangladesh SMS costs (typical): + * - ৳1.00 per SMS segment + * - UTF-16 messages cost more because they use fewer characters per segment + */ + +/** + * GSM-7 character set (basic + extended) + * Characters outside this set require UTF-16 encoding + */ +const GSM_7_BASIC_CHARS = new Set([ + // Basic GSM-7 characters (7-bit encoding) + '@', '£', '$', '¥', 'è', 'é', 'ù', 'ì', 'ò', 'Ç', '\n', 'Ø', 'ø', '\r', + 'Å', 'å', 'Δ', '_', 'Φ', 'Γ', 'Λ', 'Ω', 'Π', 'Ψ', 'Σ', 'Θ', 'Ξ', + 'Æ', 'æ', 'ß', 'É', ' ', '!', '"', '#', '¤', '%', '&', "'", '(', ')', + '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', ':', ';', '<', '=', '>', '?', '¡', 'A', 'B', 'C', 'D', 'E', + 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ä', 'Ö', 'Ñ', 'Ü', '§', '¿', 'a', + 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ä', 'ö', 'ñ', + 'ü', 'à', +]); + +/** + * Extended GSM-7 characters (count as 2 characters) + */ +const GSM_7_EXTENDED_CHARS = new Set([ + '|', '^', '€', '{', '}', '[', ']', '~', '\\', +]); + +/** + * SMS encoding type + */ +export type SMSEncoding = 'GSM-7' | 'UTF-16'; + +/** + * SMS calculation result + */ +export interface SMSCalculation { + /** Detected encoding type */ + encoding: SMSEncoding; + /** Total character count */ + charCount: number; + /** Maximum characters per single SMS */ + maxCharsPerSMS: number; + /** Maximum characters per SMS segment in multipart messages */ + maxCharsMultipart: number; + /** Number of SMS segments required */ + smsCount: number; + /** Characters remaining in current segment */ + remainingChars: number; + /** Estimated cost in BDT (৳1.00 per segment) */ + costBDT: number; + /** Whether the message is multipart */ + isMultipart: boolean; + /** Characters that forced UTF-16 encoding (if any) */ + nonGsmChars: string[]; +} + +/** + * Check if a character is in the GSM-7 basic character set + */ +function isGSM7BasicChar(char: string): boolean { + return GSM_7_BASIC_CHARS.has(char); +} + +/** + * Check if a character is in the GSM-7 extended character set + */ +function isGSM7ExtendedChar(char: string): boolean { + return GSM_7_EXTENDED_CHARS.has(char); +} + +/** + * Detect if text requires UTF-16 encoding + * Returns true if any character is outside GSM-7 character set + */ +export function requiresUTF16(text: string): boolean { + for (const char of text) { + if (!isGSM7BasicChar(char) && !isGSM7ExtendedChar(char)) { + return true; + } + } + return false; +} + +/** + * Check if text contains Bengali characters + * Bengali Unicode range: U+0980 to U+09FF + */ +export function containsBengali(text: string): boolean { + return /[\u0980-\u09FF]/.test(text); +} + +/** + * Get characters that require UTF-16 encoding + */ +export function getNonGSMChars(text: string): string[] { + const nonGsmChars: string[] = []; + const seen = new Set(); + + for (const char of text) { + if (!isGSM7BasicChar(char) && !isGSM7ExtendedChar(char) && !seen.has(char)) { + nonGsmChars.push(char); + seen.add(char); + } + } + + return nonGsmChars; +} + +/** + * Count GSM-7 character length (extended chars count as 2) + */ +function countGSM7Length(text: string): number { + let length = 0; + for (const char of text) { + if (isGSM7ExtendedChar(char)) { + length += 2; // Extended chars use escape sequence + } else { + length += 1; + } + } + return length; +} + +/** + * Calculate SMS parts and cost for given text + * + * @param text - The SMS message text + * @param costPerSMS - Cost per SMS segment in BDT (default: 1.00) + * @returns SMS calculation including segment count and cost + * + * @example + * // English text (GSM-7) + * calculateSMSCost("Hello World") + * // { encoding: "GSM-7", charCount: 11, smsCount: 1, costBDT: 1, ... } + * + * // Bengali text (UTF-16) + * calculateSMSCost("আপনার অর্ডার নিশ্চিত হয়েছে") + * // { encoding: "UTF-16", charCount: 27, smsCount: 1, costBDT: 1, ... } + * + * // Long Bengali text (multipart) + * calculateSMSCost("আপনার অর্ডার নম্বর ১২৩৪৫ সফলভাবে গ্রহণ করা হয়েছে। মোট মূল্য ৳৫,০০০। ডেলিভারি ৩-৫ কার্যদিবস।") + * // { encoding: "UTF-16", charCount: 85, smsCount: 2, costBDT: 2, ... } + */ +export function calculateSMSCost( + text: string, + costPerSMS = 1.0 +): SMSCalculation { + const isUTF16 = requiresUTF16(text); + const encoding: SMSEncoding = isUTF16 ? 'UTF-16' : 'GSM-7'; + + // Character limits based on encoding + const maxCharsPerSMS = isUTF16 ? 70 : 160; + const maxCharsMultipart = isUTF16 ? 67 : 153; + + // Calculate character count + const charCount = isUTF16 ? text.length : countGSM7Length(text); + + // Calculate SMS count + let smsCount: number; + let isMultipart: boolean; + + if (charCount === 0) { + smsCount = 0; + isMultipart = false; + } else if (charCount <= maxCharsPerSMS) { + smsCount = 1; + isMultipart = false; + } else { + smsCount = Math.ceil(charCount / maxCharsMultipart); + isMultipart = true; + } + + // Calculate remaining characters + let remainingChars: number; + if (charCount === 0) { + remainingChars = maxCharsPerSMS; + } else if (!isMultipart) { + remainingChars = maxCharsPerSMS - charCount; + } else { + const usedInLastSegment = charCount % maxCharsMultipart; + remainingChars = usedInLastSegment === 0 ? 0 : maxCharsMultipart - usedInLastSegment; + } + + // Get non-GSM characters if UTF-16 + const nonGsmChars = isUTF16 ? getNonGSMChars(text) : []; + + return { + encoding, + charCount, + maxCharsPerSMS, + maxCharsMultipart, + smsCount, + remainingChars, + costBDT: smsCount * costPerSMS, + isMultipart, + nonGsmChars, + }; +} + +/** + * Format SMS cost for display + * + * @example + * formatSMSCost(2.5) // "৳2.50" + * formatSMSCost(2.5, true) // "৳২.৫০" + */ +export function formatSMSCost( + costBDT: number, + useBengaliNumerals = false +): string { + const formatted = `৳${costBDT.toFixed(2)}`; + + if (useBengaliNumerals) { + const bengaliDigits = ['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯']; + return formatted.replace(/\d/g, (d) => bengaliDigits[parseInt(d, 10)]); + } + + return formatted; +} + +/** + * Get SMS character counter display text + * + * @example + * getSMSCounterText("Hello") + * // "5/160 characters | 1 SMS | ৳1.00" + * + * getSMSCounterText("আপনার অর্ডার নিশ্চিত", true) + * // "২০/৭০ অক্ষর | ১ SMS | ৳১.০০" + */ +export function getSMSCounterText( + text: string, + useBengali = false, + costPerSMS = 1.0 +): string { + const calc = calculateSMSCost(text, costPerSMS); + const maxChars = calc.isMultipart ? calc.maxCharsMultipart : calc.maxCharsPerSMS; + + if (useBengali) { + const toBengaliNum = (n: number | string) => { + const bengaliDigits = ['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯']; + return String(n).replace(/\d/g, (d) => bengaliDigits[parseInt(d, 10)]); + }; + + const charText = `${toBengaliNum(calc.charCount)}/${toBengaliNum(maxChars)} অক্ষর`; + const smsText = `${toBengaliNum(calc.smsCount)} SMS`; + const costText = formatSMSCost(calc.costBDT, true); + + return `${charText} | ${smsText} | ${costText}`; + } + + const charText = `${calc.charCount}/${maxChars} characters`; + const smsText = `${calc.smsCount} SMS`; + const costText = formatSMSCost(calc.costBDT, false); + + return `${charText} | ${smsText} | ${costText}`; +} + +/** + * SMS templates for common notifications + */ +export const SMS_TEMPLATES = { + /** OTP verification (keep short for single SMS) */ + otp: { + en: (code: string, minutes = 10) => + `Your verification code is: ${code}. Valid for ${minutes} minutes.`, + bn: (code: string, minutes = 10) => + `আপনার যাচাই কোড: ${code}। ${minutes} মিনিট পর্যন্ত বৈধ।`, + }, + + /** Order confirmation */ + orderConfirmation: { + en: (orderNumber: string, total: string) => + `Order #${orderNumber} confirmed! Total: ${total}. We'll notify you when shipped.`, + bn: (orderNumber: string, total: string) => + `অর্ডার #${orderNumber} নিশ্চিত! মোট: ${total}। শিপমেন্টের সময় জানাব।`, + }, + + /** Order shipped */ + orderShipped: { + en: (orderNumber: string, trackingUrl: string) => + `Order #${orderNumber} shipped! Track: ${trackingUrl}`, + bn: (orderNumber: string, trackingUrl: string) => + `অর্ডার #${orderNumber} শিপ হয়েছে! ট্র্যাক: ${trackingUrl}`, + }, + + /** Order delivered */ + orderDelivered: { + en: (orderNumber: string) => + `Order #${orderNumber} delivered! Thank you for shopping with us.`, + bn: (orderNumber: string) => + `অর্ডার #${orderNumber} ডেলিভার হয়েছে! কেনাকাটার জন্য ধন্যবাদ।`, + }, + + /** Delivery OTP */ + deliveryOTP: { + en: (code: string) => + `Your delivery OTP: ${code}. Share ONLY with delivery person.`, + bn: (code: string) => + `ডেলিভারি OTP: ${code}। শুধু ডেলিভারি বয়কে দিন।`, + }, +} as const; + +/** + * Get estimated SMS cost for a template + * + * @example + * getTemplateSMSCost('otp', 'bn', ['123456', 10]) + * // { charCount: 32, smsCount: 1, costBDT: 1 } + */ +export function getTemplateSMSCost< + T extends keyof typeof SMS_TEMPLATES, + L extends keyof typeof SMS_TEMPLATES[T] +>( + template: T, + locale: L, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params: Parameters string ? typeof SMS_TEMPLATES[T][L] : never> +): Pick { + const templateFn = SMS_TEMPLATES[template][locale] as (...args: typeof params) => string; + const text = templateFn(...params); + const { charCount, smsCount, costBDT } = calculateSMSCost(text); + + return { charCount, smsCount, costBDT }; +} diff --git a/src/messages/bn.json b/src/messages/bn.json new file mode 100644 index 00000000..e8078043 --- /dev/null +++ b/src/messages/bn.json @@ -0,0 +1,797 @@ +{ + "common": { + "home": "হোম", + "products": "পণ্য", + "categories": "ক্যাটাগরি", + "brands": "ব্র্যান্ড", + "cart": "কার্ট", + "checkout": "চেকআউট", + "login": "লগইন", + "signup": "সাইনআপ", + "logout": "লগআউট", + "search": "খুঁজুন", + "searchPlaceholder": "পণ্য খুঁজুন...", + "filter": "ফিল্টার", + "sort": "সাজান", + "sortBy": "সাজানোর ক্রম", + "addToCart": "কার্টে যোগ করুন", + "buyNow": "এখনই কিনুন", + "viewDetails": "বিস্তারিত দেখুন", + "viewAll": "সব দেখুন", + "showMore": "আরো দেখুন", + "showLess": "কম দেখুন", + "loading": "লোড হচ্ছে...", + "error": "ত্রুটি", + "success": "সফল", + "warning": "সতর্কতা", + "info": "তথ্য", + "confirm": "নিশ্চিত করুন", + "cancel": "বাতিল", + "save": "সংরক্ষণ", + "edit": "সম্পাদনা", + "delete": "মুছুন", + "remove": "সরান", + "close": "বন্ধ", + "back": "পেছনে", + "next": "পরবর্তী", + "previous": "পূর্ববর্তী", + "submit": "জমা দিন", + "reset": "রিসেট", + "clear": "মুছে ফেলুন", + "apply": "প্রয়োগ করুন", + "yes": "হ্যাঁ", + "no": "না", + "ok": "ঠিক আছে", + "optional": "ঐচ্ছিক", + "required": "আবশ্যক", + "all": "সব", + "none": "কিছুই না", + "select": "বাছুন", + "selected": "নির্বাচিত", + "noResults": "কোনো ফলাফল পাওয়া যায়নি", + "noData": "কোনো তথ্য নেই", + "tryAgain": "আবার চেষ্টা করুন", + "learnMore": "আরো জানুন", + "seeAll": "সব দেখুন", + "share": "শেয়ার করুন", + "copy": "কপি করুন", + "copied": "কপি হয়েছে!", + "download": "ডাউনলোড", + "upload": "আপলোড", + "refresh": "রিফ্রেশ", + "settings": "সেটিংস", + "profile": "প্রোফাইল", + "account": "অ্যাকাউন্ট", + "help": "সাহায্য", + "support": "সাপোর্ট", + "contactUs": "যোগাযোগ করুন", + "aboutUs": "আমাদের সম্পর্কে", + "termsOfService": "সেবার শর্তাবলী", + "privacyPolicy": "গোপনীয়তা নীতি", + "faq": "প্রশ্নোত্তর", + "language": "ভাষা", + "theme": "থিম", + "darkMode": "ডার্ক মোড", + "lightMode": "লাইট মোড", + "systemDefault": "সিস্টেম ডিফল্ট", + "notifications": "বিজ্ঞপ্তি", + "welcome": "স্বাগতম", + "greeting": { + "morning": "সুপ্রভাত", + "afternoon": "শুভ অপরাহ্ন", + "evening": "শুভ সন্ধ্যা", + "night": "শুভ রাত্রি" + }, + "currency": "মুদ্রা", + "price": "দাম", + "total": "মোট", + "subtotal": "সাবটোটাল", + "tax": "ট্যাক্স", + "discount": "ছাড়", + "shipping": "ডেলিভারি চার্জ", + "free": "বিনামূল্যে", + "freeShipping": "ফ্রি ডেলিভারি", + "quantity": "পরিমাণ", + "inStock": "স্টক আছে", + "outOfStock": "স্টক শেষ", + "lowStock": "স্টক কম", + "available": "পাওয়া যাচ্ছে", + "unavailable": "পাওয়া যাচ্ছে না", + "new": "নতুন", + "sale": "সেল", + "hot": "হট", + "featured": "ফিচার্ড", + "popular": "জনপ্রিয়", + "trending": "ট্রেন্ডিং", + "bestSeller": "বেস্ট সেলার", + "recommended": "প্রস্তাবিত", + "dateFormat": { + "short": "সংক্ষিপ্ত", + "long": "বিস্তারিত", + "relative": "আপেক্ষিক" + } + }, + "navigation": { + "mainMenu": "প্রধান মেনু", + "myAccount": "আমার অ্যাকাউন্ট", + "myOrders": "আমার অর্ডার", + "myWishlist": "আমার উইশলিস্ট", + "myAddresses": "আমার ঠিকানা", + "orderHistory": "অর্ডার ইতিহাস", + "trackOrder": "অর্ডার ট্র্যাক করুন", + "customerService": "কাস্টমার সার্ভিস", + "shippingInfo": "ডেলিভারি তথ্য", + "returnPolicy": "রিটার্ন নীতি", + "paymentMethods": "পেমেন্ট পদ্ধতি" + }, + "auth": { + "loginTitle": "আপনার অ্যাকাউন্টে লগইন করুন", + "loginSubtitle": "স্বাগতম! অনুগ্রহ করে আপনার তথ্য দিন", + "signupTitle": "নতুন অ্যাকাউন্ট তৈরি করুন", + "signupSubtitle": "আমাদের সাথে যোগ দিন এবং শপিং শুরু করুন", + "email": "ইমেইল", + "emailPlaceholder": "আপনার ইমেইল লিখুন", + "password": "পাসওয়ার্ড", + "passwordPlaceholder": "আপনার পাসওয়ার্ড লিখুন", + "confirmPassword": "পাসওয়ার্ড নিশ্চিত করুন", + "confirmPasswordPlaceholder": "পাসওয়ার্ড আবার লিখুন", + "name": "পূর্ণ নাম", + "namePlaceholder": "আপনার পূর্ণ নাম লিখুন", + "phone": "মোবাইল নম্বর", + "phonePlaceholder": "আপনার মোবাইল নম্বর লিখুন", + "rememberMe": "আমাকে মনে রাখুন", + "forgotPassword": "পাসওয়ার্ড ভুলে গেছেন?", + "resetPassword": "পাসওয়ার্ড রিসেট করুন", + "resetPasswordTitle": "আপনার পাসওয়ার্ড রিসেট করুন", + "resetPasswordSubtitle": "রিসেট লিংক পেতে আপনার ইমেইল দিন", + "sendResetLink": "রিসেট লিংক পাঠান", + "newPassword": "নতুন পাসওয়ার্ড", + "newPasswordPlaceholder": "নতুন পাসওয়ার্ড লিখুন", + "loginButton": "লগইন", + "signupButton": "সাইনআপ", + "orContinueWith": "অথবা চালিয়ে যান", + "socialLogin": { + "google": "গুগল দিয়ে লগইন", + "facebook": "ফেসবুক দিয়ে লগইন", + "apple": "অ্যাপল দিয়ে লগইন" + }, + "alreadyHaveAccount": "ইতিমধ্যে অ্যাকাউন্ট আছে?", + "dontHaveAccount": "অ্যাকাউন্ট নেই?", + "logoutConfirm": "আপনি কি লগআউট করতে চান?", + "logoutSuccess": "সফলভাবে লগআউট হয়েছে", + "loginSuccess": "স্বাগতম!", + "signupSuccess": "অ্যাকাউন্ট সফলভাবে তৈরি হয়েছে!", + "verifyEmail": "ইমেইল যাচাই করুন", + "verifyEmailSent": "আপনার ইমেইলে একটি যাচাই লিংক পাঠানো হয়েছে", + "resendVerification": "যাচাই ইমেইল পুনরায় পাঠান", + "verificationExpired": "যাচাই লিংকের মেয়াদ শেষ হয়ে গেছে", + "accountPending": "অ্যাকাউন্ট অনুমোদনের অপেক্ষায়", + "accountPendingMessage": "আপনার অ্যাকাউন্ট পর্যালোচনা করা হচ্ছে। অনুমোদিত হলে ইমেইল পাবেন।", + "accountSuspended": "অ্যাকাউন্ট স্থগিত", + "accountSuspendedMessage": "আপনার অ্যাকাউন্ট স্থগিত করা হয়েছে। সাপোর্টে যোগাযোগ করুন।", + "businessInfo": { + "title": "ব্যবসার তথ্য", + "businessName": "ব্যবসার নাম", + "businessNamePlaceholder": "আপনার ব্যবসার নাম লিখুন", + "businessDescription": "ব্যবসার বিবরণ", + "businessDescriptionPlaceholder": "আপনার ব্যবসা সম্পর্কে লিখুন", + "businessCategory": "ব্যবসার ক্যাটাগরি", + "businessCategoryPlaceholder": "ক্যাটাগরি বাছুন" + } + }, + "product": { + "title": "পণ্য", + "products": "পণ্য", + "allProducts": "সব পণ্য", + "productDetails": "পণ্যের বিবরণ", + "price": "দাম", + "regularPrice": "নিয়মিত দাম", + "salePrice": "বিক্রয় মূল্য", + "youSave": "আপনার সঞ্চয়", + "brand": "ব্র্যান্ড", + "category": "ক্যাটাগরি", + "sku": "SKU", + "barcode": "বারকোড", + "weight": "ওজন", + "dimensions": "মাপ", + "inStock": "স্টক আছে", + "outOfStock": "স্টক শেষ", + "lowStock": "মাত্র {count} টি বাকি আছে", + "stockStatus": "স্টক স্ট্যাটাস", + "availability": "প্রাপ্যতা", + "description": "বিবরণ", + "shortDescription": "সংক্ষিপ্ত বিবরণ", + "specifications": "বৈশিষ্ট্য", + "features": "ফিচার", + "reviews": "রিভিউ", + "customerReviews": "কাস্টমার রিভিউ", + "writeReview": "রিভিউ লিখুন", + "rating": "রেটিং", + "ratings": "রেটিং", + "averageRating": "গড় রেটিং", + "noReviews": "এখনো কোনো রিভিউ নেই", + "beFirstToReview": "এই পণ্যে প্রথম রিভিউ দিন", + "reviewTitle": "রিভিউ শিরোনাম", + "reviewComment": "আপনার রিভিউ", + "submitReview": "রিভিউ জমা দিন", + "verifiedPurchase": "যাচাইকৃত ক্রয়", + "relatedProducts": "সম্পর্কিত পণ্য", + "similarProducts": "একই ধরনের পণ্য", + "youMayAlsoLike": "আপনার পছন্দ হতে পারে", + "recentlyViewed": "সম্প্রতি দেখা", + "compare": "তুলনা করুন", + "addToWishlist": "উইশলিস্টে যোগ করুন", + "removeFromWishlist": "উইশলিস্ট থেকে সরান", + "shareProduct": "পণ্য শেয়ার করুন", + "productAdded": "পণ্য কার্টে যোগ হয়েছে", + "productRemoved": "পণ্য কার্ট থেকে সরানো হয়েছে", + "selectOptions": "অপশন বাছুন", + "selectVariant": "ভ্যারিয়েন্ট বাছুন", + "color": "রঙ", + "size": "সাইজ", + "quantity": "পরিমাণ", + "maxQuantity": "সর্বোচ্চ পরিমাণ: {max}", + "minQuantity": "সর্বনিম্ন পরিমাণ: {min}", + "filter": { + "title": "ফিল্টার", + "price": "দাম", + "priceRange": "দামের সীমা", + "minPrice": "সর্বনিম্ন দাম", + "maxPrice": "সর্বোচ্চ দাম", + "brand": "ব্র্যান্ড", + "category": "ক্যাটাগরি", + "color": "রঙ", + "size": "সাইজ", + "rating": "রেটিং", + "availability": "প্রাপ্যতা", + "clearAll": "সব ফিল্টার মুছুন", + "apply": "ফিল্টার প্রয়োগ করুন", + "noFilters": "কোনো ফিল্টার নেই" + }, + "sort": { + "newest": "নতুন", + "oldest": "পুরাতন", + "priceLowToHigh": "দাম: কম থেকে বেশি", + "priceHighToLow": "দাম: বেশি থেকে কম", + "nameAZ": "নাম: A থেকে Z", + "nameZA": "নাম: Z থেকে A", + "popularity": "জনপ্রিয়তা", + "rating": "রেটিং", + "bestSelling": "বেস্ট সেলিং" + } + }, + "cart": { + "title": "আপনার কার্ট", + "yourCart": "আপনার কার্ট", + "shoppingCart": "শপিং কার্ট", + "empty": "আপনার কার্ট খালি", + "emptyMessage": "মনে হচ্ছে আপনি এখনো কার্টে কিছু যোগ করেননি", + "startShopping": "শপিং শুরু করুন", + "subtotal": "সাবটোটাল", + "shipping": "ডেলিভারি চার্জ", + "shippingCalculatedAtCheckout": "চেকআউটে হিসাব হবে", + "tax": "ট্যাক্স", + "discount": "ছাড়", + "total": "মোট", + "estimatedTotal": "আনুমানিক মোট", + "items": "{count, plural, =0 {কোনো আইটেম নেই} =1 {১ টি আইটেম} other {# টি আইটেম}}", + "removeItem": "সরান", + "removeItemConfirm": "এই আইটেম কার্ট থেকে সরাতে চান?", + "updateQuantity": "পরিমাণ আপডেট করুন", + "saveForLater": "পরে জন্য সংরক্ষণ করুন", + "moveToWishlist": "উইশলিস্টে সরান", + "continueShopping": "কেনাকাটা চালিয়ে যান", + "proceedToCheckout": "চেকআউটে যান", + "viewCart": "কার্ট দেখুন", + "clearCart": "কার্ট খালি করুন", + "clearCartConfirm": "আপনি কি কার্ট খালি করতে চান?", + "itemAdded": "আইটেম কার্টে যোগ হয়েছে", + "itemRemoved": "আইটেম কার্ট থেকে সরানো হয়েছে", + "itemUpdated": "কার্ট আপডেট হয়েছে", + "cartUpdated": "কার্ট সফলভাবে আপডেট হয়েছে", + "promoCode": "প্রোমো কোড", + "enterPromoCode": "প্রোমো কোড লিখুন", + "applyPromoCode": "প্রয়োগ করুন", + "promoCodeApplied": "প্রোমো কোড প্রয়োগ হয়েছে", + "promoCodeInvalid": "অবৈধ প্রোমো কোড", + "promoCodeExpired": "প্রোমো কোডের মেয়াদ শেষ", + "promoCodeRemoved": "প্রোমো কোড সরানো হয়েছে", + "freeShipping": "ফ্রি ডেলিভারি", + "freeShippingMessage": "আপনি ফ্রি ডেলিভারির জন্য যোগ্য!", + "freeShippingProgress": "ফ্রি ডেলিভারির জন্য আরো {amount} যোগ করুন", + "orderNote": "অর্ডার নোট", + "orderNotePlaceholder": "অর্ডারে একটি নোট যোগ করুন...", + "giftWrapping": "গিফট র‍্যাপিং", + "giftWrappingMessage": "গিফট মেসেজ যোগ করুন", + "estimatedDelivery": "আনুমানিক ডেলিভারি", + "secureCheckout": "নিরাপদ চেকআউট" + }, + "checkout": { + "title": "চেকআউট", + "secureCheckout": "নিরাপদ চেকআউট", + "step": "ধাপ {current} / {total}", + "shippingAddress": "ডেলিভারি ঠিকানা", + "billingAddress": "বিলিং ঠিকানা", + "sameAsShipping": "ডেলিভারি ঠিকানার মতোই", + "differentBillingAddress": "ভিন্ন বিলিং ঠিকানা ব্যবহার করুন", + "savedAddresses": "সংরক্ষিত ঠিকানা", + "addNewAddress": "নতুন ঠিকানা যোগ করুন", + "editAddress": "ঠিকানা সম্পাদনা করুন", + "deleteAddress": "ঠিকানা মুছুন", + "setAsDefault": "ডিফল্ট হিসেবে সেট করুন", + "defaultAddress": "ডিফল্ট ঠিকানা", + "fullName": "পূর্ণ নাম", + "fullNamePlaceholder": "আপনার পূর্ণ নাম লিখুন", + "phone": "মোবাইল নম্বর", + "phonePlaceholder": "আপনার মোবাইল নম্বর লিখুন", + "email": "ইমেইল", + "emailPlaceholder": "আপনার ইমেইল লিখুন", + "address": "ঠিকানা", + "addressPlaceholder": "আপনার ঠিকানা লিখুন", + "addressLine2": "এপার্টমেন্ট, সুইট, ইত্যাদি", + "addressLine2Placeholder": "এপার্টমেন্ট, সুইট, ইউনিট, বিল্ডিং, ফ্লোর, ইত্যাদি", + "city": "শহর", + "cityPlaceholder": "শহর লিখুন", + "area": "এলাকা", + "areaPlaceholder": "এলাকা/জেলা লিখুন", + "state": "বিভাগ", + "statePlaceholder": "বিভাগ লিখুন", + "postalCode": "পোস্ট কোড", + "postalCodePlaceholder": "পোস্ট কোড লিখুন", + "country": "দেশ", + "countryPlaceholder": "দেশ বাছুন", + "shippingMethod": "ডেলিভারি পদ্ধতি", + "selectShippingMethod": "ডেলিভারি পদ্ধতি বাছুন", + "standardShipping": "স্ট্যান্ডার্ড ডেলিভারি", + "expressShipping": "এক্সপ্রেস ডেলিভারি", + "overnightShipping": "ওভারনাইট ডেলিভারি", + "localPickup": "স্থানীয় পিকআপ", + "estimatedDelivery": "আনুমানিক ডেলিভারি", + "deliveryDays": "{days} কার্যদিবস", + "paymentMethod": "পেমেন্ট পদ্ধতি", + "selectPaymentMethod": "পেমেন্ট পদ্ধতি বাছুন", + "creditCard": "ক্রেডিট কার্ড", + "debitCard": "ডেবিট কার্ড", + "cashOnDelivery": "ক্যাশ অন ডেলিভারি", + "bkash": "বিকাশ", + "nagad": "নগদ", + "rocket": "রকেট", + "bankTransfer": "ব্যাংক ট্রান্সফার", + "mobileBanking": "মোবাইল ব্যাংকিং", + "paymentMethods": { + "bkash": { + "title": "বিকাশ", + "description": "বিকাশ মোবাইল ওয়ালেট দিয়ে পেমেন্ট করুন", + "phoneNumber": "বিকাশ অ্যাকাউন্ট নম্বর", + "transactionId": "ট্রানজেকশন আইডি" + }, + "nagad": { + "title": "নগদ", + "description": "নগদ মোবাইল ওয়ালেট দিয়ে পেমেন্ট করুন", + "phoneNumber": "নগদ অ্যাকাউন্ট নম্বর", + "transactionId": "ট্রানজেকশন আইডি" + }, + "cod": { + "title": "ক্যাশ অন ডেলিভারি", + "description": "পণ্য হাতে পেয়ে টাকা দিন", + "note": "ডেলিভারির সময় নগদ টাকা দিন" + }, + "card": { + "title": "ক্রেডিট/ডেবিট কার্ড", + "description": "নিরাপদে কার্ড দিয়ে পেমেন্ট করুন", + "cardNumber": "কার্ড নম্বর", + "expiryDate": "মেয়াদ শেষের তারিখ", + "cvv": "CVV", + "nameOnCard": "কার্ডে নাম" + } + }, + "placeOrder": "অর্ডার করুন", + "confirmOrder": "অর্ডার নিশ্চিত করুন", + "orderSummary": "অর্ডার সারাংশ", + "reviewOrder": "আপনার অর্ডার পর্যালোচনা করুন", + "orderNote": "অর্ডার নোট", + "orderNotePlaceholder": "অর্ডারের জন্য কোনো বিশেষ নির্দেশনা?", + "termsAgreement": "আমি শর্তাবলী মানছি", + "privacyAgreement": "আমি গোপনীয়তা নীতি মানছি", + "orderProcessing": "অর্ডার প্রক্রিয়া করা হচ্ছে...", + "orderSuccess": "অর্ডার সফলভাবে সম্পন্ন!", + "orderSuccessMessage": "আপনার অর্ডারের জন্য ধন্যবাদ। শীঘ্রই একটি কনফার্মেশন ইমেইল পাবেন।", + "orderFailed": "অর্ডার ব্যর্থ", + "orderFailedMessage": "কিছু সমস্যা হয়েছে। আবার চেষ্টা করুন।", + "orderNumber": "অর্ডার নম্বর", + "continueShopping": "কেনাকাটা চালিয়ে যান", + "viewOrder": "অর্ডার দেখুন", + "downloadInvoice": "ইনভয়েস ডাউনলোড করুন", + "guest": { + "title": "গেস্ট চেকআউট", + "subtitle": "অ্যাকাউন্ট ছাড়াই চেকআউট করুন", + "or": "অথবা", + "createAccount": "দ্রুত চেকআউটের জন্য অ্যাকাউন্ট তৈরি করুন" + } + }, + "order": { + "orders": "অর্ডার", + "myOrders": "আমার অর্ডার", + "orderNumber": "অর্ডার নম্বর", + "orderDate": "অর্ডারের তারিখ", + "orderStatus": "অর্ডার স্ট্যাটাস", + "orderTotal": "অর্ডার মোট", + "orderDetails": "অর্ডার বিবরণ", + "orderHistory": "অর্ডার ইতিহাস", + "noOrders": "এখনো কোনো অর্ডার নেই", + "noOrdersMessage": "আপনি এখনো কোনো অর্ডার করেননি", + "status": { + "pending": "অপেক্ষমাণ", + "confirmed": "নিশ্চিত", + "processing": "প্রসেস হচ্ছে", + "shipped": "শিপ করা হয়েছে", + "inTransit": "ট্রানজিটে আছে", + "outForDelivery": "ডেলিভারির জন্য বের হয়েছে", + "delivered": "ডেলিভার হয়েছে", + "cancelled": "বাতিল", + "refunded": "রিফান্ড হয়েছে", + "returned": "ফেরত দেওয়া হয়েছে", + "failed": "ব্যর্থ" + }, + "paymentStatus": { + "pending": "পেমেন্ট অপেক্ষমাণ", + "authorized": "পেমেন্ট অনুমোদিত", + "paid": "পেমেন্ট সম্পন্ন", + "failed": "পেমেন্ট ব্যর্থ", + "refunded": "রিফান্ড হয়েছে" + }, + "trackOrder": "অর্ডার ট্র্যাক করুন", + "trackingNumber": "ট্র্যাকিং নম্বর", + "trackingUrl": "শিপমেন্ট ট্র্যাক করুন", + "estimatedDelivery": "আনুমানিক ডেলিভারি", + "deliveryAddress": "ডেলিভারি ঠিকানা", + "paymentMethod": "পেমেন্ট পদ্ধতি", + "itemsOrdered": "অর্ডার করা পণ্য", + "orderItems": "অর্ডার আইটেম", + "downloadInvoice": "ইনভয়েস ডাউনলোড করুন", + "reorder": "পুনরায় অর্ডার করুন", + "cancelOrder": "অর্ডার বাতিল করুন", + "cancelOrderConfirm": "আপনি কি এই অর্ডার বাতিল করতে চান?", + "cancelOrderReason": "বাতিলের কারণ", + "cancelOrderSuccess": "অর্ডার সফলভাবে বাতিল হয়েছে", + "requestReturn": "রিটার্ন অনুরোধ করুন", + "requestRefund": "রিফান্ড অনুরোধ করুন", + "contactSupport": "সাপোর্টে যোগাযোগ করুন", + "orderPlaced": "অর্ডার সম্পন্ন", + "orderConfirmed": "অর্ডার নিশ্চিত", + "orderShipped": "অর্ডার শিপ হয়েছে", + "orderDelivered": "অর্ডার ডেলিভার হয়েছে", + "timeline": { + "placed": "অর্ডার সম্পন্ন", + "confirmed": "অর্ডার নিশ্চিত", + "processing": "প্রসেস হচ্ছে", + "shipped": "শিপ হয়েছে", + "inTransit": "ট্রানজিটে", + "delivered": "ডেলিভার হয়েছে" + }, + "summary": { + "subtotal": "সাবটোটাল", + "shipping": "ডেলিভারি চার্জ", + "tax": "ট্যাক্স", + "discount": "ছাড়", + "total": "মোট", + "paid": "পরিশোধিত", + "due": "বাকি" + } + }, + "dashboard": { + "title": "ড্যাশবোর্ড", + "overview": "ওভারভিউ", + "welcome": "স্বাগতম, {name}!", + "quickStats": "দ্রুত পরিসংখ্যান", + "recentOrders": "সাম্প্রতিক অর্ডার", + "recentActivity": "সাম্প্রতিক কার্যকলাপ", + "totalOrders": "মোট অর্ডার", + "totalRevenue": "মোট আয়", + "totalProducts": "মোট পণ্য", + "totalCustomers": "মোট কাস্টমার", + "orders": "অর্ডার", + "products": "পণ্য", + "customers": "কাস্টমার", + "analytics": "বিশ্লেষণ", + "reports": "রিপোর্ট", + "settings": "সেটিংস", + "inventory": "ইনভেন্টরি", + "marketing": "মার্কেটিং", + "discounts": "ছাড়", + "coupons": "কুপন", + "staff": "স্টাফ", + "roles": "রোল", + "permissions": "পারমিশন", + "addProduct": "পণ্য যোগ করুন", + "editProduct": "পণ্য সম্পাদনা করুন", + "deleteProduct": "পণ্য মুছুন", + "addCategory": "ক্যাটাগরি যোগ করুন", + "editCategory": "ক্যাটাগরি সম্পাদনা করুন", + "deleteCategory": "ক্যাটাগরি মুছুন", + "addBrand": "ব্র্যান্ড যোগ করুন", + "editBrand": "ব্র্যান্ড সম্পাদনা করুন", + "deleteBrand": "ব্র্যান্ড মুছুন", + "viewOrder": "অর্ডার দেখুন", + "updateStatus": "স্ট্যাটাস আপডেট করুন", + "exportData": "ডেটা এক্সপোর্ট করুন", + "importData": "ডেটা ইমপোর্ট করুন", + "bulkActions": "বাল্ক অ্যাকশন", + "selectAll": "সব বাছুন", + "deselectAll": "সব বাছাই বাতিল করুন", + "deleteSelected": "নির্বাচিত মুছুন", + "archiveSelected": "নির্বাচিত আর্কাইভ করুন", + "publishSelected": "নির্বাচিত প্রকাশ করুন", + "sales": { + "title": "বিক্রয়", + "today": "আজকের বিক্রয়", + "thisWeek": "এই সপ্তাহ", + "thisMonth": "এই মাস", + "thisYear": "এই বছর", + "comparison": "গত সময়ের তুলনায়", + "growth": "প্রবৃদ্ধি", + "revenue": "আয়", + "averageOrderValue": "গড় অর্ডার মূল্য", + "conversionRate": "কনভার্শন রেট" + }, + "inventory": { + "title": "ইনভেন্টরি", + "inStock": "স্টক আছে", + "lowStock": "স্টক কম", + "outOfStock": "স্টক শেষ", + "totalValue": "মোট মূল্য", + "alerts": "ইনভেন্টরি সতর্কতা" + }, + "store": { + "title": "স্টোর", + "storeSettings": "স্টোর সেটিংস", + "storeName": "স্টোরের নাম", + "storeUrl": "স্টোর URL", + "storeLogo": "স্টোর লোগো", + "storeDescription": "স্টোরের বিবরণ", + "contactInfo": "যোগাযোগের তথ্য", + "businessHours": "ব্যবসার সময়", + "socialMedia": "সোশ্যাল মিডিয়া", + "paymentSettings": "পেমেন্ট সেটিংস", + "shippingSettings": "ডেলিভারি সেটিংস", + "taxSettings": "ট্যাক্স সেটিংস", + "emailSettings": "ইমেইল সেটিংস" + } + }, + "admin": { + "title": "অ্যাডমিন প্যানেল", + "dashboard": "অ্যাডমিন ড্যাশবোর্ড", + "users": "ব্যবহারকারী", + "stores": "স্টোর", + "pendingApprovals": "অনুমোদনের অপেক্ষায়", + "allUsers": "সব ব্যবহারকারী", + "activeUsers": "সক্রিয় ব্যবহারকারী", + "pendingUsers": "অপেক্ষমাণ ব্যবহারকারী", + "suspendedUsers": "স্থগিত ব্যবহারকারী", + "userDetails": "ব্যবহারকারীর বিবরণ", + "approveUser": "ব্যবহারকারী অনুমোদন করুন", + "rejectUser": "ব্যবহারকারী প্রত্যাখ্যান করুন", + "suspendUser": "ব্যবহারকারী স্থগিত করুন", + "activateUser": "ব্যবহারকারী সক্রিয় করুন", + "deleteUser": "ব্যবহারকারী মুছুন", + "createStore": "স্টোর তৈরি করুন", + "editStore": "স্টোর সম্পাদনা করুন", + "deleteStore": "স্টোর মুছুন", + "storeRequests": "স্টোর অনুরোধ", + "roleRequests": "রোল অনুরোধ", + "systemSettings": "সিস্টেম সেটিংস", + "auditLogs": "অডিট লগ", + "platformActivity": "প্ল্যাটফর্ম কার্যকলাপ", + "notifications": "বিজ্ঞপ্তি" + }, + "settings": { + "title": "সেটিংস", + "general": "সাধারণ", + "account": "অ্যাকাউন্ট", + "security": "নিরাপত্তা", + "notifications": "বিজ্ঞপ্তি", + "privacy": "গোপনীয়তা", + "language": "ভাষা", + "theme": "থিম", + "profile": { + "title": "প্রোফাইল", + "personalInfo": "ব্যক্তিগত তথ্য", + "name": "নাম", + "email": "ইমেইল", + "phone": "ফোন", + "avatar": "প্রোফাইল ছবি", + "changeAvatar": "ছবি পরিবর্তন করুন", + "removeAvatar": "ছবি সরান" + }, + "security": { + "title": "নিরাপত্তা", + "changePassword": "পাসওয়ার্ড পরিবর্তন করুন", + "currentPassword": "বর্তমান পাসওয়ার্ড", + "newPassword": "নতুন পাসওয়ার্ড", + "confirmNewPassword": "নতুন পাসওয়ার্ড নিশ্চিত করুন", + "twoFactor": "দুই-স্তর যাচাইকরণ", + "enable2FA": "2FA সক্রিয় করুন", + "disable2FA": "2FA নিষ্ক্রিয় করুন", + "activeSessions": "সক্রিয় সেশন", + "logoutAllDevices": "সব ডিভাইস থেকে লগআউট" + }, + "notifications": { + "title": "বিজ্ঞপ্তি সেটিংস", + "email": "ইমেইল বিজ্ঞপ্তি", + "push": "পুশ বিজ্ঞপ্তি", + "sms": "SMS বিজ্ঞপ্তি", + "orderUpdates": "অর্ডার আপডেট", + "promotions": "প্রমোশন ও অফার", + "newsletter": "নিউজলেটার", + "productUpdates": "পণ্য আপডেট" + }, + "language": { + "title": "ভাষা ও অঞ্চল", + "selectLanguage": "ভাষা বাছুন", + "selectCurrency": "মুদ্রা বাছুন", + "selectTimezone": "টাইমজোন বাছুন" + } + }, + "store": { + "title": "স্টোর", + "storefront": "স্টোরফ্রন্ট", + "aboutStore": "স্টোর সম্পর্কে", + "storeInfo": "স্টোরের তথ্য", + "contactStore": "স্টোরে যোগাযোগ করুন", + "visitStore": "স্টোর দেখুন", + "allProducts": "সব পণ্য", + "newArrivals": "নতুন পণ্য", + "bestSellers": "বেস্ট সেলার", + "onSale": "সেল চলছে", + "collections": "কালেকশন", + "featuredProducts": "ফিচার্ড পণ্য", + "storeNotFound": "স্টোর পাওয়া যায়নি", + "storeNotFoundMessage": "আপনি যে স্টোর খুঁজছেন সেটি নেই বা সরানো হয়েছে।" + }, + "footer": { + "aboutUs": "আমাদের সম্পর্কে", + "contactUs": "যোগাযোগ করুন", + "careers": "ক্যারিয়ার", + "press": "প্রেস", + "blog": "ব্লগ", + "help": "সাহায্য", + "faq": "প্রশ্নোত্তর", + "shipping": "ডেলিভারি", + "returns": "রিটার্ন", + "sizeGuide": "সাইজ গাইড", + "trackOrder": "অর্ডার ট্র্যাক করুন", + "legal": "আইনি", + "terms": "সেবার শর্তাবলী", + "privacy": "গোপনীয়তা নীতি", + "cookies": "কুকি নীতি", + "followUs": "আমাদের অনুসরণ করুন", + "newsletter": "নিউজলেটার", + "newsletterSubtitle": "আপডেট এবং অফার পেতে সাবস্ক্রাইব করুন", + "emailPlaceholder": "আপনার ইমেইল লিখুন", + "subscribe": "সাবস্ক্রাইব", + "subscribeSuccess": "সাবস্ক্রিপশনের জন্য ধন্যবাদ!", + "copyright": "© {year} {name}। সর্বস্বত্ব সংরক্ষিত।", + "paymentMethods": "পেমেন্ট পদ্ধতি", + "securePayment": "নিরাপদ পেমেন্ট" + }, + "errors": { + "required": "এই ফিল্ডটি আবশ্যক", + "invalidEmail": "সঠিক ইমেইল ঠিকানা দিন", + "invalidPhone": "সঠিক মোবাইল নম্বর দিন", + "invalidUrl": "সঠিক URL দিন", + "passwordTooShort": "পাসওয়ার্ড কমপক্ষে ৮ অক্ষরের হতে হবে", + "passwordTooWeak": "পাসওয়ার্ড খুব দুর্বল", + "passwordMismatch": "পাসওয়ার্ড মিলছে না", + "minLength": "কমপক্ষে {min} অক্ষর হতে হবে", + "maxLength": "সর্বোচ্চ {max} অক্ষর হতে পারে", + "minValue": "সর্বনিম্ন {min} হতে হবে", + "maxValue": "সর্বোচ্চ {max} হতে পারে", + "invalidFormat": "অবৈধ ফরম্যাট", + "invalidDate": "অবৈধ তারিখ", + "futureDate": "তারিখ ভবিষ্যতে হতে হবে", + "pastDate": "তারিখ অতীতে হতে হবে", + "networkError": "নেটওয়ার্ক ত্রুটি। আপনার সংযোগ পরীক্ষা করে আবার চেষ্টা করুন।", + "serverError": "সার্ভার ত্রুটি। পরে আবার চেষ্টা করুন।", + "notFound": "পাওয়া যায়নি", + "unauthorized": "এই কাজ করার অনুমতি নেই", + "forbidden": "প্রবেশ নিষেধ", + "sessionExpired": "আপনার সেশনের মেয়াদ শেষ। আবার লগইন করুন।", + "rateLimited": "অনেক বেশি অনুরোধ। কিছুক্ষণ পর আবার চেষ্টা করুন।", + "validationError": "আপনার তথ্য পরীক্ষা করে আবার চেষ্টা করুন", + "unknownError": "একটি অপ্রত্যাশিত ত্রুটি হয়েছে", + "pageNotFound": "পৃষ্ঠা পাওয়া যায়নি", + "pageNotFoundMessage": "আপনি যে পৃষ্ঠা খুঁজছেন সেটি নেই বা সরানো হয়েছে।", + "goHome": "হোমে যান" + }, + "sms": { + "otp": "আপনার যাচাই কোড: {code}। {minutes} মিনিট পর্যন্ত বৈধ।", + "orderConfirmation": "অর্ডার #{orderNumber} নিশ্চিত! মোট: {total}। ট্র্যাক করুন: {trackingUrl}", + "orderShipped": "সুখবর! অর্ডার #{orderNumber} শিপ হয়েছে। ট্র্যাক করুন: {trackingUrl}", + "orderDelivered": "অর্ডার #{orderNumber} ডেলিভার হয়েছে! কেনাকাটার জন্য ধন্যবাদ।", + "deliveryOTP": "আপনার ডেলিভারি OTP: {code}। শুধুমাত্র ডেলিভারি বয়কে দিন।", + "characterCount": "{current}/{max} অক্ষর", + "smsCount": "{count} SMS", + "cost": "খরচ: {amount}", + "encoding": { + "gsm7": "GSM-7 (ইংরেজি)", + "utf16": "UTF-16 (বাংলা)" + } + }, + "email": { + "subject": { + "welcome": "{storeName} এ স্বাগতম!", + "orderConfirmation": "অর্ডার নিশ্চিত - #{orderNumber}", + "orderShipped": "আপনার অর্ডার শিপ হয়েছে - #{orderNumber}", + "orderDelivered": "অর্ডার ডেলিভার হয়েছে - #{orderNumber}", + "passwordReset": "আপনার পাসওয়ার্ড রিসেট করুন", + "accountApproved": "অ্যাকাউন্ট অনুমোদিত - স্বাগতম!", + "accountPending": "আবেদন গ্রহণ করা হয়েছে", + "verifyEmail": "আপনার ইমেইল যাচাই করুন" + } + }, + "meta": { + "defaultTitle": "স্টর্মকম - ই-কমার্স প্ল্যাটফর্ম", + "defaultDescription": "সেরা পণ্য সেরা দামে কিনুন। দ্রুত ডেলিভারি এবং নিরাপদ পেমেন্ট।", + "productTitle": "{productName} | {storeName}", + "categoryTitle": "{categoryName} | {storeName}", + "searchTitle": "\"{query}\" এর জন্য সার্চ ফলাফল | {storeName}", + "cartTitle": "শপিং কার্ট | {storeName}", + "checkoutTitle": "চেকআউট | {storeName}", + "orderTitle": "অর্ডার #{orderNumber} | {storeName}" + }, + "calendar": { + "months": { + "january": "জানুয়ারি", + "february": "ফেব্রুয়ারি", + "march": "মার্চ", + "april": "এপ্রিল", + "may": "মে", + "june": "জুন", + "july": "জুলাই", + "august": "আগস্ট", + "september": "সেপ্টেম্বর", + "october": "অক্টোবর", + "november": "নভেম্বর", + "december": "ডিসেম্বর" + }, + "weekdays": { + "sunday": "রবিবার", + "monday": "সোমবার", + "tuesday": "মঙ্গলবার", + "wednesday": "বুধবার", + "thursday": "বৃহস্পতিবার", + "friday": "শুক্রবার", + "saturday": "শনিবার" + }, + "bengaliCalendar": { + "boishakh": "বৈশাখ", + "joistho": "জ্যৈষ্ঠ", + "ashar": "আষাঢ়", + "srabon": "শ্রাবণ", + "bhadro": "ভাদ্র", + "ashwin": "আশ্বিন", + "kartik": "কার্তিক", + "ogrohayon": "অগ্রহায়ণ", + "poush": "পৌষ", + "magh": "মাঘ", + "falgun": "ফাল্গুন", + "choitro": "চৈত্র" + }, + "festivals": { + "pohelaBoishakh": "পহেলা বৈশাখ", + "eidUlFitr": "ঈদ উল ফিতর", + "eidUlAdha": "ঈদ উল আযহা", + "durga Puja": "দুর্গা পূজা", + "victory Day": "বিজয় দিবস", + "independence Day": "স্বাধীনতা দিবস" + } + }, + "seasonal": { + "eidMubarak": "ঈদ মোবারক", + "eidGreeting": "আপনাকে এবং আপনার পরিবারকে ঈদের শুভেচ্ছা!", + "pohelaBoishakhGreeting": "শুভ নববর্ষ! ১৪৩২ বাংলা সাল শুভ হোক।", + "durga PujaGreeting": "শুভ শারদীয়। মা দুর্গার আশীর্বাদ সকলের উপর বর্ষিত হোক।", + "victoryDayGreeting": "বিজয় দিবসের শুভেচ্ছা! জয় বাংলা!", + "newYearGreeting": "শুভ নববর্ষ! নতুন বছরে শুভকামনা।" + } +} diff --git a/src/messages/en.json b/src/messages/en.json new file mode 100644 index 00000000..b73c62bd --- /dev/null +++ b/src/messages/en.json @@ -0,0 +1,742 @@ +{ + "common": { + "home": "Home", + "products": "Products", + "categories": "Categories", + "brands": "Brands", + "cart": "Cart", + "checkout": "Checkout", + "login": "Login", + "signup": "Sign Up", + "logout": "Logout", + "search": "Search", + "searchPlaceholder": "Search products...", + "filter": "Filter", + "sort": "Sort", + "sortBy": "Sort by", + "addToCart": "Add to Cart", + "buyNow": "Buy Now", + "viewDetails": "View Details", + "viewAll": "View All", + "showMore": "Show More", + "showLess": "Show Less", + "loading": "Loading...", + "error": "Error", + "success": "Success", + "warning": "Warning", + "info": "Info", + "confirm": "Confirm", + "cancel": "Cancel", + "save": "Save", + "edit": "Edit", + "delete": "Delete", + "remove": "Remove", + "close": "Close", + "back": "Back", + "next": "Next", + "previous": "Previous", + "submit": "Submit", + "reset": "Reset", + "clear": "Clear", + "apply": "Apply", + "yes": "Yes", + "no": "No", + "ok": "OK", + "optional": "Optional", + "required": "Required", + "all": "All", + "none": "None", + "select": "Select", + "selected": "Selected", + "noResults": "No results found", + "noData": "No data available", + "tryAgain": "Try Again", + "learnMore": "Learn More", + "seeAll": "See All", + "share": "Share", + "copy": "Copy", + "copied": "Copied!", + "download": "Download", + "upload": "Upload", + "refresh": "Refresh", + "settings": "Settings", + "profile": "Profile", + "account": "Account", + "help": "Help", + "support": "Support", + "contactUs": "Contact Us", + "aboutUs": "About Us", + "termsOfService": "Terms of Service", + "privacyPolicy": "Privacy Policy", + "faq": "FAQ", + "language": "Language", + "theme": "Theme", + "darkMode": "Dark Mode", + "lightMode": "Light Mode", + "systemDefault": "System Default", + "notifications": "Notifications", + "welcome": "Welcome", + "greeting": { + "morning": "Good Morning", + "afternoon": "Good Afternoon", + "evening": "Good Evening", + "night": "Good Night" + }, + "currency": "Currency", + "price": "Price", + "total": "Total", + "subtotal": "Subtotal", + "tax": "Tax", + "discount": "Discount", + "shipping": "Shipping", + "free": "Free", + "freeShipping": "Free Shipping", + "quantity": "Quantity", + "inStock": "In Stock", + "outOfStock": "Out of Stock", + "lowStock": "Low Stock", + "available": "Available", + "unavailable": "Unavailable", + "new": "New", + "sale": "Sale", + "hot": "Hot", + "featured": "Featured", + "popular": "Popular", + "trending": "Trending", + "bestSeller": "Best Seller", + "recommended": "Recommended", + "dateFormat": { + "short": "Short", + "long": "Long", + "relative": "Relative" + } + }, + "navigation": { + "mainMenu": "Main Menu", + "myAccount": "My Account", + "myOrders": "My Orders", + "myWishlist": "My Wishlist", + "myAddresses": "My Addresses", + "orderHistory": "Order History", + "trackOrder": "Track Order", + "customerService": "Customer Service", + "shippingInfo": "Shipping Info", + "returnPolicy": "Return Policy", + "paymentMethods": "Payment Methods" + }, + "auth": { + "loginTitle": "Login to Your Account", + "loginSubtitle": "Welcome back! Please enter your credentials", + "signupTitle": "Create New Account", + "signupSubtitle": "Join us and start shopping", + "email": "Email", + "emailPlaceholder": "Enter your email", + "password": "Password", + "passwordPlaceholder": "Enter your password", + "confirmPassword": "Confirm Password", + "confirmPasswordPlaceholder": "Confirm your password", + "name": "Full Name", + "namePlaceholder": "Enter your full name", + "phone": "Phone Number", + "phonePlaceholder": "Enter your phone number", + "rememberMe": "Remember me", + "forgotPassword": "Forgot Password?", + "resetPassword": "Reset Password", + "resetPasswordTitle": "Reset Your Password", + "resetPasswordSubtitle": "Enter your email to receive a reset link", + "sendResetLink": "Send Reset Link", + "newPassword": "New Password", + "newPasswordPlaceholder": "Enter new password", + "loginButton": "Login", + "signupButton": "Sign Up", + "orContinueWith": "Or continue with", + "socialLogin": { + "google": "Continue with Google", + "facebook": "Continue with Facebook", + "apple": "Continue with Apple" + }, + "alreadyHaveAccount": "Already have an account?", + "dontHaveAccount": "Don't have an account?", + "logoutConfirm": "Are you sure you want to logout?", + "logoutSuccess": "You have been logged out successfully", + "loginSuccess": "Welcome back!", + "signupSuccess": "Account created successfully!", + "verifyEmail": "Verify Your Email", + "verifyEmailSent": "We've sent a verification link to your email", + "resendVerification": "Resend Verification Email", + "verificationExpired": "Verification link has expired", + "accountPending": "Account Pending Approval", + "accountPendingMessage": "Your account is being reviewed. You'll receive an email once approved.", + "accountSuspended": "Account Suspended", + "accountSuspendedMessage": "Your account has been suspended. Please contact support.", + "businessInfo": { + "title": "Business Information", + "businessName": "Business Name", + "businessNamePlaceholder": "Enter your business name", + "businessDescription": "Business Description", + "businessDescriptionPlaceholder": "Describe your business", + "businessCategory": "Business Category", + "businessCategoryPlaceholder": "Select category" + } + }, + "product": { + "title": "Product", + "products": "Products", + "allProducts": "All Products", + "productDetails": "Product Details", + "price": "Price", + "regularPrice": "Regular Price", + "salePrice": "Sale Price", + "youSave": "You Save", + "brand": "Brand", + "category": "Category", + "sku": "SKU", + "barcode": "Barcode", + "weight": "Weight", + "dimensions": "Dimensions", + "inStock": "In Stock", + "outOfStock": "Out of Stock", + "lowStock": "Only {count} left in stock", + "stockStatus": "Stock Status", + "availability": "Availability", + "description": "Description", + "shortDescription": "Short Description", + "specifications": "Specifications", + "features": "Features", + "reviews": "Reviews", + "customerReviews": "Customer Reviews", + "writeReview": "Write a Review", + "rating": "Rating", + "ratings": "Ratings", + "averageRating": "Average Rating", + "noReviews": "No reviews yet", + "beFirstToReview": "Be the first to review this product", + "reviewTitle": "Review Title", + "reviewComment": "Your Review", + "submitReview": "Submit Review", + "verifiedPurchase": "Verified Purchase", + "relatedProducts": "Related Products", + "similarProducts": "Similar Products", + "youMayAlsoLike": "You May Also Like", + "recentlyViewed": "Recently Viewed", + "compare": "Compare", + "addToWishlist": "Add to Wishlist", + "removeFromWishlist": "Remove from Wishlist", + "shareProduct": "Share Product", + "productAdded": "Product added to cart", + "productRemoved": "Product removed from cart", + "selectOptions": "Select Options", + "selectVariant": "Select Variant", + "color": "Color", + "size": "Size", + "quantity": "Quantity", + "maxQuantity": "Maximum quantity: {max}", + "minQuantity": "Minimum quantity: {min}", + "filter": { + "title": "Filters", + "price": "Price", + "priceRange": "Price Range", + "minPrice": "Min Price", + "maxPrice": "Max Price", + "brand": "Brand", + "category": "Category", + "color": "Color", + "size": "Size", + "rating": "Rating", + "availability": "Availability", + "clearAll": "Clear All Filters", + "apply": "Apply Filters", + "noFilters": "No filters applied" + }, + "sort": { + "newest": "Newest", + "oldest": "Oldest", + "priceLowToHigh": "Price: Low to High", + "priceHighToLow": "Price: High to Low", + "nameAZ": "Name: A to Z", + "nameZA": "Name: Z to A", + "popularity": "Popularity", + "rating": "Rating", + "bestSelling": "Best Selling" + } + }, + "cart": { + "title": "Your Cart", + "yourCart": "Your Cart", + "shoppingCart": "Shopping Cart", + "empty": "Your cart is empty", + "emptyMessage": "Looks like you haven't added anything to your cart yet", + "startShopping": "Start Shopping", + "subtotal": "Subtotal", + "shipping": "Shipping", + "shippingCalculatedAtCheckout": "Calculated at checkout", + "tax": "Tax", + "discount": "Discount", + "total": "Total", + "estimatedTotal": "Estimated Total", + "items": "{count, plural, =0 {No items} =1 {1 item} other {# items}}", + "removeItem": "Remove", + "removeItemConfirm": "Remove this item from cart?", + "updateQuantity": "Update Quantity", + "saveForLater": "Save for Later", + "moveToWishlist": "Move to Wishlist", + "continueShopping": "Continue Shopping", + "proceedToCheckout": "Proceed to Checkout", + "viewCart": "View Cart", + "clearCart": "Clear Cart", + "clearCartConfirm": "Are you sure you want to clear your cart?", + "itemAdded": "Item added to cart", + "itemRemoved": "Item removed from cart", + "itemUpdated": "Cart updated", + "cartUpdated": "Cart updated successfully", + "promoCode": "Promo Code", + "enterPromoCode": "Enter promo code", + "applyPromoCode": "Apply", + "promoCodeApplied": "Promo code applied", + "promoCodeInvalid": "Invalid promo code", + "promoCodeExpired": "Promo code has expired", + "promoCodeRemoved": "Promo code removed", + "freeShipping": "Free Shipping", + "freeShippingMessage": "You qualify for free shipping!", + "freeShippingProgress": "Add {amount} more for free shipping", + "orderNote": "Order Note", + "orderNotePlaceholder": "Add a note to your order...", + "giftWrapping": "Gift Wrapping", + "giftWrappingMessage": "Add a gift message", + "estimatedDelivery": "Estimated Delivery", + "secureCheckout": "Secure Checkout" + }, + "checkout": { + "title": "Checkout", + "secureCheckout": "Secure Checkout", + "step": "Step {current} of {total}", + "shippingAddress": "Shipping Address", + "billingAddress": "Billing Address", + "sameAsShipping": "Same as shipping address", + "differentBillingAddress": "Use a different billing address", + "savedAddresses": "Saved Addresses", + "addNewAddress": "Add New Address", + "editAddress": "Edit Address", + "deleteAddress": "Delete Address", + "setAsDefault": "Set as Default", + "defaultAddress": "Default Address", + "fullName": "Full Name", + "fullNamePlaceholder": "Enter your full name", + "phone": "Phone Number", + "phonePlaceholder": "Enter your phone number", + "email": "Email", + "emailPlaceholder": "Enter your email", + "address": "Address", + "addressPlaceholder": "Enter your address", + "addressLine2": "Apartment, suite, etc.", + "addressLine2Placeholder": "Apartment, suite, unit, building, floor, etc.", + "city": "City", + "cityPlaceholder": "Enter city", + "area": "Area", + "areaPlaceholder": "Enter area/district", + "state": "State/Province", + "statePlaceholder": "Enter state or province", + "postalCode": "Postal Code", + "postalCodePlaceholder": "Enter postal code", + "country": "Country", + "countryPlaceholder": "Select country", + "shippingMethod": "Shipping Method", + "selectShippingMethod": "Select shipping method", + "standardShipping": "Standard Shipping", + "expressShipping": "Express Shipping", + "overnightShipping": "Overnight Shipping", + "localPickup": "Local Pickup", + "estimatedDelivery": "Estimated Delivery", + "deliveryDays": "{days} business days", + "paymentMethod": "Payment Method", + "selectPaymentMethod": "Select payment method", + "creditCard": "Credit Card", + "debitCard": "Debit Card", + "cashOnDelivery": "Cash on Delivery", + "bkash": "bKash", + "nagad": "Nagad", + "rocket": "Rocket", + "bankTransfer": "Bank Transfer", + "mobileBanking": "Mobile Banking", + "paymentMethods": { + "bkash": { + "title": "bKash", + "description": "Pay with bKash mobile wallet", + "phoneNumber": "bKash Account Number", + "transactionId": "Transaction ID" + }, + "nagad": { + "title": "Nagad", + "description": "Pay with Nagad mobile wallet", + "phoneNumber": "Nagad Account Number", + "transactionId": "Transaction ID" + }, + "cod": { + "title": "Cash on Delivery", + "description": "Pay when you receive your order", + "note": "Pay with cash upon delivery" + }, + "card": { + "title": "Credit/Debit Card", + "description": "Pay securely with your card", + "cardNumber": "Card Number", + "expiryDate": "Expiry Date", + "cvv": "CVV", + "nameOnCard": "Name on Card" + } + }, + "placeOrder": "Place Order", + "confirmOrder": "Confirm Order", + "orderSummary": "Order Summary", + "reviewOrder": "Review Your Order", + "orderNote": "Order Note", + "orderNotePlaceholder": "Any special instructions for your order?", + "termsAgreement": "I agree to the Terms and Conditions", + "privacyAgreement": "I agree to the Privacy Policy", + "orderProcessing": "Processing your order...", + "orderSuccess": "Order Placed Successfully!", + "orderSuccessMessage": "Thank you for your order. You will receive a confirmation email shortly.", + "orderFailed": "Order Failed", + "orderFailedMessage": "Something went wrong. Please try again.", + "orderNumber": "Order Number", + "continueShopping": "Continue Shopping", + "viewOrder": "View Order", + "downloadInvoice": "Download Invoice", + "guest": { + "title": "Guest Checkout", + "subtitle": "Checkout without creating an account", + "or": "or", + "createAccount": "Create an account for faster checkout next time" + } + }, + "order": { + "orders": "Orders", + "myOrders": "My Orders", + "orderNumber": "Order Number", + "orderDate": "Order Date", + "orderStatus": "Order Status", + "orderTotal": "Order Total", + "orderDetails": "Order Details", + "orderHistory": "Order History", + "noOrders": "No orders yet", + "noOrdersMessage": "You haven't placed any orders yet", + "status": { + "pending": "Pending", + "confirmed": "Confirmed", + "processing": "Processing", + "shipped": "Shipped", + "inTransit": "In Transit", + "outForDelivery": "Out for Delivery", + "delivered": "Delivered", + "cancelled": "Cancelled", + "refunded": "Refunded", + "returned": "Returned", + "failed": "Failed" + }, + "paymentStatus": { + "pending": "Payment Pending", + "authorized": "Payment Authorized", + "paid": "Paid", + "failed": "Payment Failed", + "refunded": "Refunded" + }, + "trackOrder": "Track Order", + "trackingNumber": "Tracking Number", + "trackingUrl": "Track Shipment", + "estimatedDelivery": "Estimated Delivery", + "deliveryAddress": "Delivery Address", + "paymentMethod": "Payment Method", + "itemsOrdered": "Items Ordered", + "orderItems": "Order Items", + "downloadInvoice": "Download Invoice", + "reorder": "Reorder", + "cancelOrder": "Cancel Order", + "cancelOrderConfirm": "Are you sure you want to cancel this order?", + "cancelOrderReason": "Reason for cancellation", + "cancelOrderSuccess": "Order cancelled successfully", + "requestReturn": "Request Return", + "requestRefund": "Request Refund", + "contactSupport": "Contact Support", + "orderPlaced": "Order Placed", + "orderConfirmed": "Order Confirmed", + "orderShipped": "Order Shipped", + "orderDelivered": "Order Delivered", + "timeline": { + "placed": "Order Placed", + "confirmed": "Order Confirmed", + "processing": "Processing", + "shipped": "Shipped", + "inTransit": "In Transit", + "delivered": "Delivered" + }, + "summary": { + "subtotal": "Subtotal", + "shipping": "Shipping", + "tax": "Tax", + "discount": "Discount", + "total": "Total", + "paid": "Paid", + "due": "Due" + } + }, + "dashboard": { + "title": "Dashboard", + "overview": "Overview", + "welcome": "Welcome back, {name}!", + "quickStats": "Quick Stats", + "recentOrders": "Recent Orders", + "recentActivity": "Recent Activity", + "totalOrders": "Total Orders", + "totalRevenue": "Total Revenue", + "totalProducts": "Total Products", + "totalCustomers": "Total Customers", + "orders": "Orders", + "products": "Products", + "customers": "Customers", + "analytics": "Analytics", + "reports": "Reports", + "settings": "Settings", + "inventory": "Inventory", + "marketing": "Marketing", + "discounts": "Discounts", + "coupons": "Coupons", + "staff": "Staff", + "roles": "Roles", + "permissions": "Permissions", + "addProduct": "Add Product", + "editProduct": "Edit Product", + "deleteProduct": "Delete Product", + "addCategory": "Add Category", + "editCategory": "Edit Category", + "deleteCategory": "Delete Category", + "addBrand": "Add Brand", + "editBrand": "Edit Brand", + "deleteBrand": "Delete Brand", + "viewOrder": "View Order", + "updateStatus": "Update Status", + "exportData": "Export Data", + "importData": "Import Data", + "bulkActions": "Bulk Actions", + "selectAll": "Select All", + "deselectAll": "Deselect All", + "deleteSelected": "Delete Selected", + "archiveSelected": "Archive Selected", + "publishSelected": "Publish Selected", + "sales": { + "title": "Sales", + "today": "Today's Sales", + "thisWeek": "This Week", + "thisMonth": "This Month", + "thisYear": "This Year", + "comparison": "vs last period", + "growth": "Growth", + "revenue": "Revenue", + "averageOrderValue": "Average Order Value", + "conversionRate": "Conversion Rate" + }, + "inventory": { + "title": "Inventory", + "inStock": "In Stock", + "lowStock": "Low Stock", + "outOfStock": "Out of Stock", + "totalValue": "Total Value", + "alerts": "Inventory Alerts" + }, + "store": { + "title": "Store", + "storeSettings": "Store Settings", + "storeName": "Store Name", + "storeUrl": "Store URL", + "storeLogo": "Store Logo", + "storeDescription": "Store Description", + "contactInfo": "Contact Information", + "businessHours": "Business Hours", + "socialMedia": "Social Media", + "paymentSettings": "Payment Settings", + "shippingSettings": "Shipping Settings", + "taxSettings": "Tax Settings", + "emailSettings": "Email Settings" + } + }, + "admin": { + "title": "Admin Panel", + "dashboard": "Admin Dashboard", + "users": "Users", + "stores": "Stores", + "pendingApprovals": "Pending Approvals", + "allUsers": "All Users", + "activeUsers": "Active Users", + "pendingUsers": "Pending Users", + "suspendedUsers": "Suspended Users", + "userDetails": "User Details", + "approveUser": "Approve User", + "rejectUser": "Reject User", + "suspendUser": "Suspend User", + "activateUser": "Activate User", + "deleteUser": "Delete User", + "createStore": "Create Store", + "editStore": "Edit Store", + "deleteStore": "Delete Store", + "storeRequests": "Store Requests", + "roleRequests": "Role Requests", + "systemSettings": "System Settings", + "auditLogs": "Audit Logs", + "platformActivity": "Platform Activity", + "notifications": "Notifications" + }, + "settings": { + "title": "Settings", + "general": "General", + "account": "Account", + "security": "Security", + "notifications": "Notifications", + "privacy": "Privacy", + "language": "Language", + "theme": "Theme", + "profile": { + "title": "Profile", + "personalInfo": "Personal Information", + "name": "Name", + "email": "Email", + "phone": "Phone", + "avatar": "Profile Picture", + "changeAvatar": "Change Picture", + "removeAvatar": "Remove Picture" + }, + "security": { + "title": "Security", + "changePassword": "Change Password", + "currentPassword": "Current Password", + "newPassword": "New Password", + "confirmNewPassword": "Confirm New Password", + "twoFactor": "Two-Factor Authentication", + "enable2FA": "Enable 2FA", + "disable2FA": "Disable 2FA", + "activeSessions": "Active Sessions", + "logoutAllDevices": "Logout All Devices" + }, + "notifications": { + "title": "Notification Settings", + "email": "Email Notifications", + "push": "Push Notifications", + "sms": "SMS Notifications", + "orderUpdates": "Order Updates", + "promotions": "Promotions & Offers", + "newsletter": "Newsletter", + "productUpdates": "Product Updates" + }, + "language": { + "title": "Language & Region", + "selectLanguage": "Select Language", + "selectCurrency": "Select Currency", + "selectTimezone": "Select Timezone" + } + }, + "store": { + "title": "Store", + "storefront": "Storefront", + "aboutStore": "About Store", + "storeInfo": "Store Information", + "contactStore": "Contact Store", + "visitStore": "Visit Store", + "allProducts": "All Products", + "newArrivals": "New Arrivals", + "bestSellers": "Best Sellers", + "onSale": "On Sale", + "collections": "Collections", + "featuredProducts": "Featured Products", + "storeNotFound": "Store Not Found", + "storeNotFoundMessage": "The store you're looking for doesn't exist or has been removed." + }, + "footer": { + "aboutUs": "About Us", + "contactUs": "Contact Us", + "careers": "Careers", + "press": "Press", + "blog": "Blog", + "help": "Help", + "faq": "FAQ", + "shipping": "Shipping", + "returns": "Returns", + "sizeGuide": "Size Guide", + "trackOrder": "Track Order", + "legal": "Legal", + "terms": "Terms of Service", + "privacy": "Privacy Policy", + "cookies": "Cookie Policy", + "followUs": "Follow Us", + "newsletter": "Newsletter", + "newsletterSubtitle": "Subscribe to our newsletter for updates and offers", + "emailPlaceholder": "Enter your email", + "subscribe": "Subscribe", + "subscribeSuccess": "Thank you for subscribing!", + "copyright": "© {year} {name}. All rights reserved.", + "paymentMethods": "Payment Methods", + "securePayment": "Secure Payment" + }, + "errors": { + "required": "This field is required", + "invalidEmail": "Please enter a valid email address", + "invalidPhone": "Please enter a valid phone number", + "invalidUrl": "Please enter a valid URL", + "passwordTooShort": "Password must be at least 8 characters", + "passwordTooWeak": "Password is too weak", + "passwordMismatch": "Passwords do not match", + "minLength": "Must be at least {min} characters", + "maxLength": "Must be no more than {max} characters", + "minValue": "Must be at least {min}", + "maxValue": "Must be no more than {max}", + "invalidFormat": "Invalid format", + "invalidDate": "Invalid date", + "futureDate": "Date must be in the future", + "pastDate": "Date must be in the past", + "networkError": "Network error. Please check your connection and try again.", + "serverError": "Server error. Please try again later.", + "notFound": "Not found", + "unauthorized": "You are not authorized to perform this action", + "forbidden": "Access denied", + "sessionExpired": "Your session has expired. Please login again.", + "rateLimited": "Too many requests. Please try again later.", + "validationError": "Please check your input and try again", + "unknownError": "An unexpected error occurred", + "pageNotFound": "Page Not Found", + "pageNotFoundMessage": "The page you're looking for doesn't exist or has been moved.", + "goHome": "Go to Home" + }, + "sms": { + "otp": "Your verification code is: {code}. Valid for {minutes} minutes.", + "orderConfirmation": "Order #{orderNumber} confirmed! Total: {total}. Track at: {trackingUrl}", + "orderShipped": "Good news! Order #{orderNumber} has been shipped. Track: {trackingUrl}", + "orderDelivered": "Order #{orderNumber} delivered! Thank you for shopping with us.", + "deliveryOTP": "Your delivery OTP is: {code}. Share only with delivery person.", + "characterCount": "{current}/{max} characters", + "smsCount": "{count} SMS", + "cost": "Cost: {amount}", + "encoding": { + "gsm7": "GSM-7 (English)", + "utf16": "UTF-16 (Bengali)" + } + }, + "email": { + "subject": { + "welcome": "Welcome to {storeName}!", + "orderConfirmation": "Order Confirmed - #{orderNumber}", + "orderShipped": "Your Order Has Shipped - #{orderNumber}", + "orderDelivered": "Order Delivered - #{orderNumber}", + "passwordReset": "Reset Your Password", + "accountApproved": "Account Approved - Welcome!", + "accountPending": "Application Received", + "verifyEmail": "Verify Your Email" + } + }, + "meta": { + "defaultTitle": "StormCom - E-commerce Platform", + "defaultDescription": "Shop the best products at great prices. Fast shipping and secure payments.", + "productTitle": "{productName} | {storeName}", + "categoryTitle": "{categoryName} | {storeName}", + "searchTitle": "Search Results for \"{query}\" | {storeName}", + "cartTitle": "Shopping Cart | {storeName}", + "checkoutTitle": "Checkout | {storeName}", + "orderTitle": "Order #{orderNumber} | {storeName}" + } +} From 955ca66fe7cba3cf4e82b23825b83613fe2235a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:07:07 +0000 Subject: [PATCH 03/14] feat: Add translation API endpoints and bilingual email templates Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com> --- .../categories/[categoryId]/route.ts | 312 +++++++++++++++++ .../products/[productId]/route.ts | 316 ++++++++++++++++++ src/lib/email-templates.ts | 162 +++++++++ 3 files changed, 790 insertions(+) create mode 100644 src/app/api/translations/categories/[categoryId]/route.ts create mode 100644 src/app/api/translations/products/[productId]/route.ts diff --git a/src/app/api/translations/categories/[categoryId]/route.ts b/src/app/api/translations/categories/[categoryId]/route.ts new file mode 100644 index 00000000..3b00b6b9 --- /dev/null +++ b/src/app/api/translations/categories/[categoryId]/route.ts @@ -0,0 +1,312 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { z } from 'zod'; + +/** + * Schema for creating/updating category translations + */ +const translationSchema = z.object({ + locale: z.enum(['en', 'bn']), + name: z.string().min(1, 'Name is required'), + description: z.string().optional(), + metaTitle: z.string().optional(), + metaDescription: z.string().optional(), +}); + +const bulkTranslationSchema = z.object({ + translations: z.array(translationSchema), +}); + +/** + * GET /api/translations/categories/[categoryId] + * Get all translations for a category + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ categoryId: string }> } +) { + try { + const { categoryId } = await params; + const { searchParams } = new URL(request.url); + const locale = searchParams.get('locale'); + + const where: { categoryId: string; locale?: string } = { categoryId }; + if (locale) { + where.locale = locale; + } + + const translations = await prisma.categoryTranslation.findMany({ + where, + orderBy: { locale: 'asc' }, + }); + + // If specific locale requested and not found, return the category's default values + if (locale && translations.length === 0) { + const category = await prisma.category.findUnique({ + where: { id: categoryId }, + select: { + name: true, + description: true, + metaTitle: true, + metaDescription: true, + }, + }); + + if (category) { + return NextResponse.json({ + locale, + ...category, + isFallback: true, + }); + } + } + + return NextResponse.json(translations); + } catch (error) { + console.error('Error fetching category translations:', error); + return NextResponse.json( + { error: 'Failed to fetch translations' }, + { status: 500 } + ); + } +} + +/** + * POST /api/translations/categories/[categoryId] + * Create or update a category translation + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ categoryId: string }> } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { categoryId } = await params; + const body = await request.json(); + + const parsed = translationSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid data', details: parsed.error.issues }, + { status: 400 } + ); + } + + const { locale, ...translationData } = parsed.data; + + // Verify the category exists and user has access + const category = await prisma.category.findUnique({ + where: { id: categoryId }, + include: { store: true }, + }); + + if (!category) { + return NextResponse.json( + { error: 'Category not found' }, + { status: 404 } + ); + } + + // Check user has access to this store + const hasAccess = await prisma.storeStaff.findFirst({ + where: { + storeId: category.storeId, + userId: session.user.id, + isActive: true, + }, + }); + + if (!hasAccess) { + return NextResponse.json( + { error: 'Access denied' }, + { status: 403 } + ); + } + + // Upsert the translation + const translation = await prisma.categoryTranslation.upsert({ + where: { + categoryId_locale: { + categoryId, + locale, + }, + }, + update: translationData, + create: { + categoryId, + locale, + ...translationData, + }, + }); + + return NextResponse.json(translation); + } catch (error) { + console.error('Error saving category translation:', error); + return NextResponse.json( + { error: 'Failed to save translation' }, + { status: 500 } + ); + } +} + +/** + * PUT /api/translations/categories/[categoryId] + * Bulk update translations for a category + */ +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ categoryId: string }> } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { categoryId } = await params; + const body = await request.json(); + + const parsed = bulkTranslationSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid data', details: parsed.error.issues }, + { status: 400 } + ); + } + + // Verify the category exists and user has access + const category = await prisma.category.findUnique({ + where: { id: categoryId }, + include: { store: true }, + }); + + if (!category) { + return NextResponse.json( + { error: 'Category not found' }, + { status: 404 } + ); + } + + // Check user has access to this store + const hasAccess = await prisma.storeStaff.findFirst({ + where: { + storeId: category.storeId, + userId: session.user.id, + isActive: true, + }, + }); + + if (!hasAccess) { + return NextResponse.json( + { error: 'Access denied' }, + { status: 403 } + ); + } + + // Upsert all translations in a transaction + const results = await prisma.$transaction( + parsed.data.translations.map(({ locale, ...translationData }) => + prisma.categoryTranslation.upsert({ + where: { + categoryId_locale: { + categoryId, + locale, + }, + }, + update: translationData, + create: { + categoryId, + locale, + ...translationData, + }, + }) + ) + ); + + return NextResponse.json(results); + } catch (error) { + console.error('Error bulk updating category translations:', error); + return NextResponse.json( + { error: 'Failed to save translations' }, + { status: 500 } + ); + } +} + +/** + * DELETE /api/translations/categories/[categoryId] + * Delete a specific translation + */ +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ categoryId: string }> } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { categoryId } = await params; + const { searchParams } = new URL(request.url); + const locale = searchParams.get('locale'); + + if (!locale) { + return NextResponse.json( + { error: 'Locale is required' }, + { status: 400 } + ); + } + + // Verify the category exists and user has access + const category = await prisma.category.findUnique({ + where: { id: categoryId }, + }); + + if (!category) { + return NextResponse.json( + { error: 'Category not found' }, + { status: 404 } + ); + } + + // Check user has access to this store + const hasAccess = await prisma.storeStaff.findFirst({ + where: { + storeId: category.storeId, + userId: session.user.id, + isActive: true, + }, + }); + + if (!hasAccess) { + return NextResponse.json( + { error: 'Access denied' }, + { status: 403 } + ); + } + + await prisma.categoryTranslation.delete({ + where: { + categoryId_locale: { + categoryId, + locale, + }, + }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error deleting category translation:', error); + return NextResponse.json( + { error: 'Failed to delete translation' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/translations/products/[productId]/route.ts b/src/app/api/translations/products/[productId]/route.ts new file mode 100644 index 00000000..f8d6f300 --- /dev/null +++ b/src/app/api/translations/products/[productId]/route.ts @@ -0,0 +1,316 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { z } from 'zod'; + +/** + * Schema for creating/updating product translations + */ +const translationSchema = z.object({ + locale: z.enum(['en', 'bn']), + name: z.string().min(1, 'Name is required'), + description: z.string().optional(), + shortDescription: z.string().optional(), + metaTitle: z.string().optional(), + metaDescription: z.string().optional(), + metaKeywords: z.string().optional(), +}); + +const bulkTranslationSchema = z.object({ + translations: z.array(translationSchema), +}); + +/** + * GET /api/translations/products/[productId] + * Get all translations for a product + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ productId: string }> } +) { + try { + const { productId } = await params; + const { searchParams } = new URL(request.url); + const locale = searchParams.get('locale'); + + const where: { productId: string; locale?: string } = { productId }; + if (locale) { + where.locale = locale; + } + + const translations = await prisma.productTranslation.findMany({ + where, + orderBy: { locale: 'asc' }, + }); + + // If specific locale requested and not found, return the product's default values + if (locale && translations.length === 0) { + const product = await prisma.product.findUnique({ + where: { id: productId }, + select: { + name: true, + description: true, + shortDescription: true, + metaTitle: true, + metaDescription: true, + metaKeywords: true, + }, + }); + + if (product) { + return NextResponse.json({ + locale, + ...product, + isFallback: true, + }); + } + } + + return NextResponse.json(translations); + } catch (error) { + console.error('Error fetching product translations:', error); + return NextResponse.json( + { error: 'Failed to fetch translations' }, + { status: 500 } + ); + } +} + +/** + * POST /api/translations/products/[productId] + * Create or update a product translation + */ +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ productId: string }> } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { productId } = await params; + const body = await request.json(); + + const parsed = translationSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid data', details: parsed.error.issues }, + { status: 400 } + ); + } + + const { locale, ...translationData } = parsed.data; + + // Verify the product exists and user has access + const product = await prisma.product.findUnique({ + where: { id: productId }, + include: { store: true }, + }); + + if (!product) { + return NextResponse.json( + { error: 'Product not found' }, + { status: 404 } + ); + } + + // Check user has access to this store (simplified - you may want to add proper permission checks) + const hasAccess = await prisma.storeStaff.findFirst({ + where: { + storeId: product.storeId, + userId: session.user.id, + isActive: true, + }, + }); + + if (!hasAccess) { + return NextResponse.json( + { error: 'Access denied' }, + { status: 403 } + ); + } + + // Upsert the translation + const translation = await prisma.productTranslation.upsert({ + where: { + productId_locale: { + productId, + locale, + }, + }, + update: translationData, + create: { + productId, + locale, + ...translationData, + }, + }); + + return NextResponse.json(translation); + } catch (error) { + console.error('Error saving product translation:', error); + return NextResponse.json( + { error: 'Failed to save translation' }, + { status: 500 } + ); + } +} + +/** + * PUT /api/translations/products/[productId] + * Bulk update translations for a product + */ +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ productId: string }> } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { productId } = await params; + const body = await request.json(); + + const parsed = bulkTranslationSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid data', details: parsed.error.issues }, + { status: 400 } + ); + } + + // Verify the product exists and user has access + const product = await prisma.product.findUnique({ + where: { id: productId }, + include: { store: true }, + }); + + if (!product) { + return NextResponse.json( + { error: 'Product not found' }, + { status: 404 } + ); + } + + // Check user has access to this store + const hasAccess = await prisma.storeStaff.findFirst({ + where: { + storeId: product.storeId, + userId: session.user.id, + isActive: true, + }, + }); + + if (!hasAccess) { + return NextResponse.json( + { error: 'Access denied' }, + { status: 403 } + ); + } + + // Upsert all translations in a transaction + const results = await prisma.$transaction( + parsed.data.translations.map(({ locale, ...translationData }) => + prisma.productTranslation.upsert({ + where: { + productId_locale: { + productId, + locale, + }, + }, + update: translationData, + create: { + productId, + locale, + ...translationData, + }, + }) + ) + ); + + return NextResponse.json(results); + } catch (error) { + console.error('Error bulk updating product translations:', error); + return NextResponse.json( + { error: 'Failed to save translations' }, + { status: 500 } + ); + } +} + +/** + * DELETE /api/translations/products/[productId] + * Delete a specific translation + */ +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ productId: string }> } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { productId } = await params; + const { searchParams } = new URL(request.url); + const locale = searchParams.get('locale'); + + if (!locale) { + return NextResponse.json( + { error: 'Locale is required' }, + { status: 400 } + ); + } + + // Verify the product exists and user has access + const product = await prisma.product.findUnique({ + where: { id: productId }, + }); + + if (!product) { + return NextResponse.json( + { error: 'Product not found' }, + { status: 404 } + ); + } + + // Check user has access to this store + const hasAccess = await prisma.storeStaff.findFirst({ + where: { + storeId: product.storeId, + userId: session.user.id, + isActive: true, + }, + }); + + if (!hasAccess) { + return NextResponse.json( + { error: 'Access denied' }, + { status: 403 } + ); + } + + await prisma.productTranslation.delete({ + where: { + productId_locale: { + productId, + locale, + }, + }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error deleting product translation:', error); + return NextResponse.json( + { error: 'Failed to delete translation' }, + { status: 500 } + ); + } +} diff --git a/src/lib/email-templates.ts b/src/lib/email-templates.ts index afb3234d..2c6afe98 100644 --- a/src/lib/email-templates.ts +++ b/src/lib/email-templates.ts @@ -680,6 +680,168 @@ export function adminNewUserEmail({ userName, userEmail, businessName, businessC `.trim(); } +/** + * Bilingual Order confirmation email template (Bengali + English) + * For Bangladesh market with 85% Bengali preference + */ +export function orderConfirmationEmailBilingual({ + customerName, + orderNumber, + orderTotal, + orderItems, + shippingAddress, + storeName, + appUrl = 'https://stormcom.app', + locale = 'bn', +}: { + customerName: string; + orderNumber: string; + orderTotal: string; + orderItems: Array<{ name: string; quantity: number; price: string }>; + shippingAddress: { address: string; city: string; state?: string; postalCode: string; country: string }; + storeName: string; + appUrl?: string; + locale?: 'en' | 'bn'; +}): string { + // Escape user-provided content to prevent XSS attacks + const safeCustomerName = escapeHtml(customerName); + const safeOrderNumber = escapeHtml(orderNumber); + const safeStoreName = escapeHtml(storeName); + const safeAddress = { + address: escapeHtml(shippingAddress.address), + city: escapeHtml(shippingAddress.city), + state: shippingAddress.state ? escapeHtml(shippingAddress.state) : undefined, + postalCode: escapeHtml(shippingAddress.postalCode), + country: escapeHtml(shippingAddress.country), + }; + + const itemsHtml = orderItems + .map( + (item) => ` + + ${escapeHtml(item.name)} + ${item.quantity} + ${escapeHtml(item.price)} + + ` + ) + .join(''); + + // Bengali translations + const bn = { + title: 'অর্ডার নিশ্চিতকরণ', + thankYou: 'আপনার অর্ডারের জন্য ধন্যবাদ! 🎉', + greeting: `প্রিয় ${safeCustomerName},`, + received: 'আমরা আপনার অর্ডার পেয়েছি এবং এটি প্রক্রিয়া করা হচ্ছে। নিচে বিস্তারিত দেখুন:', + orderDetails: 'অর্ডার বিবরণ', + orderNumber: 'অর্ডার নম্বর', + total: 'মোট', + orderItems: 'অর্ডার আইটেম', + product: 'পণ্য', + quantity: 'পরিমাণ', + price: 'দাম', + shippingAddress: 'ডেলিভারি ঠিকানা', + notification: 'অর্ডার শিপ হলে আমরা আপনাকে ইমেইল করব। কোনো প্রশ্ন থাকলে আমাদের সাপোর্ট টিমে যোগাযোগ করুন।', + visitStore: 'স্টোর দেখুন', + rights: 'সর্বস্বত্ব সংরক্ষিত।', + reason: 'আপনি এই ইমেইল পাচ্ছেন কারণ আপনি আমাদের কাছে অর্ডার করেছেন।', + }; + + // English translations + const en = { + title: 'Order Confirmation', + thankYou: 'Thank You for Your Order! 🎉', + greeting: `Hi ${safeCustomerName},`, + received: "We've received your order and it's being processed. Here are the details:", + orderDetails: 'Order Details', + orderNumber: 'Order Number', + total: 'Total', + orderItems: 'Order Items', + product: 'Product', + quantity: 'Quantity', + price: 'Price', + shippingAddress: 'Shipping Address', + notification: "We'll send you another email when your order ships. If you have any questions, please contact our support team.", + visitStore: 'Visit Store', + rights: 'All rights reserved.', + reason: "You're receiving this email because you placed an order with us.", + }; + + const t = locale === 'bn' ? bn : en; + const alt = locale === 'bn' ? en : bn; // Alternative language for bilingual support + + return ` + + + + + + ${t.title} - ${safeStoreName} + + + +
+
+ +
+
+ +

${t.thankYou}

+

${t.greeting}

+

${t.received}

+ +
+

${t.orderDetails}

+

${t.orderNumber}: ${safeOrderNumber}

+

${t.total}: ${orderTotal}

+
+ +

${t.orderItems}

+ + + + + + + + + + ${itemsHtml} + +
${t.product}${t.quantity}${t.price}
+ +

${t.shippingAddress}

+
+

+ ${safeAddress.address}
+ ${safeAddress.city}${safeAddress.state ? `, ${safeAddress.state}` : ''} ${safeAddress.postalCode}
+ ${safeAddress.country} +

+
+ +

${t.notification}

+ +

+ ${t.visitStore} +

+ + +
+

+ ${alt.greeting}
+ ${alt.received} +

+
+ +
+ + + `.trim(); +} + /** * Order confirmation email template */ From dd221977c9fdae8cbe2deeac9a522f6e44318224 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:14:53 +0000 Subject: [PATCH 04/14] fix: Address code review feedback - use env vars for URLs, fix hardcoded year, simplify SMS template types Co-authored-by: rafiqul4 <124497017+rafiqul4@users.noreply.github.com> --- src/components/storefront/store-header.tsx | 4 ++++ src/lib/email-templates.ts | 5 ++++- src/lib/utils/sms-counter.ts | 15 ++++++--------- src/messages/bn.json | 2 +- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/storefront/store-header.tsx b/src/components/storefront/store-header.tsx index 45ad4f55..154dbab0 100644 --- a/src/components/storefront/store-header.tsx +++ b/src/components/storefront/store-header.tsx @@ -18,6 +18,7 @@ import { import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { useCart } from "@/lib/stores/cart-store"; +import { LanguageSwitcher } from "@/components/language-switcher"; interface Category { id: string; @@ -185,6 +186,9 @@ export function StoreHeader({ store, categories = [] }: StoreHeaderProps) { {/* Actions */}
+ {/* Language Switcher */} + + {/* Search Button */}
)} @@ -449,7 +469,7 @@ export function HeroSection({ config, storeName, storeSlug }: HeroSectionProps) className="text-sm font-semibold shadow-lg backdrop-blur-sm" > - Welcome to {storeName} + {t("welcomeTo", { storeName })} )} diff --git a/src/components/storefront/trust-badges.tsx b/src/components/storefront/trust-badges.tsx index a20f1375..7ec9b28f 100644 --- a/src/components/storefront/trust-badges.tsx +++ b/src/components/storefront/trust-badges.tsx @@ -21,6 +21,7 @@ import { import { cn } from "@/lib/utils"; import type { TrustBadge, TrustBadgesSection, TrustBadgeIcon } from "@/lib/storefront/types"; import { useState, useEffect } from "react"; +import { useTranslations } from "next-intl"; interface TrustBadgesProps { config: TrustBadgesSection; diff --git a/src/messages/bn.json b/src/messages/bn.json index 50d9ad51..a95febb5 100644 --- a/src/messages/bn.json +++ b/src/messages/bn.json @@ -646,7 +646,35 @@ "collections": "কালেকশন", "featuredProducts": "ফিচার্ড পণ্য", "storeNotFound": "স্টোর পাওয়া যায়নি", - "storeNotFoundMessage": "আপনি যে স্টোর খুঁজছেন সেটি নেই বা সরানো হয়েছে।" + "storeNotFoundMessage": "আপনি যে স্টোর খুঁজছেন সেটি নেই বা সরানো হয়েছে।", + "welcomeTo": "{storeName} এ স্বাগতম", + "discoverProducts": "আপনার জন্য নির্বাচিত দুর্দান্ত পণ্য আবিষ্কার করুন", + "shopAllProducts": "সব পণ্য দেখুন", + "browseCategories": "ক্যাটাগরি ব্রাউজ করুন", + "happyCustomers": "{count}+ সন্তুষ্ট গ্রাহক", + "freeShipping": "ফ্রি ডেলিভারি", + "onOrdersOver": "{amount} এর উপরে অর্ডারে", + "securePayment": "নিরাপদ পেমেন্ট", + "secureTransactions": "১০০% নিরাপদ লেনদেন", + "qualityGuarantee": "মান গ্যারান্টি", + "verifiedProducts": "শুধুমাত্র যাচাইকৃত পণ্য", + "shopByCategory": "ক্যাটাগরি অনুযায়ী কিনুন", + "exploreCollections": "আমাদের নির্বাচিত সংগ্রহ অন্বেষণ করুন", + "viewAllCategories": "সব ক্যাটাগরি দেখুন", + "items": "{count} পণ্য", + "item": "{count} পণ্য", + "handPickedFavorites": "আপনার জন্য হ্যান্ডপিক করা প্রিয় পণ্য", + "viewAll": "সব দেখুন", + "freshAdditions": "আমাদের সংগ্রহে নতুন সংযোজন", + "viewAllNew": "সব নতুন দেখুন", + "mostPopular": "আমাদের সবচেয়ে জনপ্রিয় পণ্য", + "viewAllPopular": "সব জনপ্রিয় দেখুন", + "home": "হোম", + "quickLinks": "দ্রুত লিংক", + "contact": "যোগাযোগ", + "poweredBy": "পাওয়ার্ড বাই", + "allRightsReserved": "সর্বস্বত্ব সংরক্ষিত", + "multiTenantPlatform": "মাল্টি-টেন্যান্ট ই-কমার্স প্ল্যাটফর্ম" }, "footer": { "aboutUs": "আমাদের সম্পর্কে", diff --git a/src/messages/en.json b/src/messages/en.json index b73c62bd..36b3f8a9 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -646,7 +646,35 @@ "collections": "Collections", "featuredProducts": "Featured Products", "storeNotFound": "Store Not Found", - "storeNotFoundMessage": "The store you're looking for doesn't exist or has been removed." + "storeNotFoundMessage": "The store you're looking for doesn't exist or has been removed.", + "welcomeTo": "Welcome to {storeName}", + "discoverProducts": "Discover amazing products curated just for you", + "shopAllProducts": "Shop All Products", + "browseCategories": "Browse Categories", + "happyCustomers": "{count}+ Happy Customers", + "freeShipping": "Free Shipping", + "onOrdersOver": "On orders over {amount}", + "securePayment": "Secure Payment", + "secureTransactions": "100% secure transactions", + "qualityGuarantee": "Quality Guarantee", + "verifiedProducts": "Verified products only", + "shopByCategory": "Shop by Category", + "exploreCollections": "Explore our curated collections", + "viewAllCategories": "View All Categories", + "items": "{count} Items", + "item": "{count} Item", + "handPickedFavorites": "Hand-picked favorites just for you", + "viewAll": "View All", + "freshAdditions": "Fresh additions to our collection", + "viewAllNew": "View All New", + "mostPopular": "Our most popular products", + "viewAllPopular": "View All Popular", + "home": "Home", + "quickLinks": "Quick Links", + "contact": "Contact", + "poweredBy": "Powered By", + "allRightsReserved": "All rights reserved", + "multiTenantPlatform": "Multi-tenant E-commerce Platform" }, "footer": { "aboutUs": "About Us", From b792daa7bb34327d9a75f18cd342098ddecf5c44 Mon Sep 17 00:00:00 2001 From: Rafiqul Islam Date: Sat, 3 Jan 2026 14:00:35 +0600 Subject: [PATCH 14/14] up --- src/app/store/[slug]/page.tsx | 59 ++++++++++++++++------ src/components/storefront/store-footer.tsx | 25 +++++---- src/components/storefront/trust-badges.tsx | 51 +++++++++++++++++-- src/messages/bn.json | 6 ++- src/messages/en.json | 6 ++- 5 files changed, 114 insertions(+), 33 deletions(-) diff --git a/src/app/store/[slug]/page.tsx b/src/app/store/[slug]/page.tsx index 468a63aa..46203841 100644 --- a/src/app/store/[slug]/page.tsx +++ b/src/app/store/[slug]/page.tsx @@ -13,6 +13,18 @@ import { TestimonialsSection } from "@/components/storefront/testimonials-sectio import { BrandsCarousel } from "@/components/storefront/brands-carousel"; import { parseStorefrontConfig } from "@/lib/storefront/defaults"; import { ArrowRight, Sparkles, TrendingUp } from "lucide-react"; +import { getTranslations } from "next-intl/server"; + +// Translation mappings for config-provided titles +const titleTranslations: Record = { + "Shop by Category": "shopByCategory", + "Featured Products": "featuredProducts", +}; + +const subtitleTranslations: Record = { + "Explore our curated collections": "exploreCollections", + "Hand-picked favorites just for you": "handPickedFavorites", +}; interface StoreHomePageProps { params: Promise<{ slug: string }>; @@ -51,6 +63,9 @@ export default async function StoreHomePage({ params }: StoreHomePageProps) { // Parse storefront configuration const config = parseStorefrontConfig(store.storefrontConfig, store.name); + // Get translations for store page + const t = await getTranslations("store"); + // Fetch featured products const featuredProducts = await prisma.product.findMany({ where: { @@ -191,11 +206,15 @@ export default async function StoreHomePage({ params }: StoreHomePageProps) {

- {config.categories.title} + {titleTranslations[config.categories.title] + ? t(titleTranslations[config.categories.title]) + : config.categories.title}

{config.categories.subtitle && (

- {config.categories.subtitle} + {subtitleTranslations[config.categories.subtitle] + ? t(subtitleTranslations[config.categories.subtitle]) + : config.categories.subtitle}

)}
@@ -207,7 +226,7 @@ export default async function StoreHomePage({ params }: StoreHomePageProps) { className="group hover:border-primary transition-all" > - View All Categories + {t("viewAllCategories")} @@ -252,7 +271,9 @@ export default async function StoreHomePage({ params }: StoreHomePageProps) { {category.name}

- {category._count.products} {category._count.products === 1 ? 'Item' : 'Items'} + {category._count.products === 1 + ? t("item", { count: category._count.products }) + : t("items", { count: category._count.products })}

@@ -273,17 +294,23 @@ export default async function StoreHomePage({ params }: StoreHomePageProps) {
-

{config.featuredProducts.title}

+

+ {titleTranslations[config.featuredProducts.title] + ? t(titleTranslations[config.featuredProducts.title]) + : config.featuredProducts.title} +

{config.featuredProducts.subtitle && (

- {config.featuredProducts.subtitle} + {subtitleTranslations[config.featuredProducts.subtitle] + ? t(subtitleTranslations[config.featuredProducts.subtitle]) + : config.featuredProducts.subtitle}

)}
{config.featuredProducts.showViewAll && ( @@ -293,13 +320,13 @@ export default async function StoreHomePage({ params }: StoreHomePageProps) { {featuredProducts.length === 0 ? (
🛍️
-

No Featured Products Yet

+

{t("noFeaturedProducts")}

- Check back soon for amazing deals! + {t("checkBackSoon")}

@@ -321,11 +348,11 @@ export default async function StoreHomePage({ params }: StoreHomePageProps) {

- New Arrivals + {t("newArrivals")}

- Fresh additions to our collection + {t("freshAdditions")}

@@ -356,11 +383,11 @@ export default async function StoreHomePage({ params }: StoreHomePageProps) {

- Best Sellers + {t("bestSellers")}

- Our most popular products + {t("mostPopular")}

diff --git a/src/components/storefront/store-footer.tsx b/src/components/storefront/store-footer.tsx index cdac649d..9b1f8bfe 100644 --- a/src/components/storefront/store-footer.tsx +++ b/src/components/storefront/store-footer.tsx @@ -1,4 +1,7 @@ +"use client"; + import Link from "next/link"; +import { useTranslations } from "next-intl"; interface StoreFooterProps { store: { @@ -21,6 +24,8 @@ interface StoreFooterProps { */ export function StoreFooter({ store }: StoreFooterProps) { const currentYear = new Date().getFullYear(); + const t = useTranslations("store"); + const tCommon = useTranslations("common"); return (