From bb6f41390ef611354a3f77d608f99e29044b7876 Mon Sep 17 00:00:00 2001 From: Faruk Bakovic Date: Fri, 30 May 2025 20:40:15 +0200 Subject: [PATCH 1/7] Languages page added --- package-lock.json | 121 ++++++++++-- package.json | 4 + public/locales/ba/translation.json | 143 ++++++++++++++ public/locales/en/translation.json | 134 +++++++++++++ src/App.jsx | 2 + src/components/ConfirmDialog.jsx | 11 +- src/components/DeleteConfirmModal.jsx | 11 +- src/components/ImageUploader.jsx | 12 +- src/components/Sidebar.jsx | 7 + src/i18n.js | 55 ++++++ src/pages/LanguageManagementPage.jsx | 259 ++++++++++++++++++++++++++ src/routes/Router.jsx | 14 ++ 12 files changed, 749 insertions(+), 24 deletions(-) create mode 100644 public/locales/ba/translation.json create mode 100644 public/locales/en/translation.json create mode 100644 src/i18n.js create mode 100644 src/pages/LanguageManagementPage.jsx diff --git a/package-lock.json b/package-lock.json index 960a3b7..6f1dd16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,9 @@ "crypto-js": "^4.2.0", "date-fns": "^4.1.0", "dayjs": "^1.11.13", + "i18next": "^25.2.1", + "i18next-browser-languagedetector": "^8.1.0", + "i18next-http-backend": "^3.0.2", "js-sha256": "^0.11.0", "jwt-decode": "^4.0.0", "lucide-react": "^0.487.0", @@ -34,6 +37,7 @@ "react-dropzone": "^14.3.8", "react-funnel-pipeline": "^0.2.0", "react-hot-toast": "^2.5.2", + "react-i18next": "^15.5.2", "react-icons": "^5.5.0", "react-redux": "^9.2.0", "react-router-dom": "^7.4.1", @@ -281,12 +285,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", - "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.3.tgz", + "integrity": "sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -2590,6 +2592,15 @@ "node": ">=0.8" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3707,12 +3718,70 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/hyphenate-style-name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, + "node_modules/i18next": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.2.1.tgz", + "integrity": "sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz", + "integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", + "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4685,6 +4754,32 @@ "react-dom": ">=16" } }, + "node_modules/react-i18next": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.2.tgz", + "integrity": "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", @@ -5004,11 +5099,6 @@ "redux": "^5.0.0" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -5632,6 +5722,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/web-admin": { "resolved": "", "link": true diff --git a/package.json b/package.json index 70e8d4c..e138c38 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "crypto-js": "^4.2.0", "date-fns": "^4.1.0", "dayjs": "^1.11.13", + "i18next": "^25.2.1", + "i18next-browser-languagedetector": "^8.1.0", + "i18next-http-backend": "^3.0.2", "js-sha256": "^0.11.0", "jwt-decode": "^4.0.0", "lucide-react": "^0.487.0", @@ -36,6 +39,7 @@ "react-dropzone": "^14.3.8", "react-funnel-pipeline": "^0.2.0", "react-hot-toast": "^2.5.2", + "react-i18next": "^15.5.2", "react-icons": "^5.5.0", "react-redux": "^9.2.0", "react-router-dom": "^7.4.1", diff --git a/public/locales/ba/translation.json b/public/locales/ba/translation.json new file mode 100644 index 0000000..1d25d08 --- /dev/null +++ b/public/locales/ba/translation.json @@ -0,0 +1,143 @@ +{ + "common": { + "searchPlaceholder": "Pretraži...", + "addToCart": "Dodaj u korpu", + "viewDetails": "Pogledaj detalje", + "price": "Cijena", + "quantity": "Količina", + "subtotal": "Međuzbir", + "total": "Ukupno", + "checkout": "Plaćanje", + "continueShopping": "Nastavi kupovinu", + "save": "Sačuvaj", + "cancel": "Otkaži", + "oops": "Ups! Nešto je pošlo po zlu.", + "loading": "Učitavanje...", + "adminPanel": "Administratorski Panel", + "first": "Prva", + "last": "Zadnja", + "confirm": "Potvrdi", + "delete": "Obriši", + "edit": "Uredi", + "view": "Pogledaj", + "close": "Zatvori", + "done": "Gotovo", + "browseFiles": "Pretraži fajlove", + "dragFilesToUpload": "Prevucite fajlove za upload", + "or": "ili", + "maxFileSize": "Maksimalna veličina fajla: 50MB — Podržani formati: JPG, PNG, GIF, SVG, WEBP", + "currentLanguage": "Trenutni jezik", + "availableLanguages": "Dostupni jezici", + "addLanguage": "Dodaj jezik", + "addNewLanguage": "Dodaj novi jezik", + "languageCode": "Kod jezika", + "languageName": "Ime jezika", + "translations": "Prevodi", + "pasteTranslations": "Zalijepi svoje prevode JSON ovdje", + "changeLanguage": "Promijeni jezik" + }, + "roles": { + "Buyer": "Kupac", + "Seller": "Prodavač", + "Admin": "Administrator", + "Unknown": "Nepoznata uloga" + }, + "nav": { + "analytics": "Analitika", + "users": "Korisnici", + "requests": "Zahtjevi", + "stores": "Prodavnice", + "categories": "Kategorije", + "orders": "Narudžbe", + "advertisements": "Oglasi", + "chat": "Chat", + "routes": "Rute", + "languages": "Jezici" + }, + "usersPage": { + "username": "Korisničko ime", + "email": "Email", + "role": "Uloga", + "active": "Aktivan", + "actions": "Akcije", + "addUser": "Dodaj korisnika", + "searchUser": "Pretraži korisnika", + "title": "Upravljanje korisnicima", + "phoneNumber": "Broj telefona", + "saveChanges": "Sačuvaj izmjene", + "deleteUser": "Obriši korisnika", + "confirmDeleteUser": "Da li ste sigurni da želite obrisati ovog korisnika?", + "noUsers": "Nema dostupnih korisnika", + "loadingUsers": "Učitavanje korisnika..." + }, + "analytics": { + "dashboardTitle": "Analitika kontrolne table", + "totalAds": "Ukupno oglasa", + "totalViews": "Ukupno pregleda", + "totalClicks": "Ukupno klikova", + "totalConversions": "Ukupno konverzija", + "conversionRevenue": "Prihod od konverzija", + "clicksRevenue": "Prihod od klikova", + "viewsRevenue": "Prihod od pregleda", + "totalProducts": "Ukupno proizvoda", + "productPerformance": "Učinak proizvoda", + "noProducts": "Nema dostupnih proizvoda ili se još učitavaju...", + "storeEarnings": "Zarada prodavnice (prošli mjesec)", + "realtimeEvents": "Događaji u stvarnom vremenu", + "noEvents": "Još uvijek nema događaja", + "lastError": "Zadnja greška" + }, + "sellerAnalytics": { + "totalEarnings": "Ukupna zarada", + "sellerProfit": "Profit prodavača", + "clickRevenue": "Prihod od klikova", + "viewRevenue": "Prihod od pregleda", + "conversionRevenue": "Prihod od konverzija", + "clickRevenueOverTime": "Prihod od klikova kroz vrijeme", + "viewRevenueOverTime": "Prihod od pregleda kroz vrijeme", + "conversionRevenueOverTime": "Prihod od konverzija kroz vrijeme" + }, + "routes": { + "availableRoutes": "Dostupne rute", + "noRoutes": "Nema dostupnih ruta", + "loadingMap": "Učitavanje mape za rutu ID: {{id}}...", + "selectRoute": "Odaberite rutu sa liste za prikaz na mapi", + "routeDetails": "Detalji rute", + "directions": "Upute", + "routeId": "ID rute: {{id}}" + }, + "ads": { + "adType": "Tip oglasa", + "triggers": "Okidači", + "startTime": "Vrijeme početka", + "endTime": "Vrijeme završetka", + "isActive": "Aktivan", + "advertisementItems": "Stavke oglasa", + "conversionPrice": "Cijena konverzije", + "storeName": "Naziv prodavnice", + "address": "Adresa", + "description": "Opis", + "place": "Mjesto", + "category": "Kategorija" + }, + "auth": { + "login": "Prijava", + "logout": "Odjava", + "email": "Email", + "password": "Lozinka", + "forgotPassword": "Zaboravljena lozinka?", + "signIn": "Prijavi se", + "signUp": "Registruj se", + "rememberMe": "Zapamti me" + }, + "errors": { + "connectionError": "Greška u vezi", + "failedToConnect": "Neuspješno povezivanje", + "authTokenMissing": "Nedostaje autentifikacijski token", + "loadingError": "Neuspješno učitavanje podataka", + "deleteError": "Brisanje nije uspjelo", + "updateError": "Ažuriranje nije uspjelo", + "createError": "Kreiranje nije uspjelo" + } + } + \ No newline at end of file diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json new file mode 100644 index 0000000..7bfd0d3 --- /dev/null +++ b/public/locales/en/translation.json @@ -0,0 +1,134 @@ +{ + "common": { + "searchPlaceholder": "Search...", + "addToCart": "Add to Cart", + "viewDetails": "View Details", + "price": "Price", + "quantity": "Quantity", + "subtotal": "Subtotal", + "total": "Total", + "checkout": "Checkout", + "continueShopping": "Continue Shopping", + "save": "Save", + "cancel": "Cancel", + "oops": "Oops! Something went wrong.", + "loading": "Loading...", + "adminPanel": "Admin Panel", + "first": "First", + "last": "Last", + "confirm": "Confirm", + "delete": "Delete", + "edit": "Edit", + "view": "View", + "close": "Close", + "done": "Done", + "browseFiles": "Browse Files", + "dragFilesToUpload": "Drag files to upload", + "or": "or", + "maxFileSize": "Max file size: 50MB — Supported types: JPG, PNG, GIF, SVG, WEBP" + }, + "roles": { + "Buyer": "Buyer", + "Seller": "Seller", + "Admin": "Administrator", + "Unknown": "Unknown Role" + }, + "nav": { + "analytics": "Analytics", + "users": "Users", + "requests": "Requests", + "stores": "Stores", + "categories": "Categories", + "orders": "Orders", + "advertisements": "Advertisements", + "chat": "Chat", + "routes": "Routes", + "languages": "Languages" + }, + "usersPage": { + "username": "Username", + "email": "Email", + "role": "Role", + "active": "Active", + "actions": "Actions", + "addUser": "Add User", + "searchUser": "Search User", + "title": "User Management", + "phoneNumber": "Phone Number", + "saveChanges": "Save Changes", + "deleteUser": "Delete User", + "confirmDeleteUser": "Are you sure you want to delete this user?", + "noUsers": "No users available", + "loadingUsers": "Loading users..." + }, + "analytics": { + "dashboardTitle": "Dashboard Analytics", + "totalAds": "Total Ads", + "totalViews": "Total Views", + "totalClicks": "Total Clicks", + "totalConversions": "Total Conversions", + "conversionRevenue": "Conversion Revenue", + "clicksRevenue": "Clicks Revenue", + "viewsRevenue": "Views Revenue", + "totalProducts": "Total Products", + "productPerformance": "Product Performance", + "noProducts": "No products to display or still loading...", + "storeEarnings": "Store Earnings (Past Month)", + "realtimeEvents": "Realtime Events", + "noEvents": "No events received yet", + "lastError": "Last Error" + }, + "sellerAnalytics": { + "totalEarnings": "Total Earnings", + "sellerProfit": "Seller Profit", + "clickRevenue": "Click Revenue", + "viewRevenue": "View Revenue", + "conversionRevenue": "Conversion Revenue", + "clickRevenueOverTime": "Click Revenue Over Time", + "viewRevenueOverTime": "View Revenue Over Time", + "conversionRevenueOverTime": "Conversion Revenue Over Time" + }, + "routes": { + "availableRoutes": "Available Routes", + "noRoutes": "No routes available", + "loadingMap": "Loading map for Route ID: {{id}}...", + "selectRoute": "Select a route from the list to view it on the map", + "routeDetails": "Route Details", + "directions": "Directions", + "routeId": "Route ID: {{id}}" + }, + "ads": { + "adType": "Ad Type", + "triggers": "Triggers", + "startTime": "Start Time", + "endTime": "End Time", + "isActive": "Is Active", + "advertisementItems": "Advertisement Items", + "conversionPrice": "Conversion Price", + "storeName": "Store Name", + "address": "Address", + "description": "Description", + "place": "Place", + "category": "Category" + }, + "auth": { + "login": "Login", + "logout": "Logout", + "email": "Email", + "password": "Password", + "forgotPassword": "Forgot Password?", + "signIn": "Sign In", + "signUp": "Sign Up", + "rememberMe": "Remember Me" + }, + "errors": { + "connectionError": "Connection Error", + "failedToConnect": "Failed to connect", + "authTokenMissing": "Auth Token Missing", + "loadingError": "Failed to load data", + "deleteError": "Failed to delete item", + "updateError": "Failed to update item", + "createError": "Failed to create item" + }, + "dalje": "po istom patternu" + } \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index b1f3d47..96fda9c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,10 @@ +import React from 'react'; import { ThemeProvider } from "@mui/material/styles"; import CssBaseline from "@mui/material/CssBaseline"; import theme from "@styles/theme"; import AppRoutes from "./routes/Router"; import "./App.css"; +import './i18n'; // Import i18n configuration function App() { return ( diff --git a/src/components/ConfirmDialog.jsx b/src/components/ConfirmDialog.jsx index 5c43c36..822f7f4 100644 --- a/src/components/ConfirmDialog.jsx +++ b/src/components/ConfirmDialog.jsx @@ -6,20 +6,23 @@ import { Button, Typography, } from "@mui/material"; + import { useTranslation } from 'react-i18next'; export default function ConfirmDialog({ open, onClose, onConfirm, message }) { + const { t } = useTranslation(); + return ( - Confirm Action + {t('common.confirm')} - {message || "Are you sure you want to proceed?"} + {message || t('common.confirmAction')} diff --git a/src/components/DeleteConfirmModal.jsx b/src/components/DeleteConfirmModal.jsx index da16b3e..3057695 100644 --- a/src/components/DeleteConfirmModal.jsx +++ b/src/components/DeleteConfirmModal.jsx @@ -9,6 +9,7 @@ import { Typography, } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; +import { useTranslation } from 'react-i18next'; export default function DeleteConfirmModal({ open, @@ -16,23 +17,25 @@ export default function DeleteConfirmModal({ onConfirm, ticketTitle, }) { + const { t } = useTranslation(); + return ( - Delete Ticket + {t('common.delete')} - Are you sure you want to delete ticket {ticketTitle}? + {t('common.confirmDelete', { item: ticketTitle })} diff --git a/src/components/ImageUploader.jsx b/src/components/ImageUploader.jsx index 6f3c003..125c2d2 100644 --- a/src/components/ImageUploader.jsx +++ b/src/components/ImageUploader.jsx @@ -8,11 +8,13 @@ import { IconButton, } from '@mui/material'; import { CloudUpload, Cancel } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; const MAX_SIZE_MB = 50; const ImageUploader = ({ onFilesSelected }) => { const [files, setFiles] = useState([]); + const { t } = useTranslation(); const onDrop = useCallback( (acceptedFiles) => { @@ -54,7 +56,7 @@ const ImageUploader = ({ onFilesSelected }) => { border: '2px dashed #ccc', borderRadius: 4, px: 4, - py:1, + py: 1, textAlign: 'center', backgroundColor: isDragActive ? '#f0f0f0' : '#fafafa', cursor: 'pointer', @@ -64,16 +66,16 @@ const ImageUploader = ({ onFilesSelected }) => { - Drag files to upload + {t('common.dragFilesToUpload')} - or + {t('common.or')} - Max file size: 50MB — Supported types: JPG, PNG, GIF, SVG, WEBP + {t('common.maxFileSize')} diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index e5d05f4..be5db5f 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -30,6 +30,7 @@ import { FiBarChart2 } from 'react-icons/fi'; import { HiOutlineMegaphone } from 'react-icons/hi2'; import { FiMessageCircle } from 'react-icons/fi'; import { FaRoute } from 'react-icons/fa'; +import { FaLanguage } from 'react-icons/fa'; const Sidebar = () => { const navigate = useNavigate(); @@ -89,6 +90,12 @@ const Sidebar = () => { path: '/routes', badge: null, }, + { + icon: , + label: 'Languages', + path: '/languages', + badge: null, + }, ]; const [isDark, setIsDark] = useState(false); const toggleTheme = () => setIsDark(!isDark); diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..514a37d --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,55 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import HttpBackend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +// Predefined language codes +const PREDEFINED_LANG_CODES = ['en', 'de', 'es']; +const API_BASE_URL = import.meta.env.VITE_API_URL; + +i18n + .use(HttpBackend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + debug: process.env.NODE_ENV === 'development', + ns: ['translation'], + defaultNS: 'translation', + interpolation: { + escapeValue: false, // React already escapes values + }, + backend: { + loadPath: (langs, namespaces) => { + const lang = langs[0]; + const ns = namespaces[0]; + + // Check if it's a predefined language + if (PREDEFINED_LANG_CODES.includes(lang)) { + // Load from public/locales directory + return `/locales/${lang}/${ns}.json`; + } else { + // Load from backend API + return `${API_BASE_URL}/api/translations/${lang}/${ns}`; + } + }, + }, + }); + +// Function to fetch and set supported languages +async function fetchAndSetSupportedLanguages() { + try { + const response = await fetch(`${API_BASE_URL}/api/languages`); + const allLangsFromServer = await response.json(); + i18n.options.supportedLngs = allLangsFromServer.map(l => l.code); + } catch (error) { + console.error('Failed to fetch supported languages:', error); + // Fallback to predefined languages if API call fails + i18n.options.supportedLngs = PREDEFINED_LANG_CODES; + } +} + +// Call the function on startup +fetchAndSetSupportedLanguages(); + +export default i18n; diff --git a/src/pages/LanguageManagementPage.jsx b/src/pages/LanguageManagementPage.jsx new file mode 100644 index 0000000..7d6cbf0 --- /dev/null +++ b/src/pages/LanguageManagementPage.jsx @@ -0,0 +1,259 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Typography, + Card, + CardContent, + Button, + Grid, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + IconButton, + Alert, +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; + +const LanguageManagementPage = () => { + const { t, i18n } = useTranslation(); + const [languages, setLanguages] = useState([]); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const [newLanguage, setNewLanguage] = useState({ + code: '', + name: '', + translations: {}, + }); + const [error, setError] = useState(''); + + useEffect(() => { + // Fetch available languages from the backend + fetchLanguages(); + }, []); + + const fetchLanguages = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/languages`); + const data = await response.json(); + setLanguages(data); + } catch (error) { + console.error('Failed to fetch languages:', error); + setError('Failed to load languages'); + } + }; + + const handleAddLanguage = () => { + setIsAddModalOpen(true); + }; + + const handleCloseModal = () => { + setIsAddModalOpen(false); + setNewLanguage({ + code: '', + name: '', + translations: {}, + }); + setError(''); + }; + + const handleSaveLanguage = async () => { + try { + // Validate the language code + if (!newLanguage.code || !newLanguage.name) { + setError('Language code and name are required'); + return; + } + + // Send the new language to the backend + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/languages`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newLanguage), + }); + + if (!response.ok) { + throw new Error('Failed to add language'); + } + + // Refresh the languages list + await fetchLanguages(); + handleCloseModal(); + } catch (error) { + console.error('Error adding language:', error); + setError('Failed to add language'); + } + }; + + const handleDeleteLanguage = async (code) => { + try { + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/languages/${code}`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new Error('Failed to delete language'); + } + + // Refresh the languages list + await fetchLanguages(); + } catch (error) { + console.error('Error deleting language:', error); + setError('Failed to delete language'); + } + }; + + const handleChangeLanguage = (code) => { + i18n.changeLanguage(code); + }; + + return ( + + + + {t('common.languageManagement')} + + + {/* Current Language */} + + + + {t('common.currentLanguage')} + + + {languages.find(lang => lang.code === i18n.language)?.name || 'English'} ({i18n.language}) + + + + + {/* Available Languages */} + + + + {t('common.availableLanguages')} + + + + + + {languages.map((language) => ( + + + + + + {language.name} ({language.code}) + + + handleChangeLanguage(language.code)} + disabled={language.code === i18n.language} + > + + + handleDeleteLanguage(language.code)} + disabled={language.code === 'en'} // Prevent deleting English + > + + + + + + + + ))} + + + + {/* Add Language Modal */} + + {t('common.addNewLanguage')} + + {error && ( + + {error} + + )} + + setNewLanguage({ ...newLanguage, code: e.target.value })} + placeholder="e.g., fr, de, es" + sx={{ mb: 2 }} + /> + setNewLanguage({ ...newLanguage, name: e.target.value })} + placeholder="e.g., French, German, Spanish" + sx={{ mb: 2 }} + /> + { + try { + const translations = JSON.parse(e.target.value); + setNewLanguage({ ...newLanguage, translations }); + } catch (error) { + // Invalid JSON, don't update + } + }} + placeholder={t('common.pasteTranslations')} + /> + + + + + + + + + + ); +}; + +export default LanguageManagementPage; \ No newline at end of file diff --git a/src/routes/Router.jsx b/src/routes/Router.jsx index 90052a3..f882e09 100644 --- a/src/routes/Router.jsx +++ b/src/routes/Router.jsx @@ -21,6 +21,7 @@ import SellerAnalyticsPage from '@pages/SellerAnalyticsPage'; import ChatPage from '@pages/ChatPage'; import RoutesPage from '@pages/RoutesPage'; import RoutesPage2 from '../pages/DelRoutePage'; +import LanguageManagementPage from '../pages/LanguageManagementPage'; const isAuthenticated = () => { console.log(localStorage.getItem('auth')); @@ -169,6 +170,19 @@ const AppRoutes = () => { } /> + + + + + + + + + } + /> Date: Mon, 2 Jun 2025 02:00:49 +0200 Subject: [PATCH 2/7] add language --- public/locales/en/translation.json | 298 ++++++++++++++----------- src/App.jsx | 2 +- src/i18n.js | 25 +-- src/main.jsx | 1 + src/pages/LanguageManagementPage.jsx | 10 +- src/sections/AdminChatSection.jsx | 5 +- src/sections/AdsManagementHeader.jsx | 10 +- src/sections/CategoriesHeader.jsx | 10 +- src/sections/LoginFormSection.jsx | 8 +- src/sections/OrdersHeader.jsx | 15 +- src/sections/PendingUsersHeader.jsx | 8 +- src/sections/RoutesHeader.jsx | 8 +- src/sections/StoresHeader.jsx | 10 +- src/sections/UserCreateSection.jsx | 14 +- src/sections/UserDetailsSection.jsx | 4 +- src/sections/UserManagementHeader.jsx | 10 +- src/sections/UserManagementSection.jsx | 4 +- 17 files changed, 241 insertions(+), 201 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 7bfd0d3..810c4f4 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -1,134 +1,166 @@ { - "common": { - "searchPlaceholder": "Search...", - "addToCart": "Add to Cart", - "viewDetails": "View Details", - "price": "Price", - "quantity": "Quantity", - "subtotal": "Subtotal", - "total": "Total", - "checkout": "Checkout", - "continueShopping": "Continue Shopping", - "save": "Save", - "cancel": "Cancel", - "oops": "Oops! Something went wrong.", - "loading": "Loading...", - "adminPanel": "Admin Panel", - "first": "First", - "last": "Last", - "confirm": "Confirm", - "delete": "Delete", - "edit": "Edit", - "view": "View", - "close": "Close", - "done": "Done", - "browseFiles": "Browse Files", - "dragFilesToUpload": "Drag files to upload", - "or": "or", - "maxFileSize": "Max file size: 50MB — Supported types: JPG, PNG, GIF, SVG, WEBP" - }, - "roles": { - "Buyer": "Buyer", - "Seller": "Seller", - "Admin": "Administrator", - "Unknown": "Unknown Role" - }, - "nav": { - "analytics": "Analytics", - "users": "Users", - "requests": "Requests", - "stores": "Stores", - "categories": "Categories", - "orders": "Orders", - "advertisements": "Advertisements", - "chat": "Chat", - "routes": "Routes", - "languages": "Languages" - }, - "usersPage": { - "username": "Username", - "email": "Email", - "role": "Role", - "active": "Active", - "actions": "Actions", - "addUser": "Add User", - "searchUser": "Search User", - "title": "User Management", - "phoneNumber": "Phone Number", - "saveChanges": "Save Changes", - "deleteUser": "Delete User", - "confirmDeleteUser": "Are you sure you want to delete this user?", - "noUsers": "No users available", - "loadingUsers": "Loading users..." - }, - "analytics": { - "dashboardTitle": "Dashboard Analytics", - "totalAds": "Total Ads", - "totalViews": "Total Views", - "totalClicks": "Total Clicks", - "totalConversions": "Total Conversions", - "conversionRevenue": "Conversion Revenue", - "clicksRevenue": "Clicks Revenue", - "viewsRevenue": "Views Revenue", - "totalProducts": "Total Products", - "productPerformance": "Product Performance", - "noProducts": "No products to display or still loading...", - "storeEarnings": "Store Earnings (Past Month)", - "realtimeEvents": "Realtime Events", - "noEvents": "No events received yet", - "lastError": "Last Error" - }, - "sellerAnalytics": { - "totalEarnings": "Total Earnings", - "sellerProfit": "Seller Profit", - "clickRevenue": "Click Revenue", - "viewRevenue": "View Revenue", - "conversionRevenue": "Conversion Revenue", - "clickRevenueOverTime": "Click Revenue Over Time", - "viewRevenueOverTime": "View Revenue Over Time", - "conversionRevenueOverTime": "Conversion Revenue Over Time" - }, - "routes": { - "availableRoutes": "Available Routes", - "noRoutes": "No routes available", - "loadingMap": "Loading map for Route ID: {{id}}...", - "selectRoute": "Select a route from the list to view it on the map", - "routeDetails": "Route Details", - "directions": "Directions", - "routeId": "Route ID: {{id}}" - }, - "ads": { - "adType": "Ad Type", - "triggers": "Triggers", - "startTime": "Start Time", - "endTime": "End Time", - "isActive": "Is Active", - "advertisementItems": "Advertisement Items", - "conversionPrice": "Conversion Price", - "storeName": "Store Name", - "address": "Address", - "description": "Description", - "place": "Place", - "category": "Category" - }, - "auth": { - "login": "Login", - "logout": "Logout", - "email": "Email", - "password": "Password", - "forgotPassword": "Forgot Password?", - "signIn": "Sign In", - "signUp": "Sign Up", - "rememberMe": "Remember Me" - }, - "errors": { - "connectionError": "Connection Error", - "failedToConnect": "Failed to connect", - "authTokenMissing": "Auth Token Missing", - "loadingError": "Failed to load data", - "deleteError": "Failed to delete item", - "updateError": "Failed to update item", - "createError": "Failed to create item" - }, - "dalje": "po istom patternu" - } \ No newline at end of file + "ads.adsManagement": "Ads Management", + "ads.adminPanel": "Admin Panel", + "ads.advertisements": "Advertisements", + "ads.createAd": "Create Ad", + "ads.searchAds": "Search Ads", + "categories.categories": "Categories", + "categories.addCategory": "Add Category", + "categories.searchCategory": "Search Category", + "common.searchPlaceholder": "Search...", + "common.welcome": "Welcome", + "common.orders": "Orders", + "common.status": "Status", + "common.all": "All", + "common.searchOrders": "Search Orders", + "common.loginToContinue": "Login to continue", + "common.login": "Login", + "common.requests": "Requests", + "common.searchUser": "Search User", + "common.addToCart": "Add to Cart", + "common.viewDetails": "View Details", + "common.price": "Price", + "common.quantity": "Quantity", + "common.subtotal": "Subtotal", + "common.total": "Total", + "common.checkout": "Checkout", + "common.continueShopping": "Continue Shopping", + "common.save": "Save", + "common.cancel": "Cancel", + "common.oops": "Oops! Something went wrong.", + "common.loading": "Loading...", + "common.adminPanel": "Admin Panel", + "common.first": "First", + "common.last": "Last", + "common.confirm": "Confirm", + "common.delete": "Delete", + "common.edit": "Edit", + "common.view": "View", + "common.close": "Close", + "common.done": "Done", + "common.browseFiles": "Browse Files", + "common.dragFilesToUpload": "Drag files to upload", + "common.or": "or", + "common.maxFileSize": "Max file size: 50MB — Supported types: JPG, PNG, GIF, SVG, WEBP", + "common.languageManagement": "Language Management", + "common.currentLanguage": "Current Language", + "common.languageCode": "Language Code", + "common.languageName": "Language Name", + "common.actions": "Actions", + "common.addLanguage": "Add Language", + "common.editLanguage": "Edit Language", + "common.deleteLanguage": "Delete Language", + "common.availableLanguages": "Available Languages", + "common.addNewLanguage": "Add New Language", + "common.saveLanguage": "Save Language", + "common.translations": "Translations", + "common.allRoutes": "All Routes", + "common.routes": "Routes", + "common.createRoute": "Create Route", + "chat.openTicketToViewChat": "Open this ticket to view the chat.", + "roles.Buyer": "Buyer", + "roles.Seller": "Seller", + "roles.Admin": "Administrator", + "roles.Unknown": "Unknown Role", + "nav.analytics": "Analytics", + "nav.users": "Users", + "nav.requests": "Requests", + "nav.stores": "Stores", + "nav.categories": "Categories", + "nav.orders": "Orders", + "nav.advertisements": "Advertisements", + "nav.chat": "Chat", + "nav.routes": "Routes", + "nav.languages": "Languages", + "usersPage.username": "Username", + "usersPage.email": "Email", + "usersPage.role": "Role", + "usersPage.active": "Active", + "usersPage.actions": "Actions", + "usersPage.addUser": "Add User", + "usersPage.searchUser": "Search User", + "usersPage.title": "User Management", + "usersPage.phoneNumber": "Phone Number", + "usersPage.saveChanges": "Save Changes", + "usersPage.deleteUser": "Delete User", + "usersPage.confirmDeleteUser": "Are you sure you want to delete this user?", + "usersPage.noUsers": "No users available", + "usersPage.loadingUsers": "Loading users...", + "analytics.dashboardTitle": "Dashboard Analytics", + "analytics.totalAds": "Total Ads", + "analytics.totalViews": "Total Views", + "analytics.totalClicks": "Total Clicks", + "analytics.totalConversions": "Total Conversions", + "analytics.conversionRevenue": "Conversion Revenue", + "analytics.clicksRevenue": "Clicks Revenue", + "analytics.viewsRevenue": "Views Revenue", + "analytics.totalProducts": "Total Products", + "analytics.productPerformance": "Product Performance", + "analytics.noProducts": "No products to display or still loading...", + "analytics.storeEarnings": "Store Earnings (Past Month)", + "analytics.realtimeEvents": "Realtime Events", + "analytics.noEvents": "No events received yet", + "analytics.lastError": "Last Error", + "sellerAnalytics.totalEarnings": "Total Earnings", + "sellerAnalytics.sellerProfit": "Seller Profit", + "sellerAnalytics.clickRevenue": "Click Revenue", + "sellerAnalytics.viewRevenue": "View Revenue", + "sellerAnalytics.conversionRevenue": "Conversion Revenue", + "sellerAnalytics.clickRevenueOverTime": "Click Revenue Over Time", + "sellerAnalytics.viewRevenueOverTime": "View Revenue Over Time", + "sellerAnalytics.conversionRevenueOverTime": "Conversion Revenue Over Time", + "routes.availableRoutes": "Available Routes", + "routes.noRoutes": "No routes available", + "routes.loadingMap": "Loading map for Route ID: {{id}}...", + "routes.selectRoute": "Select a route from the list to view it on the map", + "routes.routeDetails": "Route Details", + "routes.directions": "Directions", + "routes.routeId": "Route ID: {{id}}", + "ads.adType": "Ad Type", + "ads.triggers": "Triggers", + "ads.startTime": "Start Time", + "ads.endTime": "End Time", + "ads.isActive": "Is Active", + "ads.advertisementItems": "Advertisement Items", + "ads.conversionPrice": "Conversion Price", + "ads.storeName": "Store Name", + "ads.address": "Address", + "ads.description": "Description", + "ads.place": "Place", + "ads.category": "Category", + "auth.login": "Login", + "auth.logout": "Logout", + "auth.email": "Email", + "auth.password": "Password", + "auth.forgotPassword": "Forgot Password?", + "auth.signIn": "Sign In", + "auth.signUp": "Sign Up", + "auth.rememberMe": "Remember Me", + "errors.connectionError": "Connection Error", + "errors.failedToConnect": "Failed to connect", + "errors.authTokenMissing": "Auth Token Missing", + "errors.loadingError": "Failed to load data", + "errors.deleteError": "Failed to delete item", + "errors.updateError": "Failed to update item", + "errors.createError": "Failed to create item", + "stores.stores": "Stores", + "stores.searchStore": "Search Store", + "stores.addStore": "Add Store", + "common.userCreatedSuccessfully": "User created successfully!", + "common.createNewUser": "Create New User", + "common.role": "Role", + "common.createUser": "Create User", + "common.loadingUserData": "Loading user data...", + "common.userDetails": "User Details", + "common.name": "Name", + "common.email": "Email", + "common.phoneNumber": "Phone Number", + "common.saveChanges": "Save Changes", + "common.deleteUser": "Delete User", + "common.confirmDeleteUser": "Are you sure you want to delete this user?", + "common.noUsers": "No users available", + "common.userManagement": "User Management", + "common.addUser": "Add User", + "common.confirmDeleteUser": "Are you sure you want to delete this user?" + +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 96fda9c..6f1e226 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,4 +15,4 @@ function App() { ); } -export default App; +export default App; \ No newline at end of file diff --git a/src/i18n.js b/src/i18n.js index 514a37d..ae9e2ea 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -4,8 +4,8 @@ import HttpBackend from 'i18next-http-backend'; import LanguageDetector from 'i18next-browser-languagedetector'; // Predefined language codes -const PREDEFINED_LANG_CODES = ['en', 'de', 'es']; -const API_BASE_URL = import.meta.env.VITE_API_URL; +const PREDEFINED_LANG_CODES = ['en', 'ba', 'es']; +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; i18n .use(HttpBackend) @@ -18,28 +18,13 @@ i18n defaultNS: 'translation', interpolation: { escapeValue: false, // React already escapes values - }, - backend: { - loadPath: (langs, namespaces) => { - const lang = langs[0]; - const ns = namespaces[0]; - - // Check if it's a predefined language - if (PREDEFINED_LANG_CODES.includes(lang)) { - // Load from public/locales directory - return `/locales/${lang}/${ns}.json`; - } else { - // Load from backend API - return `${API_BASE_URL}/api/translations/${lang}/${ns}`; - } - }, - }, + } }); // Function to fetch and set supported languages async function fetchAndSetSupportedLanguages() { try { - const response = await fetch(`${API_BASE_URL}/api/languages`); + const response = await fetch('${API_BASE_URL}/api/translations/languages'); const allLangsFromServer = await response.json(); i18n.options.supportedLngs = allLangsFromServer.map(l => l.code); } catch (error) { @@ -52,4 +37,4 @@ async function fetchAndSetSupportedLanguages() { // Call the function on startup fetchAndSetSupportedLanguages(); -export default i18n; +export default i18n; \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index 237ad75..91296b9 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -7,6 +7,7 @@ import theme from "@styles/theme"; import "./App.css"; import "./index.css"; import { PendingUsersProvider } from "./context/PendingUsersContext"; +import './i18n'; createRoot(document.getElementById("root")).render( diff --git a/src/pages/LanguageManagementPage.jsx b/src/pages/LanguageManagementPage.jsx index 7d6cbf0..9baaceb 100644 --- a/src/pages/LanguageManagementPage.jsx +++ b/src/pages/LanguageManagementPage.jsx @@ -37,7 +37,7 @@ const LanguageManagementPage = () => { const fetchLanguages = async () => { try { - const response = await fetch(`${import.meta.env.VITE_API_URL}/api/languages`); + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/translations/languages`); const data = await response.json(); setLanguages(data); } catch (error) { @@ -69,7 +69,7 @@ const LanguageManagementPage = () => { } // Send the new language to the backend - const response = await fetch(`${import.meta.env.VITE_API_URL}/api/languages`, { + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/translations/languages`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -92,7 +92,7 @@ const LanguageManagementPage = () => { const handleDeleteLanguage = async (code) => { try { - const response = await fetch(`${import.meta.env.VITE_API_URL}/api/languages/${code}`, { + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/translations/languages/${code}`, { method: 'DELETE', }); @@ -142,7 +142,7 @@ const LanguageManagementPage = () => { {t('common.currentLanguage')} - {languages.find(lang => lang.code === i18n.language)?.name || 'English'} ({i18n.language}) + {languages.find(lang => lang.code === i18n.language)?.name (i18n.language) || 'English (en)'} @@ -226,7 +226,7 @@ const LanguageManagementPage = () => { placeholder="e.g., French, German, Spanish" sx={{ mb: 2 }} /> - {locked && ( - + )} diff --git a/src/sections/AdsManagementHeader.jsx b/src/sections/AdsManagementHeader.jsx index 7ef7167..08c11e8 100644 --- a/src/sections/AdsManagementHeader.jsx +++ b/src/sections/AdsManagementHeader.jsx @@ -7,8 +7,10 @@ import { InputAdornment, } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; +import { useTranslation } from 'react-i18next'; const AdsManagementHeader = ({ onCreateAd, searchTerm, setSearchTerm }) => { + const { t } = useTranslation(); return ( { > - Ads Management + {t('ads.adsManagement')} - Admin Panel > Advertisements + {t('ads.adminPanel')} > {t('ads.advertisements')} setSearchTerm(e.target.value)} @@ -66,7 +68,7 @@ const AdsManagementHeader = ({ onCreateAd, searchTerm, setSearchTerm }) => { }, }} > - Create Ad + {t('ads.createAd')} diff --git a/src/sections/CategoriesHeader.jsx b/src/sections/CategoriesHeader.jsx index 63a9293..cbd564c 100644 --- a/src/sections/CategoriesHeader.jsx +++ b/src/sections/CategoriesHeader.jsx @@ -7,8 +7,10 @@ import { Button, } from "@mui/material"; import SearchIcon from "@mui/icons-material/Search"; +import { useTranslation } from 'react-i18next'; const CategoriesHeader = ({ searchTerm, setSearchTerm, onAddCategory }) => { + const { t } = useTranslation(); return ( { > - Categories + {t('categories.categories')} - Admin Panel > Categories + {t('common.adminPanel')} > {t('categories.categories')} setSearchTerm(e.target.value)} @@ -66,7 +68,7 @@ const CategoriesHeader = ({ searchTerm, setSearchTerm, onAddCategory }) => { }, }} > - Add Category + {t('categories.addCategory')} diff --git a/src/sections/LoginFormSection.jsx b/src/sections/LoginFormSection.jsx index be55daf..eb00a53 100644 --- a/src/sections/LoginFormSection.jsx +++ b/src/sections/LoginFormSection.jsx @@ -15,8 +15,10 @@ import { apiLoginUserAsync } from "../api/api.js"; import { useNavigate } from "react-router-dom"; import axios from "axios"; import { api } from "../utils/apiroutes"; +import { useTranslation } from 'react-i18next'; const LoginFormSection = () => { + const { t } = useTranslation(); var baseURL = import.meta.env.VITE_API_BASE_URL; const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); @@ -62,10 +64,10 @@ const LoginFormSection = () => { return ( - Welcome + {t('common.welcome')} - Login to continue + {t('common.loginToContinue')} { /> - LOGIN + {t('common.login')} ); diff --git a/src/sections/OrdersHeader.jsx b/src/sections/OrdersHeader.jsx index 83f3764..e323f2a 100644 --- a/src/sections/OrdersHeader.jsx +++ b/src/sections/OrdersHeader.jsx @@ -8,7 +8,7 @@ import { } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; import { FormControl, InputLabel, Select, MenuItem } from '@mui/material'; - +import { useTranslation } from 'react-i18next'; const OrdersHeader = ({ searchTerm, @@ -16,6 +16,7 @@ const OrdersHeader = ({ statusFilter, setStatusFilter, }) => { + const { t } = useTranslation(); return ( - Orders + {t('common.orders')} - Admin Panel > Orders + {t('common.adminPanel')} > {t('common.orders')} {/* 🔽 Filter by Status */} - Status + {t('common.status')} - Buyer - Seller + {t('roles.buyer')} + {t('roles.seller')} - Create User + {t('common.createUser')} { severity="success" sx={{ width: '100%' }} > - User created successfully! + {t('common.userCreatedSuccessfully')} diff --git a/src/sections/UserDetailsSection.jsx b/src/sections/UserDetailsSection.jsx index 2f2382c..1293188 100644 --- a/src/sections/UserDetailsSection.jsx +++ b/src/sections/UserDetailsSection.jsx @@ -9,10 +9,12 @@ import UserRoles from "../components/UserRoles.jsx"; import UserEditForm from "../components/userEditForm.jsx"; import { getUsers, updateUser } from "../data/usersDetails.js"; +import { useTranslation } from 'react-i18next'; const UserDetailsSection = () => { const [selectedUser, setSelectedUser] = useState(null); const [isEditing, setIsEditing] = useState(false); + const { t } = useTranslation(); useEffect(() => { const user = getUsers()[0]; @@ -29,7 +31,7 @@ const UserDetailsSection = () => { }; if (!selectedUser) { - return Loading user data...; + return {t('common.loadingUserData')}; } return ( diff --git a/src/sections/UserManagementHeader.jsx b/src/sections/UserManagementHeader.jsx index f15bb94..806d134 100644 --- a/src/sections/UserManagementHeader.jsx +++ b/src/sections/UserManagementHeader.jsx @@ -7,8 +7,10 @@ import { InputAdornment, } from "@mui/material"; import SearchIcon from "@mui/icons-material/Search"; +import { useTranslation } from 'react-i18next'; const UserManagementHeader = ({ onAddUser, searchTerm, setSearchTerm }) => { + const { t } = useTranslation(); return ( { > - User Management + {t('common.userManagement')} - Admin Panel > User Management + {t('common.adminPanel')} > {t('common.userManagement')} setSearchTerm(e.target.value)} @@ -65,7 +67,7 @@ const UserManagementHeader = ({ onAddUser, searchTerm, setSearchTerm }) => { }, }} > - Add User + {t('common.addUser')} diff --git a/src/sections/UserManagementSection.jsx b/src/sections/UserManagementSection.jsx index a18394b..7dc68e3 100644 --- a/src/sections/UserManagementSection.jsx +++ b/src/sections/UserManagementSection.jsx @@ -4,6 +4,7 @@ import UserList from '../components/UserList.jsx'; import ConfirmDialog from '../components/ConfirmDialog.jsx'; import UserDetailsModal from '@components/UserDetailsModal'; import { apiUpdateUserAsync, apiToggleUserAvailabilityAsync } from '@api/api'; +import { useTranslation } from 'react-i18next'; export default function UserManagementSection({ allUsers, @@ -16,6 +17,7 @@ export default function UserManagementSection({ }) { const [confirmDialogOpen, setConfirmDialogOpen] = useState(false); const [userToDelete, setUserToDelete] = useState(null); + const { t } = useTranslation(); const handleDelete = (userId) => { setUserToDelete(userId); @@ -73,7 +75,7 @@ export default function UserManagementSection({ open={confirmDialogOpen} onClose={cancelDelete} onConfirm={confirmDelete} - message='Are you sure you want to delete this user?' + message={t('common.confirmDeleteUser')} /> ); From 2feb254b160374204a0a3ccf1bf0df3e3dca9e07 Mon Sep 17 00:00:00 2001 From: Faruk Bakovic Date: Mon, 2 Jun 2025 02:54:35 +0200 Subject: [PATCH 3/7] change language --- public/locales/ba/translation.json | 305 ++++++++++++++------------- public/locales/en/translation.json | 4 +- public/locales/es/translation.json | 140 ++++++++++++ src/pages/LanguageManagementPage.jsx | 81 +++---- 4 files changed, 336 insertions(+), 194 deletions(-) create mode 100644 public/locales/es/translation.json diff --git a/public/locales/ba/translation.json b/public/locales/ba/translation.json index 1d25d08..ce68372 100644 --- a/public/locales/ba/translation.json +++ b/public/locales/ba/translation.json @@ -1,143 +1,164 @@ { - "common": { - "searchPlaceholder": "Pretraži...", - "addToCart": "Dodaj u korpu", - "viewDetails": "Pogledaj detalje", - "price": "Cijena", - "quantity": "Količina", - "subtotal": "Međuzbir", - "total": "Ukupno", - "checkout": "Plaćanje", - "continueShopping": "Nastavi kupovinu", - "save": "Sačuvaj", - "cancel": "Otkaži", - "oops": "Ups! Nešto je pošlo po zlu.", - "loading": "Učitavanje...", - "adminPanel": "Administratorski Panel", - "first": "Prva", - "last": "Zadnja", - "confirm": "Potvrdi", - "delete": "Obriši", - "edit": "Uredi", - "view": "Pogledaj", - "close": "Zatvori", - "done": "Gotovo", - "browseFiles": "Pretraži fajlove", - "dragFilesToUpload": "Prevucite fajlove za upload", - "or": "ili", - "maxFileSize": "Maksimalna veličina fajla: 50MB — Podržani formati: JPG, PNG, GIF, SVG, WEBP", - "currentLanguage": "Trenutni jezik", - "availableLanguages": "Dostupni jezici", - "addLanguage": "Dodaj jezik", - "addNewLanguage": "Dodaj novi jezik", - "languageCode": "Kod jezika", - "languageName": "Ime jezika", - "translations": "Prevodi", - "pasteTranslations": "Zalijepi svoje prevode JSON ovdje", - "changeLanguage": "Promijeni jezik" - }, - "roles": { - "Buyer": "Kupac", - "Seller": "Prodavač", - "Admin": "Administrator", - "Unknown": "Nepoznata uloga" - }, - "nav": { - "analytics": "Analitika", - "users": "Korisnici", - "requests": "Zahtjevi", - "stores": "Prodavnice", - "categories": "Kategorije", - "orders": "Narudžbe", - "advertisements": "Oglasi", - "chat": "Chat", - "routes": "Rute", - "languages": "Jezici" - }, - "usersPage": { - "username": "Korisničko ime", - "email": "Email", - "role": "Uloga", - "active": "Aktivan", - "actions": "Akcije", - "addUser": "Dodaj korisnika", - "searchUser": "Pretraži korisnika", - "title": "Upravljanje korisnicima", - "phoneNumber": "Broj telefona", - "saveChanges": "Sačuvaj izmjene", - "deleteUser": "Obriši korisnika", - "confirmDeleteUser": "Da li ste sigurni da želite obrisati ovog korisnika?", - "noUsers": "Nema dostupnih korisnika", - "loadingUsers": "Učitavanje korisnika..." - }, - "analytics": { - "dashboardTitle": "Analitika kontrolne table", - "totalAds": "Ukupno oglasa", - "totalViews": "Ukupno pregleda", - "totalClicks": "Ukupno klikova", - "totalConversions": "Ukupno konverzija", - "conversionRevenue": "Prihod od konverzija", - "clicksRevenue": "Prihod od klikova", - "viewsRevenue": "Prihod od pregleda", - "totalProducts": "Ukupno proizvoda", - "productPerformance": "Učinak proizvoda", - "noProducts": "Nema dostupnih proizvoda ili se još učitavaju...", - "storeEarnings": "Zarada prodavnice (prošli mjesec)", - "realtimeEvents": "Događaji u stvarnom vremenu", - "noEvents": "Još uvijek nema događaja", - "lastError": "Zadnja greška" - }, - "sellerAnalytics": { - "totalEarnings": "Ukupna zarada", - "sellerProfit": "Profit prodavača", - "clickRevenue": "Prihod od klikova", - "viewRevenue": "Prihod od pregleda", - "conversionRevenue": "Prihod od konverzija", - "clickRevenueOverTime": "Prihod od klikova kroz vrijeme", - "viewRevenueOverTime": "Prihod od pregleda kroz vrijeme", - "conversionRevenueOverTime": "Prihod od konverzija kroz vrijeme" - }, - "routes": { - "availableRoutes": "Dostupne rute", - "noRoutes": "Nema dostupnih ruta", - "loadingMap": "Učitavanje mape za rutu ID: {{id}}...", - "selectRoute": "Odaberite rutu sa liste za prikaz na mapi", - "routeDetails": "Detalji rute", - "directions": "Upute", - "routeId": "ID rute: {{id}}" - }, - "ads": { - "adType": "Tip oglasa", - "triggers": "Okidači", - "startTime": "Vrijeme početka", - "endTime": "Vrijeme završetka", - "isActive": "Aktivan", - "advertisementItems": "Stavke oglasa", - "conversionPrice": "Cijena konverzije", - "storeName": "Naziv prodavnice", - "address": "Adresa", - "description": "Opis", - "place": "Mjesto", - "category": "Kategorija" - }, - "auth": { - "login": "Prijava", - "logout": "Odjava", - "email": "Email", - "password": "Lozinka", - "forgotPassword": "Zaboravljena lozinka?", - "signIn": "Prijavi se", - "signUp": "Registruj se", - "rememberMe": "Zapamti me" - }, - "errors": { - "connectionError": "Greška u vezi", - "failedToConnect": "Neuspješno povezivanje", - "authTokenMissing": "Nedostaje autentifikacijski token", - "loadingError": "Neuspješno učitavanje podataka", - "deleteError": "Brisanje nije uspjelo", - "updateError": "Ažuriranje nije uspjelo", - "createError": "Kreiranje nije uspjelo" - } - } - \ No newline at end of file + "ads.adsManagement": "Upravljanje oglasima", + "ads.adminPanel": "Administratorska ploča", + "ads.advertisements": "Oglasi", + "ads.createAd": "Kreiraj oglas", + "ads.searchAds": "Pretraži oglase", + "categories.categories": "Kategorije", + "categories.addCategory": "Dodaj kategoriju", + "categories.searchCategory": "Pretraži kategoriju", + "common.searchPlaceholder": "Pretraži...", + "common.welcome": "Dobrodošli", + "common.orders": "Narudžbe", + "common.status": "Status", + "common.all": "Sve", + "common.searchOrders": "Pretraži narudžbe", + "common.loginToContinue": "Prijavite se za nastavak", + "common.login": "Prijava", + "common.requests": "Zahtjevi", + "common.searchUser": "Pretraži korisnika", + "common.addToCart": "Dodaj u korpu", + "common.viewDetails": "Pogledaj detalje", + "common.price": "Cijena", + "common.quantity": "Količina", + "common.subtotal": "Međuzbir", + "common.total": "Ukupno", + "common.checkout": "Plaćanje", + "common.continueShopping": "Nastavi kupovinu", + "common.save": "Sačuvaj", + "common.cancel": "Otkaži", + "common.oops": "Ups! Nešto je pošlo po zlu.", + "common.loading": "Učitavanje...", + "common.adminPanel": "Administratorska ploča", + "common.first": "Prva", + "common.last": "Posljednja", + "common.confirm": "Potvrdi", + "common.delete": "Obriši", + "common.edit": "Uredi", + "common.view": "Pogledaj", + "common.close": "Zatvori", + "common.done": "Gotovo", + "common.browseFiles": "Pregledaj fajlove", + "common.dragFilesToUpload": "Prevucite fajlove za učitavanje", + "common.or": "ili", + "common.maxFileSize": "Maksimalna veličina fajla: 50MB — Podržani formati: JPG, PNG, GIF, SVG, WEBP", + "common.languageManagement": "Upravljanje jezicima", + "common.currentLanguage": "Trenutni jezik", + "common.languageCode": "Kod jezika", + "common.languageName": "Naziv jezika", + "common.actions": "Akcije", + "common.addLanguage": "Dodaj jezik", + "common.editLanguage": "Uredi jezik", + "common.deleteLanguage": "Obriši jezik", + "common.availableLanguages": "Dostupni jezici", + "common.addNewLanguage": "Dodaj novi jezik", + "common.saveLanguage": "Sačuvaj jezik", + "common.translations": "Prijevodi", + "common.allRoutes": "Sve rute", + "common.routes": "Rute", + "common.createRoute": "Kreiraj rutu", + "chat.openTicketToViewChat": "Otvorite ovaj tiket da biste vidjeli chat.", + "roles.Buyer": "Kupac", + "roles.Seller": "Prodavač", + "roles.Admin": "Administrator", + "roles.Unknown": "Nepoznata uloga", + "nav.analytics": "Analitika", + "nav.users": "Korisnici", + "nav.requests": "Zahtjevi", + "nav.stores": "Prodavnice", + "nav.categories": "Kategorije", + "nav.orders": "Narudžbe", + "nav.advertisements": "Oglasi", + "nav.chat": "Chat", + "nav.routes": "Rute", + "nav.languages": "Jezici", + "usersPage.username": "Korisničko ime", + "usersPage.email": "Email", + "usersPage.role": "Uloga", + "usersPage.active": "Aktivan", + "usersPage.actions": "Akcije", + "usersPage.addUser": "Dodaj korisnika", + "usersPage.searchUser": "Pretraži korisnika", + "usersPage.title": "Upravljanje korisnicima", + "usersPage.phoneNumber": "Broj telefona", + "usersPage.saveChanges": "Sačuvaj promjene", + "usersPage.deleteUser": "Obriši korisnika", + "usersPage.confirmDeleteUser": "Jeste li sigurni da želite obrisati ovog korisnika?", + "usersPage.noUsers": "Nema dostupnih korisnika", + "usersPage.loadingUsers": "Učitavanje korisnika...", + "analytics.dashboardTitle": "Analitika kontrolne ploče", + "analytics.totalAds": "Ukupno oglasa", + "analytics.totalViews": "Ukupno pregleda", + "analytics.totalClicks": "Ukupno klikova", + "analytics.totalConversions": "Ukupno konverzija", + "analytics.conversionRevenue": "Prihod od konverzija", + "analytics.clicksRevenue": "Prihod od klikova", + "analytics.viewsRevenue": "Prihod od pregleda", + "analytics.totalProducts": "Ukupno proizvoda", + "analytics.productPerformance": "Učinkovitost proizvoda", + "analytics.noProducts": "Nema proizvoda za prikaz ili se još učitavaju...", + "analytics.storeEarnings": "Zarada prodavnice (prošli mjesec)", + "analytics.realtimeEvents": "Događaji u stvarnom vremenu", + "analytics.noEvents": "Još nema primljenih događaja", + "analytics.lastError": "Posljednja greška", + "sellerAnalytics.totalEarnings": "Ukupna zarada", + "sellerAnalytics.sellerProfit": "Profit prodavača", + "sellerAnalytics.clickRevenue": "Prihod od klikova", + "sellerAnalytics.viewRevenue": "Prihod od pregleda", + "sellerAnalytics.conversionRevenue": "Prihod od konverzija", + "sellerAnalytics.clickRevenueOverTime": "Prihod od klikova kroz vrijeme", + "sellerAnalytics.viewRevenueOverTime": "Prihod od pregleda kroz vrijeme", + "sellerAnalytics.conversionRevenueOverTime": "Prihod od konverzija kroz vrijeme", + "routes.availableRoutes": "Dostupne rute", + "routes.noRoutes": "Nema dostupnih ruta", + "routes.loadingMap": "Učitavanje mape za ID rute: {{id}}...", + "routes.selectRoute": "Odaberite rutu sa liste da biste je vidjeli na mapi", + "routes.routeDetails": "Detalji rute", + "routes.directions": "Upute", + "routes.routeId": "ID rute: {{id}}", + "ads.adType": "Tip oglasa", + "ads.triggers": "Okidači", + "ads.startTime": "Vrijeme početka", + "ads.endTime": "Vrijeme završetka", + "ads.isActive": "Aktivan", + "ads.advertisementItems": "Stavke oglasa", + "ads.conversionPrice": "Cijena konverzije", + "ads.storeName": "Naziv prodavnice", + "ads.address": "Adresa", + "ads.description": "Opis", + "ads.place": "Mjesto", + "ads.category": "Kategorija", + "auth.login": "Prijava", + "auth.logout": "Odjava", + "auth.email": "Email", + "auth.password": "Lozinka", + "auth.forgotPassword": "Zaboravili ste lozinku?", + "auth.signIn": "Prijavite se", + "auth.signUp": "Registrujte se", + "auth.rememberMe": "Zapamti me", + "errors.connectionError": "Greška u vezi", + "errors.failedToConnect": "Neuspješno povezivanje", + "errors.authTokenMissing": "Nedostaje autentifikacijski token", + "errors.loadingError": "Neuspješno učitavanje podataka", + "errors.deleteError": "Neuspješno brisanje stavke", + "errors.updateError": "Neuspješno ažuriranje stavke", + "errors.createError": "Neuspješno kreiranje stavke", + "stores.stores": "Prodavnice", + "stores.searchStore": "Pretraži prodavnicu", + "stores.addStore": "Dodaj prodavnicu", + "common.userCreatedSuccessfully": "Korisnik je uspješno kreiran!", + "common.createNewUser": "Kreiraj novog korisnika", + "common.role": "Uloga", + "common.createUser": "Kreiraj korisnika", + "common.loadingUserData": "Učitavanje podataka korisnika...", + "common.userDetails": "Detalji korisnika", + "common.name": "Ime", + "common.email": "Email", + "common.phoneNumber": "Broj telefona", + "common.saveChanges": "Sačuvaj promjene", + "common.deleteUser": "Obriši korisnika", + "common.confirmDeleteUser": "Jeste li sigurni da želite obrisati ovog korisnika?", + "common.noUsers": "Nema dostupnih korisnika", + "common.userManagement": "Upravljanje korisnicima", + "common.addUser": "Dodaj korisnika" +} diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 810c4f4..a873609 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -160,7 +160,5 @@ "common.confirmDeleteUser": "Are you sure you want to delete this user?", "common.noUsers": "No users available", "common.userManagement": "User Management", - "common.addUser": "Add User", - "common.confirmDeleteUser": "Are you sure you want to delete this user?" - + "common.addUser": "Add User" } \ No newline at end of file diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json new file mode 100644 index 0000000..44269a7 --- /dev/null +++ b/public/locales/es/translation.json @@ -0,0 +1,140 @@ +{ + "ads.adsManagement": "Gestión de anuncios", + "ads.adminPanel": "Panel de administración", + "ads.advertisements": "Anuncios", + "ads.createAd": "Crear anuncio", + "ads.searchAds": "Buscar anuncios", + "categories.categories": "Categorías", + "categories.addCategory": "Agregar categoría", + "categories.searchCategory": "Buscar categoría", + "common.searchPlaceholder": "Buscar...", + "common.welcome": "Bienvenido", + "common.orders": "Pedidos", + "common.status": "Estado", + "common.all": "Todos", + "common.searchOrders": "Buscar pedidos", + "common.loginToContinue": "Inicia sesión para continuar", + "common.login": "Iniciar sesión", + "common.requests": "Solicitudes", + "common.searchUser": "Buscar usuario", + "common.addToCart": "Añadir al carrito", + "common.viewDetails": "Ver detalles", + "common.price": "Precio", + "common.quantity": "Cantidad", + "common.subtotal": "Subtotal", + "common.total": "Total", + "common.checkout": "Pagar", + "common.continueShopping": "Seguir comprando", + "common.save": "Guardar", + "common.cancel": "Cancelar", + "common.oops": "¡Ups! Algo salió mal.", + "common.loading": "Cargando...", + "common.adminPanel": "Panel de administración", + "common.first": "Primera", + "common.last": "Última", + "common.confirm": "Confirmar", + "common.delete": "Eliminar", + "common.edit": "Editar", + "common.view": "Ver", + "common.close": "Cerrar", + "common.done": "Hecho", + "common.browseFiles": "Explorar archivos", + "common.dragFilesToUpload": "Arrastra archivos para subir", + "common.or": "o", + "common.maxFileSize": "Tamaño máximo de archivo: 50MB — Formatos soportados: JPG, PNG, GIF, SVG, WEBP", + "common.languageManagement": "Gestión de idiomas", + "common.currentLanguage": "Idioma actual", + "common.languageCode": "Código del idioma", + "common.languageName": "Nombre del idioma", + "common.actions": "Acciones", + "common.addLanguage": "Agregar idioma", + "common.editLanguage": "Editar idioma", + "common.deleteLanguage": "Eliminar idioma", + "common.availableLanguages": "Idiomas disponibles", + "common.addNewLanguage": "Agregar nuevo idioma", + "common.saveLanguage": "Guardar idioma", + "common.translations": "Traducciones", + "common.allRoutes": "Todas las rutas", + "common.routes": "Rutas", + "common.createRoute": "Crear ruta", + "chat.openTicketToViewChat": "Abre este ticket para ver el chat.", + "roles.Buyer": "Comprador", + "roles.Seller": "Vendedor", + "roles.Admin": "Administrador", + "roles.Unknown": "Rol desconocido", + "nav.analytics": "Analítica", + "nav.users": "Usuarios", + "nav.requests": "Solicitudes", + "nav.stores": "Tiendas", + "nav.categories": "Categorías", + "nav.orders": "Pedidos", + "nav.advertisements": "Anuncios", + "nav.chat": "Chat", + "nav.routes": "Rutas", + "nav.languages": "Idiomas", + "usersPage.username": "Nombre de usuario", + "usersPage.email": "Correo electrónico", + "usersPage.role": "Rol", + "usersPage.active": "Activo", + "usersPage.actions": "Acciones", + "usersPage.addUser": "Agregar usuario", + "usersPage.searchUser": "Buscar usuario", + "usersPage.title": "Gestión de usuarios", + "usersPage.phoneNumber": "Número de teléfono", + "usersPage.saveChanges": "Guardar cambios", + "usersPage.deleteUser": "Eliminar usuario", + "usersPage.confirmDeleteUser": "¿Estás seguro de que deseas eliminar este usuario?", + "usersPage.noUsers": "No hay usuarios disponibles", + "usersPage.loadingUsers": "Cargando usuarios...", + "analytics.dashboardTitle": "Panel de analítica", + "analytics.totalAds": "Total de anuncios", + "analytics.totalViews": "Total de vistas", + "analytics.totalClicks": "Total de clics", + "analytics.totalConversions": "Total de conversiones", + "analytics.conversionRevenue": "Ingresos por conversiones", + "analytics.clicksRevenue": "Ingresos por clics", + "analytics.viewsRevenue": "Ingresos por vistas", + "analytics.totalProducts": "Total de productos", + "analytics.productPerformance": "Rendimiento de productos", + "analytics.noProducts": "No hay productos para mostrar o aún se están cargando...", + "analytics.storeEarnings": "Ganancias de la tienda (último mes)", + "analytics.realtimeEvents": "Eventos en tiempo real", + "analytics.noEvents": "Aún no se han recibido eventos", + "analytics.lastError": "Último error", + "sellerAnalytics.totalEarnings": "Ganancias totales", + "sellerAnalytics.sellerProfit": "Ganancias del vendedor", + "sellerAnalytics.clickRevenue": "Ingresos por clics", + "sellerAnalytics.viewRevenue": "Ingresos por vistas", + "sellerAnalytics.conversionRevenue": "Ingresos por conversiones", + "sellerAnalytics.clickRevenueOverTime": "Ingresos por clics a lo largo del tiempo", + "sellerAnalytics.viewRevenueOverTime": "Ingresos por vistas a lo largo del tiempo", + "sellerAnalytics.conversionRevenueOverTime": "Ingresos por conversiones a lo largo del tiempo", + "routes.availableRoutes": "Rutas disponibles", + "routes.noRoutes": "No hay rutas disponibles", + "routes.loadingMap": "Cargando mapa para la ruta ID: {{id}}...", + "routes.selectRoute": "Selecciona una ruta de la lista para verla en el mapa", + "routes.routeDetails": "Detalles de la ruta", + "routes.directions": "Direcciones", + "routes.routeId": "ID de ruta: {{id}}", + "ads.adType": "Tipo de anuncio", + "ads.triggers": "Disparadores", + "ads.startTime": "Hora de inicio", + "ads.endTime": "Hora de finalización", + "ads.isActive": "Activo", + "ads.advertisementItems": "Elementos del anuncio", + "ads.conversionPrice": "Precio por conversión", + "ads.storeName": "Nombre de la tienda", + "ads.address": "Dirección", + "ads.description": "Descripción", + "ads.place": "Lugar", + "ads.category": "Categoría", + "auth.login": "Iniciar sesión", + "auth.logout": "Cerrar sesión", + "auth.email": "Correo electrónico", + "auth.password": "Contraseña", + "auth.forgotPassword": "¿Olvidaste tu contraseña?", + "auth.signIn": "Iniciar sesión", + "auth.signUp": "Registrarse", + "auth.rememberMe": "Recuérdame" + } + \ No newline at end of file diff --git a/src/pages/LanguageManagementPage.jsx b/src/pages/LanguageManagementPage.jsx index 9baaceb..2236d8a 100644 --- a/src/pages/LanguageManagementPage.jsx +++ b/src/pages/LanguageManagementPage.jsx @@ -29,9 +29,10 @@ const LanguageManagementPage = () => { translations: {}, }); const [error, setError] = useState(''); + const [translationsText, setTranslationsText] = useState(''); + const [jsonError, setJsonError] = useState(''); useEffect(() => { - // Fetch available languages from the backend fetchLanguages(); }, []); @@ -57,18 +58,23 @@ const LanguageManagementPage = () => { name: '', translations: {}, }); + setTranslationsText(''); + setJsonError(''); setError(''); }; const handleSaveLanguage = async () => { try { - // Validate the language code if (!newLanguage.code || !newLanguage.name) { setError('Language code and name are required'); return; } - // Send the new language to the backend + if (jsonError) { + setError('Fix translation JSON errors before saving.'); + return; + } + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/translations/languages`, { method: 'POST', headers: { @@ -81,7 +87,6 @@ const LanguageManagementPage = () => { throw new Error('Failed to add language'); } - // Refresh the languages list await fetchLanguages(); handleCloseModal(); } catch (error) { @@ -100,7 +105,6 @@ const LanguageManagementPage = () => { throw new Error('Failed to delete language'); } - // Refresh the languages list await fetchLanguages(); } catch (error) { console.error('Error deleting language:', error); @@ -113,51 +117,31 @@ const LanguageManagementPage = () => { }; return ( - - + + {t('common.languageManagement')} - {/* Current Language */} {t('common.currentLanguage')} - {languages.find(lang => lang.code === i18n.language)?.name (i18n.language) || 'English (en)'} + { + languages.find(lang => lang.code === i18n.language)?.name + ? `${languages.find(lang => lang.code === i18n.language).name} (${i18n.language})` + : `English (${i18n.language})` + } - {/* Available Languages */} - - {t('common.availableLanguages')} - - @@ -182,7 +166,7 @@ const LanguageManagementPage = () => { handleDeleteLanguage(language.code)} - disabled={language.code === 'en'} // Prevent deleting English + disabled={language.code === 'en'} > @@ -195,13 +179,7 @@ const LanguageManagementPage = () => { - {/* Add Language Modal */} - + {t('common.addNewLanguage')} {error && ( @@ -226,21 +204,26 @@ const LanguageManagementPage = () => { placeholder="e.g., French, German, Spanish" sx={{ mb: 2 }} /> - { + const text = e.target.value; + setTranslationsText(text); try { - const translations = JSON.parse(e.target.value); - setNewLanguage({ ...newLanguage, translations }); - } catch (error) { - // Invalid JSON, don't update + const parsed = JSON.parse(text); + setNewLanguage({ ...newLanguage, translations: parsed }); + setJsonError(''); + } catch { + setJsonError('Invalid JSON'); } }} placeholder={t('common.pasteTranslations')} + error={!!jsonError} + helperText={jsonError || t('common.translationJsonHint')} /> @@ -256,4 +239,4 @@ const LanguageManagementPage = () => { ); }; -export default LanguageManagementPage; \ No newline at end of file +export default LanguageManagementPage; From 4a1b2f967578ef6f0bbac035d1a7674e268410ac Mon Sep 17 00:00:00 2001 From: Faruk Bakovic Date: Mon, 2 Jun 2025 11:08:23 +0200 Subject: [PATCH 4/7] Bug fix --- public/locales/ba/translation.json | 13 ++++++++++++- public/locales/en/translation.json | 13 ++++++++++++- public/locales/es/translation.json | 12 +++++++++++- src/components/Sidebar.jsx | 27 +++++++++++++++------------ 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/public/locales/ba/translation.json b/public/locales/ba/translation.json index ce68372..f2e6899 100644 --- a/public/locales/ba/translation.json +++ b/public/locales/ba/translation.json @@ -160,5 +160,16 @@ "common.confirmDeleteUser": "Jeste li sigurni da želite obrisati ovog korisnika?", "common.noUsers": "Nema dostupnih korisnika", "common.userManagement": "Upravljanje korisnicima", - "common.addUser": "Dodaj korisnika" + "common.addUser": "Dodaj korisnika", + "common.administrator": "Administrator", + "common.logout": "Odjava", + "common.analytics": "Analitika", + "common.users": "Korisnici", + "common.stores": "Prodavnice", + "common.categories": "Kategorije", + "common.advertisements": "Oglasi", + "common.chat": "Razgovori", + "common.languages": "Jezici", + "common.pasteTranslations" : "Zalijepi prijevod", + "common.translationJsonHint" : "Dodajte tekst u JSON formatu" } diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index a873609..02af80c 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -160,5 +160,16 @@ "common.confirmDeleteUser": "Are you sure you want to delete this user?", "common.noUsers": "No users available", "common.userManagement": "User Management", - "common.addUser": "Add User" + "common.addUser": "Add User", + "common.administrator": "Administrator", + "common.logout": "Logout", + "common.analytics": "Analytics", + "common.users": "Users", + "common.stores": "Stores", + "common.categories": "Categories", + "common.advertisements": "Advertisements", + "common.chat": "Chat", + "common.languages": "Languages", + "common.pasteTranslations" : "Paste Translations", + "common.translationJsonHint" : "Add a text in JSON format" } \ No newline at end of file diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 44269a7..263dd5a 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -135,6 +135,16 @@ "auth.forgotPassword": "¿Olvidaste tu contraseña?", "auth.signIn": "Iniciar sesión", "auth.signUp": "Registrarse", - "auth.rememberMe": "Recuérdame" + "auth.rememberMe": "Recuérdame", + "common.administrator": "Administrador", + "common.logout": "Cerrar sesión", + "common.analytics": "Analíticas", + "common.users": "Usuarios", + "common.stores": "Tiendas", + "common.categories": "Categorías", + "common.advertisements": "Anuncios", + "common.chat": "Chat", + "common.languages": "Idiomas" + } \ No newline at end of file diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index be5db5f..f42a689 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -31,68 +31,71 @@ import { HiOutlineMegaphone } from 'react-icons/hi2'; import { FiMessageCircle } from 'react-icons/fi'; import { FaRoute } from 'react-icons/fa'; import { FaLanguage } from 'react-icons/fa'; +import { useTranslation } from 'react-i18next'; + const Sidebar = () => { + const { t } = useTranslation(); const navigate = useNavigate(); const { pendingUsers } = usePendingUsers(); const menuItems = [ { icon: , - label: 'Analytics', + label: t('common.analytics'), path: '/analytics', badge: null, }, { icon: , - label: 'Users', + label: t('common.users'), path: '/users', badge: null, }, { icon: , - label: 'Requests', + label: t('common.requests'), path: '/requests', badge: pendingUsers.length, }, { icon: , - label: 'Stores', + label: t('common.stores'), path: '/stores', badge: null, }, { icon: , - label: 'Categories', + label: t('common.categories'), path: '/categories', badge: null, }, { icon: , - label: 'Orders', + label: t('common.orders'), path: '/orders', badge: null, }, { icon: , - label: 'Advertisements', + label: t('common.advertisements'), path: '/ads', badge: null, }, { icon: , - label: 'Chat', + label: t('common.chat'), path: '/chat', badge: null, }, { icon: , - label: 'Routes', + label: t('common.routes'), path: '/routes', badge: null, }, { icon: , - label: 'Languages', + label: t('common.languages'), path: '/languages', badge: null, }, @@ -150,7 +153,7 @@ const Sidebar = () => { > Bazaar - Administrator + {t('common.administrator')} @@ -200,7 +203,7 @@ const Sidebar = () => { onClick={handleLogout} startIcon={} // Optional icon > - Logout + {t('common.logout')} } From e08001371e62557337e1508c0e3ce4c5855605d6 Mon Sep 17 00:00:00 2001 From: lilhast1 Date: Mon, 2 Jun 2025 12:03:33 +0200 Subject: [PATCH 5/7] bekend config fix --- src/i18n.js | 71 +++++++++++++++------------- src/pages/LanguageManagementPage.jsx | 11 ++++- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/i18n.js b/src/i18n.js index ae9e2ea..a64ba49 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,40 +1,43 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; -import HttpBackend from 'i18next-http-backend'; -import LanguageDetector from 'i18next-browser-languagedetector'; + import i18n from 'i18next'; + import { initReactI18next } from 'react-i18next'; + import HttpBackend from 'i18next-http-backend'; + import LanguageDetector from 'i18next-browser-languagedetector'; -// Predefined language codes -const PREDEFINED_LANG_CODES = ['en', 'ba', 'es']; -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; + // Predefined language codes + const PREDEFINED_LANG_CODES = ['en', 'es']; + const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; -i18n - .use(HttpBackend) - .use(LanguageDetector) - .use(initReactI18next) - .init({ - fallbackLng: 'en', - debug: process.env.NODE_ENV === 'development', - ns: ['translation'], - defaultNS: 'translation', - interpolation: { - escapeValue: false, // React already escapes values - } - }); + i18n + .use(HttpBackend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'en', + debug: process.env.NODE_ENV === 'development', + ns: ['translation'], + defaultNS: 'translation', + interpolation: { + escapeValue: false, // React already escapes values + }, + backend: { + loadPath: `${API_BASE_URL}/api/translations/{{lng}}` + } + }); -// Function to fetch and set supported languages -async function fetchAndSetSupportedLanguages() { - try { - const response = await fetch('${API_BASE_URL}/api/translations/languages'); - const allLangsFromServer = await response.json(); - i18n.options.supportedLngs = allLangsFromServer.map(l => l.code); - } catch (error) { - console.error('Failed to fetch supported languages:', error); - // Fallback to predefined languages if API call fails - i18n.options.supportedLngs = PREDEFINED_LANG_CODES; + // Function to fetch and set supported languages + async function fetchAndSetSupportedLanguages() { + try { + const response = await fetch(`${API_BASE_URL}/api/translations/languages`); + const allLangsFromServer = await response.json(); + i18n.options.supportedLngs = allLangsFromServer.map(l => l.code); + } catch (error) { + console.error('Failed to fetch supported languages:', error); + // Fallback to predefined languages if API call fails + i18n.options.supportedLngs = PREDEFINED_LANG_CODES; + } } -} -// Call the function on startup -fetchAndSetSupportedLanguages(); + // Call the function on startup + fetchAndSetSupportedLanguages(); -export default i18n; \ No newline at end of file + export default i18n; \ No newline at end of file diff --git a/src/pages/LanguageManagementPage.jsx b/src/pages/LanguageManagementPage.jsx index 2236d8a..5ab3b40 100644 --- a/src/pages/LanguageManagementPage.jsx +++ b/src/pages/LanguageManagementPage.jsx @@ -41,6 +41,15 @@ const LanguageManagementPage = () => { const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/translations/languages`); const data = await response.json(); setLanguages(data); + const masterkeys = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/translations/master-keys`); + const keydata = await masterkeys.json(); + console.log(keydata); + let obj = "{\n"; + for (let index = 0; index < keydata.length; index++) { + obj += `\t "${keydata[index]}": "",\n` + } + obj += "}" + setTranslationsText(obj); } catch (error) { console.error('Failed to fetch languages:', error); setError('Failed to load languages'); @@ -74,7 +83,7 @@ const LanguageManagementPage = () => { setError('Fix translation JSON errors before saving.'); return; } - + console.log(JSON.stringify(newLanguage)); const response = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/translations/languages`, { method: 'POST', headers: { From dd32b5b44af775893ca3dcad49d059c04c8a7a11 Mon Sep 17 00:00:00 2001 From: Faruk Bakovic Date: Mon, 2 Jun 2025 13:00:15 +0200 Subject: [PATCH 6/7] AnalyticsPage translated --- public/locales/en/translation.json | 40 ++++++++++++++++++++++++- src/components/AdFunnelChart.jsx | 16 +++++----- src/components/AdStackedBarChart.jsx | 6 ++-- src/components/AnalyticsChart.jsx | 18 +++++++----- src/components/CountryStatsPanel.jsx | 8 +++-- src/components/OrdersByStatus.jsx | 4 ++- src/components/ProductsSummary.jsx | 12 ++++---- src/components/RevenueByStore.jsx | 4 ++- src/components/RevenueMetrics.jsx | 32 ++++++++++---------- src/components/UserDistribution.jsx | 6 ++-- src/pages/AnalyticsPage.jsx | 44 ++++++++++++++-------------- src/pages/SellerAnalyticsPage.jsx | 26 ++++++++-------- 12 files changed, 137 insertions(+), 79 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 02af80c..0e83bf6 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -101,6 +101,25 @@ "analytics.realtimeEvents": "Realtime Events", "analytics.noEvents": "No events received yet", "analytics.lastError": "Last Error", + "analytics.ordersRevenueByRegions": "Orders Revenue by Regions", + "analytics.ordersByRegions": "Orders by Regions", + "analytics.revenue": "Revenue", + "analytics.orders": "Orders", + "analytics.adTriggersBreakdown": "Ad Triggers Breakdown", + "analytics.search": "Search", + "analytics.order": "Order", + "analytics.view": "View", + "analytics.conversionRate": "Conversion Rate (All Ads)", + "analytics.conversions": "conversions", + "analytics.clicks": "clicks", + "analytics.topStoresByAdRevenue": "Top Stores by Ad Revenue", + "analytics.salesFunnelAnalysis": "Sales Funnel Analysis", + "analytics.viewed": "Viewed", + "analytics.clicked": "Clicked", + "analytics.converted": "Converted", + "analytics.combinationChart": "Combination Chart: Fixed vs PopUp Ads", + "analytics.fixed": "Fixed", + "analytics.popup": "PopUp", "sellerAnalytics.totalEarnings": "Total Earnings", "sellerAnalytics.sellerProfit": "Seller Profit", "sellerAnalytics.clickRevenue": "Click Revenue", @@ -171,5 +190,24 @@ "common.chat": "Chat", "common.languages": "Languages", "common.pasteTranslations" : "Paste Translations", - "common.translationJsonHint" : "Add a text in JSON format" + "common.translationJsonHint" : "Add a text in JSON format", + "common.totalAds": "Total Ads", + "analytics.storePerformance": "Store Performance", + "analytics.revenueAndProfitAnalysis": "Revenue & Profit Analysis", + "analytics.totalRevenue": "Total Revenue", + "analytics.fromAllAdvertisingSources": "From all advertising sources", + "analytics.clickRevenue": "Click Revenue", + "analytics.fromClicks": "From {{count}} clicks", + "analytics.viewRevenue": "View Revenue", + "analytics.fromViews": "From {{count}} views", + "analytics.fromConversions": "From {{count}} conversions", + "analytics.revenueBySourceOverTime": "Revenue by Source Over Time", + "analytics.revenueDistribution": "Revenue Distribution", + "analytics.totalEarnedProfitFromAds": "Total Earned Profit from Ads", + "common.unknownProduct": "Unknown Product", + "analytics.detailedAnalyticsFor": "Detailed Analytics for {{storeName}}", + "analytics.unknownStore": "Unknown Store", + "analytics.storeEarningsPastMonth": "Store Earnings (Past Month)", + "analytics.noProductsToDisplay": "No products to display or still loading...", + "analytics.noStoresToDisplay": "No stores to display or still loading..." } \ No newline at end of file diff --git a/src/components/AdFunnelChart.jsx b/src/components/AdFunnelChart.jsx index b72976e..537f38e 100644 --- a/src/components/AdFunnelChart.jsx +++ b/src/components/AdFunnelChart.jsx @@ -8,6 +8,7 @@ import { } from '@mui/icons-material'; import { apiGetAllAdsAsync } from '../api/api.js'; import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; +import { useTranslation } from 'react-i18next'; const baseUrl = import.meta.env.VITE_API_BASE_URL || ''; const HUB_ENDPOINT_PATH = '/Hubs/AdvertisementHub'; @@ -21,23 +22,24 @@ const funnelIcons = [ ]; export default function AdFunnelChart() { + const { t } = useTranslation(); const [funnelSteps, setFunnelSteps] = useState([ { - label: 'Viewed', + label: t('analytics.viewed'), value: 0, percent: 100, color: funnelColors[0], icon: funnelIcons[0], }, { - label: 'Clicked', + label: t('analytics.clicked'), value: 0, percent: 0, color: funnelColors[1], icon: funnelIcons[1], }, { - label: 'Converted', + label: t('analytics.converted'), value: 0, percent: 0, color: funnelColors[2], @@ -124,14 +126,14 @@ export default function AdFunnelChart() { setFunnelSteps([ { - label: 'Viewed', + label: t('analytics.viewed'), value: totalViews, percent: 100, color: funnelColors[0], icon: funnelIcons[0], }, { - label: 'Clicked', + label: t('analytics.clicked'), value: totalClicks, percent: totalViews > 0 ? Math.round((totalClicks / totalViews) * 100) : 0, @@ -139,7 +141,7 @@ export default function AdFunnelChart() { icon: funnelIcons[1], }, { - label: 'Converted', + label: t('analytics.converted'), value: totalConversions, percent: totalClicks > 0 @@ -162,7 +164,7 @@ export default function AdFunnelChart() { }} > - Sales Funnel Analysis + {t('analytics.salesFunnelAnalysis')} { const fetchData = async () => { @@ -133,7 +135,7 @@ export default function AdStackedBarChart() { textAlign: 'center', }} > - Combination Chart: Fixed vs PopUp Ads + {t('analytics.combinationChart')} - {label} + {t(`analytics.${label.toLowerCase()}`)} ))} diff --git a/src/components/AnalyticsChart.jsx b/src/components/AnalyticsChart.jsx index 4643f84..608242d 100644 --- a/src/components/AnalyticsChart.jsx +++ b/src/components/AnalyticsChart.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import { Tabs, Tab, Box, Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; import { LineChart, Line, @@ -37,6 +38,7 @@ function generateTargets(realValues, minOffset = -0.1, maxOffset = 0.15) { } const AdsRevenueChart = () => { + const { t } = useTranslation(); const [tab, setTab] = useState(0); const [chartData, setChartData] = useState({ conversions: [], @@ -199,9 +201,9 @@ const AdsRevenueChart = () => { }} > - {tab === 0 && 'Conversions Revenue'} - {tab === 1 && 'Clicks Revenue'} - {tab === 2 && 'Views Revenue'} + {tab === 0 && t('analytics.conversionsRevenue')} + {tab === 1 && t('analytics.clicksRevenue')} + {tab === 2 && t('analytics.viewsRevenue')} { textColor='primary' indicatorColor='primary' > - - - + + + @@ -233,14 +235,14 @@ const AdsRevenueChart = () => { dataKey='revenue' stroke='#0f766e' strokeWidth={2} - name='Total Revenue' + name={t('analytics.totalRevenue')} /> diff --git a/src/components/CountryStatsPanel.jsx b/src/components/CountryStatsPanel.jsx index 32bd99a..864bc58 100644 --- a/src/components/CountryStatsPanel.jsx +++ b/src/components/CountryStatsPanel.jsx @@ -14,8 +14,10 @@ import { apiFetchOrdersAsync, apiGetGeographyAsync, } from '../api/api.js'; +import { useTranslation } from 'react-i18next'; const CountryStatsPanel = () => { + const { t } = useTranslation(); const [tab, setTab] = useState(0); const [data, setData] = useState({ revenue: [], orders: [] }); @@ -140,7 +142,7 @@ const CountryStatsPanel = () => { fetchData(); }, []); - const labels = ['Orders Revenue by Regions', 'Orders by Regions']; + const labels = [t('analytics.ordersRevenueByRegions'), t('analytics.ordersByRegions')]; const keys = ['revenue', 'orders']; const currentData = data[keys[tab]] || []; @@ -158,8 +160,8 @@ const CountryStatsPanel = () => { indicatorColor='primary' sx={{ mb: 2 }} > - - + + {currentData.map((item, index) => ( diff --git a/src/components/OrdersByStatus.jsx b/src/components/OrdersByStatus.jsx index 1d0b5a3..0271e41 100644 --- a/src/components/OrdersByStatus.jsx +++ b/src/components/OrdersByStatus.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Card, CardContent, Typography, Box } from '@mui/material'; import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts'; import { apiGetAllAdsAsync } from '../api/api.js'; +import { useTranslation } from 'react-i18next'; // Dodijeli boje svakom triggeru const triggerColors = { @@ -13,6 +14,7 @@ const triggerColors = { const triggerLabels = ['Search', 'Order', 'View']; const OrdersBystatus = () => { + const { t } = useTranslation(); const [data, setData] = useState([]); useEffect(() => { @@ -56,7 +58,7 @@ const OrdersBystatus = () => { > - Ad Triggers Breakdown + {t('analytics.adTriggersBreakdown')} diff --git a/src/components/ProductsSummary.jsx b/src/components/ProductsSummary.jsx index 423d0a0..b480849 100644 --- a/src/components/ProductsSummary.jsx +++ b/src/components/ProductsSummary.jsx @@ -1,9 +1,11 @@ import React, { useEffect, useState } from 'react'; import { Paper, Typography, Box, Grid, Divider } from '@mui/material'; import { Megaphone, ShoppingBag, CheckCircle, TrendingUp } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { apiFetchAdsWithProfitAsync } from '@api/api'; const ProductSummary = ({ product, ads }) => { + const { t } = useTranslation(); const [adsData, setAdsData] = useState([]); useEffect(() => { @@ -48,7 +50,7 @@ const ProductSummary = ({ product, ads }) => { {product?.name.length > 54 ? `${product.name.substring(0, 54)}...` - : product.name || 'Unknown Product'}{' '} + : product.name || t('common.unknownProduct')}{' '} @@ -59,7 +61,7 @@ const ProductSummary = ({ product, ads }) => { - Views + {t('analytics.totalViews')} @@ -73,7 +75,7 @@ const ProductSummary = ({ product, ads }) => { - Clicks + {t('analytics.totalClicks')} @@ -87,7 +89,7 @@ const ProductSummary = ({ product, ads }) => { - Conversions + {t('analytics.totalConversions')} @@ -109,7 +111,7 @@ const ProductSummary = ({ product, ads }) => { > - Total Earned Profit from Ads + {t('analytics.totalEarnedProfitFromAds')} ${totalProfit > 0 ? totalProfit.toFixed(2) : 0} diff --git a/src/components/RevenueByStore.jsx b/src/components/RevenueByStore.jsx index 3dd9bb4..572f69a 100644 --- a/src/components/RevenueByStore.jsx +++ b/src/components/RevenueByStore.jsx @@ -10,11 +10,13 @@ import { Cell, } from 'recharts'; import { apiGetAllStoresAsync, apiGetAllAdsAsync } from '../api/api.js'; +import { useTranslation } from 'react-i18next'; const barColor = '#6366F1'; const TOP_N = 5; const RevenueByStore = () => { + const { t } = useTranslation(); const [data, setData] = useState([]); useEffect(() => { @@ -74,7 +76,7 @@ const RevenueByStore = () => { > - Top Stores by Ad Revenue + {t('analytics.topStoresByAdRevenue')} diff --git a/src/components/RevenueMetrics.jsx b/src/components/RevenueMetrics.jsx index a9c296a..e5418a3 100644 --- a/src/components/RevenueMetrics.jsx +++ b/src/components/RevenueMetrics.jsx @@ -3,6 +3,7 @@ import { Grid, Paper, Typography, Box } from '@mui/material'; import { LineChart } from '@mui/x-charts/LineChart'; import { PieChart } from '@mui/x-charts/PieChart'; import { DollarSign, Eye, MousePointerClick, ShoppingCart } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import MetricCard from './MetricCard'; import { apiFetchAdsWithProfitAsync } from '@api/api'; @@ -32,6 +33,7 @@ const groupByDay = (ads, eventType) => { }; const RevenueMetrics = () => { + const { t } = useTranslation(); const [ads, setAds] = useState([]); useEffect(() => { @@ -89,51 +91,51 @@ const RevenueMetrics = () => { return ( - Revenue & Profit Analysis + {t('analytics.revenueAndProfitAnalysis')} } color='success' - tooltipText='Total revenue generated from all sources' + tooltipText={t('analytics.fromAllAdvertisingSources')} /> s + a.clicks, 0).toLocaleString()} clicks`} + subtitle={t('analytics.fromClicks', { count: ads.reduce((s, a) => s + a.clicks, 0).toLocaleString() })} icon={} color='info' - tooltipText='Revenue from ad clicks' + tooltipText={t('analytics.clickRevenue')} /> s + a.views, 0).toLocaleString()} views`} + subtitle={t('analytics.fromViews', { count: ads.reduce((s, a) => s + a.views, 0).toLocaleString() })} icon={} color='secondary' - tooltipText='Revenue from ad views' + tooltipText={t('analytics.viewRevenue')} /> s + a.conversions, 0).toLocaleString()} conversions`} + subtitle={t('analytics.fromConversions', { count: ads.reduce((s, a) => s + a.conversions, 0).toLocaleString() })} icon={} color='success' - tooltipText='Revenue from conversions' + tooltipText={t('analytics.conversionRevenue')} /> @@ -142,7 +144,7 @@ const RevenueMetrics = () => { - Revenue by Source Over Time + {t('analytics.revenueBySourceOverTime')} { - Revenue Distribution + {t('analytics.revenueDistribution')} { + const { t } = useTranslation(); const [conversionRate, setConversionRate] = useState(0); const [totalConversions, setTotalConversions] = useState(0); const [totalClicks, setTotalClicks] = useState(0); @@ -162,10 +164,10 @@ const UserDistribution = () => { > - Conversion Rate (All Ads) + {t('analytics.conversionRate')} - {totalConversions} conversions / {totalClicks} clicks + {totalConversions} {t('analytics.conversions')} / {totalClicks} {t('analytics.clicks')} diff --git a/src/pages/AnalyticsPage.jsx b/src/pages/AnalyticsPage.jsx index 6a26eb6..4c33dd1 100644 --- a/src/pages/AnalyticsPage.jsx +++ b/src/pages/AnalyticsPage.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { Grid, Typography, Box, Pagination } from '@mui/material'; +import { useTranslation } from 'react-i18next'; import KpiCard from '@components/KpiCard'; import AnalyticsChart from '@components/AnalyticsChart'; import CountryStatsPanel from '@components/CountryStatsPanel'; @@ -42,6 +43,7 @@ const HUB_ENDPOINT_PATH = '/Hubs/AdvertisementHub'; const HUB_URL = `${baseUrl}${HUB_ENDPOINT_PATH}`; const AnalyticsPage = () => { + const { t } = useTranslation(); // --- State from develop --- const [totalAdminProfit, setTotalAdminProfit] = useState(0); const [ads, setAds] = useState([]); // For general ad data, updated by SignalR @@ -570,53 +572,53 @@ const AnalyticsPage = () => { {/* Adjusted spacing and maxWidth */} {[ { - label: 'Total Ads', + label: t('analytics.totalAds'), value: kpi.totalAds, change: kpi.totalAdsChange, type: 'totalAds', }, { - label: 'Total Views', + label: t('analytics.totalViews'), value: kpi.totalViews, change: kpi.viewsChange, type: 'views', }, { - label: 'Total Clicks', + label: t('analytics.totalClicks'), value: kpi.totalClicks, change: kpi.clicksChange, type: 'clicks', }, { - label: 'Total Conversions', + label: t('analytics.totalConversions'), value: kpi.totalConversions, change: kpi.conversionsChange, type: 'conversions', - }, // Changed type + }, { - label: 'Conversion Revenue', + label: t('analytics.conversionRevenue'), value: kpi.totalConversionRevenue, change: kpi.conversionRevenueChange, type: 'conversionRevenue', }, { - label: 'Clicks Revenue', + label: t('analytics.clicksRevenue'), value: kpi.totalClicksRevenue, change: kpi.clicksRevenueChange, type: 'clicksRevenue', - }, // Changed type + }, { - label: 'Views Revenue', + label: t('analytics.viewsRevenue'), value: kpi.totalViewsRevenue, change: kpi.viewsRevenueChange, type: 'viewsRevenue', - }, // Changed type + }, { - label: 'Total Products', + label: t('analytics.totalProducts'), value: kpi.totalProducts, change: kpi.productsChange, type: 'products', - }, // Changed type + }, ].map((item, i) => ( {' '} @@ -746,11 +748,11 @@ const AnalyticsPage = () => { {' '} {/* Responsive width */} - Product Performance + {t('analytics.productPerformance')} {products.length === 0 && ( - No products to display or still loading... + {t('analytics.noProductsToDisplay')} )} {paginatedProducts.map((product, i) => ( @@ -790,14 +792,12 @@ const AnalyticsPage = () => { {/* Store List with Pagination (from HEAD) */} - {' '} - {/* Responsive width */} - Store Performance + {t('analytics.storePerformance')} {stores.length === 0 && ( - No stores to display or still loading... + {t('analytics.noStoresToDisplay')} )} {stores.length && @@ -855,10 +855,10 @@ const AnalyticsPage = () => { - Store Earnings (Past Month) - - - + {t('analytics.storeEarningsPastMonth')} + + + diff --git a/src/pages/SellerAnalyticsPage.jsx b/src/pages/SellerAnalyticsPage.jsx index 8e272ef..c453501 100644 --- a/src/pages/SellerAnalyticsPage.jsx +++ b/src/pages/SellerAnalyticsPage.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Box, Typography, Grid, Card, CardContent } from '@mui/material'; +import { useTranslation } from 'react-i18next'; import { LineChart, Line, @@ -259,6 +260,7 @@ const SellerAnalytics = ({ allViews, // Expected: [{ id: adId, views: [timestamp1, ...] }, ...] allConversions, // Expected: [{ id: adId, conversions: [timestamp1, ...] }, ...] }) => { + const { t } = useTranslation(); const [stats, setStats] = useState(mockStats); const [summary, setSummary] = useState(mockRealtimeStats); @@ -304,27 +306,27 @@ const SellerAnalytics = ({ const topStats = [ { - label: 'Total Earnings', + label: t('sellerAnalytics.totalEarnings'), value: `${summary.totalEarnings.toFixed(2)} €`, change: -5.2, - }, // Change data is static for now + }, { - label: 'Seller Profit', + label: t('sellerAnalytics.sellerProfit'), value: `${summary.sellerProfit.toFixed(2)} €`, change: 2.1, }, { - label: 'Click Revenue', + label: t('sellerAnalytics.clickRevenue'), value: `${summary.earningsFromClicks.toFixed(2)} €`, change: 1.5, - }, // Added Click Revenue + }, { - label: 'View Revenue', + label: t('sellerAnalytics.viewRevenue'), value: `${summary.earningsFromViews.toFixed(2)} €`, change: -3.6, }, { - label: 'Conversion Revenue', + label: t('sellerAnalytics.conversionRevenue'), value: `${summary.earningsFromConversions.toFixed(2)} €`, change: 4.9, }, @@ -344,12 +346,12 @@ const SellerAnalytics = ({ }} > - Store Performance: {summary.sellerName} + {t('analytics.storePerformance')}: {summary.sellerName} - Detailed Analytics for {summary.sellerName} + {t('analytics.detailedAnalyticsFor', { storeName: summary.sellerName })} @@ -425,17 +427,17 @@ const SellerAnalytics = ({ {/* Adjusted spacing */} {[ { - title: 'Click Revenue Over Time', + title: t('analytics.clickRevenueOverTime'), color: '#0f766e', data: summary.earningsFromClicksOverTime, }, { - title: 'View Revenue Over Time', + title: t('analytics.viewRevenueOverTime'), color: '#f59e0b', data: summary.earningsFromViewsOverTime, }, { - title: 'Conversion Revenue Over Time', + title: t('analytics.conversionRevenueOverTime'), color: '#ef4444', data: summary.earningsFromConversionsOverTime, }, From 1b031a9e75dcfbc6f2dc701911129d1da9a7cdca Mon Sep 17 00:00:00 2001 From: Faruk Bakovic Date: Mon, 2 Jun 2025 16:31:41 +0200 Subject: [PATCH 7/7] Popravljeni prijevodi --- public/locales/ba/translation.json | 96 +++- public/locales/en/translation.json | 77 +++- public/locales/es/translation.json | 436 ++++++++++++------- src/components/AdCard.jsx | 19 +- src/components/AdRealtimeMonitor.jsx | 14 +- src/components/AddAdModal.jsx | 30 +- src/components/AddStoreModal.jsx | 19 +- src/components/AdvertisementDetailsModal.jsx | 10 +- src/components/CategoryCard.jsx | 4 +- src/components/CategoryTabs.jsx | 6 +- src/components/DealsChart.jsx | 5 +- src/components/KpiCard.jsx | 6 +- src/components/OrdersTable.jsx | 19 +- src/components/ParetoChart.jsx | 6 +- src/components/PendingUsersTable.jsx | 15 +- src/components/RevenueMetrics.jsx | 10 +- src/components/RouteCard.jsx | 7 +- src/components/StoreCard.jsx | 11 +- src/components/StoreEarningsTable.jsx | 12 +- src/components/StoreProductsList.jsx | 4 +- src/components/TicketListSection.jsx | 9 +- src/components/UserList.jsx | 23 +- src/components/UserManagementPagination.jsx | 9 +- src/pages/AnalyticsPage.jsx | 2 +- 24 files changed, 596 insertions(+), 253 deletions(-) diff --git a/public/locales/ba/translation.json b/public/locales/ba/translation.json index f2e6899..b66b840 100644 --- a/public/locales/ba/translation.json +++ b/public/locales/ba/translation.json @@ -171,5 +171,99 @@ "common.chat": "Razgovori", "common.languages": "Jezici", "common.pasteTranslations" : "Zalijepi prijevod", - "common.translationJsonHint" : "Dodajte tekst u JSON formatu" + "common.translationJsonHint" : "Dodajte tekst u JSON formatu" , + "common.totalAds": "Ukupno oglasa", + "analytics.storePerformance": "Učinkovitost prodavnice", + "analytics.revenueAndProfitAnalysis": "Analiza prihoda i profita", + "analytics.totalRevenue": "Ukupan prihod", + "analytics.fromAllAdvertisingSources": "Iz svih oglasnih izvora", + "analytics.clickRevenue": "Prihod od klikova", + "analytics.fromClicks": "Od {{count}} klikova", + "analytics.viewRevenue": "Prihod od pregleda", + "analytics.fromViews": "Od {{count}} pregleda", + "analytics.fromConversions": "Od {{count}} konverzija", + "analytics.revenueBySourceOverTime": "Prihod po izvoru kroz vrijeme", + "analytics.revenueDistribution": "Distribucija prihoda", + "analytics.totalEarnedProfitFromAds": "Ukupan zarađeni profit od oglasa", + "common.unknownProduct": "Nepoznat proizvod", + "analytics.detailedAnalyticsFor": "Detaljna analitika za {{storeName}}", + "analytics.unknownStore": "Nepoznata prodavnica", + "analytics.storeEarningsPastMonth": "Zarada prodavnice (prošli mjesec)", + "analytics.noProductsToDisplay": "Nema proizvoda za prikaz ili se još učitavaju...", + "analytics.noStoresToDisplay": "Nema prodavnica za prikaz ili se još učitavaju...", + "analytics.conversionsRevenue": "Prihod od konverzija", + "analytics.dashboardAnalytics": "Analitika kontrolne table", + "analytics.paretoChart": "Pareto dijagram", + "analytics.dealsAmount": "Iznos ponuda", + "analytics.deals": "Ponude", + "analytics.storeRevenue": "Prihod prodavnice", + "analytics.adminProfit": "Profit administratora", + "analytics.taxRate": "Poreska stopa (%)", + "analytics.topRated": "Najbolje ocijenjeno", + "analytics.lowestRated": "Najniže ocijenjeno", + "analytics.topStores": "Najbolje prodavnice", + "analytics.topProducts": "Najbolji proizvodi", + "analytics.topCategories": "Najbolje kategorije", + "analytics.topUsers": "Najbolji korisnici", + "analytics.topProductsByAdRevenue": "Najbolji proizvodi po prihodu od oglasa", + "common.noPendingUsersFound": "Nema korisnika na čekanju.", + "analytics.picture": "Slika", + "analytics.pendingUsers": "Korisnici na čekanju", + "analytics.pendingRequests": "Zahtjevi na čekanju", + "analytics.pendingRequestsTitle": "Zahtjevi na čekanju", + "analytics.pendingRequestsDescription": "Zahtjevi na čekanju su zahtjevi koji čekaju obradu od strane administratora.", + "analytics.pendingUsersTitle": "Korisnici na čekanju", + "analytics.pendingUsersDescription": "Korisnici na čekanju su korisnici koji čekaju odobrenje administratora.", + "analytics.latestAdUpdate": "Zadnje ažuriranje oglasa", + "analytics.latestView": "Zadnji pregled", + "analytics.latestClick": "Zadnji klik", + "analytics.latestConversion": "Zadnja konverzija", + "analytics.history": "Historija", + "analytics.views": "Pregledi", + "common.clicks": "Klikovi", + "analytics.active": "Aktivan", + "analytics.status": "Status", + "analytics.conversionPrice": "Cijena konverzije", + "analytics.storeName": "Naziv prodavnice", + "analytics.comparedToLastMonth": "U poređenju s prošlim mjesecom", + "analytics.comparedToLastYear": "U poređenju s prošlom godinom", + "analytics.comparedToPreviousMonth": "U poređenju s prethodnim mjesecom", + "analytics.comparedToPreviousYear": "U poređenju s prethodnom godinom", + "analytics.comparedToNextMonth": "U poređenju sa sljedećim mjesecom", + "analytics.comparedToNextYear": "U poređenju sa sljedećom godinom", + "common.picture": "Slika", + "common.details": "Detalji", + "common.addItem": "Dodaj stavku", + "common.adText": "Tekst oglasa", + "common.product": "Proizvod", + "common.username": "Korisničko ime", + "common.store": "Prodavnica", + "common.deliveryAddress": "Adresa dostave", + "common.deliveryStatus": "Status dostave", + "common.deliveryDate": "Datum dostave", + "common.deliveryTime": "Vrijeme dostave", + "common.deliveryPrice": "Cijena dostave", + "common.active": "Aktivan", + "common.inactive": "Neaktivan", + "common.saveAd": "Spasi oglas", + "common.viewPrice": "Cijena pregleda", + "common.clickPrice": "Cijena klika", + "common.conversionPrice": "Cijena konverzije", + "common.storeName": "Naziv prodavnice", + "common.storeId": "ID prodavnice", + "common.displayingPage": "Prikaz stranice", + "common.tax": "Porez", + "common.totalMonthlyIncome": "Ukupan mjesečni prihod", + "common.taxedMonthlyIncome": "Oporezovani mjesečni prihod", + "common.addProduct": "Dodaj proizvod", + "common.products": "Proizvodi", + "common.tickets": "Tiketi", + "common.searchTickets": "Pretraži tikete...", + "common.noTicketsFound": "Nijedan tiket nije pronađen.", + "common.ticket": "Tiket", + "common.views": "Pregledi", + "common.productCategory": "Kategorija proizvoda", + "common.storeCategory": "Kategorija prodavnice", + "common.orderNumber": "Broj narudžbe", + "common.created": "Kreirano" } diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 0e83bf6..ff7fdda 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -209,5 +209,80 @@ "analytics.unknownStore": "Unknown Store", "analytics.storeEarningsPastMonth": "Store Earnings (Past Month)", "analytics.noProductsToDisplay": "No products to display or still loading...", - "analytics.noStoresToDisplay": "No stores to display or still loading..." + "analytics.noStoresToDisplay": "No stores to display or still loading...", + "analytics.conversionsRevenue": "Conversions Revenue", + "analytics.dashboardAnalytics": "Dashboard Analytics", + "analytics.paretoChart": "Pareto Chart", + "analytics.dealsAmount": "Deals amount", + "analytics.deals": "Deals", + "analytics.storeRevenue": "Store Revenue", + "analytics.adminProfit": "Admin Profit", + "analytics.taxRate": "Tax Rate (%)", + "analytics.topRated": "Top Rated", + "analytics.lowestRated": "Lowest Rated", + "analytics.topStores": "Top Stores", + "analytics.topProducts": "Top Products", + "analytics.topCategories": "Top Categories", + "analytics.topUsers": "Top Users", + "analytics.topProductsByAdRevenue": "Top Products by Ad Revenue", + "common.noPendingUsersFound": "No pending users found.", + "analytics.picture": "Picture", + "analytics.pendingUsers": "Pending Users", + "analytics.pendingRequests": "Pending Requests", + "analytics.pendingRequestsTitle": "Pending Requests", + "analytics.pendingRequestsDescription": "Pending requests are requests that are waiting to be processed by the administrator.", + "analytics.pendingUsersTitle": "Pending Users", + "analytics.pendingUsersDescription": "Pending users are users that are waiting to be approved by the administrator.", + "analytics.latestAdUpdate": "Latest Ad Update", + "analytics.latestView": "Latest View", + "analytics.latestClick": "Latest Click", + "analytics.latestConversion": "Latest Conversion", + "analytics.history": "History", + "analytics.views": "Views", + "common.clicks": "Clicks", + "analytics.active": "Active", + "analytics.status": "Status", + "analytics.conversionPrice": "Conversion Price", + "analytics.storeName": "Store Name", + "analytics.comparedToLastMonth": "Compared to last month", + "analytics.comparedToLastYear": "Compared to last year", + "analytics.comparedToPreviousMonth": "Compared to previous month", + "analytics.comparedToPreviousYear": "Compared to previous year", + "analytics.comparedToNextMonth": "Compared to next month", + "analytics.comparedToNextYear": "Compared to next year", + "common.picture": "Picture", + "common.details": "Details", + "common.addItem": "Add Item", + "common.adText": "Ad Text", + "common.product": "Product", + "common.username": "Username", + "common.store": "Store", + "common.deliveryAddress": "Delivery Address", + "common.deliveryStatus": "Delivery Status", + "common.deliveryDate": "Delivery Date", + "common.deliveryTime": "Delivery Time", + "common.deliveryPrice": "Delivery Price", + "common.active": "Active", + "common.inactive": "Inactive", + "common.saveAd": "Save Ad", + "common.viewPrice": "View Price", + "common.clickPrice": "Click Price", + "common.conversionPrice": "Conversion Price", + "common.storeName": "Store Name", + "common.storeId": "Store ID", + "common.displayingPage": "Displaying Page", + "common.tax": "Tax", + "common.totalMonthlyIncome": "Total Monthly Income", + "common.taxedMonthlyIncome": "Taxed Monthly Income", + "common.addProduct": "Add Product", + "common.products": "Products", + "common.tickets": "Tickets", + "common.searchTickets": "Search tickets...", + "common.noTicketsFound": "No tickets found.", + "common.ticket": "Ticket", + "common.views": "Views", + "common.productCategory": "Product Category", + "common.storeCategory": "Store Category", + "common.orderNumber": "Order Number", + "common.created": "Created" } \ No newline at end of file diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 263dd5a..ff7fdda 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -1,150 +1,288 @@ { - "ads.adsManagement": "Gestión de anuncios", - "ads.adminPanel": "Panel de administración", - "ads.advertisements": "Anuncios", - "ads.createAd": "Crear anuncio", - "ads.searchAds": "Buscar anuncios", - "categories.categories": "Categorías", - "categories.addCategory": "Agregar categoría", - "categories.searchCategory": "Buscar categoría", - "common.searchPlaceholder": "Buscar...", - "common.welcome": "Bienvenido", - "common.orders": "Pedidos", - "common.status": "Estado", - "common.all": "Todos", - "common.searchOrders": "Buscar pedidos", - "common.loginToContinue": "Inicia sesión para continuar", - "common.login": "Iniciar sesión", - "common.requests": "Solicitudes", - "common.searchUser": "Buscar usuario", - "common.addToCart": "Añadir al carrito", - "common.viewDetails": "Ver detalles", - "common.price": "Precio", - "common.quantity": "Cantidad", - "common.subtotal": "Subtotal", - "common.total": "Total", - "common.checkout": "Pagar", - "common.continueShopping": "Seguir comprando", - "common.save": "Guardar", - "common.cancel": "Cancelar", - "common.oops": "¡Ups! Algo salió mal.", - "common.loading": "Cargando...", - "common.adminPanel": "Panel de administración", - "common.first": "Primera", - "common.last": "Última", - "common.confirm": "Confirmar", - "common.delete": "Eliminar", - "common.edit": "Editar", - "common.view": "Ver", - "common.close": "Cerrar", - "common.done": "Hecho", - "common.browseFiles": "Explorar archivos", - "common.dragFilesToUpload": "Arrastra archivos para subir", - "common.or": "o", - "common.maxFileSize": "Tamaño máximo de archivo: 50MB — Formatos soportados: JPG, PNG, GIF, SVG, WEBP", - "common.languageManagement": "Gestión de idiomas", - "common.currentLanguage": "Idioma actual", - "common.languageCode": "Código del idioma", - "common.languageName": "Nombre del idioma", - "common.actions": "Acciones", - "common.addLanguage": "Agregar idioma", - "common.editLanguage": "Editar idioma", - "common.deleteLanguage": "Eliminar idioma", - "common.availableLanguages": "Idiomas disponibles", - "common.addNewLanguage": "Agregar nuevo idioma", - "common.saveLanguage": "Guardar idioma", - "common.translations": "Traducciones", - "common.allRoutes": "Todas las rutas", - "common.routes": "Rutas", - "common.createRoute": "Crear ruta", - "chat.openTicketToViewChat": "Abre este ticket para ver el chat.", - "roles.Buyer": "Comprador", - "roles.Seller": "Vendedor", - "roles.Admin": "Administrador", - "roles.Unknown": "Rol desconocido", - "nav.analytics": "Analítica", - "nav.users": "Usuarios", - "nav.requests": "Solicitudes", - "nav.stores": "Tiendas", - "nav.categories": "Categorías", - "nav.orders": "Pedidos", - "nav.advertisements": "Anuncios", - "nav.chat": "Chat", - "nav.routes": "Rutas", - "nav.languages": "Idiomas", - "usersPage.username": "Nombre de usuario", - "usersPage.email": "Correo electrónico", - "usersPage.role": "Rol", - "usersPage.active": "Activo", - "usersPage.actions": "Acciones", - "usersPage.addUser": "Agregar usuario", - "usersPage.searchUser": "Buscar usuario", - "usersPage.title": "Gestión de usuarios", - "usersPage.phoneNumber": "Número de teléfono", - "usersPage.saveChanges": "Guardar cambios", - "usersPage.deleteUser": "Eliminar usuario", - "usersPage.confirmDeleteUser": "¿Estás seguro de que deseas eliminar este usuario?", - "usersPage.noUsers": "No hay usuarios disponibles", - "usersPage.loadingUsers": "Cargando usuarios...", - "analytics.dashboardTitle": "Panel de analítica", - "analytics.totalAds": "Total de anuncios", - "analytics.totalViews": "Total de vistas", - "analytics.totalClicks": "Total de clics", - "analytics.totalConversions": "Total de conversiones", - "analytics.conversionRevenue": "Ingresos por conversiones", - "analytics.clicksRevenue": "Ingresos por clics", - "analytics.viewsRevenue": "Ingresos por vistas", - "analytics.totalProducts": "Total de productos", - "analytics.productPerformance": "Rendimiento de productos", - "analytics.noProducts": "No hay productos para mostrar o aún se están cargando...", - "analytics.storeEarnings": "Ganancias de la tienda (último mes)", - "analytics.realtimeEvents": "Eventos en tiempo real", - "analytics.noEvents": "Aún no se han recibido eventos", - "analytics.lastError": "Último error", - "sellerAnalytics.totalEarnings": "Ganancias totales", - "sellerAnalytics.sellerProfit": "Ganancias del vendedor", - "sellerAnalytics.clickRevenue": "Ingresos por clics", - "sellerAnalytics.viewRevenue": "Ingresos por vistas", - "sellerAnalytics.conversionRevenue": "Ingresos por conversiones", - "sellerAnalytics.clickRevenueOverTime": "Ingresos por clics a lo largo del tiempo", - "sellerAnalytics.viewRevenueOverTime": "Ingresos por vistas a lo largo del tiempo", - "sellerAnalytics.conversionRevenueOverTime": "Ingresos por conversiones a lo largo del tiempo", - "routes.availableRoutes": "Rutas disponibles", - "routes.noRoutes": "No hay rutas disponibles", - "routes.loadingMap": "Cargando mapa para la ruta ID: {{id}}...", - "routes.selectRoute": "Selecciona una ruta de la lista para verla en el mapa", - "routes.routeDetails": "Detalles de la ruta", - "routes.directions": "Direcciones", - "routes.routeId": "ID de ruta: {{id}}", - "ads.adType": "Tipo de anuncio", - "ads.triggers": "Disparadores", - "ads.startTime": "Hora de inicio", - "ads.endTime": "Hora de finalización", - "ads.isActive": "Activo", - "ads.advertisementItems": "Elementos del anuncio", - "ads.conversionPrice": "Precio por conversión", - "ads.storeName": "Nombre de la tienda", - "ads.address": "Dirección", - "ads.description": "Descripción", - "ads.place": "Lugar", - "ads.category": "Categoría", - "auth.login": "Iniciar sesión", - "auth.logout": "Cerrar sesión", - "auth.email": "Correo electrónico", - "auth.password": "Contraseña", - "auth.forgotPassword": "¿Olvidaste tu contraseña?", - "auth.signIn": "Iniciar sesión", - "auth.signUp": "Registrarse", - "auth.rememberMe": "Recuérdame", - "common.administrator": "Administrador", - "common.logout": "Cerrar sesión", - "common.analytics": "Analíticas", - "common.users": "Usuarios", - "common.stores": "Tiendas", - "common.categories": "Categorías", - "common.advertisements": "Anuncios", - "common.chat": "Chat", - "common.languages": "Idiomas" - - } - \ No newline at end of file + "ads.adsManagement": "Ads Management", + "ads.adminPanel": "Admin Panel", + "ads.advertisements": "Advertisements", + "ads.createAd": "Create Ad", + "ads.searchAds": "Search Ads", + "categories.categories": "Categories", + "categories.addCategory": "Add Category", + "categories.searchCategory": "Search Category", + "common.searchPlaceholder": "Search...", + "common.welcome": "Welcome", + "common.orders": "Orders", + "common.status": "Status", + "common.all": "All", + "common.searchOrders": "Search Orders", + "common.loginToContinue": "Login to continue", + "common.login": "Login", + "common.requests": "Requests", + "common.searchUser": "Search User", + "common.addToCart": "Add to Cart", + "common.viewDetails": "View Details", + "common.price": "Price", + "common.quantity": "Quantity", + "common.subtotal": "Subtotal", + "common.total": "Total", + "common.checkout": "Checkout", + "common.continueShopping": "Continue Shopping", + "common.save": "Save", + "common.cancel": "Cancel", + "common.oops": "Oops! Something went wrong.", + "common.loading": "Loading...", + "common.adminPanel": "Admin Panel", + "common.first": "First", + "common.last": "Last", + "common.confirm": "Confirm", + "common.delete": "Delete", + "common.edit": "Edit", + "common.view": "View", + "common.close": "Close", + "common.done": "Done", + "common.browseFiles": "Browse Files", + "common.dragFilesToUpload": "Drag files to upload", + "common.or": "or", + "common.maxFileSize": "Max file size: 50MB — Supported types: JPG, PNG, GIF, SVG, WEBP", + "common.languageManagement": "Language Management", + "common.currentLanguage": "Current Language", + "common.languageCode": "Language Code", + "common.languageName": "Language Name", + "common.actions": "Actions", + "common.addLanguage": "Add Language", + "common.editLanguage": "Edit Language", + "common.deleteLanguage": "Delete Language", + "common.availableLanguages": "Available Languages", + "common.addNewLanguage": "Add New Language", + "common.saveLanguage": "Save Language", + "common.translations": "Translations", + "common.allRoutes": "All Routes", + "common.routes": "Routes", + "common.createRoute": "Create Route", + "chat.openTicketToViewChat": "Open this ticket to view the chat.", + "roles.Buyer": "Buyer", + "roles.Seller": "Seller", + "roles.Admin": "Administrator", + "roles.Unknown": "Unknown Role", + "nav.analytics": "Analytics", + "nav.users": "Users", + "nav.requests": "Requests", + "nav.stores": "Stores", + "nav.categories": "Categories", + "nav.orders": "Orders", + "nav.advertisements": "Advertisements", + "nav.chat": "Chat", + "nav.routes": "Routes", + "nav.languages": "Languages", + "usersPage.username": "Username", + "usersPage.email": "Email", + "usersPage.role": "Role", + "usersPage.active": "Active", + "usersPage.actions": "Actions", + "usersPage.addUser": "Add User", + "usersPage.searchUser": "Search User", + "usersPage.title": "User Management", + "usersPage.phoneNumber": "Phone Number", + "usersPage.saveChanges": "Save Changes", + "usersPage.deleteUser": "Delete User", + "usersPage.confirmDeleteUser": "Are you sure you want to delete this user?", + "usersPage.noUsers": "No users available", + "usersPage.loadingUsers": "Loading users...", + "analytics.dashboardTitle": "Dashboard Analytics", + "analytics.totalAds": "Total Ads", + "analytics.totalViews": "Total Views", + "analytics.totalClicks": "Total Clicks", + "analytics.totalConversions": "Total Conversions", + "analytics.conversionRevenue": "Conversion Revenue", + "analytics.clicksRevenue": "Clicks Revenue", + "analytics.viewsRevenue": "Views Revenue", + "analytics.totalProducts": "Total Products", + "analytics.productPerformance": "Product Performance", + "analytics.noProducts": "No products to display or still loading...", + "analytics.storeEarnings": "Store Earnings (Past Month)", + "analytics.realtimeEvents": "Realtime Events", + "analytics.noEvents": "No events received yet", + "analytics.lastError": "Last Error", + "analytics.ordersRevenueByRegions": "Orders Revenue by Regions", + "analytics.ordersByRegions": "Orders by Regions", + "analytics.revenue": "Revenue", + "analytics.orders": "Orders", + "analytics.adTriggersBreakdown": "Ad Triggers Breakdown", + "analytics.search": "Search", + "analytics.order": "Order", + "analytics.view": "View", + "analytics.conversionRate": "Conversion Rate (All Ads)", + "analytics.conversions": "conversions", + "analytics.clicks": "clicks", + "analytics.topStoresByAdRevenue": "Top Stores by Ad Revenue", + "analytics.salesFunnelAnalysis": "Sales Funnel Analysis", + "analytics.viewed": "Viewed", + "analytics.clicked": "Clicked", + "analytics.converted": "Converted", + "analytics.combinationChart": "Combination Chart: Fixed vs PopUp Ads", + "analytics.fixed": "Fixed", + "analytics.popup": "PopUp", + "sellerAnalytics.totalEarnings": "Total Earnings", + "sellerAnalytics.sellerProfit": "Seller Profit", + "sellerAnalytics.clickRevenue": "Click Revenue", + "sellerAnalytics.viewRevenue": "View Revenue", + "sellerAnalytics.conversionRevenue": "Conversion Revenue", + "sellerAnalytics.clickRevenueOverTime": "Click Revenue Over Time", + "sellerAnalytics.viewRevenueOverTime": "View Revenue Over Time", + "sellerAnalytics.conversionRevenueOverTime": "Conversion Revenue Over Time", + "routes.availableRoutes": "Available Routes", + "routes.noRoutes": "No routes available", + "routes.loadingMap": "Loading map for Route ID: {{id}}...", + "routes.selectRoute": "Select a route from the list to view it on the map", + "routes.routeDetails": "Route Details", + "routes.directions": "Directions", + "routes.routeId": "Route ID: {{id}}", + "ads.adType": "Ad Type", + "ads.triggers": "Triggers", + "ads.startTime": "Start Time", + "ads.endTime": "End Time", + "ads.isActive": "Is Active", + "ads.advertisementItems": "Advertisement Items", + "ads.conversionPrice": "Conversion Price", + "ads.storeName": "Store Name", + "ads.address": "Address", + "ads.description": "Description", + "ads.place": "Place", + "ads.category": "Category", + "auth.login": "Login", + "auth.logout": "Logout", + "auth.email": "Email", + "auth.password": "Password", + "auth.forgotPassword": "Forgot Password?", + "auth.signIn": "Sign In", + "auth.signUp": "Sign Up", + "auth.rememberMe": "Remember Me", + "errors.connectionError": "Connection Error", + "errors.failedToConnect": "Failed to connect", + "errors.authTokenMissing": "Auth Token Missing", + "errors.loadingError": "Failed to load data", + "errors.deleteError": "Failed to delete item", + "errors.updateError": "Failed to update item", + "errors.createError": "Failed to create item", + "stores.stores": "Stores", + "stores.searchStore": "Search Store", + "stores.addStore": "Add Store", + "common.userCreatedSuccessfully": "User created successfully!", + "common.createNewUser": "Create New User", + "common.role": "Role", + "common.createUser": "Create User", + "common.loadingUserData": "Loading user data...", + "common.userDetails": "User Details", + "common.name": "Name", + "common.email": "Email", + "common.phoneNumber": "Phone Number", + "common.saveChanges": "Save Changes", + "common.deleteUser": "Delete User", + "common.confirmDeleteUser": "Are you sure you want to delete this user?", + "common.noUsers": "No users available", + "common.userManagement": "User Management", + "common.addUser": "Add User", + "common.administrator": "Administrator", + "common.logout": "Logout", + "common.analytics": "Analytics", + "common.users": "Users", + "common.stores": "Stores", + "common.categories": "Categories", + "common.advertisements": "Advertisements", + "common.chat": "Chat", + "common.languages": "Languages", + "common.pasteTranslations" : "Paste Translations", + "common.translationJsonHint" : "Add a text in JSON format", + "common.totalAds": "Total Ads", + "analytics.storePerformance": "Store Performance", + "analytics.revenueAndProfitAnalysis": "Revenue & Profit Analysis", + "analytics.totalRevenue": "Total Revenue", + "analytics.fromAllAdvertisingSources": "From all advertising sources", + "analytics.clickRevenue": "Click Revenue", + "analytics.fromClicks": "From {{count}} clicks", + "analytics.viewRevenue": "View Revenue", + "analytics.fromViews": "From {{count}} views", + "analytics.fromConversions": "From {{count}} conversions", + "analytics.revenueBySourceOverTime": "Revenue by Source Over Time", + "analytics.revenueDistribution": "Revenue Distribution", + "analytics.totalEarnedProfitFromAds": "Total Earned Profit from Ads", + "common.unknownProduct": "Unknown Product", + "analytics.detailedAnalyticsFor": "Detailed Analytics for {{storeName}}", + "analytics.unknownStore": "Unknown Store", + "analytics.storeEarningsPastMonth": "Store Earnings (Past Month)", + "analytics.noProductsToDisplay": "No products to display or still loading...", + "analytics.noStoresToDisplay": "No stores to display or still loading...", + "analytics.conversionsRevenue": "Conversions Revenue", + "analytics.dashboardAnalytics": "Dashboard Analytics", + "analytics.paretoChart": "Pareto Chart", + "analytics.dealsAmount": "Deals amount", + "analytics.deals": "Deals", + "analytics.storeRevenue": "Store Revenue", + "analytics.adminProfit": "Admin Profit", + "analytics.taxRate": "Tax Rate (%)", + "analytics.topRated": "Top Rated", + "analytics.lowestRated": "Lowest Rated", + "analytics.topStores": "Top Stores", + "analytics.topProducts": "Top Products", + "analytics.topCategories": "Top Categories", + "analytics.topUsers": "Top Users", + "analytics.topProductsByAdRevenue": "Top Products by Ad Revenue", + "common.noPendingUsersFound": "No pending users found.", + "analytics.picture": "Picture", + "analytics.pendingUsers": "Pending Users", + "analytics.pendingRequests": "Pending Requests", + "analytics.pendingRequestsTitle": "Pending Requests", + "analytics.pendingRequestsDescription": "Pending requests are requests that are waiting to be processed by the administrator.", + "analytics.pendingUsersTitle": "Pending Users", + "analytics.pendingUsersDescription": "Pending users are users that are waiting to be approved by the administrator.", + "analytics.latestAdUpdate": "Latest Ad Update", + "analytics.latestView": "Latest View", + "analytics.latestClick": "Latest Click", + "analytics.latestConversion": "Latest Conversion", + "analytics.history": "History", + "analytics.views": "Views", + "common.clicks": "Clicks", + "analytics.active": "Active", + "analytics.status": "Status", + "analytics.conversionPrice": "Conversion Price", + "analytics.storeName": "Store Name", + "analytics.comparedToLastMonth": "Compared to last month", + "analytics.comparedToLastYear": "Compared to last year", + "analytics.comparedToPreviousMonth": "Compared to previous month", + "analytics.comparedToPreviousYear": "Compared to previous year", + "analytics.comparedToNextMonth": "Compared to next month", + "analytics.comparedToNextYear": "Compared to next year", + "common.picture": "Picture", + "common.details": "Details", + "common.addItem": "Add Item", + "common.adText": "Ad Text", + "common.product": "Product", + "common.username": "Username", + "common.store": "Store", + "common.deliveryAddress": "Delivery Address", + "common.deliveryStatus": "Delivery Status", + "common.deliveryDate": "Delivery Date", + "common.deliveryTime": "Delivery Time", + "common.deliveryPrice": "Delivery Price", + "common.active": "Active", + "common.inactive": "Inactive", + "common.saveAd": "Save Ad", + "common.viewPrice": "View Price", + "common.clickPrice": "Click Price", + "common.conversionPrice": "Conversion Price", + "common.storeName": "Store Name", + "common.storeId": "Store ID", + "common.displayingPage": "Displaying Page", + "common.tax": "Tax", + "common.totalMonthlyIncome": "Total Monthly Income", + "common.taxedMonthlyIncome": "Taxed Monthly Income", + "common.addProduct": "Add Product", + "common.products": "Products", + "common.tickets": "Tickets", + "common.searchTickets": "Search tickets...", + "common.noTicketsFound": "No tickets found.", + "common.ticket": "Ticket", + "common.views": "Views", + "common.productCategory": "Product Category", + "common.storeCategory": "Store Category", + "common.orderNumber": "Order Number", + "common.created": "Created" +} \ No newline at end of file diff --git a/src/components/AdCard.jsx b/src/components/AdCard.jsx index 60caba4..bef9601 100644 --- a/src/components/AdCard.jsx +++ b/src/components/AdCard.jsx @@ -25,7 +25,7 @@ import EditAdModal from './EditAdModal'; import { apiFetchApprovedUsersAsync } from '../api/api'; const baseApiUrl = import.meta.env.VITE_API_BASE_URL; import defaultAdImage from '@images/bazaarAd.jpg'; - +import { useTranslation } from 'react-i18next'; const IconStat = ({ icon, value, label, bg }) => ( { const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false); const [sellers, setSellers] = useState([]); + const { t } = useTranslation(); useEffect(() => { const fetchUsers = async () => { @@ -219,7 +220,7 @@ const AdCard = ({ ad, stores, onDelete, onEdit, onViewDetails }) => { } value={ad.views} - label='Views' + label={t('common.views')} bg='#0284c7' /> @@ -227,7 +228,7 @@ const AdCard = ({ ad, stores, onDelete, onEdit, onViewDetails }) => { } value={ad.clicks} - label='Clicks' + label={t('common.clicks')} bg='#0d9488' /> @@ -235,7 +236,7 @@ const AdCard = ({ ad, stores, onDelete, onEdit, onViewDetails }) => { } value={dateRange} - label='Active' + label={t('common.active')} bg='#8b5cf6' /> @@ -262,7 +263,7 @@ const AdCard = ({ ad, stores, onDelete, onEdit, onViewDetails }) => { color='text.secondary' sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }} > - Click Price:{' '} + {t('common.clickPrice')}:{' '} {ad.clickPrice ?? 'Mock'} { color='text.secondary' sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }} > - View Price:{' '} + {t('common.viewPrice')}:{' '} {ad.viewPrice ?? 'Mock'} { color='text.secondary' sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }} > - Conversion Price:{' '} + {t('common.conversionPrice')}:{' '} {ad.conversionPrice ?? 'Mock'} @@ -292,8 +293,8 @@ const AdCard = ({ ad, stores, onDelete, onEdit, onViewDetails }) => { ) } - value={ad.isActive ? 'Active' : 'Inactive'} - label='Status' + value={ad.isActive ? t('common.active') : t('common.inactive')} + label={t('common.status')} bg={ad.isActive ? '#22c55e' : '#f87171'} /> diff --git a/src/components/AdRealtimeMonitor.jsx b/src/components/AdRealtimeMonitor.jsx index 957d036..dfe7e29 100644 --- a/src/components/AdRealtimeMonitor.jsx +++ b/src/components/AdRealtimeMonitor.jsx @@ -1,7 +1,7 @@ // AdRealtimeMonitor.jsx import React from 'react'; import { useAdSignalR } from '../hooks/useAdSignalR'; // putanja do custom hooka - +import { useTranslation } from 'react-i18next'; export default function AdRealtimeMonitor() { const { connectionStatus, @@ -12,24 +12,26 @@ export default function AdRealtimeMonitor() { adUpdatesHistory, } = useAdSignalR(); + const { t } = useTranslation(); + return (
Status: {connectionStatus}
- Latest Ad Update:{' '} + {t('common.latestAdUpdate')}:{' '} {latestAdUpdate ? JSON.stringify(latestAdUpdate) : 'None'}
- Latest Click: {latestClickTime} + {t('common.latestClick')}: {latestClickTime}
- Latest View: {latestViewTime} + {t('common.latestView')}: {latestViewTime}
- Latest Conversion: {latestConversionTime} + {t('common.latestConversion')}: {latestConversionTime}
- History: + {t('common.history')}:
    {adUpdatesHistory.map((item, idx) => (
  • diff --git a/src/components/AddAdModal.jsx b/src/components/AddAdModal.jsx index df9f445..0158760 100644 --- a/src/components/AddAdModal.jsx +++ b/src/components/AddAdModal.jsx @@ -16,6 +16,7 @@ import { apiFetchApprovedUsersAsync, apiCreateAdAsync, } from '@api/api'; +import { useTranslation } from 'react-i18next'; const triggerArrayToBitmask = (arr) => { const triggerMap = { @@ -27,6 +28,7 @@ const triggerArrayToBitmask = (arr) => { }; const AddAdModal = ({ open, onClose, onAddAd }) => { + const { t } = useTranslation(); const [formData, setFormData] = useState({ sellerId: '', Views: 0, @@ -168,7 +170,7 @@ const AddAdModal = ({ open, onClose, onAddAd }) => { - Create Ad + {t('common.createAd')} @@ -178,7 +180,7 @@ const AddAdModal = ({ open, onClose, onAddAd }) => { select size="small" name="sellerId" - label="Seller" + label={t('common.seller')} value={formData.sellerId} onChange={handleChange} error={!!formErrors.sellerId} @@ -213,7 +215,7 @@ const AddAdModal = ({ open, onClose, onAddAd }) => { { { { { { renderValue: (selected) => selected.join(', '), }} name="Triggers" - label="Triggers" + label={t('common.triggers')} value={Array.isArray(formData.Triggers) ? formData.Triggers : []} onChange={(e) => { const { value } = e.target; @@ -342,7 +344,7 @@ const AddAdModal = ({ open, onClose, onAddAd }) => { borderRadius: 2, }} > - Add Item + {t('common.addItem')} {formData.AdData.map((item, index) => ( { sx={{ p: 1, border: '1px solid #ddd', borderRadius: 2, mb: 1 }} > - Ad Text: {item.Description} + {t('common.adText')}: {item.Description} - Store: {item.StoreLink} + {t('common.store')}: {item.StoreLink} - Product: {item.ProductLink} + {t('common.product')}: {item.ProductLink} ))} @@ -373,7 +375,7 @@ const AddAdModal = ({ open, onClose, onAddAd }) => { px: 3, }} > - Cancel + {t('common.cancel')} diff --git a/src/components/AddStoreModal.jsx b/src/components/AddStoreModal.jsx index 3b34323..593fbba 100644 --- a/src/components/AddStoreModal.jsx +++ b/src/components/AddStoreModal.jsx @@ -9,8 +9,11 @@ import { } from '@mui/material'; import StoreMallDirectoryIcon from '@mui/icons-material/StoreMallDirectory'; import { apiGetStoreCategoriesAsync, apiFetchGeographyAsync } from '@api/api'; +import { useTranslation } from 'react-i18next'; + const AddStoreModal = ({ open, onClose, onAddStore }) => { + const { t } = useTranslation(); const [formData, setFormData] = useState({ name: '', address: '', @@ -63,13 +66,13 @@ const AddStoreModal = ({ open, onClose, onAddStore }) => { - Add New Store + {t('common.addNewStore')} { { { { { px: 3, }} > - Cancel + {t('common.cancel')} diff --git a/src/components/AdvertisementDetailsModal.jsx b/src/components/AdvertisementDetailsModal.jsx index 3002afb..7f619df 100644 --- a/src/components/AdvertisementDetailsModal.jsx +++ b/src/components/AdvertisementDetailsModal.jsx @@ -9,8 +9,10 @@ import AdContentCard from '@components/AdContentCard'; import HorizontalScroll from './HorizontalScroll'; import { apiGetAllStoresAsync, apiGetStoreProductsAsync } from '@api/api'; import { useAdSignalR } from '@hooks/useAdSignalR'; +import { useTranslation } from 'react-i18next'; const AdvertisementDetailsModal = ({ open, onClose, ad, onSave, onDelete }) => { + const { t } = useTranslation(); const [isEditing, setIsEditing] = useState(false); const [editedData, setEditedData] = useState({ adData: ad?.adData || [], @@ -181,20 +183,20 @@ const AdvertisementDetailsModal = ({ open, onClose, ad, onSave, onDelete }) => { }} > - Click Price: {adToShow.clickPrice ?? '1000'} + {t('common.clickPrice')}: {adToShow.clickPrice ?? '1000'} - View Price: {adToShow.viewPrice ?? '1000'} + {t('common.viewPrice')}: {adToShow.viewPrice ?? '1000'} - Conversion Price: {adToShow.conversionPrice ?? '1000'} + {t('common.conversionPrice')}: {adToShow.conversionPrice ?? '1000'} {/* Content Section */} - Advertisement Content + {t('common.advertisementContent')} {adToShow.adData.map((item, index) => ( diff --git a/src/components/CategoryCard.jsx b/src/components/CategoryCard.jsx index 75e1dbd..2ed9185 100644 --- a/src/components/CategoryCard.jsx +++ b/src/components/CategoryCard.jsx @@ -10,11 +10,13 @@ import { import CategoryIcon from "@mui/icons-material/Category"; import { FiEdit2, FiTrash } from "react-icons/fi"; import ConfirmDeleteModal from "@components/ConfirmDeleteModal"; +import { useTranslation } from 'react-i18next'; const CategoryCard = ({ category, onUpdateCategory, onDeleteCategory }) => { const [openDeleteModal, setOpenDeleteModal] = useState(false); const [isEditing, setIsEditing] = useState(false); const [editedName, setEditedName] = useState(category.name); + const { t } = useTranslation(); const handleEditToggle = () => setIsEditing(true); @@ -123,7 +125,7 @@ const CategoryCard = ({ category, onUpdateCategory, onDeleteCategory }) => { {/* Label */} { + const { t } = useTranslation(); return ( { }} > - Product Categories + {t('common.productCategories')} { }} > - Store Categories + {t('common.storeCategories')} diff --git a/src/components/DealsChart.jsx b/src/components/DealsChart.jsx index 6b03d7b..78895c2 100644 --- a/src/components/DealsChart.jsx +++ b/src/components/DealsChart.jsx @@ -21,6 +21,8 @@ import { Legend, } from 'chart.js'; import { apiGetAllStoresAsync, apiGetAllAdsAsync } from '../api/api.js'; // From develop +import { useTranslation } from 'react-i18next'; + ChartJS.register( CategoryScale, @@ -32,6 +34,7 @@ ChartJS.register( ); function DealsChart() { + const { t } = useTranslation(); const [filterType, setFilterType] = useState('topRated'); // 'topRated' or 'lowestRated' const [anchorEl, setAnchorEl] = useState(null); const [storesData, setStoresData] = useState({ @@ -328,7 +331,7 @@ function DealsChart() { {/* Text at the bottom - "by store" from develop */} - Deals amount + {t('analytics.dealsAmount')} { const isPositive = percentageChange >= 0; - + const { t } = useTranslation(); return ( { > {isPositive ? : } - {Math.abs(Number(percentageChange)).toFixed(2)}% Compared to last - month{' '} + {Math.abs(Number(percentageChange)).toFixed(2)}% {t('analytics.comparedToLastMonth')} diff --git a/src/components/OrdersTable.jsx b/src/components/OrdersTable.jsx index 0945b00..e4eea7e 100644 --- a/src/components/OrdersTable.jsx +++ b/src/components/OrdersTable.jsx @@ -16,6 +16,7 @@ import { FaTrash } from 'react-icons/fa6'; import CircleIcon from '@mui/icons-material/FiberManualRecord'; import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { useTranslation } from 'react-i18next'; const getStatusColor = (status) => { switch (status) { @@ -52,16 +53,16 @@ const OrdersTable = ({ }; const formatOrderId = (id) => `#${String(id).padStart(5, '0')}`; - + const { t } = useTranslation(); const columns = [ - { label: 'Order #', field: 'id' }, - { label: 'Buyer', field: 'buyerName' }, - { label: 'Store', field: 'storeName' }, - { label: 'Delivery Address', field: 'deliveryAddress' }, // NOVA KOLONA - { label: 'Store Address', field: 'storeAddress' }, // NOVA KOLONA - { label: 'Status', field: 'status' }, - { label: 'Total', field: 'totalPrice' }, - { label: 'Created', field: 'createdAt' }, + { label: t('common.orderNumber'), field: 'id' }, + { label: t('common.buyer'), field: 'buyerName' }, + { label: t('common.store'), field: 'storeName' }, + { label: t('common.deliveryAddress'), field: 'deliveryAddress' }, // NOVA KOLONA + { label: t('common.storeAddress'), field: 'storeAddress' }, // NOVA KOLONA + { label: t('common.status'), field: 'status' }, + { label: t('common.total'), field: 'totalPrice' }, + { label: t('common.created'), field: 'createdAt' }, { label: '', field: 'actions' }, ]; diff --git a/src/components/ParetoChart.jsx b/src/components/ParetoChart.jsx index 9b1d0ec..623ac26 100644 --- a/src/components/ParetoChart.jsx +++ b/src/components/ParetoChart.jsx @@ -15,6 +15,7 @@ import { Box, Typography } from '@mui/material'; import { apiGetAllAdsAsync } from '../api/api.js'; import { format, parseISO } from 'date-fns'; import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; +import { useTranslation } from 'react-i18next'; const baseUrl = import.meta.env.VITE_API_BASE_URL || ''; const HUB_ENDPOINT_PATH = '/Hubs/AdvertisementHub'; @@ -38,7 +39,8 @@ function groupByMonth(ads) { const ParetoChart = () => { const [data, setData] = useState([]); const [ads, setAds] = useState([]); - const connectionRef = useRef(null); + const connectionRef = useRef(null); + const { t } = useTranslation(); useEffect(() => { const fetchData = async () => { @@ -137,7 +139,7 @@ const ParetoChart = () => { marginBottom: '20px', }} > - Pareto Chart + {t('analytics.paretoChart')} { + const { t } = useTranslation(); const [orderBy, setOrderBy] = useState("submitDate"); const [order, setOrder] = useState("desc"); @@ -91,14 +92,14 @@ const PendingUsersTable = ({ # - Picture + {t('common.picture')} handleRequestSort("name")} > - Name + {t('common.name')} @@ -107,7 +108,7 @@ const PendingUsersTable = ({ direction={order} onClick={() => handleRequestSort("email")} > - Email + {t('common.email')} @@ -116,10 +117,10 @@ const PendingUsersTable = ({ direction={order} onClick={() => handleRequestSort("role")} > - Role + {t('common.role')} - Actions + {t('common.actions')} @@ -209,7 +210,7 @@ const PendingUsersTable = ({ {users.length === 0 && ( - No pending users found. + {t('common.noPendingUsersFound')} )} diff --git a/src/components/RevenueMetrics.jsx b/src/components/RevenueMetrics.jsx index e5418a3..154a115 100644 --- a/src/components/RevenueMetrics.jsx +++ b/src/components/RevenueMetrics.jsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'; import MetricCard from './MetricCard'; import { apiFetchAdsWithProfitAsync } from '@api/api'; + const formatCurrency = (value, currency = 'USD') => new Intl.NumberFormat('en-US', { style: 'currency', @@ -33,9 +34,8 @@ const groupByDay = (ads, eventType) => { }; const RevenueMetrics = () => { - const { t } = useTranslation(); const [ads, setAds] = useState([]); - + const { t } = useTranslation(); useEffect(() => { const fetchData = async () => { const adsData = await apiFetchAdsWithProfitAsync(); @@ -150,19 +150,19 @@ const RevenueMetrics = () => { series={[ { data: clickRevenueByDay, - label: 'Click Revenue', + label: t('analytics.clickRevenue'), color: '#3B82F6', showMark: false, }, { data: viewRevenueByDay, - label: 'View Revenue', + label: t('analytics.viewRevenue'), color: '#0D9488', showMark: false, }, { data: conversionRevenueByDay, - label: 'Conversion Revenue', + label: t('analytics.conversionRevenue'), color: '#10B981', showMark: false, }, diff --git a/src/components/RouteCard.jsx b/src/components/RouteCard.jsx index da103fb..09818df 100644 --- a/src/components/RouteCard.jsx +++ b/src/components/RouteCard.jsx @@ -3,8 +3,9 @@ import { Box, Typography, Button } from '@mui/material'; import mapa from '@images/routing-pointa-ppointb.png'; import DeleteConfirmationModal from './DeleteRouteConfirmation'; import RouteDetailsModal from './RouteDetailsModal'; +import { useTranslation } from 'react-i18next'; const RouteCard = ({route, onViewDetails, onDelete, googleMapsApiKey}) => { - + const { t } = useTranslation(); const [deleteOpen, setDeleteOpen] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false); @@ -71,7 +72,7 @@ const RouteCard = ({route, onViewDetails, onDelete, googleMapsApiKey}) => { onClick={() => setDetailsOpen(true)} sx={{ flex: 1 }} > - Details + {t('common.details')} { + const { t } = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); const [menuAnchor, setMenuAnchor] = useState(null); const [storeData, setStoreData] = useState(store); @@ -290,7 +293,7 @@ const StoreCard = ({ store }) => { fontSize: '0.85rem', }} > - Tax: {(storeData.tax*100).toFixed(2)} + {t('common.tax')}: {(storeData.tax*100).toFixed(2)} { fontSize: '0.85rem', }} > - Total monthly income: {revenue.totalIncome} + {t('common.totalMonthlyIncome')}: {revenue.totalIncome} { fontSize: '0.85rem', }} > - Taxed monthly income: {revenue.taxedIncome} + {t('common.taxedMonthlyIncome')}: {revenue.taxedIncome} {/* Buttons */} @@ -343,7 +346,7 @@ const StoreCard = ({ store }) => { }, }} > - Add Product + {t('common.addProduct')} { + const { t } = useTranslation(); const [page, setPage] = useState(0); const rowsPerPage = 5; const [orderBy, setOrderBy] = useState('storeRevenue'); @@ -30,14 +32,16 @@ const StoreEarningsTable = ({ data }) => { - Store Name + + {t('common.storeName')} + handleSort('storeRevenue')} > - Store Revenue + {t('analytics.storeRevenue')} @@ -46,7 +50,7 @@ const StoreEarningsTable = ({ data }) => { direction={order} onClick={() => handleSort('adminProfit')} > - Admin Profit + {t('analytics.adminProfit')} @@ -55,7 +59,7 @@ const StoreEarningsTable = ({ data }) => { direction={order} onClick={() => handleSort('taxRate')} > - Tax Rate (%) + {t('analytics.taxRate')} diff --git a/src/components/StoreProductsList.jsx b/src/components/StoreProductsList.jsx index ccd88e3..5a967fa 100644 --- a/src/components/StoreProductsList.jsx +++ b/src/components/StoreProductsList.jsx @@ -9,12 +9,14 @@ import { } from '@api/api'; import EditProductModal from './EditProductModal'; import ProductDetailsModal from './ProductDetailsModal'; +import { useTranslation } from 'react-i18next'; const StoreProductsList = ({ storeId }) => { const [products, setProducts] = useState([]); const [openEditModal, setOpenEditModal] = useState(false); const [openDetailsModal, setOpenDetailsModal] = useState(false); const [selectedProduct, setSelectedProduct] = useState(null); + const { t } = useTranslation(); useEffect(() => { const fetchProducts = async () => { @@ -95,7 +97,7 @@ const StoreProductsList = ({ storeId }) => { return ( - Products + {t('common.products')} { const ticket = tickets.find((t) => t.id === ticketId); if (ticket.status === 'Requested') { @@ -77,10 +78,10 @@ export default function TicketListSection({ }} > - Tickets + {t('common.tickets')} {filteredTickets.length === 0 ? ( - No tickets found. + {t('common.noTicketsFound')} ) : ( filteredTickets.map((ticket) => ( diff --git a/src/components/UserList.jsx b/src/components/UserList.jsx index 96c1da1..4f50278 100644 --- a/src/components/UserList.jsx +++ b/src/components/UserList.jsx @@ -22,6 +22,7 @@ import DeleteUserButton from './DeleteUserButton'; import { FiEdit2 } from 'react-icons/fi'; import { FaUser, FaUserSlash } from 'react-icons/fa'; import { MdDone } from 'react-icons/md'; +import { useTranslation } from 'react-i18next'; const getStatus = (user) => { if (user.isApproved === true) return 'Approved'; @@ -82,7 +83,7 @@ export default function UserList({ const [order, setOrder] = useState('asc'); const [editingUserId, setEditingUserId] = useState(null); const [editedUser, setEditedUser] = useState({}); - + const { t } = useTranslation(); const handleSort = (field) => { const isAsc = orderBy === field && order === 'asc'; setOrder(isAsc ? 'desc' : 'asc'); @@ -134,14 +135,14 @@ export default function UserList({ # - Pic + {t('common.picture')} handleSort('userName')} > - Username + {t('common.username')} @@ -150,7 +151,7 @@ export default function UserList({ direction={order} onClick={() => handleSort('email')} > - Email + {t('common.email')} @@ -159,7 +160,7 @@ export default function UserList({ direction={order} onClick={() => handleSort('role')} > - Role + {t('common.role')} @@ -168,7 +169,7 @@ export default function UserList({ direction={order} onClick={() => handleSort('isActive')} > - Active + {t('common.active')} {/* @@ -189,7 +190,7 @@ export default function UserList({ Status */} - Actions + {t('common.actions')} @@ -246,8 +247,8 @@ export default function UserList({ onChange={handleFieldChange} variant='standard' > - Buyer - Seller + {t('common.buyer')} + {t('common.seller')} ) : ( user.roles[0] @@ -267,8 +268,8 @@ export default function UserList({ } variant='standard' > - Online - Offline + {t('common.online')} + {t('common.offline')} ) : ( diff --git a/src/components/UserManagementPagination.jsx b/src/components/UserManagementPagination.jsx index 61bbdeb..2f68827 100644 --- a/src/components/UserManagementPagination.jsx +++ b/src/components/UserManagementPagination.jsx @@ -2,6 +2,7 @@ import React from "react"; import { Box, Typography, IconButton, Button } from "@mui/material"; import NavigateBeforeIcon from "@mui/icons-material/NavigateBefore"; import NavigateNextIcon from "@mui/icons-material/NavigateNext"; +import { useTranslation } from 'react-i18next'; const UserManagementPagination = ({ currentPage, @@ -21,6 +22,8 @@ const UserManagementPagination = ({ return pages; }; + const { t } = useTranslation(); + return ( - displaying page + {t('common.displayingPage')} @@ -45,7 +48,7 @@ const UserManagementPagination = ({ onClick={() => onPageChange(1)} disabled={currentPage === 1} > - First + {t('common.first')} onPageChange(totalPages)} disabled={currentPage === totalPages} > - Last + {t('common.last')} diff --git a/src/pages/AnalyticsPage.jsx b/src/pages/AnalyticsPage.jsx index 4c33dd1..e71ec91 100644 --- a/src/pages/AnalyticsPage.jsx +++ b/src/pages/AnalyticsPage.jsx @@ -548,7 +548,7 @@ const AnalyticsPage = () => { lineHeight: 1.2, }} > - Dashboard Analytics{' '} + {t('analytics.dashboardAnalytics')}