UNPKG

locize

Version:

This package adds the incontext editor to your i18next setup.

351 lines (302 loc) 9.82 kB
import { unwrap, containsHiddenMeta, containsHiddenStartMarker } from 'i18next-subliminal' import { store } from './store.js' import { uninstrumentedStore } from './uninstrumentedStore.js' import { validAttributes, ignoreElements } from './vars.js' import { getI18nMetaFromNode } from './utils' import './shims/uniqueID.js' let currentSourceLng let i18n let ignoreMergedEleUniqueIds = [] // ignore elements that are part of a translation containing html export function setImplementation (impl) { i18n = impl } function walk (node, func) { if (node.dataset && node.dataset.i18nextEditorElement === 'true') return // parse node func(node) // check if it is is any store - if so remove parent from uninstrumented // this avoids situations where a div has inner text (not instrumented) and some children that are instrumented showing a big box around all const instr = store.get(node.uniqueID) const uninstr = uninstrumentedStore.get(node.uniqueID) if (instr || uninstr) { const id = node.parentElement?.uniqueID uninstrumentedStore.remove(id, node.parentElement) } // parse children const children = node.childNodes for ( let i = 0; i < children.length; i++ // Children are siblings to each other ) { walk(children[i], func) } } function extractHiddenMeta (id, type, meta, children) { const { invisibleMeta, text } = meta if (!invisibleMeta || !invisibleMeta.key || !invisibleMeta.ns) return if (!currentSourceLng) currentSourceLng = i18n.getSourceLng() return { eleUniqueID: id, textType: type, children: children && children.map ? children.map(c => c.childIndex).join(',') : null, qualifiedKey: `${invisibleMeta.ns}:${invisibleMeta.key}`, ...invisibleMeta, extractedText: text, i18nTargetLng: i18n?.getLng(), i18nSourceLng: currentSourceLng, i18nRawText: { [`${invisibleMeta.lng}`]: invisibleMeta.source === 'translation' && i18n ? i18n?.getResource( invisibleMeta.lng, invisibleMeta.ns, invisibleMeta.key ) : null, [`${currentSourceLng}`]: invisibleMeta.source === 'translation' && i18n ? i18n?.getResource( currentSourceLng, invisibleMeta.ns, invisibleMeta.key ) : null } } } export function extractNodeMeta (id, type, nodeMeta = {}, text, children) { const meta = nodeMeta[type] if (!meta) return if (!currentSourceLng) currentSourceLng = i18n.getSourceLng() const i18nTargetLng = i18n.getLng() // console.warn('lng', i18nTargetLng) return { eleUniqueID: id, textType: type, children: children && children.map ? children.map(c => c.childIndex).join(',') : null, qualifiedKey: meta.key && (meta.ns || i18n?.getDefaultNS()) ? `${meta.ns || i18n?.getDefaultNS()}:${meta.key}` : null, key: meta.key, ns: meta.ns || i18n?.getDefaultNS(), extractedText: text, i18nTargetLng, i18nSourceLng: currentSourceLng, i18nRawText: { [`${i18nTargetLng}`]: i18n && meta.ns && meta.key ? i18n?.getResource(i18nTargetLng, meta.ns, meta.key) || text : text, [`${currentSourceLng}`]: i18n && meta.ns && meta.key ? i18n?.getResource(currentSourceLng, meta.ns, meta.key) : null } } } function containsOnlySpaces (str) { return /^\s*$/.test(str) } function storeIfQualifiedKey ( id, subliminal, type, nodeI18nMeta, node, children, txt ) { // if we got some key, ns from locize and stored that - reuse it on this run const stored = store.get(id) const storedMeta = (stored && stored.keys[`${type}`]) || {} const typeMeta = nodeI18nMeta[`${type}`] || {} if (!typeMeta.key && storedMeta.key) typeMeta.key = storedMeta.key if (!typeMeta.ns && storedMeta.ns) typeMeta.ns = storedMeta.ns nodeI18nMeta[`${type}`] = typeMeta // extract metas const meta = extractNodeMeta(id, type, nodeI18nMeta, txt, children) // if we can 100% identify that ns:key store - else uninstrumented if (meta.qualifiedKey) { store.save(id, null, type, meta, node, children) uninstrumentedStore.removeKey(id, type, node) } else { uninstrumentedStore.save(id, type, node, txt) } } function handleNode (node) { if (ignoreElements.indexOf(node.nodeName) > -1) return const nodeI18nMeta = getI18nMetaFromNode(node) let usedSubliminalForText = false // test for inner text - but ignore text for elements merged to html containing translation if (node.childNodes && !ignoreMergedEleUniqueIds.includes(node.uniqueID)) { let merge = [] node.childNodes.forEach((child, i) => { if (merge.length && child.nodeName !== '#text') { ignoreMergedEleUniqueIds.push(child.uniqueID) merge.push({ childIndex: i, child }) } if (child.nodeName !== '#text') return const txt = child.textContent if (containsOnlySpaces(txt)) return const hasHiddenMeta = containsHiddenMeta(txt) const hasHiddenStartMarker = containsHiddenStartMarker(txt) if (hasHiddenMeta) usedSubliminalForText = true // console.warn( // 'child', // child, // child.nodeName, // child.innerText, // hasHiddenStartMarker, // hasHiddenMeta // ) if (hasHiddenStartMarker && hasHiddenMeta) { const meta = unwrap(txt) uninstrumentedStore.remove(node.uniqueID, node) // might be instrumented later and already in uninstrumentedStore - so remove it there first store.save( node.uniqueID, meta.invisibleMeta, 'text', extractHiddenMeta(node.uniqueID, 'text', meta), node ) } else if (hasHiddenStartMarker) { merge.push({ childIndex: i, child, text: txt }) } else if (merge.length && !hasHiddenMeta) { merge.push({ childIndex: i, child, text: txt }) } else if (merge.length && hasHiddenMeta) { merge.push({ childIndex: i, child, text: txt }) const meta = unwrap( merge.reduce((mem, item) => { return mem + item.text }, '') ) uninstrumentedStore.removeKey(node.uniqueID, 'html', node, txt) // might be instrumented later and already in uninstrumentedStore - so remove it there first store.save( node.uniqueID, meta.invisibleMeta, 'html', extractHiddenMeta(node.uniqueID, 'html', meta, merge), node, merge ) // reset merge = [] } }) // no subliminal in text if (!usedSubliminalForText) { node.childNodes.forEach((child, i) => { if (merge.length && child.nodeName !== '#text') { ignoreMergedEleUniqueIds.push(child.uniqueID) // merge.push({ childIndex: i, child }) // will be pushed regular below with txt } // if (child.nodeName !== '#text') return const txt = child.textContent // if (containsOnlySpaces(txt)) return // merge and add data-i18n=[html]key if ( nodeI18nMeta && nodeI18nMeta.html && i < node.childNodes.length - 1 ) { merge.push({ childIndex: i, child, text: txt }) } else if ( nodeI18nMeta && nodeI18nMeta.html && i === node.childNodes.length - 1 ) { merge.push({ childIndex: i, child, text: txt }) storeIfQualifiedKey( node.uniqueID, null, 'html', nodeI18nMeta, node, merge, node.innerHTML ) // reset merge = [] } else if (txt) { // console.warn( // 'nodeI18nMeta', // txt, // nodeI18nMeta, // hasHiddenMeta, // hasHiddenStartMarker // ) // add data-i18n=key (inner text) if (nodeI18nMeta && nodeI18nMeta.text) { storeIfQualifiedKey( node.uniqueID, null, 'text', nodeI18nMeta, node, undefined, txt ) } else if (child.nodeName === '#text' && !containsOnlySpaces(txt)) { // if no metas at all and is a text node that is not just some spaces (html indent) // add to uninstrumented for a lookup (locize search) uninstrumentedStore.save(node.uniqueID, 'text', node, txt) } } }) } } // test attibutes if (!node.getAttribute) return validAttributes.forEach(attr => { const txt = node.getAttribute(attr) if (containsHiddenMeta(txt)) { const meta = unwrap(txt) uninstrumentedStore.removeKey(node.uniqueID, attr, node) // might be instrumented later and already in uninstrumentedStore - so remove it there first store.save( node.uniqueID, meta.invisibleMeta, attr, extractHiddenMeta(node.uniqueID, `${attr}`, meta), node ) } else if (txt) { if (nodeI18nMeta && nodeI18nMeta[attr]) { storeIfQualifiedKey( node.uniqueID, null, attr, nodeI18nMeta, node, undefined, txt ) } else { uninstrumentedStore.save(node.uniqueID, attr, node, txt) } } }) // console.warn('store', store) // TODO: how to handle react Trans things?!? } export function parseTree (node) { // reset currentSourceLng = undefined // walk walk(node, handleNode) store.clean() // cleanup ignoreMergedEleUniqueIds = [] return store.data }