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:
alam00000
2026-01-14 21:04:56 +05:30
parent 90346d7ea9
commit abf7ae8a00
126 changed files with 25187 additions and 1447 deletions

View File

@@ -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);
});
};