| "use client"; |
|
|
| import { DEFAULT_LOCALE, LOCALE_COOKIE, normalizeLocale } from "./config"; |
|
|
| let translationMap = {}; |
| let currentLocale = DEFAULT_LOCALE; |
| let reloadCallbacks = []; |
|
|
| |
| function getLocaleFromCookie() { |
| if (typeof document === "undefined") return DEFAULT_LOCALE; |
| const cookie = document.cookie |
| .split(";") |
| .find((c) => c.trim().startsWith(`${LOCALE_COOKIE}=`)); |
| const value = cookie ? decodeURIComponent(cookie.split("=")[1]) : DEFAULT_LOCALE; |
| return normalizeLocale(value); |
| } |
|
|
| |
| async function loadTranslations(locale) { |
| if (locale === "en") { |
| translationMap = {}; |
| return; |
| } |
| |
| try { |
| const response = await fetch(`/i18n/literals/${locale}.json`); |
| translationMap = await response.json(); |
| } catch (err) { |
| console.error("Failed to load translations:", err); |
| translationMap = {}; |
| } |
| } |
|
|
| |
| export function translate(text) { |
| if (!text || typeof text !== "string") return text; |
| const trimmed = text.trim(); |
| if (!trimmed) return text; |
| if (currentLocale === "en") return text; |
| return translationMap[trimmed] || text; |
| } |
|
|
| |
| export function getCurrentLocale() { |
| return currentLocale; |
| } |
|
|
| |
| export function onLocaleChange(callback) { |
| reloadCallbacks.push(callback); |
| return () => { |
| reloadCallbacks = reloadCallbacks.filter(cb => cb !== callback); |
| }; |
| } |
|
|
| |
| function processTextNode(node) { |
| if (!node.nodeValue || !node.nodeValue.trim()) return; |
| |
| |
| const parent = node.parentElement; |
| if (!parent) return; |
| |
| |
| let element = parent; |
| while (element) { |
| if (element.hasAttribute && element.hasAttribute('data-i18n-skip')) { |
| return; |
| } |
| element = element.parentElement; |
| } |
| |
| const tagName = parent.tagName?.toLowerCase(); |
| |
| |
| const skipTags = [ |
| "script", "style", "code", "pre", |
| "colgroup", "table", "thead", "tbody", "tfoot", "tr", |
| "select", "datalist", "optgroup" |
| ]; |
| |
| if (skipTags.includes(tagName)) return; |
| |
| |
| if (!node._originalText) { |
| node._originalText = node.nodeValue; |
| } |
| |
| |
| const original = node._originalText; |
| const translated = translate(original); |
| |
| |
| if (translated !== node.nodeValue) { |
| node.nodeValue = translated; |
| } |
| } |
|
|
| |
| function processElement(element) { |
| if (!element) return; |
| |
| const walker = document.createTreeWalker( |
| element, |
| NodeFilter.SHOW_TEXT, |
| null, |
| false |
| ); |
| |
| let node; |
| const nodesToProcess = []; |
| |
| |
| while ((node = walker.nextNode())) { |
| nodesToProcess.push(node); |
| } |
| |
| |
| nodesToProcess.forEach(processTextNode); |
| } |
|
|
| |
| export async function initRuntimeI18n() { |
| if (typeof window === "undefined") return; |
| |
| currentLocale = getLocaleFromCookie(); |
| await loadTranslations(currentLocale); |
| |
| |
| processElement(document.body); |
| |
| |
| const observer = new MutationObserver((mutations) => { |
| mutations.forEach((mutation) => { |
| mutation.addedNodes.forEach((node) => { |
| if (node.nodeType === Node.ELEMENT_NODE) { |
| processElement(node); |
| } else if (node.nodeType === Node.TEXT_NODE) { |
| processTextNode(node); |
| } |
| }); |
| }); |
| }); |
| |
| observer.observe(document.body, { |
| childList: true, |
| subtree: true, |
| }); |
| } |
|
|
| |
| export async function reloadTranslations() { |
| currentLocale = getLocaleFromCookie(); |
| await loadTranslations(currentLocale); |
| |
| |
| reloadCallbacks.forEach(callback => callback()); |
| |
| |
| processElement(document.body); |
| } |
|
|