Files
planet/frontend/src/i18n/LegacyI18nBridge.tsx
linkong fbecf30513
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.73.0
2026-06-29 17:04:05 +08:00

160 lines
7.7 KiB
TypeScript

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
}