From 5664f818b39942ce6508f79de5338adaf6f595a9 Mon Sep 17 00:00:00 2001 From: Antolius Date: Sat, 17 Dec 2022 00:07:49 +0000 Subject: [PATCH 01/11] Set up i18next and extract translations from Login & Home Set up internationalization using i18next library and its integration with React. Extract translation strings from Login and Home pages. --- package-lock.json | 236 ++++++++++++++++++++++++++++++++-- package.json | 4 + public/locales/en/common.json | 26 ++++ public/locales/en/login.json | 6 + src/components/Header.js | 6 +- src/components/ModalLogout.js | 12 +- src/components/Sidebar.js | 32 ++--- src/i18n.js | 14 ++ src/index.js | 5 +- src/pages/Home.js | 32 ++--- src/pages/Login.js | 12 +- 11 files changed, 328 insertions(+), 57 deletions(-) create mode 100644 public/locales/en/common.json create mode 100644 public/locales/en/login.json create mode 100644 src/i18n.js diff --git a/package-lock.json b/package-lock.json index c53d535..af2db25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,12 @@ "electron-is-dev": "^2.0.0", "electron-packager": "^15.5.1", "file-saver": "^2.0.5", + "i18next": "^22.4.5", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-http-backend": "^2.1.0", "react": "^18.1.0", "react-dom": "^18.1.0", + "react-i18next": "^12.1.1", "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", @@ -1803,11 +1807,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", + "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", "dependencies": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.13.11" }, "engines": { "node": ">=6.9.0" @@ -5444,6 +5448,14 @@ "yarn": ">=1" } }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dependencies": { + "node-fetch": "2.6.7" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8542,6 +8554,14 @@ "node": ">= 12" } }, + "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==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-webpack-plugin": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", @@ -8681,6 +8701,44 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "22.4.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.5.tgz", + "integrity": "sha512-Kc+Ow0guRetUq+kv02tj0Yof9zveROPBAmJ8UxxNODLVBRSwsM4iD0Gw3BEieOmkWemF6clU3K1fbnCuTqiN2Q==", + "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" + } + ], + "dependencies": { + "@babel/runtime": "^7.20.6" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz", + "integrity": "sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==", + "dependencies": { + "@babel/runtime": "^7.19.4" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.1.0.tgz", + "integrity": "sha512-rTVhhFrpnZJnNvCCdC6RjhFPk0S6mJ2VAix93vbDD19ixlrSJtoNqkk49wvR10PImBSsuGJf35gMQwn2mjer6A==", + "dependencies": { + "cross-fetch": "3.1.5" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -10808,6 +10866,44 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -12935,6 +13031,27 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-i18next": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz", + "integrity": "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -13271,9 +13388,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regenerator-transform": { "version": "0.15.0", @@ -15219,6 +15336,14 @@ "node": ">= 0.8" } }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -17370,11 +17495,11 @@ } }, "@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", + "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", "requires": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.13.11" } }, "@babel/runtime-corejs3": { @@ -20060,6 +20185,14 @@ "cross-spawn": "^7.0.1" } }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "requires": { + "node-fetch": "2.6.7" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -22335,6 +22468,14 @@ } } }, + "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==", + "requires": { + "void-elements": "3.1.0" + } + }, "html-webpack-plugin": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", @@ -22431,6 +22572,30 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, + "i18next": { + "version": "22.4.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.5.tgz", + "integrity": "sha512-Kc+Ow0guRetUq+kv02tj0Yof9zveROPBAmJ8UxxNODLVBRSwsM4iD0Gw3BEieOmkWemF6clU3K1fbnCuTqiN2Q==", + "requires": { + "@babel/runtime": "^7.20.6" + } + }, + "i18next-browser-languagedetector": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz", + "integrity": "sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==", + "requires": { + "@babel/runtime": "^7.19.4" + } + }, + "i18next-http-backend": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.1.0.tgz", + "integrity": "sha512-rTVhhFrpnZJnNvCCdC6RjhFPk0S6mJ2VAix93vbDD19ixlrSJtoNqkk49wvR10PImBSsuGJf35gMQwn2mjer6A==", + "requires": { + "cross-fetch": "3.1.5" + } + }, "iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -24004,6 +24169,35 @@ "tslib": "^2.0.3" } }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } + }, "node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -25411,6 +25605,15 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-i18next": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz", + "integrity": "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==", + "requires": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -25669,9 +25872,9 @@ } }, "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regenerator-transform": { "version": "0.15.0", @@ -27143,6 +27346,11 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "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==" + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index e9fc6f1..2663b46 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,12 @@ "electron-is-dev": "^2.0.0", "electron-packager": "^15.5.1", "file-saver": "^2.0.5", + "i18next": "^22.4.5", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-http-backend": "^2.1.0", "react": "^18.1.0", "react-dom": "^18.1.0", + "react-i18next": "^12.1.1", "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000..0f59310 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,26 @@ +{ + "appName": "BudLib", + "profile": "Profile", + "logout": "Logout", + "cancel": "Cancel", + "admin": "Admin", + "books": "Books", + "loaners": "Loaners", + "heading": "Home", + "transact": "Transact", + "borrowBooks": "Borrow books", + "returnBooks": "Return books", + "extendBooks": "Extend books", + "pastTransactions": "Past transactions", + "database": "Database", + "searchBooks": "Search books", + "addBook": "Add a book", + "searchLoaners": "Search loaners", + "addLoaner": "Add a loaner", + "dashboard": "Dashboard", + "dashboardHome": "Dashboard home", + "librarianManagement": "Librarian management", + "expiredSession": "Your session has expired. Please login again.", + "alert": "Alert", + "confirmLogout": "Are you sure you want to logout?" +} \ No newline at end of file diff --git a/public/locales/en/login.json b/public/locales/en/login.json new file mode 100644 index 0000000..60f340c --- /dev/null +++ b/public/locales/en/login.json @@ -0,0 +1,6 @@ +{ + "header": "Login to BudLib", + "emailPlaceholder": "Enter Email Address...", + "passwordPlaceholder": "Password", + "loginButton": "Login" +} \ No newline at end of file diff --git a/src/components/Header.js b/src/components/Header.js index a77f3bb..bf9de56 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -1,8 +1,10 @@ import React from 'react'; import { Link } from 'react-router-dom'; import profilePic from '../assets/img/undraw_profile_3.svg'; +import { useTranslation } from 'react-i18next'; const Header = (props) => { + const { t } = useTranslation('common'); let loggedName = window.localStorage.getItem('username'); let loggedId = window.localStorage.getItem('id'); let loggedRole = window.localStorage.getItem('role'); @@ -45,7 +47,7 @@ const Header = (props) => { - Profile + {t('profile')}
@@ -54,7 +56,7 @@ const Header = (props) => { )} diff --git a/src/components/ModalLogout.js b/src/components/ModalLogout.js index 62aebab..c351046 100644 --- a/src/components/ModalLogout.js +++ b/src/components/ModalLogout.js @@ -1,8 +1,10 @@ import React, { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../helpers/useAuth'; +import { useTranslation } from 'react-i18next'; const ModalLogout = () => { + const { t } = useTranslation('common'); const { logout } = useAuth(); const navigate = useNavigate(); @@ -25,7 +27,7 @@ const ModalLogout = () => { let now = new Date(); if (now > tokenExpiry) { - window.alert('Your session has expired. Please login again.'); + window.alert(t('expiredSession')); handleLogout(); } }, []); @@ -36,19 +38,19 @@ const ModalLogout = () => {
- Alert + {t('alert')}
-
Are you sure you want to logout?
+
{t('confirmLogout')}
diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js index a2dd8d7..7f882d3 100644 --- a/src/components/Sidebar.js +++ b/src/components/Sidebar.js @@ -1,7 +1,9 @@ import React from 'react'; import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; const Sidebar = () => { + const { t } = useTranslation('common'); const loggedRole = window.localStorage.getItem('role'); function handleToggle() { @@ -15,45 +17,45 @@ const Sidebar = () => {
-
BudLib
+
{t('appName')}

{loggedRole === 'ADMIN' ? ( -
Transact
+
{t('transact')}
  • - Borrow books + {t('borrowBooks')}
  • - Return books + {t('returnBooks')}
  • - Provide extension + {t('extendBooks')}
  • - Past transactions + {t('pastTransactions')}

  • -
    Database
    +
    {t('database')}
    ) : ( '' @@ -61,17 +63,17 @@ const Sidebar = () => {
  • - Books + {t('books')}
    - Search books + {t('searchBooks')} {loggedRole === 'ADMIN' ? ( - Add a book + {t('addBook')} ) : ( @@ -86,15 +88,15 @@ const Sidebar = () => {
  • - Loaners + {t('loaners')}
    - Search loaners + {t('searchLoaners')} - Add a loaner + {t('addLoaner')}
    @@ -102,12 +104,12 @@ const Sidebar = () => {
    -
    Admin
    +
    {t('admin')}
  • - Dashboard + {t('dashboard')}
  • diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..39768c7 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,14 @@ +import React from 'react'; +import i18n from 'i18next'; +import { useTranslation, initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import Backend from 'i18next-http-backend'; + +i18n + .use(initReactI18next) + .use(LanguageDetector) + .use(Backend) + .init({ + debug: true, + fallbackLang: 'en' + }); \ No newline at end of file diff --git a/src/index.js b/src/index.js index 2b5bb3e..46a1cd3 100644 --- a/src/index.js +++ b/src/index.js @@ -6,12 +6,15 @@ import './assets/sb-admin/theme.css'; import './assets/sb-admin/custom.css'; import './assets/sb-admin/theme.js'; import './assets/datatables/dataTables.bootstrap4.min.css'; +import './i18n'; const container = document.getElementById('root'); const root = createRoot(container); root.render( - + + + ); diff --git a/src/pages/Home.js b/src/pages/Home.js index 119e17f..35d14b0 100644 --- a/src/pages/Home.js +++ b/src/pages/Home.js @@ -5,8 +5,10 @@ import MenuCard from '../components/MenuCard'; import ModalLogout from '../components/ModalLogout'; import ScrollTop from '../components/ScrollTop'; import Sidebar from '../components/Sidebar'; +import { useTranslation } from 'react-i18next'; const Home = () => { + const { t } = useTranslation('common'); const loggedRole = window.localStorage.getItem('role'); return ( @@ -16,7 +18,7 @@ const Home = () => {
    -
    +
    {loggedRole === 'ADMIN' ? ( @@ -29,13 +31,13 @@ const Home = () => { }} className='pt-3' > - Transact + {t('transact')}
    - - - - + + + +

    { }} className='pt-3' > - Database + {t('database')}

    - - - - + + + +

    { }} className='pt-3' > - Dashboard + {t('dashboard')}

    - - + +
    ) : (
    - +
    )} diff --git a/src/pages/Login.js b/src/pages/Login.js index ac10f34..0926257 100644 --- a/src/pages/Login.js +++ b/src/pages/Login.js @@ -1,9 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState, useTransition } from 'react'; import { useNavigate } from 'react-router-dom'; import { postCall } from '../helpers/postCall'; import { useAuth } from '../helpers/useAuth'; +import { useTranslation } from 'react-i18next'; const Login = () => { + const { t } = useTranslation("login"); const navigate = useNavigate(); const { login } = useAuth(); @@ -53,7 +55,7 @@ const Login = () => {
    -

    Login to BudLib

    +

    {t('header')}

    @@ -65,7 +67,7 @@ const Login = () => { maxLength='100' value={message['email']} required - placeholder='Enter Email Address...' + placeholder={t('emailPlaceholder')} onChange={(e) => { setMessage({ ...message, email: e.target.value }); }} @@ -79,14 +81,14 @@ const Login = () => { className='form-control form-control-user' value={message['password']} required - placeholder='Password' + placeholder={t('passwordPlaceholder')} onChange={(e) => { setMessage({ ...message, password: e.target.value }); }} />
    From 3a286f52651679d619e3a1c66447f2777820e4c9 Mon Sep 17 00:00:00 2001 From: Antolius Date: Sun, 18 Dec 2022 22:54:50 +0000 Subject: [PATCH 02/11] Extract translations from SearchBooks page Extract translation strings from SearchBooks page and Footer. Add namespace and interpolation related init config for i18next. --- public/locales/en/common.json | 8 +++++++- public/locales/en/search-books.json | 18 +++++++++++++++++ src/components/BookCard.js | 7 +++++-- src/components/BookCards.js | 5 ++++- src/components/BookSearchBar.js | 30 +++++++++++++++-------------- src/components/Footer.js | 5 ++++- src/components/Header.js | 2 +- src/components/ModalLogout.js | 2 +- src/components/Sidebar.js | 2 +- src/i18n.js | 16 ++++++++++++--- src/pages/Books/SearchBooks.js | 4 +++- src/pages/Home.js | 2 +- 12 files changed, 74 insertions(+), 27 deletions(-) create mode 100644 public/locales/en/search-books.json diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 0f59310..1e4ddcc 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -3,6 +3,7 @@ "profile": "Profile", "logout": "Logout", "cancel": "Cancel", + "clear": "Clear", "admin": "Admin", "books": "Books", "loaners": "Loaners", @@ -22,5 +23,10 @@ "librarianManagement": "Librarian management", "expiredSession": "Your session has expired. Please login again.", "alert": "Alert", - "confirmLogout": "Are you sure you want to logout?" + "confirmLogout": "Are you sure you want to logout?", + "footer": "BudLib OpenSource Library System", + "PARENT_LIBRARY": "Parent Library", + "FACULTY_LIBRARY": "Faculty Library", + "CLASS_SETS": "Class Sets", + "CHILDREN_LIBRARY": "Children Library" } \ No newline at end of file diff --git a/public/locales/en/search-books.json b/public/locales/en/search-books.json new file mode 100644 index 0000000..aae96a2 --- /dev/null +++ b/public/locales/en/search-books.json @@ -0,0 +1,18 @@ +{ + "missingFilter": "Searching requires a filter", + "searchBy": "Search by", + "noFilter": "No Filter", + "librarySection": "Library Section", + "title": "Title", + "author": "Author", + "publisher": "Publisher", + "isbn": "ISBN", + "tags": "Tags", + "language": "Language", + "searchTerm": "Search term", + "searchTermPlaceholder": "Your search term goes here", + "search": "Search", + "notFound": "No books found", + "by": "by {{authors}}", + "section": "Section: {{section}}" +} \ No newline at end of file diff --git a/src/components/BookCard.js b/src/components/BookCard.js index e51532e..06fe295 100644 --- a/src/components/BookCard.js +++ b/src/components/BookCard.js @@ -1,11 +1,14 @@ import React from 'react'; import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; const BookCard = ({ bookId, title, subtitle, authors, publisher, librarySection, imageLink, tags }) => { + const { t } = useTranslation('search-books'); subtitle = subtitle || ''; imageLink = imageLink || `${process.env.PUBLIC_URL + '/images/no_image_book_v2.jpg'}`; librarySection = librarySection || 'NA'; let bookBy = authors || publisher; + let byCount = bookBy?.split(',')?.length ?? 0; let displayTitle = title; @@ -46,7 +49,7 @@ const BookCard = ({ bookId, title, subtitle, authors, publisher, librarySection, WebkitBoxOrient: 'vertical', }} > - by {bookBy} + {t('by', {authors: bookBy, count: byCount})}

    - Section: {librarySection} + {t('section', {section: t(librarySection)})}

    -
    No books found
    +
    {t('notFound')}
    ) : ( diff --git a/src/components/BookSearchBar.js b/src/components/BookSearchBar.js index 486ce33..11dd1c6 100644 --- a/src/components/BookSearchBar.js +++ b/src/components/BookSearchBar.js @@ -1,6 +1,8 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; function BookSearchBar(props) { + const { t } = useTranslation('search-books'); const [filterOption, setfilterOption] = useState(''); const [filterText, setfilterText] = useState(''); @@ -16,7 +18,7 @@ function BookSearchBar(props) { if (filterOption !== '') { props.func([filterOption, filterText]); } else { - window.alert('Searching requires a filter'); + window.alert(t('missingFilter')); } }; @@ -41,18 +43,18 @@ function BookSearchBar(props) {
    - Search by + {t('searchBy')}
    @@ -60,7 +62,7 @@ function BookSearchBar(props) {
    - Search term + {t('searchTerm')}
    handleFilterText(e)} value={filterText} className='form-control' - placeholder='Your search term goes here' + placeholder={t('searchTermPlaceholder')} onKeyDown={(e) => triggerSearchOnEnter(e)} />
    @@ -79,14 +81,14 @@ function BookSearchBar(props) {
    diff --git a/src/components/Footer.js b/src/components/Footer.js index a932b76..f1a18ab 100644 --- a/src/components/Footer.js +++ b/src/components/Footer.js @@ -1,11 +1,14 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; const Footer = () => { + const { t } = useTranslation(); + return (
    - BudLib OpenSource Library System + {t('footer')}
    diff --git a/src/components/Header.js b/src/components/Header.js index bf9de56..dfc3ba8 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -4,7 +4,7 @@ import profilePic from '../assets/img/undraw_profile_3.svg'; import { useTranslation } from 'react-i18next'; const Header = (props) => { - const { t } = useTranslation('common'); + const { t } = useTranslation(); let loggedName = window.localStorage.getItem('username'); let loggedId = window.localStorage.getItem('id'); let loggedRole = window.localStorage.getItem('role'); diff --git a/src/components/ModalLogout.js b/src/components/ModalLogout.js index c351046..3de9559 100644 --- a/src/components/ModalLogout.js +++ b/src/components/ModalLogout.js @@ -4,7 +4,7 @@ import { useAuth } from '../helpers/useAuth'; import { useTranslation } from 'react-i18next'; const ModalLogout = () => { - const { t } = useTranslation('common'); + const { t } = useTranslation(); const { logout } = useAuth(); const navigate = useNavigate(); diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js index 7f882d3..9ab4295 100644 --- a/src/components/Sidebar.js +++ b/src/components/Sidebar.js @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; const Sidebar = () => { - const { t } = useTranslation('common'); + const { t } = useTranslation(); const loggedRole = window.localStorage.getItem('role'); function handleToggle() { diff --git a/src/i18n.js b/src/i18n.js index 39768c7..173704d 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,6 +1,5 @@ -import React from 'react'; import i18n from 'i18next'; -import { useTranslation, initReactI18next } from 'react-i18next'; +import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import Backend from 'i18next-http-backend'; @@ -10,5 +9,16 @@ i18n .use(Backend) .init({ debug: true, - fallbackLang: 'en' + fallbackLang: 'en', + defaultNS: 'common', + fallbackNS: 'common', + ns: [ + 'login', + 'common', + 'search-books' + ], + interpolation: { + // Escaping is handled by React: + escapeValue: false + } }); \ No newline at end of file diff --git a/src/pages/Books/SearchBooks.js b/src/pages/Books/SearchBooks.js index ef55b74..a873f76 100644 --- a/src/pages/Books/SearchBooks.js +++ b/src/pages/Books/SearchBooks.js @@ -6,8 +6,10 @@ import Header from '../../components/Header'; import ModalLogout from '../../components/ModalLogout'; import ScrollTop from '../../components/ScrollTop'; import Sidebar from '../../components/Sidebar'; +import { useTranslation } from 'react-i18next'; function SearchBooks() { + const { t } = useTranslation('search-books'); const [psearchBy, setSearchBy] = useState(['', '']); return ( @@ -17,7 +19,7 @@ function SearchBooks() {
    -
    +
    diff --git a/src/pages/Home.js b/src/pages/Home.js index 35d14b0..a206474 100644 --- a/src/pages/Home.js +++ b/src/pages/Home.js @@ -8,7 +8,7 @@ import Sidebar from '../components/Sidebar'; import { useTranslation } from 'react-i18next'; const Home = () => { - const { t } = useTranslation('common'); + const { t } = useTranslation(); const loggedRole = window.localStorage.getItem('role'); return ( From f9b0c4389e4aa83113c0ab120f368fe38f721145 Mon Sep 17 00:00:00 2001 From: Antolius Date: Sun, 18 Dec 2022 23:30:41 +0000 Subject: [PATCH 03/11] Extract translations from ViewBook page --- public/locales/en/view-book.json | 33 +++++++++++++++++++++++ src/components/ViewBookDetailsCard.js | 38 ++++++++++++++------------- src/components/ViewBookLoansCard.js | 17 +++++++----- src/i18n.js | 3 ++- src/pages/Books/ViewBook.js | 14 +++++++--- 5 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 public/locales/en/view-book.json diff --git a/public/locales/en/view-book.json b/public/locales/en/view-book.json new file mode 100644 index 0000000..8ecf0ca --- /dev/null +++ b/public/locales/en/view-book.json @@ -0,0 +1,33 @@ +{ + "deleteResp": { + "200": "Book deleted successfully", + "400": "{{errorMessage}}", + "unspecific": "Something went wrong." + }, + "edit": "Edit details", + "delete": "Delete Book", + "bookId": "Book ID", + "isbn10": "ISBN-10", + "isbn13": "ISBN-13", + "totalQuantity": "Total quantity", + "availableQuantity": "Available quantity", + "librarySection": "Library Section", + "title": "Title", + "subtitle": "Subtitle", + "authors": "Author(s)", + "publisher": "Publisher", + "edition": "Edition", + "year": "Year", + "language": "Language", + "tags": "Tags", + "retailPrice": "Retail price", + "libraryPrice": "Library price", + "additionalNotes": "Additional notes", + "currentLoans": "Current loans", + "noLoans": "No outstanding loans found", + "loanerId": "Loaner ID", + "fullName": "Full name", + "copies": "Copies", + "borrowDate": "Borrow date", + "dueDate": "Due date" +} \ No newline at end of file diff --git a/src/components/ViewBookDetailsCard.js b/src/components/ViewBookDetailsCard.js index d34a15a..947e7b9 100644 --- a/src/components/ViewBookDetailsCard.js +++ b/src/components/ViewBookDetailsCard.js @@ -1,6 +1,8 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; const ViewBookDetailsCard = ({ data }) => { + const { t } = useTranslation('view-book'); let customImg = data['imageLink']; let defaultImg = customImg || `${process.env.PUBLIC_URL + '/images/no_image_book_v2.jpg'}`; @@ -17,28 +19,28 @@ const ViewBookDetailsCard = ({ data }) => { - + - + - + - + - + - - + +
    Book ID{t('bookId')} {data['bookId']}
    ISBN-10{t('isbn10')} {data['isbn_10'] || '-'}
    ISBN-13{t('isbn13')} {data['isbn_13'] || '-'}
    Total quantity{t('totalQuantity')} {data['totalQuantity']}
    Available quantity{t('availableQuantity')} {data['availableQuantity']}
    Library Section{data['librarySection'] || '-'}{t('librarySection')}{t(data['librarySection']) || '-'}
    @@ -56,35 +58,35 @@ const ViewBookDetailsCard = ({ data }) => { - + - + - + - + - + - + - + - + - + - + - + diff --git a/src/components/ViewBookLoansCard.js b/src/components/ViewBookLoansCard.js index 669f40c..6507424 100644 --- a/src/components/ViewBookLoansCard.js +++ b/src/components/ViewBookLoansCard.js @@ -1,27 +1,30 @@ import React from 'react'; +import { useTranslation } from 'react-i18next'; const ViewBookLoansCard = ({ data }) => { + const { t } = useTranslation('view-book'); + return (
    -
    Current loans
    +
    {t('currentLoans')}
    {data.length === 0 ? ( - 'No outstanding loans found' + t('noLoans') ) : (
    Title{t('title')} {data['title'] || '-'}
    Subtitle{t('subtitle')} {data['subtitle'] || '-'}
    Author(s){t('authors')} {data['authors'] || '-'}
    Publisher{t('publisher')} {data['publisher'] || '-'}
    Edition{t('edition')} {data['edition'] || '-'}
    Year{t('year')} {data['year'] || '-'}
    Language{t('language')} {data['language'] || '-'}
    Tags{t('tags')} {data['tags']?.map((eachTag) => { return eachTag['tagName'] + ', '; @@ -92,15 +94,15 @@ const ViewBookDetailsCard = ({ data }) => {
    Retail price{t('retailPrice')} $ {data['priceRetail'] || '-'}
    Library price{t('libraryPrice')} $ {data['priceLibrary'] || '-'}
    Additional notes{t('additionalNotes')} {data['notes'] || '-'}
    - - - - - + + + + + diff --git a/src/i18n.js b/src/i18n.js index 173704d..bd8acca 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -15,7 +15,8 @@ i18n ns: [ 'login', 'common', - 'search-books' + 'search-books', + 'view-book' ], interpolation: { // Escaping is handled by React: diff --git a/src/pages/Books/ViewBook.js b/src/pages/Books/ViewBook.js index eee6d00..224c330 100644 --- a/src/pages/Books/ViewBook.js +++ b/src/pages/Books/ViewBook.js @@ -9,8 +9,10 @@ import ViewBookDetailsCard from '../../components/ViewBookDetailsCard'; import ViewBookLoansCard from '../../components/ViewBookLoansCard'; import { deleteCall } from '../../helpers/deleteCall'; import { useFetch } from '../../helpers/useFetch'; +import { useTranslation } from 'react-i18next'; const ViewBook = () => { + const { t } = useTranslation('view-book'); const { id } = useParams(); let bookDetailUrl = `/api/books/${id}`; @@ -23,9 +25,13 @@ const ViewBook = () => { function handleDelete(e) { deleteCall(bookDetailUrl).then((result) => { - window.alert(result['data']['message']); + const status = result['status']; + window.alert(t( + [`deleteResp.${status}`, 'deleteResp.unspecific'], + {errorMessage: result['data']['message']} + )); - if (result['status'] === 200) { + if (status === 200) { let path = `/books/search`; navigate(path); } @@ -54,10 +60,10 @@ const ViewBook = () => {
    From 916e852e59272c5cbe6e85f0c5ad8fbdea18342f Mon Sep 17 00:00:00 2001 From: Antolius Date: Mon, 19 Dec 2022 18:07:12 +0000 Subject: [PATCH 04/11] Extract translations from AddBook page --- public/locales/en/add-book.json | 41 +++++++++++++++ src/components/BookAddForm.js | 91 ++++++++++++++++++--------------- src/pages/Books/AddBook.js | 5 +- 3 files changed, 94 insertions(+), 43 deletions(-) create mode 100644 public/locales/en/add-book.json diff --git a/public/locales/en/add-book.json b/public/locales/en/add-book.json new file mode 100644 index 0000000..e37b3d0 --- /dev/null +++ b/public/locales/en/add-book.json @@ -0,0 +1,41 @@ +{ + "enterIsbn": "Enter ISBN before searching!", + "detailsNotFound": "Details not found! Please fill manually.", + "detailsPopulated": "Details populated! You may edit them accordingly.", + "postResp": { + "200": "Book added successfully", + "400": "{{errorMessage}}", + "unspecific": "Something went wrong" + }, + "preFill": "Pre-fill using ISBN", + "isbn10": "ISBN-10", + "isbn13": "ISBN-13", + "totalCopies": "Total copies", + "availableCopies": "Available copies", + "retailPrice": "Retail price", + "retailPlaceholder": "10.99", + "libraryPrice": "Library price", + "libraryPlaceholder": "6.99", + "currencySymbol": "$", + "section": "Section in library", + "image": "Image link", + "title": "Title", + "titlePlaceholder": "The Parent's Dook about Bullying", + "subtitle": "Subtitle", + "subtitlePlaceholder": "Changing the Course of Your Child's Life", + "authors": "Author(s)", + "authorsPlaceholder": "William Voors", + "language": "Language", + "languagePlaceholder": "English", + "publisher": "Publisher", + "publisherPlaceholder": "Hazelden Publishing", + "year": "Year of publication", + "yearPlaceholder": "2000", + "edition": "Edition", + "editionPlaceholder": "1", + "tags": "Tags (comma separated)", + "tagsPlaceholder": "family, relationships", + "notes": "Additional notes", + "notesPlaceholder": "Additional information", + "add": "Add book" +} \ No newline at end of file diff --git a/src/components/BookAddForm.js b/src/components/BookAddForm.js index 2435e73..dc9059b 100644 --- a/src/components/BookAddForm.js +++ b/src/components/BookAddForm.js @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { postCall } from '../helpers/postCall'; +import { useTranslation } from 'react-i18next'; function get_book_details(isbn_number) { let isbn_number_formatted = isbn_number.trim().replaceAll('-', ''); @@ -14,6 +15,8 @@ function get_book_details(isbn_number) { } const BookAddForm = () => { + const { t } = useTranslation('add-book'); + let navigate = useNavigate(); let defaultImg = `${process.env.PUBLIC_URL + '/images/no_image_book_v2.jpg'}`; @@ -45,7 +48,7 @@ const BookAddForm = () => { let isbnSearchInput = document.getElementById('formIsbn'); if (isbnSearchInput.value === '') { - messageSpan.innerHTML = 'Enter ISBN before searching!'; + messageSpan.innerHTML = t('enterIsbn'); messageSpan.setAttribute('display', 'inline'); messageSpan.className = 'px-4 text-danger'; } else { @@ -54,11 +57,11 @@ const BookAddForm = () => { const fetchedDetails = await get_book_details(isbnSearchInput.value); if (fetchedDetails['totalItems'] === 0) { - messageSpan.innerHTML = 'Details not found! Please fill manually.'; + messageSpan.innerHTML = t('detailsNotFound'); messageSpan.setAttribute('display', 'inline'); messageSpan.className = 'px-4 text-danger'; } else { - messageSpan.innerHTML = 'Details populated! You may edit them accordingly.'; + messageSpan.innerHTML = t('detailsPopulated'); messageSpan.setAttribute('display', 'inline'); messageSpan.className = 'px-4 text-success'; @@ -131,9 +134,13 @@ const BookAddForm = () => { // } postCall('/api/books', sendDetails).then((result) => { - window.alert(result['data']['message']); + const status = result['status']; + window.alert(t( + [`postResp.${status}`, 'postResp.unspecific'], + {errorMessage: result['data']['message']} + )); - if (result['status'] === 200) { + if (status === 200) { window.location.reload(); } }); @@ -148,7 +155,7 @@ const BookAddForm = () => {
    - +
    @@ -178,7 +185,7 @@ const BookAddForm = () => {
    - + { />
    - + {
    - + { />
    - + {
    - +
    - $ + {t('currencySymbol')}
    {
    - +
    - $ + {t('currencySymbol')}
    {
    - +
    - +
    {
    - + { @@ -360,12 +367,12 @@ const BookAddForm = () => { />
    - + { @@ -377,12 +384,12 @@ const BookAddForm = () => {
    - + { @@ -391,12 +398,12 @@ const BookAddForm = () => { />
    - + { @@ -408,12 +415,12 @@ const BookAddForm = () => {
    - + { @@ -422,12 +429,12 @@ const BookAddForm = () => { />
    - + { @@ -436,12 +443,12 @@ const BookAddForm = () => { />
    - + { @@ -453,12 +460,12 @@ const BookAddForm = () => {
    - + { @@ -471,13 +478,13 @@ const BookAddForm = () => {
    - +
    Loaner IDFull nameCopiesBorrow dateDue date{t('loanerId')}{t('fullName')}{t('copies')}{t('borrowDate')}{t('dueDate')}