From f5a17f14031c19737ce2930c2db9151ee126418a Mon Sep 17 00:00:00 2001 From: sina_sajjadi Date: Wed, 1 Jan 2025 13:09:22 +0330 Subject: [PATCH] feat: implement internationalization support, add translation files, and update components for language handling --- next-i18next.config.js | 10 ++ next.config.ts | 41 +++-- package-lock.json | 146 +++++++++++++++++- package.json | 3 + public/locales/ar/common.json | 37 +++++ public/locales/en/common.json | 38 +++++ src/api/http.tsx | 2 +- src/components/language-switcher.tsx | 145 +++++++++++++---- src/components/layout/header.tsx | 10 +- src/components/layout/mobile-navigation.tsx | 10 +- src/components/modals/audio-setting.tsx | 18 ++- src/components/modals/languages-modal.tsx | 20 +-- src/components/modals/reciters.tsx | 8 +- src/components/modals/search-modal.tsx | 42 ++--- src/components/modals/setting.tsx | 29 ++-- src/components/sidebar/list.tsx | 19 +-- src/components/sidebar/tabs.tsx | 19 +-- .../sticky-components/audio-controls.tsx | 11 +- .../sticky-components/download-app.tsx | 18 ++- src/components/ui/search-duas.tsx | 17 +- src/components/utils/colorize-vowels.tsx | 2 +- src/pages/_app.tsx | 7 +- src/pages/about.tsx | 11 ++ src/pages/duas/[slug].tsx | 11 ++ src/pages/index.tsx | 18 ++- tailwind.config.ts | 17 +- tsconfig.json | 2 +- 27 files changed, 548 insertions(+), 163 deletions(-) create mode 100644 next-i18next.config.js create mode 100644 public/locales/ar/common.json create mode 100644 public/locales/en/common.json diff --git a/next-i18next.config.js b/next-i18next.config.js new file mode 100644 index 0000000..6149228 --- /dev/null +++ b/next-i18next.config.js @@ -0,0 +1,10 @@ +module.exports = { + i18n: { + locales: [ + 'en', 'es', 'de', 'uz', 'pt', 'bn', 'zh', 'az', 'ur', 'fr', 'tr', 'id', 'sw', 'ru', 'ar', 'tg', 'fa', 'gu', 'ks', 'ha' + ] + , // List the languages you support + defaultLocale: 'en', // Default language + }, + reloadOnPrerender: true, // Ensures translations are reloaded on server-side rendering +}; \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index 7e13f17..030322f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,5 +1,3 @@ -// next.config.ts - import type { NextConfig } from "next"; const nextConfig: NextConfig = { @@ -7,13 +5,38 @@ const nextConfig: NextConfig = { images: { domains: ["habibapp.com"], // Add the domain for image hosting }, -// typescript : { -// ignoreBuildErrors : true -// }, -// eslint : { -// ignoreDuringBuilds : true -// } - // Add other Next.js config options here as needed + i18n: { + locales: [ + "en", + "es", + "de", + "uz", + "pt", + "bn", + "zh", + "az", + "ur", + "fr", + "tr", + "id", + "sw", + "ru", + "ar", + "tg", + "fa", + "gu", + "ks", + "ha", + ], // Add the locales you want to support + defaultLocale: "en", // Default language + }, + // Uncomment these if you want to disable TypeScript build errors or ESLint during build + // typescript: { + // ignoreBuildErrors: true + // }, + // eslint: { + // ignoreDuringBuilds: true + // } }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 66c18b1..56278c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,14 @@ "classnames": "^2.5.1", "dompurify": "^3.2.3", "framer-motion": "^11.15.0", + "i18next": "^24.2.0", "moment": "^2.30.1", "moment-hijri": "^3.0.0", "next": "15.1.0", + "next-i18next": "^15.4.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-i18next": "^15.4.0", "react-icons": "^5.4.0" }, "devDependencies": { @@ -47,6 +50,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -1758,6 +1772,16 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/core-js": { + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3084,6 +3108,57 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "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/i18next": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz", + "integrity": "sha512-ArJJTS1lV6lgKH7yEf4EpgNZ7+THl7bsGxxougPYiXRTJ/Fe1j08/TBpV9QsXCIYVfdE/HWG/xLezJ5DOlfBOA==", + "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.23.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-fs-backend": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.0.tgz", + "integrity": "sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw==" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3932,6 +4007,40 @@ } } }, + "node_modules/next-i18next": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-15.4.1.tgz", + "integrity": "sha512-n3cFqBKDpEDLXZVYD52H6k2QzLAnUSuBie02BmVpfywNZGxcNgq6QXdpVpvONQ7WkBVntPtldt867ZgtEcP1Og==", + "funding": [ + { + "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" + }, + { + "type": "individual", + "url": "https://locize.com" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2", + "core-js": "^3", + "hoist-non-react-statics": "^3.3.2", + "i18next-fs-backend": "^2.6.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "i18next": ">= 23.7.13", + "next": ">= 12.0.0", + "react": ">= 17.0.2", + "react-i18next": ">= 13.5.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4453,6 +4562,27 @@ "react": "^19.0.0" } }, + "node_modules/react-i18next": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz", + "integrity": "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-icons": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", @@ -4464,8 +4594,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/read-cache": { "version": "1.0.0", @@ -4510,6 +4639,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/regexp.prototype.flags": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", @@ -5424,6 +5558,14 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index bd82dd2..0b32cee 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,14 @@ "classnames": "^2.5.1", "dompurify": "^3.2.3", "framer-motion": "^11.15.0", + "i18next": "^24.2.0", "moment": "^2.30.1", "moment-hijri": "^3.0.0", "next": "15.1.0", + "next-i18next": "^15.4.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-i18next": "^15.4.0", "react-icons": "^5.4.0" }, "devDependencies": { diff --git a/public/locales/ar/common.json b/public/locales/ar/common.json new file mode 100644 index 0000000..13deeec --- /dev/null +++ b/public/locales/ar/common.json @@ -0,0 +1,37 @@ +{ + "home": "الرئيسية", + "about": "من نحن", + "donate": "تبرع", + "search_placeholder": "أدخل عنوان أو كلمة مفتاحية للبحث", + "loading": "جاري التحميل...", + "error_message": "فشل في جلب نتائج البحث.", + "close": "إغلاق", + "habib_app": "تطبيق حبيب", + "better_experience": "لتجربة أفضل", + "download": "تنزيل", + "timeline": "الجدول الزمني", + "settings": "الإعدادات", + "categories": "الفئات", + "famous": "الأشهر", + "near_by": "القريبين", + "today": "اليوم", + "todayDhikr": "ذكر اليوم", + "audio_settings": "إعدادات الصوت", + "reciters": "المقرئون", + "select_reciter": "اختر المقرئ", + "increase_speed": "زيادة السرعة", + "speed": "السرعة", + "choose_language": "اختر اللغة", + "loading_languages": "جاري تحميل اللغات...", + "failed_to_load_languages": "فشل في تحميل اللغات. الرجاء المحاولة مرة أخرى.", + "no_languages_available": "لا توجد لغات متاحة.", + "failed_to_load_reciters": "فشل في تحميل المقرئين. الرجاء المحاولة مرة أخرى.", + "search": "بحث", + "nothing_found": "لم يتم العثور على شيء!", + "dua_s_found": "تم العثور على دعاء(ات)", + "audio_available": "الصوت متاح", + "no_data_found": "لم يتم العثور على بيانات", + "arabic": "العربية", + "translation": "الترجمة", + "transliteration": "الترجمة الصوتية" +} diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000..282162f --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,38 @@ +{ + "home": "Home", + "about": "About us", + "donate": "Donate", + "search_placeholder": "Type a title or keyword to search", + "loading": "Loading...", + "error_message": "Failed to fetch search results.", + "close": "Close", + "habib_app": "Habib App", + "better_experience": "For Better Experience", + "download": "Download", + "timeline": "Time Line", + "settings": "Settings", + "categories": "Categories", + "famous": "Famous", + "near_by": "Near by", + "today": "Today", + "todayDhikr": "Today’s Dhikr", + "audio_settings": "Audio Settings", + "reciters": "Reciters", + "select_reciter": "Select Reciter", + "increase_speed": "Increase speed", + "speed": "Speed", + "choose_language": "Choose Language", + "loading_languages": "Loading languages...", + "failed_to_load_languages": "Failed to load languages. Please try again.", + "no_languages_available": "No languages available.", + "failed_to_load_reciters": "Failed to load reciters. Please try again.", + "search": "Search", + "nothing_found": "Nothing Found!", + "dua_s_found": "Dua(s) found", + "audio_available": "Audio Available", + "no_data_found": "No Data Found", + "arabic": "Arabic", + "translation": "Translation", + "transliteration": "Transliteration" + } + \ No newline at end of file diff --git a/src/api/http.tsx b/src/api/http.tsx index 49f6dc0..a3185f8 100644 --- a/src/api/http.tsx +++ b/src/api/http.tsx @@ -6,7 +6,7 @@ if (typeof window !== 'undefined') { locale = localStorage.getItem('locale') || 'en'; } const http = axios.create({ - baseURL: 'https://habibapp.com', + baseURL: 'https://habibapp.com', // headers: { // 'Authorization': 'Token 36d8be53bed5cab8027b66952b3f2c334cdca664', // }, diff --git a/src/components/language-switcher.tsx b/src/components/language-switcher.tsx index 7c5664d..7672894 100644 --- a/src/components/language-switcher.tsx +++ b/src/components/language-switcher.tsx @@ -1,26 +1,108 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ "use client"; import React, { useState, useEffect, useRef } from "react"; -//@ts-ignore import { IoGlobeSharp } from "react-icons/io5"; import { IoIosArrowDown } from "react-icons/io"; import { useUI } from "./context/ui.context"; import http from "@/api/http"; - +import { useTranslation } from "next-i18next"; // Import the useTranslation hook +import { useRouter } from "next/router"; + + +const data = [ + { + "name": "English", + "code": "en" + }, + { + "name": "Spanish", + "code": "es" + }, + { + "name": "German", + "code": "de" + }, + { + "name": "Uzbek", + "code": "uz" + }, + { + "name": "Portuguese", + "code": "pt" + }, + { + "name": "Bengali", + "code": "bn" + }, + { + "name": "Chinese", + "code": "zh" + }, + { + "name": "Azerbaijani", + "code": "az" + }, + { + "name": "Urdu", + "code": "ur" + }, + { + "name": "French", + "code": "fr" + }, + { + "name": "Turkish", + "code": "tr" + }, + { + "name": "Indonesian", + "code": "id" + }, + { + "name": "Swahili", + "code": "sw" + }, + { + "name": "Russian", + "code": "ru" + }, + { + "name": "Arabic", + "code": "ar" + }, + { + "name": "Tajik", + "code": "tg" + }, + { + "name": "Persian", + "code": "fa" + }, + { + "name": "Gujarati", + "code": "gu" + }, + { + "name": "Kashmiri", + "code": "ks" + }, + { + "name": "Hausa", + "code": "ha" + } +] const LanguageSwitcher: React.FC = () => { + const { t } = useTranslation('common'); // Initialize translation hook with 'common' namespace const [isOpen, setIsOpen] = useState(false); const { openModal } = useUI(); - + const router = useRouter(); const wrapperRef = useRef(null); const [windowWidth, setWindowWidth] = useState( typeof window !== "undefined" ? window.innerWidth : 0 ); - const [languages, setLanguages] = useState<{ code: string; name: string }[]>( - [] - ); + const [languages, setLanguages] = useState<{ code: string; name: string }[]>([]); const [selectedLanguage, setSelectedLanguage] = useState("en"); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -33,22 +115,27 @@ const LanguageSwitcher: React.FC = () => { }; const selectLanguage = (lang: string) => { - setSelectedLanguage(lang); - localStorage.setItem("locale", lang); // Save the selected language in localStorage - setIsOpen(false); + // Use router.replace() to update the URL with the new locale + router.push(router.asPath, undefined, { locale: lang }).then(() => { + // Optionally you can trigger the language change effect after the page is pushed + setSelectedLanguage(lang); + localStorage.setItem("locale", lang); // Save the selected language in localStorage + setIsOpen(false); + window.location.reload(); + }); }; - + useEffect(() => { const fetchLanguages = async () => { - try { - setIsLoading(true); - const response = await http.get("v1/languages/"); - setLanguages(response.data); - } catch (err) { - setError("Failed to load languages. Please try again."); - } finally { - setIsLoading(false); - } + // try { + // setIsLoading(true); + // const response = await http.get("v1/languages/"); + setLanguages(data); + // } catch (err) { + // setError(t("error_message")); // Use translation for error message + // } finally { + // setIsLoading(false); + // } }; fetchLanguages(); @@ -56,15 +143,12 @@ const LanguageSwitcher: React.FC = () => { if (savedLanguage) { setSelectedLanguage(savedLanguage); } - }, []); + }, [t]); // Add t as a dependency to ensure translation is updated useEffect(() => { const handleResize = () => setWindowWidth(window.innerWidth); const handleClickOutside = (event: MouseEvent) => { - if ( - wrapperRef.current && - !wrapperRef.current.contains(event.target as Node) - ) { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { setIsOpen(false); } }; @@ -77,6 +161,7 @@ const LanguageSwitcher: React.FC = () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); +console.log(languages); return (
@@ -91,9 +176,7 @@ const LanguageSwitcher: React.FC = () => { {selectedLanguageName} @@ -118,13 +201,13 @@ const LanguageSwitcher: React.FC = () => { )} - {isLoading && ( + {isLoading && isOpen &&(
- Loading... + {t("loading")} {/* Use translation for loading text */}
)} - {error && ( + {error && isOpen && (
{error}
diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 8710523..51ef139 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -1,11 +1,13 @@ -import Image from "next/image"; import Link from "next/link"; import LanguageSwitcher from "../language-switcher"; import SearchDuas from "../ui/search-duas"; import { useUI } from "../context/ui.context"; import Logo from "../ui/logo"; +import { useTranslation } from "next-i18next"; + const Header = () => { const { displayDownload } = useUI(); + const {t} = useTranslation("common") return (