diff --git a/src/js/i18n/i18n.ts b/src/js/i18n/i18n.ts index b5eb5a0..aa41ac5 100644 --- a/src/js/i18n/i18n.ts +++ b/src/js/i18n/i18n.ts @@ -3,155 +3,246 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import HttpBackend from 'i18next-http-backend'; // Supported languages -export const supportedLanguages = ['en', 'de', 'nl', 'vi', 'zh'] as const; +export const supportedLanguages = [ + 'en', + 'fr', + 'de', + 'es', + 'zh', + 'zh-TW', + 'vi', + 'tr', + 'id', + 'it', + 'pt', + 'nl', +] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; export const languageNames: Record = { - en: 'English', - de: 'Deutsch', - nl: 'Nederlands', - vi: 'Tiếng Việt', - zh: '中文', + en: 'English', + fr: 'Français', + de: 'Deutsch', + es: 'Español', + zh: '中文', + 'zh-TW': '繁體中文(台灣)', + vi: 'Tiếng Việt', + tr: 'Türkçe', + id: 'Bahasa Indonesia', + it: 'Italiano', + pt: 'Português', + nl: 'Nederlands', }; export const getLanguageFromUrl = (): SupportedLanguage => { - const path = window.location.pathname; - const langMatch = path.match(/^\/(en|de|nl|vi|zh)(?:\/|$)/); - 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; - } + const basePath = import.meta.env.BASE_URL.replace(/\/$/, ''); + let path = window.location.pathname; - return 'en'; + 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|nl)(?:\/|$)/ + ); + 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 => { - if (initialized) return i18next; + if (initialized) return i18next; - const currentLang = getLanguageFromUrl(); + 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, - }, - }); + 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.replace(/\/?$/, '/')}locales/{{lng}}/{{ns}}.json`, + }, + detection: { + order: ['path', 'localStorage', 'navigator'], + lookupFromPathIndex: 0, + caches: ['localStorage'], + }, + interpolation: { + escapeValue: false, + }, + }); - initialized = true; - return i18next; + initialized = true; + return i18next; }; export const t = (key: string, options?: Record): string => { - return i18next.t(key, options); + return i18next.t(key, options); }; export const changeLanguage = (lang: SupportedLanguage): void => { - if (!supportedLanguages.includes(lang)) return; + 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; - let newPath: string; - if (currentPath.match(/^\/(en|de|zh|vi)\//)) { - newPath = currentPath.replace(/^\/(en|de|nl|vi|zh)\//, `/${lang}/`); - } else if (currentPath.match(/^\/(en|de|nl|vi|zh)$/)) { - newPath = `/${lang}`; - } else { - newPath = `/${lang}${currentPath}`; - } + if (basePath && basePath !== '/' && relativePath.startsWith(basePath)) { + relativePath = relativePath.slice(basePath.length) || '/'; + } - const newUrl = newPath + window.location.search + window.location.hash; - window.location.href = newUrl; + 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 (basePath && basePath !== '/') { + newPath = basePath + newRelativePath; + } else { + newPath = newRelativePath; + } + + newPath = newPath.replace(/\/+/g, '/'); + + 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]').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-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.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; + document.documentElement.lang = i18next.language; }; export const rewriteLinks = (): void => { - const currentLang = getLanguageFromUrl(); - if (currentLang === 'en') return; + const currentLang = getLanguageFromUrl(); + if (currentLang === 'en') return; - const links = document.querySelectorAll('a[href]'); - links.forEach((link) => { - const href = link.getAttribute('href'); - if (!href) return; + const basePath = import.meta.env.BASE_URL.replace(/\/$/, ''); + const links = document.querySelectorAll('a[href]'); - if (href.startsWith('http') || - href.startsWith('mailto:') || - href.startsWith('tel:') || - href.startsWith('#') || - href.startsWith('javascript:')) { - return; - } + links.forEach((link) => { + const href = link.getAttribute('href'); + if (!href) return; - if (href.match(/^\/(en|de|zh|vi)\//)) { - 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}`; - } + if ( + href.startsWith('http') || + href.startsWith('//') || + href.startsWith('mailto:') || + href.startsWith('tel:') || + href.startsWith('#') || + href.startsWith('javascript:') + ) { + return; + } - link.setAttribute('href', newHref); - }); + if (href.includes('/assets/')) { + return; + } + + const langPrefixRegex = new RegExp( + `^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/(en|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl)(/|$)` + ); + 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); + }); }; export default i18next;