Update sitemaps and improve language handling
- Updated last modification dates in static and tools sitemaps to 2026-04-01. - Enhanced language switching in the Header component to ensure language resources are loaded before changing the language. - Added language resource loading logic in i18n configuration to support dynamic loading of language files. - Improved SEO route page to ensure correct language is set based on URL parameters. - Adjusted global CSS for deferred sections to optimize rendering. - Configured Nginx to enable Brotli compression for better performance.
This commit is contained in:
@@ -10,6 +10,8 @@ import { useDirection } from '@/hooks/useDirection';
|
||||
import { initAnalytics, trackPageView } from '@/services/analytics';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
|
||||
let clarityInitialized = false;
|
||||
|
||||
// Pages
|
||||
const HomePage = lazy(() => import('@/pages/HomePage'));
|
||||
const AboutPage = lazy(() => import('@/pages/AboutPage'));
|
||||
@@ -99,10 +101,38 @@ export default function App() {
|
||||
|
||||
// Microsoft Clarity: Run only in production and browser
|
||||
useEffect(() => {
|
||||
if (import.meta.env.PROD && typeof window !== 'undefined') {
|
||||
// ضع هنا رقم مشروع Clarity الخاص بك بدلاً من 'YOUR_CLARITY_PROJECT_ID'
|
||||
Clarity.init(import.meta.env.VITE_CLARITY_PROJECT_ID);
|
||||
}
|
||||
if (!import.meta.env.PROD || typeof window === 'undefined') return;
|
||||
|
||||
const projectId = (import.meta.env.VITE_CLARITY_PROJECT_ID || '').trim();
|
||||
if (!projectId) return;
|
||||
|
||||
const tryInitClarity = () => {
|
||||
if (clarityInitialized) return;
|
||||
try {
|
||||
const rawConsent = localStorage.getItem('cookie_consent');
|
||||
const parsed = rawConsent ? JSON.parse(rawConsent) : null;
|
||||
const hasConsent = parsed?.state === 'accepted';
|
||||
if (hasConsent) {
|
||||
Clarity.init(projectId);
|
||||
clarityInitialized = true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed consent payloads.
|
||||
}
|
||||
};
|
||||
|
||||
tryInitClarity();
|
||||
|
||||
const onConsent = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ accepted: boolean }>;
|
||||
if (customEvent.detail?.accepted && !clarityInitialized) {
|
||||
Clarity.init(projectId);
|
||||
clarityInitialized = true;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('cookie-consent', onConsent as EventListener);
|
||||
return () => window.removeEventListener('cookie-consent', onConsent as EventListener);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FileText, Moon, Sun, Menu, X, ChevronDown, UserRound } from 'lucide-react';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { ensureLanguageResources } from '@/i18n';
|
||||
interface LangOption {
|
||||
code: string;
|
||||
label: string;
|
||||
@@ -58,8 +59,9 @@ export default function Header() {
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, []);
|
||||
|
||||
const switchLang = (code: string) => {
|
||||
i18n.changeLanguage(code);
|
||||
const switchLang = async (code: string) => {
|
||||
const resolved = await ensureLanguageResources(code);
|
||||
void i18n.changeLanguage(resolved);
|
||||
setLangOpen(false);
|
||||
};
|
||||
|
||||
@@ -140,7 +142,7 @@ export default function Header() {
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => switchLang(lang.code)}
|
||||
onClick={() => void switchLang(lang.code)}
|
||||
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
|
||||
lang.code === i18n.language
|
||||
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
|
||||
|
||||
@@ -3,8 +3,57 @@ import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import en from './en.json';
|
||||
import ar from './ar.json';
|
||||
import fr from './fr.json';
|
||||
|
||||
type SupportedLanguage = 'en' | 'ar' | 'fr';
|
||||
|
||||
const loadedLanguages = new Set<SupportedLanguage>(['en']);
|
||||
|
||||
const languageLoaders: Record<Exclude<SupportedLanguage, 'en'>, () => Promise<{ default: Record<string, unknown> }>> = {
|
||||
ar: () => import('./ar.json'),
|
||||
fr: () => import('./fr.json'),
|
||||
};
|
||||
|
||||
function normalizeLanguage(language?: string): SupportedLanguage {
|
||||
const base = (language || '').split('-')[0];
|
||||
return base === 'ar' || base === 'fr' ? base : 'en';
|
||||
}
|
||||
|
||||
function getInitialLanguage(): SupportedLanguage {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'en';
|
||||
}
|
||||
|
||||
const htmlLanguage = normalizeLanguage(document.documentElement.lang);
|
||||
if (htmlLanguage !== 'en') {
|
||||
return htmlLanguage;
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem('i18nextLng');
|
||||
const fromStorage = normalizeLanguage(stored || undefined);
|
||||
if (fromStorage !== 'en') {
|
||||
return fromStorage;
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
|
||||
return normalizeLanguage(navigator.language);
|
||||
}
|
||||
|
||||
export async function ensureLanguageResources(language: string) {
|
||||
const normalized = normalizeLanguage(language);
|
||||
if (normalized === 'en' || loadedLanguages.has(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const module = await languageLoaders[normalized]();
|
||||
i18n.addResourceBundle(normalized, 'translation', module.default, true, true);
|
||||
loadedLanguages.add(normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const initialLanguage = getInitialLanguage();
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
@@ -12,11 +61,11 @@ i18n
|
||||
.init({
|
||||
resources: {
|
||||
en: { translation: en },
|
||||
ar: { translation: ar },
|
||||
fr: { translation: fr },
|
||||
},
|
||||
lng: initialLanguage,
|
||||
fallbackLng: 'en',
|
||||
supportedLngs: ['en', 'ar', 'fr'],
|
||||
load: 'languageOnly',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
@@ -26,4 +75,12 @@ i18n
|
||||
},
|
||||
});
|
||||
|
||||
if (initialLanguage !== 'en') {
|
||||
void ensureLanguageResources(initialLanguage).then((resolved) => {
|
||||
if (i18n.language !== resolved) {
|
||||
void i18n.changeLanguage(resolved);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ensureLanguageResources } from '@/i18n';
|
||||
import { getProgrammaticToolPage, getSeoCollectionPage } from '@/config/seoPages';
|
||||
import NotFoundPage from '@/pages/NotFoundPage';
|
||||
import SeoCollectionPage from '@/pages/SeoCollectionPage';
|
||||
@@ -17,9 +18,12 @@ export default function SeoRoutePage() {
|
||||
const resolvedLocale = locale === 'ar' ? 'ar' : 'en';
|
||||
|
||||
useEffect(() => {
|
||||
if (i18n.language !== resolvedLocale) {
|
||||
void i18n.changeLanguage(resolvedLocale);
|
||||
}
|
||||
if (i18n.language === resolvedLocale) return;
|
||||
|
||||
void (async () => {
|
||||
const resolved = await ensureLanguageResources(resolvedLocale);
|
||||
await i18n.changeLanguage(resolved);
|
||||
})();
|
||||
}, [i18n, resolvedLocale]);
|
||||
|
||||
if (!slug) {
|
||||
@@ -35,4 +39,4 @@ export default function SeoRoutePage() {
|
||||
}
|
||||
|
||||
return <NotFoundPage />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,5 +157,6 @@
|
||||
}
|
||||
|
||||
.deferred-section {
|
||||
content-visibility: visible;
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 1px 2000px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user