[call-me] - refactoring

This commit is contained in:
Miroslav Pejic
2026-02-10 02:23:15 +01:00
parent 3fcb815944
commit e70370de30
+100 -149
View File
@@ -14,13 +14,11 @@ async function fetchAvailableLocales() {
const response = await fetch('/locales');
if (!response.ok) throw new Error('Failed to fetch locales');
const data = await response.json();
if (data && Array.isArray(data.locales) && data.locales.length > 0) {
return data.locales;
}
if (data?.locales?.length > 0) return data.locales;
} catch (error) {
console.warn('Unable to fetch available locales, using fallback list', error);
}
return ['en', 'es', 'fr', 'it', 'de', 'pt', 'ru', 'ar', 'hi', 'zh', 'ja']; // Fallback list of common locales
return ['en', 'es', 'fr', 'it', 'de', 'pt', 'ru', 'ar', 'hi', 'zh', 'ja']; // Fallback list
}
function getLocaleLabel(locale) {
@@ -44,28 +42,21 @@ function getLocaleLabel(locale) {
* Initialize i18n
*/
async function initI18n() {
// Get saved locale from localStorage or use browser language or default
const savedLocale = localStorage.getItem('locale');
const browserLocale = navigator.language.split('-')[0]; // Get 'en' from 'en-US'
const supportedLocales = await fetchAvailableLocales();
i18n.availableLocales = supportedLocales;
// Determine which locale to use
if (savedLocale && supportedLocales.includes(savedLocale)) {
i18n.currentLocale = savedLocale;
} else if (supportedLocales.includes(browserLocale)) {
i18n.currentLocale = browserLocale;
} else {
i18n.currentLocale = i18n.defaultLocale;
}
const savedLocale = localStorage.getItem('locale');
const browserLocale = navigator.language.split('-')[0]; // Get 'en' from 'en-US'
i18n.currentLocale =
(savedLocale && supportedLocales.includes(savedLocale)) ? savedLocale :
supportedLocales.includes(browserLocale) ? browserLocale :
i18n.defaultLocale;
// Load translations
// Load translations, set up language selector, and translate the page
await loadTranslations(i18n.currentLocale);
// Set up language selector
setupLanguageSelector();
// Translate the page
translatePage();
console.log('i18n initialized with locale:', i18n.currentLocale);
@@ -121,12 +112,10 @@ function t(key, replacements = {}) {
}
// Replace placeholders in the translation
let translation = value;
for (const [placeholder, replacement] of Object.entries(replacements)) {
translation = translation.replace(new RegExp(`__${placeholder}__`, 'g'), replacement);
}
return translation;
return Object.entries(replacements).reduce(
(str, [placeholder, replacement]) => str.replace(new RegExp(`__${placeholder}__`, 'g'), replacement),
value
);
}
/**
@@ -139,9 +128,7 @@ async function changeLanguage(locale) {
// Update language selector
const languageSelect = document.getElementById('languageSelect');
if (languageSelect) {
languageSelect.value = locale;
}
if (languageSelect) languageSelect.value = locale;
// Dispatch custom event to notify other scripts that language has changed
window.dispatchEvent(new CustomEvent('languageChanged', { detail: { locale } }));
@@ -152,61 +139,74 @@ async function changeLanguage(locale) {
*/
function setupLanguageSelector() {
const languageSelect = document.getElementById('languageSelect');
if (languageSelect) {
// Rebuild options dynamically
const locales =
Array.isArray(i18n.availableLocales) && i18n.availableLocales.length ? i18n.availableLocales : ['en'];
languageSelect.innerHTML = '';
locales.forEach((locale) => {
const option = document.createElement('option');
option.value = locale;
option.textContent = getLocaleLabel(locale);
languageSelect.appendChild(option);
});
if (!languageSelect) return;
// Set current locale
languageSelect.value = i18n.currentLocale;
// Rebuild options dynamically
const locales = i18n.availableLocales.length ? i18n.availableLocales : ['en'];
languageSelect.innerHTML = locales
.map((locale) => `<option value="${locale}">${getLocaleLabel(locale)}</option>`)
.join('');
// Add change event listener
languageSelect.addEventListener('change', async (e) => {
const newLocale = e.target.value;
await changeLanguage(newLocale);
});
// Set current locale and add change event listener
languageSelect.value = i18n.currentLocale;
languageSelect.addEventListener('change', (e) => changeLanguage(e.target.value));
}
/**
* Apply translation to an element based on attribute type
* @param {Element} element
* @param {string} key - Translation key
* @param {string} attrType - 'i18n', 'placeholder', or 'title'
*/
function applyTranslation(element, key, attrType) {
const translation = t(key);
const isInput = element.tagName === 'INPUT' || element.tagName === 'TEXTAREA';
switch (attrType) {
case 'i18n':
if (isInput && (element.type === 'button' || element.type === 'submit')) {
element.value = translation;
} else if (isInput) {
element.placeholder = translation;
} else {
element.textContent = translation;
}
break;
case 'placeholder':
element.placeholder = translation;
break;
case 'title':
setElementTitle(element, translation);
break;
}
}
/**
* Translate elements matching a selector
* @param {Element} root - Root element to search within
* @param {string} selector - CSS selector to find elements
* @param {string} attribute - Attribute name (data-i18n, data-i18n-placeholder, data-i18n-title)
* @param {string} attrType - Type for applyTranslation ('i18n', 'placeholder', 'title')
*/
function translateElements(root, selector, attribute, attrType) {
root.querySelectorAll(selector).forEach((element) => {
const key = element.getAttribute(attribute);
if (key) applyTranslation(element, key, attrType);
});
}
/**
* Translate all elements with data-i18n attribute
*/
function translatePage() {
// Translate elements with data-i18n attribute
document.querySelectorAll('[data-i18n]').forEach((element) => {
const key = element.getAttribute('data-i18n');
const translation = t(key);
const translationTypes = [
{ selector: '[data-i18n]', attribute: 'data-i18n', type: 'i18n' },
{ selector: '[data-i18n-placeholder]', attribute: 'data-i18n-placeholder', type: 'placeholder' },
{ selector: '[data-i18n-title]', attribute: 'data-i18n-title', type: 'title' },
];
// Update the appropriate property based on element type
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
if (element.type === 'button' || element.type === 'submit') {
element.value = translation;
} else {
element.placeholder = translation;
}
} else {
element.textContent = translation;
}
});
// Translate elements with data-i18n-placeholder attribute
document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => {
const key = element.getAttribute('data-i18n-placeholder');
element.placeholder = t(key);
});
// Translate elements with data-i18n-title attribute (for tooltips)
document.querySelectorAll('[data-i18n-title]').forEach((element) => {
const key = element.getAttribute('data-i18n-title');
const translatedTitle = t(key);
setElementTitle(element, translatedTitle);
translationTypes.forEach(({ selector, attribute, type }) => {
translateElements(document, selector, attribute, type);
});
// Update document title
@@ -251,13 +251,10 @@ function setElementTitle(element, translatedTitle) {
* Update custom translations for specific elements
*/
function updateCustomTranslations() {
// Update app title
const appTitle = document.getElementById('appTitle');
if (appTitle) appTitle.textContent = t('appTitle');
// Update app name
const appName = document.getElementById('appName');
if (appName) appName.textContent = t('appName');
['appTitle', 'appName'].forEach((id) => {
const element = document.getElementById(id);
if (element) element.textContent = t(id);
});
}
/**
@@ -279,35 +276,22 @@ function showTranslatedAlert(titleKey, textKey, icon = 'info') {
}
// Initialize i18n when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initI18n);
} else {
// DOM already loaded, initialize immediately
initI18n();
}
(document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', initI18n)
: initI18n());
// Re-translate when dynamic content is added (for mobile compatibility)
if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
// Element node
translateElement(node);
}
});
}
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) translateElement(node); // Element node
});
});
});
// Start observing after a short delay to allow initial setup
setTimeout(() => {
observer.observe(document.body, {
childList: true,
subtree: true,
});
}, 100);
setTimeout(() => observer.observe(document.body, { childList: true, subtree: true }), 100);
}
/**
@@ -315,59 +299,26 @@ if (typeof MutationObserver !== 'undefined') {
* @param {Element} element - The element to translate
*/
function translateElement(element) {
// Translate data-i18n
if (element.hasAttribute && element.hasAttribute('data-i18n')) {
const key = element.getAttribute('data-i18n');
const translation = t(key);
if (!element.hasAttribute) return;
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
if (element.type === 'button' || element.type === 'submit') {
element.value = translation;
} else {
element.placeholder = translation;
}
} else {
element.textContent = translation;
const translationTypes = [
{ attribute: 'data-i18n', type: 'i18n' },
{ attribute: 'data-i18n-placeholder', type: 'placeholder' },
{ attribute: 'data-i18n-title', type: 'title' },
];
// Translate the element itself
translationTypes.forEach(({ attribute, type }) => {
if (element.hasAttribute(attribute)) {
const key = element.getAttribute(attribute);
if (key) applyTranslation(element, key, type);
}
}
});
// Translate data-i18n-placeholder
if (element.hasAttribute && element.hasAttribute('data-i18n-placeholder')) {
const key = element.getAttribute('data-i18n-placeholder');
element.placeholder = t(key);
}
// Translate data-i18n-title
if (element.hasAttribute && element.hasAttribute('data-i18n-title')) {
const key = element.getAttribute('data-i18n-title');
setElementTitle(element, t(key));
}
// Recursively translate children
// Translate children
if (element.querySelectorAll) {
element.querySelectorAll('[data-i18n]').forEach((child) => {
const key = child.getAttribute('data-i18n');
const translation = t(key);
if (child.tagName === 'INPUT' || child.tagName === 'TEXTAREA') {
if (child.type === 'button' || child.type === 'submit') {
child.value = translation;
} else {
child.placeholder = translation;
}
} else {
child.textContent = translation;
}
});
element.querySelectorAll('[data-i18n-placeholder]').forEach((child) => {
const key = child.getAttribute('data-i18n-placeholder');
child.placeholder = t(key);
});
element.querySelectorAll('[data-i18n-title]').forEach((child) => {
const key = child.getAttribute('data-i18n-title');
setElementTitle(child, t(key));
translationTypes.forEach(({ attribute, type }) => {
translateElements(element, `[${attribute}]`, attribute, type);
});
}
}