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'),
+ },
+ },
});