setup i18n and ported all tools to standalone pages

This commit is contained in:
abdullahalam123
2025-12-11 19:34:14 +05:30
parent fe3e54f979
commit 78dc6333f9
221 changed files with 30351 additions and 11131 deletions

154
src/js/i18n/i18n.ts Normal file
View File

@@ -0,0 +1,154 @@
import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend';
// Supported languages
export const supportedLanguages = ['en', 'de'] as const;
export type SupportedLanguage = (typeof supportedLanguages)[number];
export const languageNames: Record<SupportedLanguage, string> = {
en: 'English',
de: 'Deutsch',
};
export const getLanguageFromUrl = (): SupportedLanguage => {
const path = window.location.pathname;
const langMatch = path.match(/^\/(en|de)(?:\/|$)/);
if (langMatch && supportedLanguages.includes(langMatch[1] as SupportedLanguage)) {
return langMatch[1] as SupportedLanguage;
}
const storedLang = localStorage.getItem('i18nextLng');
if (storedLang && supportedLanguages.includes(storedLang as SupportedLanguage)) {
return storedLang as SupportedLanguage;
}
return 'en';
};
let initialized = false;
export const initI18n = async (): Promise<typeof i18next> => {
if (initialized) return i18next;
const currentLang = getLanguageFromUrl();
await i18next
.use(HttpBackend)
.use(LanguageDetector)
.init({
lng: currentLang,
fallbackLng: 'en',
supportedLngs: supportedLanguages as unknown as string[],
ns: ['common', 'tools'],
defaultNS: 'common',
backend: {
loadPath: `${import.meta.env.BASE_URL}locales/{{lng}}/{{ns}}.json`,
},
detection: {
order: ['path', 'localStorage', 'navigator'],
lookupFromPathIndex: 0,
caches: ['localStorage'],
},
interpolation: {
escapeValue: false,
},
});
initialized = true;
return i18next;
};
export const t = (key: string, options?: Record<string, unknown>): string => {
return i18next.t(key, options);
};
export const changeLanguage = (lang: SupportedLanguage): void => {
if (!supportedLanguages.includes(lang)) return;
const currentPath = window.location.pathname;
const currentLang = getLanguageFromUrl();
let newPath: string;
if (currentPath.match(/^\/(en|de)\//)) {
newPath = currentPath.replace(/^\/(en|de)\//, `/${lang}/`);
} else if (currentPath.match(/^\/(en|de)$/)) {
newPath = `/${lang}`;
} else {
newPath = `/${lang}${currentPath}`;
}
const newUrl = newPath + window.location.search + window.location.hash;
window.location.href = newUrl;
};
// Apply translations to all elements with data-i18n attribute
export const applyTranslations = (): void => {
document.querySelectorAll('[data-i18n]').forEach((element) => {
const key = element.getAttribute('data-i18n');
if (key) {
const translation = t(key);
if (translation && translation !== key) {
element.textContent = translation;
}
}
});
document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => {
const key = element.getAttribute('data-i18n-placeholder');
if (key && element instanceof HTMLInputElement) {
const translation = t(key);
if (translation && translation !== key) {
element.placeholder = translation;
}
}
});
document.querySelectorAll('[data-i18n-title]').forEach((element) => {
const key = element.getAttribute('data-i18n-title');
if (key) {
const translation = t(key);
if (translation && translation !== key) {
(element as HTMLElement).title = translation;
}
}
});
document.documentElement.lang = i18next.language;
};
export const rewriteLinks = (): void => {
const currentLang = getLanguageFromUrl();
if (currentLang === 'en') return;
const links = document.querySelectorAll('a[href]');
links.forEach((link) => {
const href = link.getAttribute('href');
if (!href) return;
if (href.startsWith('http') ||
href.startsWith('mailto:') ||
href.startsWith('tel:') ||
href.startsWith('#') ||
href.startsWith('javascript:')) {
return;
}
if (href.match(/^\/(en|de)\//)) {
return;
}
let newHref: string;
if (href.startsWith('/')) {
newHref = `/${currentLang}${href}`;
} else if (href.startsWith('./')) {
newHref = href.replace('./', `/${currentLang}/`);
} else if (href === '/' || href === '') {
newHref = `/${currentLang}/`;
} else {
newHref = `/${currentLang}/${href}`;
}
link.setAttribute('href', newHref);
});
};
export default i18next;

3
src/js/i18n/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { initI18n, t, changeLanguage, applyTranslations, rewriteLinks, getLanguageFromUrl, supportedLanguages, languageNames } from './i18n';
export type { SupportedLanguage } from './i18n';
export { createLanguageSwitcher, injectLanguageSwitcher } from './language-switcher';

View File

@@ -0,0 +1,142 @@
import {
supportedLanguages,
languageNames,
getLanguageFromUrl,
changeLanguage,
} from './i18n';
export const createLanguageSwitcher = (): HTMLElement => {
const currentLang = getLanguageFromUrl();
const container = document.createElement('div');
container.className = 'relative';
container.id = 'language-switcher';
const button = document.createElement('button');
button.className = `
inline-flex items-center gap-1.5 text-sm font-medium
bg-gray-800 text-gray-200 border border-gray-600
px-3 py-1.5 rounded-full transition-colors duration-200
shadow-sm hover:shadow-md hover:bg-gray-700
`.trim();
button.setAttribute('aria-haspopup', 'true');
button.setAttribute('aria-expanded', 'false');
const textSpan = document.createElement('span');
textSpan.className = 'font-medium';
textSpan.textContent = languageNames[currentLang];
const chevron = document.createElement('svg');
chevron.className = 'w-4 h-4';
chevron.setAttribute('fill', 'none');
chevron.setAttribute('stroke', 'currentColor');
chevron.setAttribute('viewBox', '0 0 24 24');
chevron.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>';
button.appendChild(textSpan);
button.appendChild(chevron);
const dropdown = document.createElement('div');
dropdown.className = `
hidden absolute right-0 mt-2 w-40 rounded-lg
bg-gray-800 border border-gray-700 shadow-xl
py-1 z-50
`.trim();
dropdown.setAttribute('role', 'menu');
supportedLanguages.forEach((lang) => {
const option = document.createElement('button');
option.className = `
w-full px-4 py-2 text-left text-sm text-gray-200
hover:bg-gray-700 flex items-center gap-2
${lang === currentLang ? 'bg-gray-700' : ''}
`.trim();
option.setAttribute('role', 'menuitem');
const name = document.createElement('span');
name.textContent = languageNames[lang];
option.appendChild(name);
option.addEventListener('click', () => {
if (lang !== currentLang) {
changeLanguage(lang);
}
});
dropdown.appendChild(option);
});
container.appendChild(button);
container.appendChild(dropdown);
button.addEventListener('click', (e) => {
e.stopPropagation();
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', (!isExpanded).toString());
dropdown.classList.toggle('hidden');
});
document.addEventListener('click', () => {
button.setAttribute('aria-expanded', 'false');
dropdown.classList.add('hidden');
});
return container;
};
export const injectLanguageSwitcher = (): void => {
const footer = document.querySelector('footer');
if (!footer) return;
const headings = footer.querySelectorAll('h3');
let followUsColumn: HTMLElement | null = null;
headings.forEach((h3) => {
if (h3.textContent?.trim() === 'Follow Us' || h3.textContent?.trim() === 'Folgen Sie uns') {
followUsColumn = h3.parentElement;
}
});
if (followUsColumn) {
const socialIconsContainer = followUsColumn.querySelector('.space-x-4');
if (socialIconsContainer) {
const wrapper = document.createElement('div');
wrapper.className = 'inline-flex flex-col gap-4'; // gap-4 adds space between icons and switcher
socialIconsContainer.parentNode?.insertBefore(wrapper, socialIconsContainer);
wrapper.appendChild(socialIconsContainer);
const switcher = createLanguageSwitcher();
switcher.className = 'relative w-full';
const button = switcher.querySelector('button');
if (button) {
button.className = `
flex items-center justify-between w-full text-sm font-medium
bg-gray-800 text-gray-400 border border-gray-700
px-3 py-2 rounded-lg transition-colors duration-200
hover:text-white hover:border-gray-600
`.trim();
}
const dropdown = switcher.querySelector('div[role="menu"]');
if (dropdown) {
dropdown.classList.remove('mt-2', 'w-40');
dropdown.classList.add('bottom-full', 'mb-2', 'w-full');
}
wrapper.appendChild(switcher);
} else {
const switcherContainer = document.createElement('div');
switcherContainer.className = 'mt-4 w-full';
const switcher = createLanguageSwitcher();
switcherContainer.appendChild(switcher);
followUsColumn.appendChild(switcherContainer);
}
}
};