diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0fa5602..5f1e163 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -235,32 +235,30 @@ Design patterns are reusable solutions to common problems within a given context ### Memoization Pattern - * **Concept**: A performance optimization technique to prevent unnecessary re-renders and expensive re-calculations by caching results. React provides three main tools for this. +- **Concept**: A performance optimization technique to prevent unnecessary re-renders and expensive re-calculations by caching results. React provides three main tools for this. - * **Best For**: Improving the performance of applications with complex components, large lists, or frequent state updates. - - * **The Tools**: - - * **`React.memo`**: A higher-order component that prevents a component from re-rendering if its props haven't changed. - * **`useMemo`**: A hook that caches the result of an expensive calculation. It only re-computes the value when its dependencies change. - * **`useCallback`**: A hook that caches a function definition. This is useful for preventing child components from re-rendering when you pass functions down as props. +- **Best For**: Improving the performance of applications with complex components, large lists, or frequent state updates. +- **The Tools**: + - **`React.memo`**: A higher-order component that prevents a component from re-rendering if its props haven't changed. + - **`useMemo`**: A hook that caches the result of an expensive calculation. It only re-computes the value when its dependencies change. + - **`useCallback`**: A hook that caches a function definition. This is useful for preventing child components from re-rendering when you pass functions down as props. ### Custom Data Fetching Hook - * **Concept**: A custom hook that abstracts all the logic for fetching data from an API. It typically manages the loading, error, and data states internally. +- **Concept**: A custom hook that abstracts all the logic for fetching data from an API. It typically manages the loading, error, and data states internally. - * **Best For**: Simplifying data fetching in your components, eliminating repetitive `useEffect` logic, and creating a consistent way to handle API requests across your app. +- **Best For**: Simplifying data fetching in your components, eliminating repetitive `useEffect` logic, and creating a consistent way to handle API requests across your app. - * **How it looks**: Components become much cleaner and focused on displaying the data. +- **How it looks**: Components become much cleaner and focused on displaying the data. - ```tsx - function UserProfile({ userId }) { - const { data: user, isLoading, error } = useFetch(`/api/users/${userId}`); + ```tsx + function UserProfile({ userId }) { + const { data: user, isLoading, error } = useFetch(`/api/users/${userId}`); - if (isLoading) return
Loading...
; - if (error) return
Error fetching data!
; + if (isLoading) return
Loading...
; + if (error) return
Error fetching data!
; - return
{user.name}
; - } - ``` \ No newline at end of file + return
{user.name}
; + } + ``` diff --git a/package-lock.json b/package-lock.json index 44212c1..27290ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,15 @@ "name": "harmony-club", "version": "0.0.0", "dependencies": { + "i18next": "^25.3.0", + "i18next-http-backend": "^3.0.2", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-i18next": "^15.6.0" }, "devDependencies": { "@eslint/js": "^9.29.0", + "@types/node": "^24.0.10", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@typescript-eslint/eslint-plugin": "^8.35.1", @@ -268,6 +272,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1420,6 +1433,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", + "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, "node_modules/@types/react": { "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", @@ -1940,6 +1963,15 @@ "dev": true, "license": "MIT" }, + "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", @@ -2483,6 +2515,55 @@ "node": ">=8" } }, + "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/i18next": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.0.tgz", + "integrity": "sha512-ZSQIiNGfqSG6yoLHaCvrkPp16UejHI8PCDxFYaNG/1qxtmqNmqEg4JlWKlxkrUmrin2sEjsy+Mjy1TRozBhOgw==", + "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.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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", @@ -2754,6 +2835,26 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "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-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -2984,6 +3085,32 @@ "react": "^19.1.0" } }, + "node_modules/react-i18next": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.0.tgz", + "integrity": "sha512-W135dB0rDfiFmbMipC17nOhGdttO5mzH8BivY+2ybsQBbXvxWIwl3cmeH3T9d+YPBSJu/ouyJKFJTtkK7rJofw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "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-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -3228,6 +3355,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -3258,7 +3391,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3291,6 +3424,13 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -3435,6 +3575,31 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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/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==", + "license": "BSD-2-Clause" + }, + "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==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.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 ef3abca..ffaf8cd 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,15 @@ "preview": "vite preview" }, "dependencies": { + "i18next": "^25.3.0", + "i18next-http-backend": "^3.0.2", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "react-i18next": "^15.6.0" }, "devDependencies": { "@eslint/js": "^9.29.0", + "@types/node": "^24.0.10", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@typescript-eslint/eslint-plugin": "^8.35.1", diff --git a/public/i18n/en.json b/public/i18n/en.json new file mode 100644 index 0000000..df5f62e --- /dev/null +++ b/public/i18n/en.json @@ -0,0 +1,3 @@ +{ + "title": "Loyalty Club" +} diff --git a/public/i18n/fa.json b/public/i18n/fa.json new file mode 100644 index 0000000..3c6a9d4 --- /dev/null +++ b/public/i18n/fa.json @@ -0,0 +1,3 @@ +{ + "title": "باشگاه مشتریان" +} diff --git a/src/contexts/LangaugeContext.ts b/src/contexts/LangaugeContext.ts new file mode 100644 index 0000000..92d29f9 --- /dev/null +++ b/src/contexts/LangaugeContext.ts @@ -0,0 +1,15 @@ +import { type Language } from '@/types/language'; +import { createContext } from 'react'; + +export interface LangaugeContextModel { + language: Language; + changeLanguage: (langauge: Language) => void; + t: (key: string) => string; +} + +// The context is used by the LangaugeProvider +export const LangaugeContext = createContext({ + language: 'fa', + changeLanguage: () => {}, + t: () => '', +}); diff --git a/src/hooks/useLanguage.ts b/src/hooks/useLanguage.ts new file mode 100644 index 0000000..b176261 --- /dev/null +++ b/src/hooks/useLanguage.ts @@ -0,0 +1,9 @@ +import { + LangaugeContext, + type LangaugeContextModel, +} from '@/contexts/LangaugeContext'; +import { useContext } from 'react'; + +export const useLangauge = (): LangaugeContextModel => { + return useContext(LangaugeContext); +}; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..06c0cef --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +export const useLocalStorage = ( + key: string, + defaultValue: T, +): [T, (value: T) => void] => { + const localValueOrDefault: T = (localStorage.getItem(key) ?? + defaultValue) as T; + + const [localValue, setLocalValue] = useState(localValueOrDefault); + + useEffect(() => { + localStorage.setItem(key, localValue); + }, [localValue, key]); + + return [localValue, setLocalValue]; +}; diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..0276a94 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,18 @@ +import i18n from 'i18next'; +import i18nBackend from 'i18next-http-backend'; +import { initReactI18next } from 'react-i18next'; + +i18n + .use(i18nBackend) + .use(initReactI18next) + .init({ + lng: 'en', + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: `${window.location.origin}/i18n/{{lng}}.json`, + }, + }); + +export default i18n; diff --git a/src/main.tsx b/src/main.tsx index cc14d83..de77833 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,9 +2,12 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App'; +import { LanguageProvider } from './providers/LanguageProvider'; createRoot(document.getElementById('root')!).render( - + + + , ); diff --git a/src/providers/LanguageProvider.tsx b/src/providers/LanguageProvider.tsx new file mode 100644 index 0000000..5250cfe --- /dev/null +++ b/src/providers/LanguageProvider.tsx @@ -0,0 +1,47 @@ +import type { Language } from '@/types/language'; +import { changeLanguage } from 'i18next'; +import { useLayoutEffect, type JSX, type PropsWithChildren } from 'react'; +import { initReactI18next, useTranslation } from 'react-i18next'; +import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { LangaugeContext } from '@/contexts/LangaugeContext'; +import i18n from 'i18next'; +import i18nBackend from 'i18next-http-backend'; + +i18n + .use(i18nBackend) + .use(initReactI18next) + .init({ + lng: 'en', + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: `${window.location.origin}/i18n/{{lng}}.json`, + }, + }); + +export const LanguageProvider = (props: PropsWithChildren): JSX.Element => { + const [currentLanguage, setCurrentLangauge] = useLocalStorage( + 'language', + 'fa', + ); + const { t } = useTranslation(); + + useLayoutEffect(() => { + changeLanguage(currentLanguage); + document.documentElement.dir = currentLanguage === 'fa' ? 'rtl' : 'ltr'; + document.documentElement.lang = currentLanguage; + }, [currentLanguage]); + + return ( + + {props.children} + + ); +}; diff --git a/src/types/language.ts b/src/types/language.ts new file mode 100644 index 0000000..ca7f950 --- /dev/null +++ b/src/types/language.ts @@ -0,0 +1 @@ +export type Language = 'en' | 'fa'; diff --git a/vite.config.ts b/vite.config.ts index 4a5def4..0e1b4b4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,13 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import path from 'path'; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, });