feat(i18n): add static pre-rendering for multi-language support
- Add `generate-i18n-pages.mjs` script to pre-render localized HTML files at build time - Add `generate-sitemap.mjs` script to generate language-aware sitemap.xml - Create `navbar-simple.html` and `footer-simple.html` partials for simple mode - Update all 80+ tool pages with language routing support - Expand supported languages to 12: en, de, es, fr, it, pt, tr, vi, id, zh, zh-TW - Update i18n.ts with new language names and support configuration - Implement languageRouterPlugin in vite.config.ts for dev server routing - Update nginx.conf for production static file serving from language directories - Update TRANSLATION.md with new architecture documentation and language addition guide - Fix relative paths in 404.html for static deployment compatibility - Update package.json with new build scripts and dependencies - Improves SEO through static pre-rendering and proper sitemap generation
This commit is contained in:
@@ -33,7 +33,16 @@ export const languageNames: Record<SupportedLanguage, string> = {
|
||||
};
|
||||
|
||||
export const getLanguageFromUrl = (): SupportedLanguage => {
|
||||
const path = window.location.pathname;
|
||||
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
let path = window.location.pathname;
|
||||
|
||||
if (basePath && basePath !== '/' && path.startsWith(basePath)) {
|
||||
path = path.slice(basePath.length) || '/';
|
||||
}
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
const langMatch = path.match(
|
||||
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)(?:\/|$)/
|
||||
@@ -44,6 +53,7 @@ export const getLanguageFromUrl = (): SupportedLanguage => {
|
||||
) {
|
||||
return langMatch[1] as SupportedLanguage;
|
||||
}
|
||||
|
||||
const storedLang = localStorage.getItem('i18nextLng');
|
||||
if (
|
||||
storedLang &&
|
||||
@@ -94,22 +104,47 @@ export const t = (key: string, options?: Record<string, unknown>): string => {
|
||||
|
||||
export const changeLanguage = (lang: SupportedLanguage): void => {
|
||||
if (!supportedLanguages.includes(lang)) return;
|
||||
localStorage.setItem('i18nextLng', lang);
|
||||
|
||||
const currentPath = window.location.pathname;
|
||||
const currentLang = getLanguageFromUrl();
|
||||
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
let relativePath = window.location.pathname;
|
||||
|
||||
if (basePath && basePath !== '/' && relativePath.startsWith(basePath)) {
|
||||
relativePath = relativePath.slice(basePath.length) || '/';
|
||||
}
|
||||
|
||||
if (!relativePath.startsWith('/')) {
|
||||
relativePath = '/' + relativePath;
|
||||
}
|
||||
|
||||
let pagePathWithoutLang = relativePath;
|
||||
const langPrefixMatch = relativePath.match(
|
||||
/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)(\/.*)?$/
|
||||
);
|
||||
if (langPrefixMatch) {
|
||||
pagePathWithoutLang = langPrefixMatch[2] || '/';
|
||||
}
|
||||
|
||||
if (!pagePathWithoutLang.startsWith('/')) {
|
||||
pagePathWithoutLang = '/' + pagePathWithoutLang;
|
||||
}
|
||||
|
||||
let newRelativePath: string;
|
||||
if (lang === 'en') {
|
||||
newRelativePath = pagePathWithoutLang;
|
||||
} else {
|
||||
newRelativePath = `/${lang}${pagePathWithoutLang}`;
|
||||
}
|
||||
|
||||
let newPath: string;
|
||||
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}`;
|
||||
if (basePath && basePath !== '/') {
|
||||
newPath = basePath + newRelativePath;
|
||||
} else {
|
||||
newPath = `/${lang}${currentPath}`;
|
||||
newPath = newRelativePath;
|
||||
}
|
||||
|
||||
newPath = newPath.replace(/\/+/g, '/');
|
||||
|
||||
const newUrl = newPath + window.location.search + window.location.hash;
|
||||
window.location.href = newUrl;
|
||||
};
|
||||
@@ -153,13 +188,16 @@ export const rewriteLinks = (): void => {
|
||||
const currentLang = getLanguageFromUrl();
|
||||
if (currentLang === 'en') return;
|
||||
|
||||
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
||||
const links = document.querySelectorAll('a[href]');
|
||||
|
||||
links.forEach((link) => {
|
||||
const href = link.getAttribute('href');
|
||||
if (!href) return;
|
||||
|
||||
if (
|
||||
href.startsWith('http') ||
|
||||
href.startsWith('//') ||
|
||||
href.startsWith('mailto:') ||
|
||||
href.startsWith('tel:') ||
|
||||
href.startsWith('#') ||
|
||||
@@ -168,20 +206,39 @@ export const rewriteLinks = (): void => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (href.match(/^\/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)\//)) {
|
||||
if (href.includes('/assets/')) {
|
||||
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}`;
|
||||
|
||||
const langPrefixRegex = new RegExp(
|
||||
`^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt)(/|$)`
|
||||
);
|
||||
if (langPrefixRegex.test(href)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newHref: string;
|
||||
if (basePath && basePath !== '/' && href.startsWith(basePath)) {
|
||||
const pathAfterBase = href.slice(basePath.length);
|
||||
newHref = `${basePath}/${currentLang}${pathAfterBase}`;
|
||||
} else if (href.startsWith('/')) {
|
||||
if (basePath && basePath !== '/') {
|
||||
newHref = `${basePath}/${currentLang}${href}`;
|
||||
} else {
|
||||
newHref = `/${currentLang}${href}`;
|
||||
}
|
||||
} else if (href === '' || href === 'index.html') {
|
||||
if (basePath && basePath !== '/') {
|
||||
newHref = `${basePath}/${currentLang}/`;
|
||||
} else {
|
||||
newHref = `/${currentLang}/`;
|
||||
}
|
||||
} else {
|
||||
newHref = `${currentLang}/${href}`;
|
||||
}
|
||||
|
||||
newHref = newHref.replace(/([^:])\/+/g, '$1/');
|
||||
|
||||
link.setAttribute('href', newHref);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -28,28 +28,6 @@ const init = async () => {
|
||||
).toString();
|
||||
if (__SIMPLE_MODE__) {
|
||||
const hideBrandingSections = () => {
|
||||
const nav = document.querySelector('nav');
|
||||
if (nav) {
|
||||
nav.style.display = 'none';
|
||||
|
||||
const simpleNav = document.createElement('nav');
|
||||
simpleNav.className =
|
||||
'bg-gray-800 border-b border-gray-700 sticky top-0 z-30';
|
||||
simpleNav.innerHTML = `
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-start items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertBefore(simpleNav, document.body.firstChild);
|
||||
}
|
||||
|
||||
const heroSection = document.getElementById('hero-section');
|
||||
if (heroSection) {
|
||||
heroSection.style.display = 'none';
|
||||
@@ -99,48 +77,6 @@ const init = async () => {
|
||||
usedBySection.style.display = 'none';
|
||||
}
|
||||
|
||||
const footer = document.querySelector('footer');
|
||||
if (footer && !document.querySelector('[data-simple-footer]')) {
|
||||
footer.style.display = 'none';
|
||||
|
||||
const simpleFooter = document.createElement('footer');
|
||||
simpleFooter.className = 'mt-16 border-t-2 border-gray-700 py-8';
|
||||
simpleFooter.setAttribute('data-simple-footer', 'true');
|
||||
simpleFooter.innerHTML = `
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<div class="flex items-center mb-2">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
|
||||
<span class="text-white font-bold text-lg">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2026 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version-simple">${APP_VERSION}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div id="simple-mode-lang-switcher" class="flex-shrink-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(simpleFooter);
|
||||
|
||||
const langContainer = simpleFooter.querySelector(
|
||||
'#simple-mode-lang-switcher'
|
||||
);
|
||||
if (langContainer) {
|
||||
const switcher = createLanguageSwitcher();
|
||||
const dropdown = switcher.querySelector('div[role="menu"]');
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('mt-2');
|
||||
dropdown.classList.add('bottom-full', 'mb-2');
|
||||
}
|
||||
langContainer.appendChild(switcher);
|
||||
}
|
||||
}
|
||||
|
||||
const sectionDividers = document.querySelectorAll('.section-divider');
|
||||
sectionDividers.forEach((divider) => {
|
||||
(divider as HTMLElement).style.display = 'none';
|
||||
|
||||
@@ -1,50 +1,8 @@
|
||||
import { APP_VERSION } from '../../version.js';
|
||||
import { createLanguageSwitcher } from '../i18n/language-switcher.js';
|
||||
|
||||
// Handle simple mode footer replacement for tool pages
|
||||
// Handle simple mode adjustments for tool pages
|
||||
if (__SIMPLE_MODE__) {
|
||||
const footer = document.querySelector('footer');
|
||||
if (footer && !document.querySelector('[data-simple-footer]')) {
|
||||
footer.style.display = 'none';
|
||||
|
||||
const simpleFooter = document.createElement('footer');
|
||||
simpleFooter.className = 'mt-16 border-t-2 border-gray-700 py-8';
|
||||
simpleFooter.setAttribute('data-simple-footer', 'true');
|
||||
simpleFooter.innerHTML = `
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex items-center justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<div class="flex items-center mb-2">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
|
||||
<span class="text-white font-bold text-lg">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2026 BentoPDF. All rights reserved.
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs mt-2">
|
||||
Version <span id="app-version-simple">${APP_VERSION}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div id="simple-mode-lang-switcher" class="flex-shrink-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(simpleFooter);
|
||||
|
||||
const langContainer = simpleFooter.querySelector(
|
||||
'#simple-mode-lang-switcher'
|
||||
);
|
||||
if (langContainer) {
|
||||
const switcher = createLanguageSwitcher();
|
||||
const dropdown = switcher.querySelector('div[role="menu"]');
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('mt-2');
|
||||
dropdown.classList.add('bottom-full', 'mb-2');
|
||||
}
|
||||
langContainer.appendChild(switcher);
|
||||
}
|
||||
}
|
||||
|
||||
const sectionsToHide = [
|
||||
'How It Works',
|
||||
'Related PDF Tools',
|
||||
@@ -62,26 +20,19 @@ if (__SIMPLE_MODE__) {
|
||||
}
|
||||
});
|
||||
|
||||
const nav = document.querySelector('nav');
|
||||
if (nav && !document.querySelector('[data-simple-nav]')) {
|
||||
nav.style.display = 'none';
|
||||
const versionElement = document.getElementById('app-version-simple');
|
||||
if (versionElement) {
|
||||
versionElement.textContent = APP_VERSION;
|
||||
}
|
||||
|
||||
const simpleNav = document.createElement('nav');
|
||||
simpleNav.className =
|
||||
'bg-gray-800 border-b border-gray-700 sticky top-0 z-30';
|
||||
simpleNav.setAttribute('data-simple-nav', 'true');
|
||||
simpleNav.innerHTML = `
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-start items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
||||
<span class="text-white font-bold text-xl ml-2">
|
||||
<a href="/">BentoPDF</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertBefore(simpleNav, document.body.firstChild);
|
||||
const langContainer = document.getElementById('simple-mode-lang-switcher');
|
||||
if (langContainer) {
|
||||
const switcher = createLanguageSwitcher();
|
||||
const dropdown = switcher.querySelector('div[role="menu"]');
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('mt-2');
|
||||
dropdown.classList.add('bottom-full', 'mb-2');
|
||||
}
|
||||
langContainer.appendChild(switcher);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user