feat: Simple Mode language routing and translation improvements
## Simple Mode Enhancements - Add `simple-index.html` as dedicated homepage for Simple Mode - Hide marketing sections (FAQ, How It Works, Related Tools) on tool pages - Add simplified navbar and footer for tool pages in Simple Mode - Configure vite preview server to handle language-prefixed URLs ## Language Routing - Add middleware to rewrite language-prefixed URLs (e.g., /de/merge-pdf.html) - Support all languages: en, de, es, fr, id, it, pt, tr, vi, zh, zh-TW - Create .htaccess with internal rewrites for Apache/Hostinger hosting ## Translation Updates - Add missing translations for digitalSignPdf, validateSignaturePdf, emailToPdf, fontToOutline, deskewPdf to es, pt, tr, zh-TW - Add Digital Signature and Validate Signature to homepage translation keys - Fix language regex patterns to include all supported languages - Fix typo in encrypt-pdf.html
This commit is contained in:
@@ -14,6 +14,7 @@ export const supportedLanguages = [
|
||||
'tr',
|
||||
'id',
|
||||
'it',
|
||||
'pt',
|
||||
] as const;
|
||||
export type SupportedLanguage = (typeof supportedLanguages)[number];
|
||||
|
||||
@@ -23,17 +24,20 @@ export const languageNames: Record<SupportedLanguage, string> = {
|
||||
de: 'Deutsch',
|
||||
es: 'Español',
|
||||
zh: '中文',
|
||||
"zh-TW": '繁體中文(台灣)',
|
||||
'zh-TW': '繁體中文(台灣)',
|
||||
vi: 'Tiếng Việt',
|
||||
tr: 'Türkçe',
|
||||
id: 'Bahasa Indonesia',
|
||||
it: 'Italiano',
|
||||
pt: 'Português',
|
||||
};
|
||||
|
||||
export const getLanguageFromUrl = (): SupportedLanguage => {
|
||||
const path = window.location.pathname;
|
||||
|
||||
const langMatch = path.match(/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it)(?:\/|$)/);
|
||||
const langMatch = path.match(
|
||||
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)(?:\/|$)/
|
||||
);
|
||||
if (
|
||||
langMatch &&
|
||||
supportedLanguages.includes(langMatch[1] as SupportedLanguage)
|
||||
@@ -95,9 +99,12 @@ export const changeLanguage = (lang: SupportedLanguage): void => {
|
||||
const currentLang = getLanguageFromUrl();
|
||||
|
||||
let newPath: string;
|
||||
if (currentPath.match(/^\/(en|fr|de|zh|zh-TW|vi|tr|id|it)\//)) {
|
||||
newPath = currentPath.replace(/^\/(en|fr|de|zh|zh-TW|vi|tr|id|it)\//, `/${lang}/`);
|
||||
} else if (currentPath.match(/^\/(en|fr|de|zh|zh-TW|vi|tr|id|it)$/)) {
|
||||
if (currentPath.match(/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)\//)) {
|
||||
newPath = currentPath.replace(
|
||||
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)\//,
|
||||
`/${lang}/`
|
||||
);
|
||||
} else if (currentPath.match(/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)$/)) {
|
||||
newPath = `/${lang}`;
|
||||
} else {
|
||||
newPath = `/${lang}${currentPath}`;
|
||||
@@ -161,7 +168,7 @@ export const rewriteLinks = (): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (href.match(/^\/(en|fr|de|zh|zh-TW|vi|tr|id|it)\//)) {
|
||||
if (href.match(/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)\//)) {
|
||||
return;
|
||||
}
|
||||
let newHref: string;
|
||||
|
||||
@@ -1,142 +1,157 @@
|
||||
import {
|
||||
supportedLanguages,
|
||||
languageNames,
|
||||
getLanguageFromUrl,
|
||||
changeLanguage,
|
||||
supportedLanguages,
|
||||
languageNames,
|
||||
getLanguageFromUrl,
|
||||
changeLanguage,
|
||||
} from './i18n';
|
||||
|
||||
export const createLanguageSwitcher = (): HTMLElement => {
|
||||
const currentLang = getLanguageFromUrl();
|
||||
const currentLang = getLanguageFromUrl();
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'relative';
|
||||
container.id = 'language-switcher';
|
||||
const container = document.createElement('div');
|
||||
container.className = 'relative';
|
||||
container.id = 'language-switcher';
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = `
|
||||
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');
|
||||
button.setAttribute('aria-haspopup', 'true');
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
|
||||
const textSpan = document.createElement('span');
|
||||
textSpan.className = 'font-medium';
|
||||
textSpan.textContent = languageNames[currentLang];
|
||||
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>';
|
||||
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);
|
||||
button.appendChild(textSpan);
|
||||
button.appendChild(chevron);
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = `
|
||||
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');
|
||||
dropdown.setAttribute('role', 'menu');
|
||||
|
||||
supportedLanguages.forEach((lang) => {
|
||||
const option = document.createElement('button');
|
||||
option.className = `
|
||||
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');
|
||||
option.setAttribute('role', 'menuitem');
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.textContent = languageNames[lang];
|
||||
const name = document.createElement('span');
|
||||
name.textContent = languageNames[lang];
|
||||
|
||||
option.appendChild(name);
|
||||
option.appendChild(name);
|
||||
|
||||
|
||||
|
||||
option.addEventListener('click', () => {
|
||||
if (lang !== currentLang) {
|
||||
changeLanguage(lang);
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.appendChild(option);
|
||||
option.addEventListener('click', () => {
|
||||
if (lang !== currentLang) {
|
||||
changeLanguage(lang);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(button);
|
||||
container.appendChild(dropdown);
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isExpanded = button.getAttribute('aria-expanded') === 'true';
|
||||
button.setAttribute('aria-expanded', (!isExpanded).toString());
|
||||
dropdown.classList.toggle('hidden');
|
||||
});
|
||||
container.appendChild(button);
|
||||
container.appendChild(dropdown);
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
button.setAttribute('aria-expanded', 'false');
|
||||
dropdown.classList.add('hidden');
|
||||
});
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isExpanded = button.getAttribute('aria-expanded') === 'true';
|
||||
button.setAttribute('aria-expanded', (!isExpanded).toString());
|
||||
dropdown.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
return container;
|
||||
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 simpleModeContainer = document.getElementById(
|
||||
'simple-mode-language-switcher'
|
||||
);
|
||||
if (simpleModeContainer) {
|
||||
const switcher = createLanguageSwitcher();
|
||||
simpleModeContainer.appendChild(switcher);
|
||||
return;
|
||||
}
|
||||
|
||||
const headings = footer.querySelectorAll('h3');
|
||||
let followUsColumn: HTMLElement | null = null;
|
||||
const footer = document.querySelector('footer');
|
||||
if (!footer) return;
|
||||
|
||||
headings.forEach((h3) => {
|
||||
if (h3.textContent?.trim() === 'Follow Us' || h3.textContent?.trim() === 'Folgen Sie uns' || h3.textContent?.trim() === 'Theo dõi chúng tôi') {
|
||||
followUsColumn = h3.parentElement;
|
||||
}
|
||||
});
|
||||
const headings = footer.querySelectorAll('h3');
|
||||
let followUsColumn: HTMLElement | null = null;
|
||||
|
||||
if (followUsColumn) {
|
||||
const socialIconsContainer = followUsColumn.querySelector('.space-x-4');
|
||||
headings.forEach((h3) => {
|
||||
if (
|
||||
h3.textContent?.trim() === 'Follow Us' ||
|
||||
h3.textContent?.trim() === 'Folgen Sie uns' ||
|
||||
h3.textContent?.trim() === 'Theo dõi chúng tôi'
|
||||
) {
|
||||
followUsColumn = h3.parentElement;
|
||||
}
|
||||
});
|
||||
|
||||
if (socialIconsContainer) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'inline-flex flex-col gap-4'; // gap-4 adds space between icons and switcher
|
||||
if (followUsColumn) {
|
||||
const socialIconsContainer = followUsColumn.querySelector('.space-x-4');
|
||||
|
||||
socialIconsContainer.parentNode?.insertBefore(wrapper, socialIconsContainer);
|
||||
if (socialIconsContainer) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'inline-flex flex-col gap-4'; // gap-4 adds space between icons and switcher
|
||||
|
||||
wrapper.appendChild(socialIconsContainer);
|
||||
const switcher = createLanguageSwitcher();
|
||||
socialIconsContainer.parentNode?.insertBefore(
|
||||
wrapper,
|
||||
socialIconsContainer
|
||||
);
|
||||
|
||||
switcher.className = 'relative w-full';
|
||||
wrapper.appendChild(socialIconsContainer);
|
||||
const switcher = createLanguageSwitcher();
|
||||
|
||||
const button = switcher.querySelector('button');
|
||||
if (button) {
|
||||
button.className = `
|
||||
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');
|
||||
}
|
||||
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);
|
||||
}
|
||||
wrapper.appendChild(switcher);
|
||||
} else {
|
||||
const switcherContainer = document.createElement('div');
|
||||
switcherContainer.className = 'mt-4 w-full';
|
||||
const switcher = createLanguageSwitcher();
|
||||
switcherContainer.appendChild(switcher);
|
||||
followUsColumn.appendChild(switcherContainer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user