chore(i18n): use default i18n provider, define LanguageManager component to handle direction and lng

This commit is contained in:
Sajad Mirjalili
2025-07-15 12:18:42 +03:30
parent f14bd00ed8
commit 141a827d22
20 changed files with 95 additions and 153 deletions

10
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@emotion/styled": "^11.14.1",
"@mui/material": "^7.2.0",
"i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@@ -3039,6 +3040,15 @@
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-http-backend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",

View File

@@ -15,6 +15,7 @@
"@emotion/styled": "^11.14.1",
"@mui/material": "^7.2.0",
"i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
{
"helloWorld": "hello world"
}

View File

@@ -0,0 +1,3 @@
{
"helloWorld": "سلام دنیا"
}

View File

@@ -1,10 +1,20 @@
import { CssBaseline } from '@mui/material';
import './App.css';
import { useTranslation } from 'react-i18next';
import { LanguageManager } from './components/LanguageManager';
function App() {
const { t } = useTranslation();
return (
<div>
<h1>Hello World</h1>
</div>
<>
<CssBaseline />
<LanguageManager />
<div style={{ padding: '16px' }}>
<h1>{t('helloWorld')}</h1>
<p>The main content and router will go here.</p>
</div>
</>
);
}

View File

@@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
/**
* This component listens to i18next language changes and applies
* side effects to the document. It renders no visible UI.
*/
export const LanguageManager = () => {
const { i18n } = useTranslation();
useEffect(() => {
const handleLanguageChange = (lng: string) => {
document.documentElement.dir = i18n.dir(lng);
document.documentElement.lang = lng;
};
// Set initial values on component mount
handleLanguageChange(i18n.language);
// Listen for future language changes
i18n.on('languageChanged', handleLanguageChange);
// Cleanup the event listener on unmount
return () => {
i18n.off('languageChanged', handleLanguageChange);
};
}, [i18n]);
return null;
};

22
src/config/i18n.ts Normal file
View File

@@ -0,0 +1,22 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpApi from 'i18next-http-backend';
i18n
.use(HttpApi) // Loads translations from your /public/locales folder
.use(LanguageDetector) // Detects user language
.use(initReactI18next) // Passes i18n down to react-i18next
.init({
supportedLngs: ['en', 'fa'], // Supported languages
fallbackLng: 'fa',
detection: {
order: ['localStorage', 'cookie', 'navigator'],
caches: ['localStorage', 'cookie'],
lookupLocalStorage: 'language', // The key to use in localStorage
},
ns: ['common'], // Add new namespaces here
defaultNS: 'common',
});
export default i18n;

View File

@@ -1,12 +0,0 @@
import type { AppTheme } from '@/types/theme';
import { createContext } from 'react';
export interface AppThemeContextModel {
mode: AppTheme;
changeTheme: (theme: AppTheme) => void;
}
// The context is used by the LangaugeProvider
export const AppThemeContext = createContext<AppThemeContextModel>({
mode: 'default',
changeTheme: () => {},
});

View File

@@ -1,13 +0,0 @@
import { type Language } from '@/types/language';
import { createContext } from 'react';
export interface LangaugeContextModel {
language: Language;
changeLanguage: (langauge: Language) => void;
}
// The context is used by the LangaugeProvider
export const LangaugeContext = createContext<LangaugeContextModel>({
language: 'fa',
changeLanguage: () => {},
});

View File

@@ -1,21 +0,0 @@
import {
AppThemeContext,
type AppThemeContextModel,
} from '@/contexts/AppThemeContext';
import { useTheme, type Theme } from '@mui/material';
import { useContext } from 'react';
export interface AppThemeHookModel extends AppThemeContextModel {
theme: Theme;
}
export const useAppTheme = (): AppThemeHookModel => {
const appThemeContext = useContext(AppThemeContext);
const muiTheme = useTheme();
return {
mode: appThemeContext.mode,
changeTheme: appThemeContext.changeTheme,
theme: muiTheme,
};
};

View File

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

View File

@@ -1,16 +0,0 @@
import i18next from 'i18next';
import I18NextHttpBackend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
i18next
.use(I18NextHttpBackend)
.use(initReactI18next)
.init({
lng: 'en',
interpolation: {
escapeValue: false,
},
backend: {
loadPath: `${window.location.origin}/i18n/{{lng}}.json`,
},
});

View File

@@ -1,18 +1,13 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import './lib/i18n';
import App from './App';
import { LanguageProvider } from './providers/LanguageProvider';
import { createTheme, ThemeProvider } from '@mui/material';
import { AppThemeProvider } from './providers/ThemeProvider';
import { AppProviders } from './providers/AppProvider';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<LanguageProvider>
<AppThemeProvider>
<App />
</AppThemeProvider>
</LanguageProvider>
<AppProviders>
<App />
</AppProviders>
</StrictMode>,
);

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { I18nextProvider } from 'react-i18next';
import i18n from '@/config/i18n';
export const AppProviders: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
};

View File

@@ -1,29 +0,0 @@
import type { Language } from '@/types/language';
import { changeLanguage } from 'i18next';
import { useLayoutEffect, type JSX, type PropsWithChildren } from 'react';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { LangaugeContext } from '@/contexts/LangaugeContext';
export const LanguageProvider = (props: PropsWithChildren): JSX.Element => {
const [currentLanguage, setCurrentLangauge] = useLocalStorage<Language>(
'language',
'fa',
);
useLayoutEffect(() => {
changeLanguage(currentLanguage);
document.documentElement.dir = currentLanguage === 'fa' ? 'rtl' : 'ltr';
document.documentElement.lang = currentLanguage;
}, [currentLanguage]);
return (
<LangaugeContext.Provider
value={{
language: currentLanguage,
changeLanguage: setCurrentLangauge,
}}
>
{props.children}
</LangaugeContext.Provider>
);
};

View File

@@ -1,33 +0,0 @@
import { useLayoutEffect, type JSX, type PropsWithChildren } from 'react';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import type { AppTheme } from '@/types/theme';
import { AppThemeContext } from '@/contexts/AppThemeContext';
import { createTheme, ThemeProvider } from '@mui/material';
import { useLangauge } from '@/hooks/useLanguage';
export const AppThemeProvider = (props: PropsWithChildren): JSX.Element => {
const { language } = useLangauge();
const [currentThemeMode, setCurrentTheme] = useLocalStorage<AppTheme>(
'theme',
'default',
);
const muiTheme = createTheme({
direction: language === 'fa' ? 'rtl' : 'ltr',
});
useLayoutEffect(() => {
document.body.setAttribute('theme', currentThemeMode);
}, [currentThemeMode]);
return (
<AppThemeContext.Provider
value={{
mode: currentThemeMode,
changeTheme: setCurrentTheme,
}}
>
<ThemeProvider theme={muiTheme}>{props.children}</ThemeProvider>
</AppThemeContext.Provider>
);
};

View File

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

View File

@@ -1 +0,0 @@
export type AppTheme = 'default' | 'dark';