File size: 4,243 Bytes
88c4c60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | "use client";
import { DEFAULT_LOCALE, LOCALE_COOKIE, normalizeLocale } from "./config";
let translationMap = {};
let currentLocale = DEFAULT_LOCALE;
let reloadCallbacks = [];
// Read locale from cookie
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);
}
// Load translation map
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 = {};
}
}
// Translate text - exported for use in components
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;
}
// Get current locale - exported for use in components
export function getCurrentLocale() {
return currentLocale;
}
// Register callback for locale changes
export function onLocaleChange(callback) {
reloadCallbacks.push(callback);
return () => {
reloadCallbacks = reloadCallbacks.filter(cb => cb !== callback);
};
}
// Process text node
function processTextNode(node) {
if (!node.nodeValue || !node.nodeValue.trim()) return;
// Skip if parent is script, style, code, or structural elements
const parent = node.parentElement;
if (!parent) return;
// Skip if parent or any ancestor has data-i18n-skip attribute
let element = parent;
while (element) {
if (element.hasAttribute && element.hasAttribute('data-i18n-skip')) {
return;
}
element = element.parentElement;
}
const tagName = parent.tagName?.toLowerCase();
// Skip elements that don't allow text nodes
const skipTags = [
"script", "style", "code", "pre",
"colgroup", "table", "thead", "tbody", "tfoot", "tr",
"select", "datalist", "optgroup"
];
if (skipTags.includes(tagName)) return;
// Store original text if not already stored
if (!node._originalText) {
node._originalText = node.nodeValue;
}
// Use original text for translation
const original = node._originalText;
const translated = translate(original);
// Only update if different to avoid unnecessary DOM mutations
if (translated !== node.nodeValue) {
node.nodeValue = translated;
}
}
// Process all text nodes in element
function processElement(element) {
if (!element) return;
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
const nodesToProcess = [];
// Collect all nodes first to avoid live collection issues
while ((node = walker.nextNode())) {
nodesToProcess.push(node);
}
// Process collected nodes
nodesToProcess.forEach(processTextNode);
}
// Initialize runtime i18n
export async function initRuntimeI18n() {
if (typeof window === "undefined") return;
currentLocale = getLocaleFromCookie();
await loadTranslations(currentLocale);
// Process existing DOM
processElement(document.body);
// Watch for new nodes
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,
});
}
// Reload translations when locale changes
export async function reloadTranslations() {
currentLocale = getLocaleFromCookie();
await loadTranslations(currentLocale);
// Notify all registered callbacks
reloadCallbacks.forEach(callback => callback());
// Re-process entire DOM (will use stored original text)
processElement(document.body);
}
|