feat(i18n): add i18n types, config, provider, context, and custom hook
This commit is contained in:
@@ -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
169
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
3
public/i18n/en.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"title": "Loyalty Club"
|
||||
}
|
||||
3
public/i18n/fa.json
Normal file
3
public/i18n/fa.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"title": "باشگاه مشتریان"
|
||||
}
|
||||
15
src/contexts/LangaugeContext.ts
Normal file
15
src/contexts/LangaugeContext.ts
Normal 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
9
src/hooks/useLanguage.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {
|
||||
LangaugeContext,
|
||||
type LangaugeContextModel,
|
||||
} from '@/contexts/LangaugeContext';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const useLangauge = (): LangaugeContextModel => {
|
||||
return useContext(LangaugeContext);
|
||||
};
|
||||
17
src/hooks/useLocalStorage.ts
Normal file
17
src/hooks/useLocalStorage.ts
Normal 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
18
src/i18n.ts
Normal 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;
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
47
src/providers/LanguageProvider.tsx
Normal file
47
src/providers/LanguageProvider.tsx
Normal 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
1
src/types/language.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Language = 'en' | 'fa';
|
||||
@@ -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'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user