import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { legacyUiTextEnUS } from './legacy-ui' import { normalizeLocale } from './locale' const attributeNames = ['aria-label', 'placeholder', 'title'] const selector = '.admin-theme-root, .auth-shell' const reverseLegacyUiText = Object.fromEntries( Object.entries(legacyUiTextEnUS).map(([source, target]) => [target, source]), ) type LegacyTextPattern = { match: RegExp replace: (match: RegExpMatchArray) => string } const legacyTextPatternsEnUS: LegacyTextPattern[] = [ { match: /^结果\s+(.+)\s+条$/, replace: (match) => `Results ${match[1]}` }, { match: /^筛选\s+(.+)\s+项$/, replace: (match) => `${match[1]} filters` }, { match: /^共\s+(.+)\s+条结果$/, replace: (match) => `${match[1]} results` }, { match: /^(.+)\s+条新闻。$/, replace: (match) => `${match[1]} news items.` }, { match: /^(.+)\s+个历史快照,选择后查看该版本详情。$/, replace: (match) => `${match[1]} historical snapshots. Select one to view that version.` }, { match: /^(.+)\s+字段$/, replace: (match) => `${match[1]} fields` }, { match: /^(.+)\s+个源 \/ (.+)\s+个类型$/, replace: (match) => `${match[1]} sources / ${match[2]} categories` }, { match: /^(.+)\s+个来源$/, replace: (match) => `${match[1]} sources` }, { match: /^(.+)\s+个聚合项$/, replace: (match) => `${match[1]} aggregations` }, { match: /^(.+)\s+条$/, replace: (match) => `${match[1]} items` }, { match: /^(.+)\s+行$/, replace: (match) => `${match[1]} lines` }, { match: /^(.+)\s+次$/, replace: (match) => `${match[1]} times` }, { match: /^最后更新:\s*(.+)$/, replace: (match) => `Last updated: ${match[1]}` }, { match: /^任务已创建:\s*(.+)$/, replace: (match) => `Task created: ${match[1]}` }, { match: /^执行命令\s+(.+)$/, replace: (match) => `Command ${match[1]}` }, { match: /^任务 ID\s+(.+)$/, replace: (match) => `Task ID ${match[1]}` }, { match: /^触发已选\s+(.+)$/, replace: (match) => `Trigger selected ${match[1]}` }, { match: /^新闻直播源\s+(.+)$/, replace: (match) => `News stream source ${match[1]}` }, { match: /^新增新闻源\s+(.+)$/, replace: (match) => `New news source ${match[1]}` }, { match: /^最终指标:(.+)$/, replace: (match) => `Final metric: ${match[1]}` }, { match: /^指纹\s+(.+)$/, replace: (match) => `Fingerprint ${match[1]}` }, { match: /^首次\s+(.+)\s+·\s+最近\s+(.+)$/, replace: (match) => `First ${match[1]} · Latest ${match[2]}` }, { match: /^已导出\s+(.+)$/, replace: (match) => `Exported ${match[1]}` }, { match: /^(.+)\s+采集失败$/, replace: (match) => `${match[1]} collection failed` }, { match: /^(.+)\s+采集已取消$/, replace: (match) => `${match[1]} collection cancelled` }, ] const legacyTextPatternsZhCN: LegacyTextPattern[] = [ { match: /^Results\s+(.+)$/, replace: (match) => `结果 ${match[1]} 条` }, { match: /^(.+)\s+filters$/, replace: (match) => `筛选 ${match[1]} 项` }, { match: /^(.+)\s+results$/, replace: (match) => `共 ${match[1]} 条结果` }, { match: /^(.+)\s+news items\.$/, replace: (match) => `${match[1]} 条新闻。` }, { match: /^(.+)\s+historical snapshots\. Select one to view that version\.$/, replace: (match) => `${match[1]} 个历史快照,选择后查看该版本详情。` }, { match: /^(.+)\s+fields$/, replace: (match) => `${match[1]} 字段` }, { match: /^(.+)\s+sources \/ (.+)\s+categories$/, replace: (match) => `${match[1]} 个源 / ${match[2]} 个类型` }, { match: /^(.+)\s+sources$/, replace: (match) => `${match[1]} 个来源` }, { match: /^(.+)\s+aggregations$/, replace: (match) => `${match[1]} 个聚合项` }, { match: /^(.+)\s+items$/, replace: (match) => `${match[1]} 条` }, { match: /^(.+)\s+lines$/, replace: (match) => `${match[1]} 行` }, { match: /^(.+)\s+times$/, replace: (match) => `${match[1]} 次` }, { match: /^Last updated:\s*(.+)$/, replace: (match) => `最后更新: ${match[1]}` }, { match: /^Task created:\s*(.+)$/, replace: (match) => `任务已创建: ${match[1]}` }, { match: /^Command\s+(.+)$/, replace: (match) => `执行命令 ${match[1]}` }, { match: /^Task ID\s+(.+)$/, replace: (match) => `任务 ID ${match[1]}` }, { match: /^Trigger selected\s+(.+)$/, replace: (match) => `触发已选 ${match[1]}` }, { match: /^News stream source\s+(.+)$/, replace: (match) => `新闻直播源 ${match[1]}` }, { match: /^New news source\s+(.+)$/, replace: (match) => `新增新闻源 ${match[1]}` }, { match: /^Final metric:\s*(.+)$/, replace: (match) => `最终指标:${match[1]}` }, { match: /^Fingerprint\s+(.+)$/, replace: (match) => `指纹 ${match[1]}` }, { match: /^First\s+(.+)\s+·\s+Latest\s+(.+)$/, replace: (match) => `首次 ${match[1]} · 最近 ${match[2]}` }, { match: /^Exported\s+(.+)$/, replace: (match) => `已导出 ${match[1]}` }, { match: /^(.+)\s+collection failed$/, replace: (match) => `${match[1]} 采集失败` }, { match: /^(.+)\s+collection cancelled$/, replace: (match) => `${match[1]} 采集已取消` }, ] function preserveOuterWhitespace(source: string, replacement: string) { const leading = source.match(/^\s*/)?.[0] || '' const trailing = source.match(/\s*$/)?.[0] || '' return `${leading}${replacement}${trailing}` } function translatePatternText(value: string, locale: string) { const text = value.trim() if (!text || text.length > 160) return value const patterns = normalizeLocale(locale) === 'en-US' ? legacyTextPatternsEnUS : legacyTextPatternsZhCN for (const pattern of patterns) { const matched = text.match(pattern.match) if (matched) return preserveOuterWhitespace(value, pattern.replace(matched)) } return value } function translateText(value: string, locale: string) { const text = value.trim() if (!text) return value const dictionary = normalizeLocale(locale) === 'en-US' ? legacyUiTextEnUS : reverseLegacyUiText const replacement = dictionary[text] if (replacement) return preserveOuterWhitespace(value, replacement) return translatePatternText(value, locale) } function translateElementAttributes(element: Element, locale: string) { attributeNames.forEach((attributeName) => { const value = element.getAttribute(attributeName) if (!value) return const translated = translateText(value, locale) if (translated !== value) element.setAttribute(attributeName, translated) }) } function translateNodeText(root: Element, locale: string) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT) let node = walker.nextNode() while (node) { const value = node.textContent || '' const translated = translateText(value, locale) if (translated !== value) node.textContent = translated node = walker.nextNode() } } function translateRoot(root: Element, locale: string) { translateElementAttributes(root, locale) root.querySelectorAll('*').forEach((element) => translateElementAttributes(element, locale)) translateNodeText(root, locale) } export default function LegacyI18nBridge() { const { i18n } = useTranslation() const locale = normalizeLocale(i18n.resolvedLanguage || i18n.language) useEffect(() => { let frameId = 0 const translate = () => { document.querySelectorAll(selector).forEach((root) => translateRoot(root, locale)) } const scheduleTranslate = () => { window.cancelAnimationFrame(frameId) frameId = window.requestAnimationFrame(translate) } scheduleTranslate() const observer = new MutationObserver(scheduleTranslate) if (document.body) { observer.observe(document.body, { attributes: true, attributeFilter: attributeNames, characterData: true, childList: true, subtree: true, }) } return () => { window.cancelAnimationFrame(frameId) observer.disconnect() } }, [locale]) return null }