feat(i18n): add i18n types, config, provider, context, and custom hook

This commit is contained in:
2025-07-04 00:22:01 +03:30
parent e2d37ac7b2
commit 01658964f7
13 changed files with 312 additions and 23 deletions

View File

@@ -235,24 +235,22 @@ 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 }) {

169
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

3
public/i18n/en.json Normal file
View File

@@ -0,0 +1,3 @@
{
"title": "Loyalty Club"
}

3
public/i18n/fa.json Normal file
View File

@@ -0,0 +1,3 @@
{
"title": "باشگاه مشتریان"
}

View File

@@ -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<LangaugeContextModel>({
language: 'fa',
changeLanguage: () => {},
t: () => '',
});

9
src/hooks/useLanguage.ts Normal file
View File

@@ -0,0 +1,9 @@
import {
LangaugeContext,
type LangaugeContextModel,
} from '@/contexts/LangaugeContext';
import { useContext } from 'react';
export const useLangauge = (): LangaugeContextModel => {
return useContext(LangaugeContext);
};

View File

@@ -0,0 +1,17 @@
import { useEffect, useState } from 'react';
export const useLocalStorage = <T extends string>(
key: string,
defaultValue: T,
): [T, (value: T) => void] => {
const localValueOrDefault: T = (localStorage.getItem(key) ??
defaultValue) as T;
const [localValue, setLocalValue] = useState<T>(localValueOrDefault);
useEffect(() => {
localStorage.setItem(key, localValue);
}, [localValue, key]);
return [localValue, setLocalValue];
};

18
src/i18n.ts Normal file
View File

@@ -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;

View File

@@ -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(
<StrictMode>
<LanguageProvider>
<App />
</LanguageProvider>
</StrictMode>,
);

View File

@@ -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>(
'language',
'fa',
);
const { t } = useTranslation();
useLayoutEffect(() => {
changeLanguage(currentLanguage);
document.documentElement.dir = currentLanguage === 'fa' ? 'rtl' : 'ltr';
document.documentElement.lang = currentLanguage;
}, [currentLanguage]);
return (
<LangaugeContext.Provider
value={{
language: currentLanguage,
changeLanguage: setCurrentLangauge,
t: t,
}}
>
{props.children}
</LangaugeContext.Provider>
);
};

1
src/types/language.ts Normal file
View File

@@ -0,0 +1 @@
export type Language = 'en' | 'fa';

View File

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