158 lines
4.5 KiB
JavaScript
158 lines
4.5 KiB
JavaScript
import { nextTick, onBeforeUnmount, onMounted, watch } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
|
|
import { DEFAULT_LOCALE } from 'src/i18n/languages'
|
|
import { useLocaleStore } from 'src/stores/localeStore'
|
|
import { useRuntimeTranslationStore } from 'src/stores/runtimeTranslationStore'
|
|
|
|
const textOriginals = new WeakMap()
|
|
const attrOriginals = new WeakMap()
|
|
const TRANSLATABLE_ATTRS = ['placeholder', 'title', 'aria-label']
|
|
const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA'])
|
|
|
|
function normalizeRuntimeText (value) {
|
|
return String(value || '').trim().replace(/\s+/g, ' ')
|
|
}
|
|
|
|
function preserveOuterWhitespace (original, translated) {
|
|
const text = String(original || '')
|
|
const leading = text.match(/^\s*/)?.[0] || ''
|
|
const trailing = text.match(/\s*$/)?.[0] || ''
|
|
return `${leading}${translated}${trailing}`
|
|
}
|
|
|
|
function shouldSkipNode (node) {
|
|
const parent = node?.parentElement
|
|
if (!parent) return true
|
|
if (SKIP_TAGS.has(parent.tagName)) return true
|
|
if (parent.closest?.('[data-no-runtime-i18n="true"]')) return true
|
|
return false
|
|
}
|
|
|
|
export function useRuntimeDomTranslations () {
|
|
const route = useRoute()
|
|
const localeStore = useLocaleStore()
|
|
const runtimeTranslations = useRuntimeTranslationStore()
|
|
let observer = null
|
|
let applyTimer = null
|
|
|
|
function translateTextNode (node) {
|
|
if (shouldSkipNode(node)) return
|
|
const current = String(node.nodeValue || '')
|
|
if (!normalizeRuntimeText(current)) return
|
|
|
|
if (!textOriginals.has(node)) {
|
|
textOriginals.set(node, current)
|
|
}
|
|
const original = textOriginals.get(node)
|
|
const key = normalizeRuntimeText(original)
|
|
const translated = localeStore.locale === DEFAULT_LOCALE
|
|
? key
|
|
: runtimeTranslations.byText[key]
|
|
|
|
const nextValue = preserveOuterWhitespace(original, translated || key)
|
|
if (node.nodeValue !== nextValue) {
|
|
node.nodeValue = nextValue
|
|
}
|
|
}
|
|
|
|
function translateElementAttrs (el) {
|
|
if (!el || el.nodeType !== Node.ELEMENT_NODE) return
|
|
if (el.closest?.('[data-no-runtime-i18n="true"]')) return
|
|
let originals = attrOriginals.get(el)
|
|
if (!originals) {
|
|
originals = {}
|
|
attrOriginals.set(el, originals)
|
|
}
|
|
|
|
for (const attr of TRANSLATABLE_ATTRS) {
|
|
if (!el.hasAttribute(attr)) continue
|
|
const current = el.getAttribute(attr)
|
|
if (!normalizeRuntimeText(current)) continue
|
|
if (!Object.prototype.hasOwnProperty.call(originals, attr)) {
|
|
originals[attr] = current
|
|
}
|
|
const key = normalizeRuntimeText(originals[attr])
|
|
const translated = localeStore.locale === DEFAULT_LOCALE
|
|
? key
|
|
: runtimeTranslations.byText[key]
|
|
const nextValue = translated || key
|
|
if (el.getAttribute(attr) !== nextValue) {
|
|
el.setAttribute(attr, nextValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
function translateRoot (root = document.body) {
|
|
if (typeof document === 'undefined' || !root) return
|
|
|
|
const walker = document.createTreeWalker(
|
|
root,
|
|
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
|
|
{
|
|
acceptNode (node) {
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
return normalizeRuntimeText(node.nodeValue) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
|
|
}
|
|
return NodeFilter.FILTER_ACCEPT
|
|
}
|
|
}
|
|
)
|
|
|
|
let node = walker.currentNode
|
|
while (node) {
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
translateTextNode(node)
|
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
translateElementAttrs(node)
|
|
}
|
|
node = walker.nextNode()
|
|
}
|
|
}
|
|
|
|
function scheduleApply () {
|
|
if (applyTimer) clearTimeout(applyTimer)
|
|
applyTimer = setTimeout(async () => {
|
|
applyTimer = null
|
|
await nextTick()
|
|
translateRoot(document.body)
|
|
}, 30)
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await runtimeTranslations.loadLocale(localeStore.locale)
|
|
scheduleApply()
|
|
observer = new MutationObserver(() => scheduleApply())
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
characterData: true,
|
|
attributes: true,
|
|
attributeFilter: TRANSLATABLE_ATTRS
|
|
})
|
|
})
|
|
|
|
watch(
|
|
() => localeStore.locale,
|
|
async (locale) => {
|
|
await runtimeTranslations.loadLocale(locale)
|
|
scheduleApply()
|
|
}
|
|
)
|
|
|
|
watch(
|
|
() => route.fullPath,
|
|
() => scheduleApply()
|
|
)
|
|
|
|
watch(
|
|
() => runtimeTranslations.version,
|
|
() => scheduleApply()
|
|
)
|
|
|
|
onBeforeUnmount(() => {
|
|
if (observer) observer.disconnect()
|
|
if (applyTimer) clearTimeout(applyTimer)
|
|
})
|
|
}
|