locize 3.0.5 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { PostProcessor, unwrap } from 'i18next-subliminal'
2
2
  import { start } from './process.js'
3
3
  import { startLegacy } from './processLegacy.js'
4
+ import { getQsParameterByName } from './utils.js'
4
5
 
5
6
  let isInIframe = typeof window !== 'undefined'
6
7
  try {
@@ -9,6 +10,96 @@ try {
9
10
  // eslint-disable-next-line no-empty
10
11
  } catch (e) {}
11
12
 
13
+ function configurePostProcessor (i18next, options) {
14
+ i18next.use(PostProcessor)
15
+
16
+ if (typeof options.postProcess === 'string') {
17
+ options.postProcess = [options.postProcess, 'subliminal']
18
+ } else if (Array.isArray(options.postProcess)) {
19
+ options.postProcess.push('subliminal')
20
+ } else {
21
+ options.postProcess = 'subliminal'
22
+ }
23
+
24
+ options.postProcessPassResolved = true
25
+ }
26
+
27
+ function getImplementation (i18n) {
28
+ const impl = {
29
+ getResource: (lng, ns, key) => {
30
+ return i18n.getResource(lng, ns, key)
31
+ },
32
+ setResource: (lng, ns, key, value) => {
33
+ return i18n.addResource(lng, ns, key, value, { silent: true })
34
+ },
35
+ getResourceBundle: (lng, ns, cb) => {
36
+ i18n.loadNamespaces(ns, () => {
37
+ cb(i18n.getResourceBundle(lng, ns))
38
+ })
39
+ },
40
+ getLng: () => {
41
+ return i18n.languages[0]
42
+ },
43
+ getSourceLng: () => {
44
+ const fallback = i18n.options.fallbackLng
45
+ if (typeof fallback === 'string') return fallback
46
+ if (Array.isArray(fallback)) return fallback[fallback.length - 1]
47
+
48
+ if (fallback && fallback.default) {
49
+ if (typeof fallback.default === 'string') return fallback
50
+ if (Array.isArray(fallback.default)) return fallback.default[fallback.default.length - 1]
51
+ }
52
+
53
+ if (typeof fallback === 'function') {
54
+ const res = fallback(i18n.resolvedLanguage)
55
+ if (typeof res === 'string') return res
56
+ if (Array.isArray(res)) return res[res.length - 1]
57
+ }
58
+
59
+ return 'dev'
60
+ },
61
+ getLocizeDetails: () => {
62
+ let backendName
63
+ if (i18n.services.backendConnector.backend && i18n.services.backendConnector.backend.options && i18n.services.backendConnector.backend.options.loadPath && i18n.services.backendConnector.backend.options.loadPath.indexOf('.locize.') > 0) {
64
+ backendName = 'I18NextLocizeBackend'
65
+ } else {
66
+ backendName = i18n.services.backendConnector.backend
67
+ ? i18n.services.backendConnector.backend.constructor.name
68
+ : 'options.resources'
69
+ }
70
+
71
+ const opts = {
72
+ backendName,
73
+ sourceLng: impl.getSourceLng(),
74
+ i18nFormat: i18n.options.compatibilityJSON === 'v3' ? 'i18next_v3' : 'i18next_v4',
75
+ i18nFramework: 'i18next',
76
+ isLocizify: i18n.options.isLocizify,
77
+ defaultNS: i18n.options.defaultNS
78
+ }
79
+
80
+ if (!i18n.options.backend && !i18n.options.editor) return opts
81
+ const pickFrom = i18n.options.backend || i18n.options.editor
82
+ return {
83
+ ...opts,
84
+ projectId: pickFrom.projectId,
85
+ version: pickFrom.version
86
+ }
87
+ },
88
+ bindLanguageChange: cb => {
89
+ i18n.on('languageChanged', cb)
90
+ },
91
+ bindMissingKeyHandler: cb => {
92
+ i18n.options.missingKeyHandler = (lng, ns, k, val, isUpdate, opts) => {
93
+ if (!isUpdate) cb(lng, ns, k, val)
94
+ }
95
+ },
96
+ triggerRerender: () => {
97
+ i18n.emit('editorSaved')
98
+ }
99
+ }
100
+ return impl
101
+ }
102
+
12
103
  let i18next
13
104
  export const locizePlugin = {
14
105
  type: '3rdParty',
@@ -20,92 +111,9 @@ export const locizePlugin = {
20
111
  i18next = i18n
21
112
 
22
113
  // add postProcessor and needed options now
23
- if (!isInIframe) {
24
- i18next.use(PostProcessor)
114
+ if (!isInIframe) configurePostProcessor(i18next, options)
25
115
 
26
- if (typeof options.postProcess === 'string') {
27
- options.postProcess = [options.postProcess, 'subliminal']
28
- } else if (Array.isArray(options.postProcess)) {
29
- options.postProcess.push('subliminal')
30
- } else {
31
- options.postProcess = 'subliminal'
32
- }
33
-
34
- options.postProcessPassResolved = true
35
- }
36
-
37
- const impl = {
38
- getResource: (lng, ns, key) => {
39
- return i18n.getResource(lng, ns, key)
40
- },
41
- setResource: (lng, ns, key, value) => {
42
- return i18n.addResource(lng, ns, key, value, { silent: true })
43
- },
44
- getResourceBundle: (lng, ns, cb) => {
45
- i18n.loadNamespaces(ns, () => {
46
- cb(i18n.getResourceBundle(lng, ns))
47
- })
48
- },
49
- getLng: () => {
50
- return i18n.languages[0]
51
- },
52
- getSourceLng: () => {
53
- const fallback = i18n.options.fallbackLng
54
- if (typeof fallback === 'string') return fallback
55
- if (Array.isArray(fallback)) return fallback[fallback.length - 1]
56
-
57
- if (fallback && fallback.default) {
58
- if (typeof fallback.default === 'string') return fallback
59
- if (Array.isArray(fallback.default)) return fallback.default[fallback.default.length - 1]
60
- }
61
-
62
- if (typeof fallback === 'function') {
63
- const res = fallback(i18n.resolvedLanguage)
64
- if (typeof res === 'string') return res
65
- if (Array.isArray(res)) return res[res.length - 1]
66
- }
67
-
68
- return 'dev'
69
- },
70
- getLocizeDetails: () => {
71
- let backendName
72
- if (i18n.services.backendConnector.backend && i18n.services.backendConnector.backend.options && i18n.services.backendConnector.backend.options.loadPath && i18n.services.backendConnector.backend.options.loadPath.indexOf('.locize.') > 0) {
73
- backendName = 'I18NextLocizeBackend'
74
- } else {
75
- backendName = i18n.services.backendConnector.backend
76
- ? i18n.services.backendConnector.backend.constructor.name
77
- : 'options.resources'
78
- }
79
-
80
- const opts = {
81
- backendName,
82
- sourceLng: impl.getSourceLng(),
83
- i18nFormat: i18n.options.compatibilityJSON === 'v3' ? 'i18next_v3' : 'i18next_v4',
84
- i18nFramework: 'i18next',
85
- isLocizify: i18n.options.isLocizify,
86
- defaultNS: i18n.options.defaultNS
87
- }
88
-
89
- if (!i18n.options.backend && !i18n.options.editor) return opts
90
- const pickFrom = i18n.options.backend || i18n.options.editor
91
- return {
92
- ...opts,
93
- projectId: pickFrom.projectId,
94
- version: pickFrom.version
95
- }
96
- },
97
- bindLanguageChange: cb => {
98
- i18n.on('languageChanged', cb)
99
- },
100
- bindMissingKeyHandler: cb => {
101
- i18n.options.missingKeyHandler = (lng, ns, k, val, isUpdate, opts) => {
102
- if (!isUpdate) cb(lng, ns, k, val)
103
- }
104
- },
105
- triggerRerender: () => {
106
- i18n.emit('editorSaved')
107
- }
108
- }
116
+ const impl = getImplementation(i18n)
109
117
 
110
118
  // start process and expose some implementation functions
111
119
  if (!isInIframe) {
@@ -115,6 +123,36 @@ export const locizePlugin = {
115
123
  }
116
124
  }
117
125
  }
126
+
127
+ export const locizeEditorPlugin = (opt = {}) => {
128
+ opt.qsProp = opt.qsProp || 'incontext'
129
+
130
+ return {
131
+ type: '3rdParty',
132
+
133
+ init (i18n) {
134
+ const { options } = i18n
135
+
136
+ // store for later
137
+ i18next = i18n
138
+
139
+ const showInContext = getQsParameterByName(opt.qsProp) === 'true'
140
+
141
+ // add postProcessor and needed options now
142
+ if (!isInIframe && showInContext) configurePostProcessor(i18next, options)
143
+
144
+ const impl = getImplementation(i18n)
145
+
146
+ // start process and expose some implementation functions
147
+ if (!isInIframe && showInContext) {
148
+ start(impl)
149
+ } else {
150
+ startLegacy(impl)
151
+ }
152
+ }
153
+ }
154
+ }
155
+
118
156
  export { unwrap }
119
157
 
120
158
  export function getI18next () {
package/src/observer.js CHANGED
@@ -1,7 +1,32 @@
1
1
  import { debounce } from './utils.js'
2
2
  import { validAttributes } from './vars.js'
3
3
 
4
+ const mutationTriggeringElements = {}
5
+
4
6
  function ignoreMutation (ele) {
7
+ if (ele.uniqueID) {
8
+ const info = mutationTriggeringElements[ele.uniqueID]
9
+
10
+ if (
11
+ info &&
12
+ info.triggered > 10 &&
13
+ info.lastTriggerDate + 500 < Date.now()
14
+ ) {
15
+ if (!info.warned && console) {
16
+ console.warn(
17
+ 'locize ::: ignoring element change - an element is rerendering too often in short interval',
18
+ '\n',
19
+ 'consider adding the "data-locize-editor-ignore:" attribute to the element:',
20
+ ele
21
+ )
22
+
23
+ info.warned = true
24
+ }
25
+
26
+ return true
27
+ }
28
+ }
29
+
5
30
  const ret =
6
31
  ele.dataset &&
7
32
  (ele.dataset.i18nextEditorElement === 'true' ||
@@ -51,6 +76,15 @@ export function createObserver (ele, handle) {
51
76
  return
52
77
  }
53
78
 
79
+ // cleanup mutation triggers after some time of non triggering
80
+ Object.keys(mutationTriggeringElements).forEach(k => {
81
+ const info = mutationTriggeringElements[k]
82
+
83
+ if (info.lastTriggerDate + 60000 < Date.now()) {
84
+ delete mutationTriggeringElements[k]
85
+ }
86
+ })
87
+
54
88
  // ignore mutation done by our elements
55
89
  if (mutation.type === 'childList') {
56
90
  let notOurs = 0
@@ -73,14 +107,27 @@ export function createObserver (ele, handle) {
73
107
  // eventual text relevant mutation
74
108
  triggerMutation = true
75
109
 
110
+ // add to mutationTriggers to check for spamming elements
111
+ if (mutation.target && mutation.target.uniqueID) {
112
+ const info = mutationTriggeringElements[mutation.target.uniqueID] || {
113
+ triggered: 0
114
+ }
115
+
116
+ info.triggered = info.triggered + 1
117
+ info.lastTriggerDate = Date.now()
118
+
119
+ mutationTriggeringElements[mutation.target.uniqueID] = info
120
+ }
121
+
76
122
  // test if mutated element is already part of another mutation
77
123
  const includedAlready = targetEles.reduce((mem, element) => {
78
124
  if (
79
125
  mem ||
80
126
  element.contains(mutation.target) ||
81
127
  !mutation.target.parentElement
82
- )
128
+ ) {
83
129
  return true
130
+ }
84
131
  return false
85
132
  }, false)
86
133
 
package/src/process.js CHANGED
@@ -2,7 +2,7 @@ import { parseTree, setImplementation } from './parser.js'
2
2
  import { createObserver } from './observer.js'
3
3
  import { startMouseTracking } from './ui/mouseDistance.js'
4
4
  import { initDragElement, initResizeElement } from './ui/popup.js'
5
- import { Popup } from './ui/elements/popup.js'
5
+ import { Popup, popupId } from './ui/elements/popup.js'
6
6
  import { getIframeUrl } from './vars.js'
7
7
  import { api } from './api/index.js'
8
8
 
@@ -47,13 +47,15 @@ export function start (implementation = {}) {
47
47
  startMouseTracking(observer)
48
48
 
49
49
  // append popup
50
- document.body.append(
51
- Popup(getIframeUrl(), () => {
52
- api.requestInitialize(config)
53
- })
54
- )
55
- initDragElement()
56
- initResizeElement()
50
+ if (!document.getElementById(popupId)) {
51
+ document.body.append(
52
+ Popup(getIframeUrl(), () => {
53
+ api.requestInitialize(config)
54
+ })
55
+ )
56
+ initDragElement()
57
+ initResizeElement()
58
+ }
57
59
  }
58
60
 
59
61
  if (document.body) return continueToStart()
@@ -137,9 +137,11 @@ function Minimize (popupEle, onMinimize) {
137
137
  return image
138
138
  }
139
139
 
140
+ export const popupId = 'i18next-editor-popup'
141
+
140
142
  export function Popup (url, cb) {
141
143
  const popup = document.createElement('div')
142
- popup.setAttribute('id', 'i18next-editor-popup')
144
+ popup.setAttribute('id', popupId)
143
145
  popup.classList.add('i18next-editor-popup')
144
146
  popup.style = `
145
147
  z-index: 9;
package/src/ui/utils.js CHANGED
@@ -4,14 +4,14 @@ export function isInViewport (el) {
4
4
  // Special bonus for those using jQuery
5
5
  // if (typeof jQuery !== 'undefined' && el instanceof jQuery) el = el[0]
6
6
 
7
- var rect = el.getBoundingClientRect()
7
+ const rect = el.getBoundingClientRect()
8
8
  // DOMRect { x: 8, y: 8, width: 100, height: 100, top: 8, right: 108, bottom: 108, left: 8 }
9
- var windowHeight = window.innerHeight || document.documentElement.clientHeight
10
- var windowWidth = window.innerWidth || document.documentElement.clientWidth
9
+ const windowHeight = window.innerHeight || document.documentElement.clientHeight
10
+ const windowWidth = window.innerWidth || document.documentElement.clientWidth
11
11
 
12
12
  // http://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap
13
- var vertInView = rect.top <= windowHeight && rect.top + rect.height >= 0
14
- var horInView = rect.left <= windowWidth && rect.left + rect.width >= 0
13
+ const vertInView = rect.top <= windowHeight && rect.top + rect.height >= 0
14
+ const horInView = rect.left <= windowWidth && rect.left + rect.width >= 0
15
15
 
16
16
  return vertInView && horInView
17
17
  }
package/src/utils.js CHANGED
@@ -156,3 +156,15 @@ export function getElementNamespace (el) {
156
156
 
157
157
  return found
158
158
  }
159
+
160
+ export function getQsParameterByName (name, url) {
161
+ if (typeof window === 'undefined') return null
162
+ if (!url) url = window.location.href.toLowerCase()
163
+ // eslint-disable-next-line no-useless-escape
164
+ name = name.replace(/[\[\]]/g, '\\$&')
165
+ const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)')
166
+ const results = regex.exec(url)
167
+ if (!results) return null
168
+ if (!results[2]) return ''
169
+ return decodeURIComponent(results[2].replace(/\+/g, ' '))
170
+ }