UNPKG

locize

Version:

This package adds the incontext editor to your i18next setup.

163 lines (141 loc) 6 kB
import { parseTree, setImplementation } from './parser.js' import { createObserver } from './observer.js' import { startMouseTracking } from './ui/mouseDistance.js' import { initDragElement, initResizeElement } from './ui/popup.js' import { Popup, popupId } from './ui/elements/popup.js' import { getIframeUrl } from './vars.js' import { api } from './api/index.js' import { isInIframe, getQsParameterByName } from './utils.js' import * as implementations from './implementations/index.js' const dummyImplementation = implementations.dummy.getImplementation() // eslint-disable-next-line no-unused-vars let data = [] export function start ( implementation = dummyImplementation, opt = { show: false, qsProp: 'incontext' } ) { if (typeof document === 'undefined') return const showInContext = opt.show || getQsParameterByName(opt.qsProp || 'incontext') === 'true' // get locize id, version const scriptEle = document.getElementById('locize') let config = {} ;['projectId', 'version'].forEach(attr => { if (!scriptEle) return let value = scriptEle.getAttribute(attr.toLowerCase()) || scriptEle.getAttribute('data-' + attr.toLowerCase()) if (value === 'true') value = true if (value === 'false') value = false if (value !== undefined && value !== null) config[attr] = value }) config = { ...implementation.getLocizeDetails(), ...config, ...opt } // init stuff api.config = config api.init(implementation) setImplementation(implementation) // start stuff implementation?.bindLanguageChange(lng => { api.sendCurrentTargetLanguage(implementation.getLng()) }) function continueToStart () { // don't show if not in iframe and no qs if (!isInIframe && !showInContext) return const observer = createObserver(document.body, eles => { eles.forEach(ele => { data = parseTree(ele) }) api.sendCurrentParsedContent() }) observer.start() startMouseTracking(observer) // Append the popup to <html> (sibling of <body>) rather than into // <body>, and watch for hydration-recovery removal. // // When a framework hydrates the whole document via // `hydrateRoot(document, ...)` (React Router v7 framework mode and // similar SSR setups) and the server / client render diverge — // which i18next-subliminal can cause on its own, by wrapping // client-side translation values with invisible Unicode markers // that are absent in the SSR pass — React's // `clearContainerSparingly` runs during commit and removes any DOM // children it doesn't own. It recurses through HTML / HEAD / BODY // but removes everything else at every level, so placing the // popup as a child of <html> is not enough on its own to escape // the sweep when recovery happens at the document level. // // We therefore also install a bounded MutationObserver that // re-attaches the same popup element if it's removed during the // first few seconds after init. Re-attaching the same element // (not a clone) preserves all listeners and message ports that // `initDragElement` / `initResizeElement` and the iframe URL // contents set up. The observer disconnects after a short window // so an intentional teardown later in the session is unaffected. if (!isInIframe && !document.getElementById(popupId)) { const popupEl = Popup(getIframeUrl(), () => { // The iframe `load` event fires on every navigation, including // the implicit re-navigation that the browser performs when our // resurrection observer re-attaches the popup after a hydration- // mismatch recovery. Each load is effectively a brand-new editor // session: the cached `api.source` reference points at a // discarded window, any in-flight retry interval is stale, and // the handshake needs to start over. Wipe that state before // calling requestInitialize so the new contentWindow is the one // that receives the message. api.source = document.getElementById('i18next-editor-iframe')?.contentWindow api.initialized = false if (api.initInterval) { clearInterval(api.initInterval) delete api.initInterval } api.requestInitialize(config) }) document.documentElement.append(popupEl) initDragElement() initResizeElement() if (typeof MutationObserver === 'function') { const MAX_REATTACHMENTS = 5 const WATCH_DURATION_MS = 10000 let reattachments = 0 // eslint-disable-next-line no-undef const watcher = new MutationObserver(() => { if (document.getElementById(popupId)) return if (reattachments >= MAX_REATTACHMENTS) { watcher.disconnect() return } reattachments++ document.documentElement.append(popupEl) }) watcher.observe(document.documentElement, { childList: true, subtree: true }) setTimeout(() => watcher.disconnect(), WATCH_DURATION_MS) } } // propagate url changes if (typeof window !== 'undefined') { let oldHref = window.document.location.href api.sendHrefchanged(oldHref) const bodyList = window.document.querySelector('body') const observer = new window.MutationObserver(mutations => { mutations.forEach(mutation => { if (oldHref !== window.document.location.href) { // console.warn('url changed', oldHref, document.location.href); oldHref = window.document.location.href api.sendHrefchanged(oldHref) } }) }) const config = { childList: true, subtree: true } observer.observe(bodyList, config) } } if (document.body) return continueToStart() if (typeof window !== 'undefined') { window.addEventListener('load', () => { continueToStart() }) } }